Codabra

Spin up Postgres locally — in five minutes

A reproducible local environment so the rest of the course has somewhere to run. Plus the only `docker compose` debug trick you actually need.

Prerequisite: Docker Desktop installed and running (docker.com/products/docker-desktop). The first install on Windows or macOS takes ~10 minutes. If Docker isn't ready when you start this lesson, pause here and come back — the rest of this lesson assumes a working docker compose.

Why a local stack matters

If the only place your code runs is production, you have no playground for performance experiments and no way to reproduce a bug. Worse, you start "testing in prod" — every senior engineer has at least one war story that starts with "I just wanted to check…".

The junior version of me once truncated a customer table on prod because the prod and staging shells were both on the same monitor and I clicked the wrong tab. We had backups. We did not have my self-respect.

A throwaway local stack pays for itself the first time you almost did that.

docker-compose.yml — your minimal local data stack
# Save as docker-compose.yml in a folder you don't care about.
services:
  postgres:
    image: postgres:16.4   # pin minor; "latest" is a future bug report
    environment:
      POSTGRES_USER: codabra
      POSTGRES_PASSWORD: codabra
      POSTGRES_DB: codabra
    ports: ["5432:5432"]
    volumes: ["pgdata:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "codabra"]
      interval: 5s
      retries: 10

  # Object storage stand-in for the lake parts of the course.
  # Any recent MinIO RELEASE.* tag works; we pin for reproducibility.
  minio:
    image: minio/minio:RELEASE.2025-04-22T22-12-26Z
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: minio
      MINIO_ROOT_PASSWORD: minio12345
    ports: ["9000:9000", "9001:9001"]
    volumes: ["miniodata:/data"]

volumes:
  pgdata:
  miniodata:

Run docker compose up -d then docker compose ps. Both services should say healthy (or at least running). Connect:

psql postgresql://codabra:codabra@localhost:5432/codabra -c 'SELECT 1;'

If you see 1, you have a real database to talk to. The whole rest of this course assumes this works.

A word on :latest

The Postgres image above is pinned to 16.4. There is exactly one acceptable use of :latest in production: never.

A team I knew pinned postgres:14 (major-version only) and expected stability. Six months later a minor patch in the 14.x line shipped a planner behavior change that made one of their queries several times slower. Production paged at 3 AM. Fix: pin the minor version, not just the major. Better yet, pin the digest.

The rule: pin to the version you have actually tested. Updates are a deliberate act, not a side effect of a rebuild.

The one docker compose trick that pays for itself: when something is wrong, docker compose logs --tail=200 -f <service> is the first command, not the tenth. Don't docker exec in to poke at processes — read the logs. 80% of "why doesn't it start" is right there.

psql cheat sheet — keep this open in a tab
-- inside psql:
\?               -- list every backslash command
\h SELECT        -- SQL syntax help (here for SELECT)
\dt              -- list tables in current schema
\d customers     -- describe one table (columns, types, indexes)
\timing on       -- print query duration after every statement
\x auto          -- pretty-print wide rows
\q               -- quit

Your first real query: count how many customers we have. Return a single row with one column `total` containing the count of rows in `customers`.

You add `image: postgres:16` to your compose file. Six months later a teammate's CI builds suddenly behave differently. What's the most likely cause?

Checkpoint — before moving on, confirm:

  1. docker compose up -d starts cleanly.
  2. psql postgresql://codabra:codabra@localhost:5432/codabra -c 'SELECT version();' prints a Postgres version string.
  3. You know how to read logs (docker compose logs -f postgres).

If any of those fail, fix it now — every later module assumes this works.