flask_template_codex/DEPLOYMENT.md
2026-08-08 06:08:24 +02:00

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/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).