underlay
module 066–8h

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:

dockerfileDockerfile — build once, run anywhere
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"]
yamldocker-compose.yml — the whole stack in one file
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:

bashone-time setup — then it just works
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 works

CI/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:

yaml.github/workflows/deploy.yml
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 --build

Backups 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.