Deployment
Ship anything. Recover from anything.
Deployment is the art of moving your app from a laptop to a box that never sleeps — and surviving the moment it breaks at 3am. The whole module is four tools: Docker (packaging), nginx (the front door), certbot (the padlock), and GitHub Actions (the automatic conveyor belt).
Docker: the app in a box, with everything it needs
A container is your app + its exact runtime, sealed in a box. It runs identically on your laptop and on any server. Compose assembles the full stack — app, database, cache, proxy — from one file:
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]services:
app:
build: .
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://app:app@db:5432/app
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
start_period: 5s
depends_on:
db:
condition: service_healthy
db:
image: postgres:18-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
volumes:
pgdata:nginx + Let's Encrypt: the front door and the padlock
nginx receives every request on port 443, terminates TLS, and forwards to your app container. certbot makes the certificate automatic — and renews it forever. This exact pair runs most of the internet's small servers:
apt install nginx certbot python3-certbot-nginx
# /etc/nginx/sites-available/underlay
# server {
# server_name underlay.lol;
# location / {
# proxy_pass http://127.0.0.1:3000;
# proxy_set_header Host $host;
# }
# }
ln -s /etc/nginx/sites-available/underlay /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d underlay.lol # gets the cert, wires the redirect
certbot renew --dry-run # prove auto-renewal worksCI/CD: test on push, deploy on merge
The rule that makes teams fast: nobody deploys by hand, ever. GitHub Actions runs tests on every push and deploys automatically when code lands on main:
name: deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.14" }
- run: pip install -r requirements.txt
- run: pytest -q
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy over SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /opt/underlay
git pull
docker compose up -d --buildBackups are only real when you've restored from them. The drill: pg_dump the database, ship it off-box with restic, delete a table on purpose, restore it, prove the data is back. Do it once and you'll sleep at night. Do it never and you'll learn why at 3am.
test yourself
01A container is best described as:
02502 Bad Gateway from nginx means:
03A backup is only real when:
the real check
certbot renew --dry-run passes. A merge deploys your app. A restore drill proves your backups work.
0 stamps earned·streak: 0 days
Signed out: progress lives in this browser. to carry it everywhere.
vibe-coder payoff
'Works on my machine' ends here. This module separates hobbies from businesses.