This guide walks an administrator through the initial configuration and the day-to-day operation of the Sprint Planner web application. It assumes a Linux host with Docker and Docker Compose v2 installed.
For architecture, schema, and feature history, see SPEC.md in the repository root.
docker compose plugin../data next to
docker-compose.yml; nothing else is needed for storage.git clone <repository-url> sprint_planer_web
cd sprint_planer_web
All commands in the rest of this manual are run from the repository root.
.envThe application reads its configuration from a single .env file in the
repository root. Start by copying the template:
cp .env.example .env
Open .env in an editor and fill in the values described below. The file
must be readable by the user that runs docker compose; on a shared host
restrict it with chmod 600 .env.
If your team will sign in with their Microsoft 365 / Entra accounts:
| Variable | What to put there |
|---|---|
ENTRA_TENANT_ID |
Directory (tenant) ID — a GUID, visible on the Entra "Overview" page. |
ENTRA_CLIENT_ID |
Application (client) ID of the app registration. |
ENTRA_CLIENT_SECRET |
A client secret you generated for the app registration. Treat as sensitive. |
In the Entra app registration, configure:
{APP_BASE_URL}/auth/callback,
e.g. https://sprint.example.com/auth/callback.openid, profile, email (delegated, default for
OIDC). Grant admin consent if your tenant requires it.Leave Entra fields blank if you only intend to use the local-admin
fallback. The OIDC sign-in button still appears, but clicking it will fail
until the variables are populated — keep LOCAL_ADMIN_EMAIL /
LOCAL_ADMIN_PASSWORD filled if you skip Entra entirely.
APP_BASE_URL=https://sprint.example.com
The base URL the application is reachable at, without trailing slash.
This is used to construct the OIDC redirect URI and must match exactly
what is registered in Entra. For local testing the default
http://localhost:8080 is fine — but the compose file ships the app on
port 8088, so use http://localhost:8088 if you have not edited
docker-compose.yml.
DB_PATH=/var/www/data/app.sqlite
SESSION_PATH=/var/www/data/sessions
Leave the defaults unless you are also remapping the volume. The parent
directory /var/www/data is the volume mount point inside the container
and corresponds to ./data/ on the host.
APP_ENV=production
production silences verbose PHP errors. Any other value (e.g. dev)
turns them on — useful when troubleshooting in a non-public install.
LOCAL_ADMIN_EMAIL=admin@example.com
LOCAL_ADMIN_PASSWORD=<a long passphrase>
LOCAL_ADMIN_NAME=Local Admin
If both LOCAL_ADMIN_EMAIL and LOCAL_ADMIN_PASSWORD are set, a second
sign-in form appears at /auth/local. The password is compared in
plain text against the env value — so:
.env is chmod 600 and owned by a single trusted user..env and restarting the container.The local-admin user is recorded in the database as
entra_oid = "local:<email>" with is_admin = 1. If the users table
is empty at the moment of the first successful login (OIDC or local),
that user is auto-promoted to admin and an audit entry of type
BOOTSTRAP_ADMIN is recorded. This is how the very first administrator
is created — there is no separate "create admin" step.
To disable the fallback later, blank both variables and restart.
The project ships a multi-stage Dockerfile (Node-based Tailwind build →
PHP 8.3 + Apache runtime) and a docker-compose.yml that wires the
container to the host.
docker compose up --build
What happens:
assets/css/input.css into public/assets/css/app.css. The PHP
stage installs Composer dependencies and copies the application
source into /var/www/html.docker-compose.yml).app.sqlite
inside the volume and applies all migrations under migrations/.Open http://<host>:8088. If you used the local-admin fallback, sign
in at /auth/local. Otherwise click the Entra sign-in CTA on /.
docker compose up -d --build
Logs are then read with:
docker compose logs -f
Stopping the stack:
docker compose down
down keeps the ./data volume intact — your database and sessions
survive. To wipe and start over, stop the stack and remove ./data
manually (see §5.4).
You only need to rebuild the image when the build inputs change:
Dockerfile, composer.json / composer.lock, or package.json /
package-lock.jsonTailwind sources under assets/css/ or any view file that introduces
new utility classes (the css-builder stage scans the views).
docker compose build --no-cache && docker compose up -d
For pure PHP / view edits without a rebuild, restart the container:
docker compose restart
The application exposes an unauthenticated GET /healthz route that
returns 200 OK with a small JSON body. Use it from a load balancer or
uptime monitor:
curl -fsS http://<host>:8088/healthz
Edit the ports line in docker-compose.yml:
ports:
- "8088:80"
Change 8088 to any free port on the host. After editing, docker
compose up -d is enough — no rebuild required. Update APP_BASE_URL
in .env and the redirect URI in Entra to match.
Once the bootstrap admin is signed in, additional administrators are managed through the web UI:
Guardrails enforced server-side:
If both guardrails fire at once — i.e. you are the only admin — the only safe escape is to sign in via the local-admin fallback (if configured) and promote a second account first.
Everything that needs a backup lives under ./data/:
app.sqlite — the database, including audit log.sessions/ — active PHP session files. Losing these only forces
re-login, no data loss.A safe nightly backup is a straight file copy while the container is
running. SQLite handles concurrent reads fine; for a guaranteed
consistent snapshot, prefer sqlite3 .backup:
docker compose exec app sqlite3 /var/www/data/app.sqlite \
".backup /var/www/data/app.sqlite.bak"
cp ./data/app.sqlite.bak /your/offsite/backup/location/
Every create / update / delete on a domain entity is recorded, plus sign-in events. Admins can browse and filter the log via the hamburger menu → Audit log, with filters by entity type, action, user, and date range, and collapsible JSON diffs per row.
To wipe all data and start from a blank slate:
docker compose down
rm -rf ./data
docker compose up -d
Migrations run again on the next request and you are back to "no users, first sign-in becomes admin".
git pull
docker compose build --no-cache
docker compose up -d
Schema migrations under migrations/ run automatically on the next
request after restart. Always take a backup of ./data/app.sqlite
before pulling.
| Symptom | Likely cause | Fix |
|---|---|---|
/auth/login returns "redirect URI mismatch" |
The redirect URI registered in Entra does not equal {APP_BASE_URL}/auth/callback. |
Update either APP_BASE_URL in .env or the Entra app registration. |
/auth/local returns 404 |
LOCAL_ADMIN_EMAIL or LOCAL_ADMIN_PASSWORD is blank. |
Set both, then docker compose restart. |
| Sign-in succeeds but every page shows "not authorised" | The user has no is_admin flag and is trying to access an admin-only page. |
Promote them via Users (logged in as another admin). |
| Container restarts in a loop | Most often a malformed .env line or a permission problem on ./data. |
docker compose logs will show the PHP fatal. Check that ./data is writable by the www-data user inside the container (uid 33 on the Apache image). |
| CSS looks broken / unstyled | The Node build stage was skipped or used a stale layer. | docker compose build --no-cache and restart. |
| OIDC works but PHP 8.4 deprecation warnings appear in logs | Known upstream issue in jumbojett/openid-connect-php. |
Harmless on PHP 8.3 (the shipped runtime); ignore until upstream releases a fix. |
For anything else, check the audit log first — most user-facing errors also leave a trail there with the exact request that failed.