# Deploy Steady with Docker Compose and Caddy This repository is ready for the established VPS layout: - repository directory: `~/stacks/steady` - container/service name: `steady` - public address: `https://steady.bujour.de` - private Docker port: `5000` (it is not published on the VPS) - persistent SQLite volume: `steady_data`, mounted at `/app/instance` - reverse proxy: Caddy on the external Docker network `proxy` The container runs database migrations before Gunicorn starts. Caddy terminates TLS and forwards requests over the private Docker network. ## 1. Prerequisites Install Git, Docker Engine with the Compose plugin, and Caddy in its existing Compose stack. DNS for `steady.bujour.de` must resolve to the VPS, and inbound ports 80 and 443 must be allowed by both the VPS firewall and provider firewall. Do not open port 5000. Confirm the shared proxy network exists: ```bash docker network inspect proxy >/dev/null 2>&1 || docker network create proxy ``` ## 2. Clone the application Replace the example repository URL with the actual Git remote: ```bash mkdir -p ~/stacks git clone ~/stacks/steady cd ~/stacks/steady ``` For an existing checkout, use `git pull --ff-only` instead of cloning it again. ## 3. Create the production environment Compose automatically reads `~/stacks/steady/.env`. Create it on the VPS; do not commit it: ```bash cd ~/stacks/steady umask 077 python -c "import secrets; print('SECRET_KEY=' + secrets.token_urlsafe(48))" > .env printf '%s\n' 'TRUSTED_HOSTS=steady.bujour.de' >> .env ``` `compose.yaml` supplies the remaining production settings, including the exact SQLite URL `sqlite:////app/instance/steady.db`. The named volume therefore persists the database across container replacement. Validate the rendered configuration without printing the secret in shared logs: ```bash docker compose config --quiet ``` ## 4. Build and start ```bash docker compose build --pull docker compose up -d docker compose ps docker compose logs --tail=100 steady ``` Startup fails if the migration fails, so do not ignore an unhealthy or restarting container. Check the private endpoint from another container on the proxy network: ```bash docker run --rm --network proxy curlimages/curl:8.14.1 -fsS http://steady:5000/health ``` The expected body is `{"status":"ok"}`. `curl localhost:5000` intentionally does not work because the application port is not published to the host. ## 5. Configure Caddy Add this site block to the existing Caddyfile in `~/stacks/caddy/`: ```caddyfile steady.bujour.de { reverse_proxy steady:5000 } ``` Ensure the Caddy service also joins the external `proxy` network. Validate and reload Caddy using its actual Compose service name (the examples assume `caddy`): ```bash cd ~/stacks/caddy docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile ``` Caddy obtains and renews the TLS certificate automatically after DNS and ports 80/443 are correct. ## 6. Verify the public deployment ```bash curl -fsS https://steady.bujour.de/health curl -I https://steady.bujour.de/ ``` Confirm the health JSON, a valid HTTPS certificate, and security headers such as `Strict-Transport-Security`, `Content-Security-Policy`, and `X-Content-Type-Options`. Then register an account and exercise a normal task workflow in the browser. ## Updates Back up first, then pull, rebuild, and replace the service: ```bash cd ~/stacks/steady docker compose exec -T steady python -c "import sqlite3; source=sqlite3.connect('/app/instance/steady.db'); backup=sqlite3.connect('/app/instance/steady-backup.db'); source.backup(backup); backup.close(); source.close()" git pull --ff-only docker compose build --pull docker compose up -d docker compose ps docker compose logs --tail=100 steady ``` Copy the backup off the named volume to protected storage as part of the VPS backup process. The in-volume backup is only an immediate pre-update snapshot. Test restore procedures regularly. ## Operations and rollback - Probe `https://steady.bujour.de/health` for process availability. - View logs with `docker compose logs --tail=200 steady` or add `-f` to follow. - Restart with `docker compose restart steady`. - Keep `FEATURE_ADMIN=false` unless the guarded, data-free placeholder is needed. - Never expose Flask/Gunicorn directly or use `flask run` in production. - Rotate `SECRET_KEY` only when intentionally invalidating all sessions. Application rollback requires both the prior Git revision/image and a compatible database backup. Do not run destructive migration downgrades without reviewing the migration and recovery plan. ## Troubleshooting - **Compose says `SECRET_KEY` is missing:** create `.env` as shown above. - **Container restarts:** inspect `docker compose logs steady`; migration or configuration errors appear before Gunicorn starts. - **Caddy returns 502:** verify both containers join `proxy`, the service is healthy, and the upstream is exactly `steady:5000`. - **Host rejected with HTTP 400:** make `TRUSTED_HOSTS` match the public hostname. - **No certificate:** verify public DNS and inbound ports 80/443; inspect Caddy's logs, not the application logs. - **Permission error for SQLite:** verify `/app/instance` is the `steady_data` volume. The image runs as the unprivileged user `steady` (UID 10001).