strichliste ([ʃtʀɪçˈlɪstə], German word for tally sheet) is a tool to replace a tally sheet inside a hackerspace.
This repository is a single Symfony 8.1 application that serves:
- The UI as server-rendered Twig pages at the real user-facing routes
(
/,/user/active,/user/{id},/articles/*,/split-invoice,/metrics,/search-results,/settings). The UI is the primary way to use the application — it is operable without JavaScript (kiosk-grade progressive enhancement via Symfony UX: Turbo, Stimulus, twig-component). - The legacy REST API at
/api/*for any external client (Android apps, third-party kiosk software, etc.). The API contract is frozen at the shape it had before the Twig UI shipped. Interactive OpenAPI documentation is served at/api/doc(Swagger UI; raw spec at/api/doc.json), maintained as#[OA\*]attributes onsrc/Controller/Api/*and the schema carriers insrc/ApiDoc/*.
The previous standalone React SPA (strichliste-web-frontend/) has been
removed; its README and roadmap are preserved under ../specs/_archive/
for historical reference.
The container setup follows
dunglas/symfony-docker:
FrankenPHP (PHP 8.5, Caddy) running Symfony
in worker mode — the app boots once and stays resident — plus
Postgres 16. There is one Dockerfile with a dev and a prod stage, and
three compose files:
| File | Role |
|---|---|
compose.yaml |
Base: app (FrankenPHP) + database (Postgres). |
compose.override.yaml |
Dev override, picked up automatically: source bind-mounted, worker restarts on file changes, Xdebug available, Postgres reachable from the host on 127.0.0.1:5433. |
compose.prod.yaml |
Prod override: baked image, compiled assets and warmed cache. |
Requires a reasonably current Docker (Engine 25+ with Compose v2.30+ —
any recent Docker Desktop or docker-ce qualifies).
make up
(equivalent to docker compose up -d --build --wait, but with a generous
first-boot timeout — the initial build downloads dependencies, which can
outlast the default wait). No make? Run the docker compose line and
add --wait-timeout 300.
Open https://localhost. Caddy serves TLS out of the box using its
local CA, so browsers (which try HTTPS first these days) connect
directly; plain http://localhost is redirected. On the first visit
your browser shows a certificate warning — accept it once, or trust the
CA root permanently (works on Linux, macOS and Windows):
make tls
The CA lives in the caddy_data volume, so the trust survives
container rebuilds (Windows: run make tls from cmd.exe). The
entrypoint waits for the database, installs vendor/ if missing and
applies migrations before serving traffic; code changes restart the
worker automatically (watch mode). To step-debug, start with
XDEBUG_MODE=debug docker compose up -d. make help lists the
shortcuts (make up, make logs, make test, ...).
On Linux hosts note that the dev container runs as root, so files it
creates on the bind mount (vendor/, assets/vendor/) end up
root-owned — same trade-off as upstream symfony-docker. Run
make fix-perms to hand them back to your user.
Set a real APP_SECRET in .env (the committed value is publicly
known; the container warns loudly if you keep it), then:
docker compose -f compose.yaml -f compose.prod.yaml up -d --build --wait
or make prod. The prod image bakes the code, compiled assets and a
warmed cache; nothing is bind-mounted. Migrations still run on boot.
Two production guardrails:
- Uncomment
COMPOSE_FILE=compose.yaml:compose.prod.yamlin.envon the production host. Without it, a plaindocker compose up -dweeks later silently loads the development override — bind mount, dev image, and an empty anonymousvar/volume that makes a SQLite setup look like all balances vanished. - The prod image bakes
.envinto it (composer dump-env). Fine for an image built and run on the same box — but don't push an image to a registry with real secrets in.env; pass those at runtime instead.
If host ports 80/443 are already taken, remap them in .env
(HTTP_PORT=8080, HTTPS_PORT=8443, HTTP3_PORT=8443) and open
https://localhost:8443 directly (the HTTP→HTTPS redirect targets the
standard port, so skip the HTTP URL when remapping).
Everything database-related is driven by .env — no compose editing:
-
Default: the bundled Postgres service (
COMPOSE_PROFILES=databasein.envkeeps it enabled). Credentials and version are tunable viaPOSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB/POSTGRES_VERSION(note: Postgres only applies the password on the very first start, before thedatabase_datavolume exists). -
Anything else: set
DATABASE_URLto any Doctrine DSN and switch the bundled Postgres off by settingCOMPOSE_PROFILES=(empty). The image ships the SQLite, MySQL/MariaDB and Postgres drivers:# single-container SQLite (stored in the app_var volume) DATABASE_URL="sqlite:////app/var/data.db" COMPOSE_PROFILES= # external MariaDB DATABASE_URL="mysql://user:pass@192.168.1.10:3306/strichliste?serverVersion=10.11.2-MariaDB&charset=utf8mb4" COMPOSE_PROFILES=
Other knobs (all in .env, see the comments there):
| Variable | Default | Purpose |
|---|---|---|
APP_SECRET |
dev value (publicly known!) | Set a unique secret for any real deployment. |
SERVER_NAME |
localhost (self-signed TLS) |
Set a real hostname (e.g. strichliste.example.com) for automatic Let's Encrypt certificates, or ":80" for plain HTTP only (e.g. LAN-IP kiosks). |
HTTP_PORT / HTTPS_PORT / HTTP3_PORT |
80 / 443 / 443 |
Published host ports. |
What the containers do for you:
-
Migrations on boot — the entrypoint waits for the database and applies pending migrations before serving traffic.
-
Settings without rebuilding — bind-mount your own settings file into the
appservice; the entrypoint recompiles the container cache on boot so it is picked up. Put the snippet in a new file, e.g.compose.custom.yaml(notcompose.override.yaml— that one is the development override and is not loaded in production), and extend theCOMPOSE_FILEpin in.envtocompose.yaml:compose.prod.yaml:compose.custom.yaml:# compose.custom.yaml services: app: volumes: - ./config/strichliste.yaml:/app/config/strichliste.yaml:ro
-
Health checks, restart policies and log rotation are preconfigured;
--waitblocks until the app actually serves traffic. Ifup --waithangs or fails,docker compose logs appshows exactly what the entrypoint is waiting for.
Single container without compose (generate the secret once and keep it — don't regenerate it on every run):
docker build --target frankenphp_prod -t strichliste .
echo "APP_SECRET=$(openssl rand -hex 32)" > strichliste.env # keep this file
docker run -d --restart unless-stopped \
-p 80:80 -p 443:443 -p 443:443/udp \
-e SERVER_NAME=localhost \
-e DATABASE_URL="sqlite:////app/var/data.db" \
--env-file strichliste.env \
-v strichliste-data:/app/var \
strichliste
Already running strichliste and want to keep your data? You do not need an import step — point the app at the old database and start it. The schema migrations are written to be safe to run on a populated database (they detect an existing schema and skip it), so the entrypoint brings an old database up to the current version on first boot.
-
An existing SQLite file (e.g.
data.dbfrom an older install): copy it into theapp_varvolume and pointDATABASE_URLat it.DATABASE_URL="sqlite:////app/var/data.db" COMPOSE_PROFILES=
docker compose cp ./your-old-data.db app:/app/var/data.db docker compose restart app -
An existing MySQL/MariaDB or Postgres server: set
DATABASE_URLto its DSN and disable the bundled Postgres (COMPOSE_PROFILES=):DATABASE_URL="mysql://user:pass@192.168.1.10:3306/strichliste?serverVersion=10.11.2-MariaDB&charset=utf8mb4" COMPOSE_PROFILES=
-
An old dump, loaded into the bundled Postgres: keep
COMPOSE_PROFILES=database, start the stack, then restore into it —docker compose exec -T database psql -U strichliste strichliste < dump.sql.
Back up first — and on MySQL/MariaDB this is not optional: DDL there is not transactional, so a forward migration that fails halfway cannot roll itself back. The pre-migration backup is your only safety net (see the backup one-liners below).
Coming from the much older strichliste 1 (different schema)? That one
does need a conversion: php bin/console app:import old.sqlite. It
replaces all current data and refuses to run against a non-empty
database without --force.
The state lives in named volumes: database_data (Postgres),
app_var (the SQLite database, if you use one) and caddy_data (the
TLS certificates / local CA). docker compose down -v deletes all of
them — and with them every balance. Plain down / up is always
safe.
Back up before every upgrade (cron-able one-liners):
# bundled Postgres
docker compose exec database pg_dump -U strichliste strichliste > strichliste-$(date +%F).sql
# SQLite (consistent snapshot even while running)
docker compose exec app php bin/console dbal:run-sql "VACUUM INTO '/app/var/backup.db'"
docker compose cp app:/app/var/backup.db strichliste-$(date +%F).db
Restore Postgres with docker compose exec -T database psql -U strichliste strichliste < dump.sql (into an empty database).
Upgrading is git pull && make prod — migrations apply automatically
on boot, and make prod re-pulls the base images so FrankenPHP/PHP and
Postgres security patches arrive too (a plain up --build reuses the
cached base layers forever). To roll back to an older build afterwards, first revert the
schema while the new code still runs:
docker compose exec app php bin/console doctrine:migrations:migrate prev --no-interaction
then start the older build (or simply restore the backup). Note for MariaDB/MySQL users: DDL is not transactional there, so a migration that fails halfway cannot roll itself back — on those databases the pre-upgrade backup is your only safety net.
Point .env.local at a database first — SQLite needs nothing else:
echo 'DATABASE_URL="sqlite:///%kernel.project_dir%/var/dev.db"' > .env.local
composer install
php bin/console doctrine:migrations:migrate
php bin/console importmap:install
APP_ENV=dev APP_DEBUG=1 symfony serve
Open http://127.0.0.1:8000. To use the compose Postgres instead, run
docker compose up -d database and set
DATABASE_URL="postgresql://strichliste:strichliste@127.0.0.1:5433/strichliste?serverVersion=16&charset=utf8"
in .env.local (the dev override publishes it loopback-only on 5433).
For a bare-metal production build:
composer install --no-dev --optimize-autoloader
php bin/console importmap:install
php bin/console asset-map:compile
All app-level settings live in config/strichliste.yaml (currency, idle
timeout, deposit/dispense step buttons, account boundaries, PayPal,
etc.). The same settings power both the Twig UI and the /api/settings
endpoint.
php bin/phpunitruns the existing API tests; the API contract must stay byte-identical after any UI change.