Back to the curriculum
Part 3 · Lesson 06
Shipping Real Apps

Deploying Your First App

From git clone to a live HTTPS URL, end to end.

intermediate18 min readUpdated 2026-04-11

What we are building

We will deploy a Next.js app with a Postgres database behind Traefik with an automatic Let's Encrypt cert, on a fresh Hetzner CX22 running Ubuntu 24.04. The app could be anything — Next.js, Laravel, Django, WordPress, Rails — the shape of the deployment is identical. Docker is the great equalizer.

The compose.yml that does the whole job

Three services: the app, Postgres, and Traefik. Traefik owns ports 80 and 443. The app and the database talk to each other over the internal network. Nothing except Traefik is exposed to the internet:

services:
  traefik:
    image: traefik:v3
    command:
      - --providers.docker=true
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - [email protected]
      - --certificatesresolvers.le.acme.tlschallenge=true
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
    ports: ["80:80", "443:443"]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt

  app:
    build: .
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
    labels:
      - traefik.enable=true
      - traefik.http.routers.app.rule=Host(`example.com`)
      - traefik.http.routers.app.tls.certresolver=le

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  letsencrypt:
  pgdata:

git clone, .env, docker compose up

SSH into the box as your non-root user, clone the repo, create a .env file with DB_PASSWORD=$(openssl rand -hex 32), and run docker compose up -d --build. On the first boot Traefik will request a cert from Let's Encrypt and your site is live on HTTPS. If the cert fails, check that your A record points to this server — it is always DNS.

Verify the three things that matter

curl -I https://example.com should return 200 with a valid cert chain. docker compose logs -f app should show your framework's startup banner with no errors. docker compose exec db psql -U app -c "\dt" should list your tables (or nothing, if this is a fresh DB before migrations). If all three pass, you have shipped.

What to do when it breaks

First rule: read docker compose logs. Ninety percent of first-deploy failures are either a missing env var (app crashes on startup) or DNS not propagated (cert request times out). Second rule: docker compose ps tells you which container is unhealthy. Third rule: if you are rebuilding in a loop, docker compose down -v nukes the volumes and starts over. Save that for a fresh setup — it deletes your database.

Key takeaways

  • One Compose file owns the entire app, DB, and proxy
  • Traefik labels wire the domain and cert automatically
  • First-deploy failures are usually DNS or missing env vars
  • `curl -I`, `logs`, and `ps` are the three debug commands you need

Related documentation

Related templates