158 lines
5.3 KiB
Markdown
158 lines
5.3 KiB
Markdown
# 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 <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:
|
|
|
|
```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).
|