Skip to main content
Everything so far ran on your laptop. This part puts it on the internet properly: your domain, a real certificate, a database that gets backed up, and a deploy that runs on a merge to main. It’s about €7 a month, on one machine. The app, Postgres, Redis and TLS termination all share the box, and the deployment is eight files feather new already put in your repository. This part splits differently from the others. The repository work is a prompt like any other. The server work is a set of commands you run yourself, and the prompt is told not to touch them.

What you’ll have

  • The app at https://your-domain, with a certificate that renews itself
  • A deploy script that migrates once, swaps containers and waits for health
  • Nightly database dumps with retention
  • A GitHub Actions workflow that tests, then deploys, on every push to main

Which parts of Feather this uses

Docker layout

What feather docker init writes, and the two details worth not breaking.

Health checks

/health checks the database, which is what makes a health-gated deploy meaningful.

Security check

A config audit that exits non-zero, so it works as a pre-launch gate.

Configuration

TRUSTED_HOSTS, OAUTH_CALLBACK_URL and the keys compose sets for you.

Step 1: What you need

  • A working app from part 4 or part 5
  • A domain whose DNS you control
  • A VPS you can SSH into as root. Two vCPU and 4 GB is enough to start
  • Docker locally
Check whether the deployment files are already there:
If that errors, add them. It never overwrites an existing file.

Step 2: Run the prompt

This covers the repository side and prepares the commands you’ll run on the server.
The prompt

Step 3: Set up the server yourself

Work through the list the prompt gave you. Two things are worth stating here because getting them wrong is expensive.
DNS before anything else. Caddy asks Let’s Encrypt for a certificate the moment it starts, and Let’s Encrypt checks the name resolves to the machine asking. Confirm with dig +short kanban.example.com before you deploy.
Keep a second terminal open while hardening SSH. After you set PermitRootLogin no and PasswordAuthentication no and restart ssh, confirm ssh deploy@<ip> works in the second terminal before closing the first. Otherwise you’ve locked yourself out of your own server.
The production .env never enters git and never goes into the image. At minimum it needs DOMAIN, POSTGRES_PASSWORD, SECRET_KEY, your Google credentials, OAUTH_CALLBACK_URL and TRUSTED_HOSTS. Leave FLASK_CONFIG, DATABASE_URL, REDIS_URL and JOB_BACKEND out of it, because compose sets those on the container and a duplicate is one more thing to disagree.
Then deploy:

Step 4: Test it

  1. https://your-domain loads over a valid certificate.
  2. Sign in with Google. If this works, your proxy headers are right.
  3. curl https://your-domain/health returns healthy, including the database check.
  4. Push a commit to main and watch the workflow test, then deploy.
  5. Run ./deploy/backup.sh, then restore the dump into the database.
Then the gate before you tell anyone the URL:

Step 5: The pre-launch review

Worth a second prompt once it’s live.
Pre-launch review prompt
Keep the assistant off your production server unless you’ve decided otherwise deliberately. Having it prepare commands you run yourself costs almost nothing and keeps a review step in front of the script that applies migrations.

What the framework did

Migrations ran exactly once, before the swap. deploy/deploy.sh runs feather db upgrade in a throwaway container built from the new image, while the old containers are still serving. A failed migration stops the deploy and leaves the site up. It’s also why CMD doesn’t run migrations: two web containers starting together would both begin the same Alembic upgrade, and Alembic doesn’t arbitrate that. Healthy means the database is reachable. The container healthcheck curls /health, which Feather registers and which checks the database rather than just confirming Python is alive. A deploy that waits on it is therefore waiting on something meaningful. See Health checks. TLS has no cron job. Caddy obtains and renews certificates for every hostname in the Caddyfile. What does need attention is X-Real-IP: Caddy sets Host and X-Forwarded-Proto by default, but not that one, and rate limiting reads it. The generated config adds it, and deleting that line makes every request look like it came from the same address. CI holds no application secrets. The server builds its own images, so the workflow needs two repository secrets and no registry. Your .env never leaves the box. See Continuous deployment.

Where to go next

  • A staging host. A second A record, a second checkout, a second compose project name, and the same scripts.
  • Log aggregation. The app writes JSON logs in production. Ship them somewhere you can search.
  • Error alerting. /admin/logs records exceptions. Notify yourself when the rate spikes.
  • Off-site backups. A dump on the same disk as the database isn’t a backup.