5.3 KiB
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:
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:
mkdir -p ~/stacks
git clone <repository-url> ~/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:
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:
docker compose config --quiet
4. Build and start
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:
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/:
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):
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
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:
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/healthfor process availability. - View logs with
docker compose logs --tail=200 steadyor add-fto follow. - Restart with
docker compose restart steady. - Keep
FEATURE_ADMIN=falseunless the guarded, data-free placeholder is needed. - Never expose Flask/Gunicorn directly or use
flask runin production. - Rotate
SECRET_KEYonly 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_KEYis missing: create.envas 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 exactlysteady:5000. - Host rejected with HTTP 400: make
TRUSTED_HOSTSmatch 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/instanceis thesteady_datavolume. The image runs as the unprivileged usersteady(UID 10001).