Docker Compose (single-origin)
This setup keeps a single origin via a reverse proxy (Caddy) so cookie auth and CSRF work the same as production.
1Password CLI injection
Use op run to inject secrets at runtime (no secrets committed to repo).
Example with an env file:
op run --env-file env.1password -- docker compose up -d
The env reference file can reference 1Password fields directly, for example:
SESSION_SECRET=op://<vault>/<item>/session_secret
JWT_SECRET=op://<vault>/<item>/jwt_secret
See docs/platform-config/1password-cli.md for setup and troubleshooting.
Runtime DB user separation (RLS)
Use a non-superuser app role so RLS is enforced:
POSTGRES_USER=op://<vault>/<item>/POSTGRES_USER # admin role
POSTGRES_PASSWORD=op://<vault>/<item>/POSTGRES_PASSWORD
POSTGRES_DB=op://<vault>/<item>/POSTGRES_DB
APP_DB_USER=op://<vault>/<item>/APP_DB_USER # runtime role
APP_DB_PASSWORD=op://<vault>/<item>/APP_DB_PASSWORD
Database URL (avoid localhost mistakes)
Prefer composing DATABASE_URL inside docker-compose.yml:
DATABASE_URL=postgres://\${APP_DB_USER}:\${APP_DB_PASSWORD}@db:5432/\${POSTGRES_DB}?sslmode=disable
This guarantees the service name is correct when running in Compose.
MinIO readiness
Avoid healthchecks that depend on tools like curl inside the MinIO image. Instead:
- use
depends_on: service_startedforminio, and - add a retry loop in
minio-init(which already has themcclient).
MinIO console (local only)
By default MinIO has no host port mappings (single-origin setup). For local debugging, use a compose override to expose the console, for example:
services:
minio:
ports:
- "9001:9001"
Apply it with:
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d
Frontend env naming
Pick a single API base env var that matches your SvelteKit code (for example VITE_API_BASE)
and remove the unused variant to avoid confusion.
Local HTTPS (recommended)
For local auth flows with Secure cookies, use the local Caddy file:
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d
This binds https://localhost:8443 and uses Caddyfile.local with internal TLS.
Cloudflare Tunnel With Dockerized Connector
If cloudflared runs as a Compose service, ingress targets must be Compose service names, not loopback.
- Correct inside Docker (default stable mode):
http://web:3000,http://api:8081 - Correct inside Docker (explicit HMR mode):
http://web:5173,http://api:8081 - Incorrect inside Docker:
http://127.0.0.1:5173,http://127.0.0.1:8081
Reason: in-container 127.0.0.1 is the connector container itself.
If you keep Cloudflare host routes in Zero Trust, they override local ingress entries from the mounted config file.
When switching between two local workspace copies on the same machine, set a fixed
COMPOSE_PROJECT_NAME before running compose commands in both workspaces. This keeps
service DNS/network naming stable so cloudflared can still resolve web and api
after a workspace switch.
Frontend API Proxy Runtime Value
For containerized frontend (web service), BACKEND_BASE_URL must resolve in-container (for example http://api:8081).
Avoid build-time localhost values for server-side fetch paths.
Browser-facing frontend code follows a stricter invariant:
- Public/browser entrypoints stay on
https://app-dev.evalium.me(or another public HTTPS host such ashttps://api-dev.evalium.me). - Browser requests should use same-origin
/api/...paths or another public HTTPS domain. - Browser bundles must not default to loopback/internal origins such as
http://127.0.0.1:8081,http://localhost:8081,http://api:8081, orhttp://web:3000.
That means BACKEND_BASE_URL=http://api:8081 is correct only for the server-side/container hop inside web; it is never a browser default.
One-shot DB init (dev only)
Run the schema reset inside a db-init helper container:
op run --env-file env.1password -- \
docker compose -f docker-compose.yml -f docker-compose.local.yml --profile local run --rm db-init
This creates the test DB (if missing) and runs backend/reset-and-reinit.sh against the Compose DB.
Common Docker commands we use
Bring everything up (local TLS + exposed db/minio ports):
op run --env-file env.1password -- \
docker compose -f docker-compose.yml -f docker-compose.local.yml --profile local up -d --build
Force a rebuild/recreate (use after backend changes):
op run --env-file env.1password -- \
docker compose -f docker-compose.yml -f docker-compose.local.yml --profile local up -d --build --force-recreate
View logs:
op run --env-file env.1password -- \
docker compose -f docker-compose.yml -f docker-compose.local.yml --profile local logs --tail=200 api
Stop and wipe volumes (destructive):
docker compose -f docker-compose.yml -f docker-compose.local.yml --profile local down -v
Run the non-destructive migrator (for prod-like flow):
op run --env-file env.1password -- \
docker compose --profile migrate run --rm db-migrate
Run the destructive reset job (safe for dev):
op run --env-file env.1password -- \
docker compose --profile reset run --rm db-reset
Run smokes against local TLS:
CURL_INSECURE=1 BASE_URL=https://localhost:8443/api/v1 \
op run --env-file env.1password -- bash backend/tests/test_all_smoke.sh