Skip to content

Day 149 — docker-compose for a local stack

Month 6 · Week 2 · ⬅ Day 148 · Day 150 ➡ · Journal index

🎯 Learning Objective

Define a reproducible local stack (Go app + Postgres + Redis) with Docker Compose, wired together by service-name DNS, depends-on/healthchecks, and env-file config.

📚 Topics

  • compose.yaml: services, networks, volumes, depends_on with condition
  • Service discovery by service name on the default bridge network
  • healthcheck, env_file, and the *_FILE secret convention

📖 Reading / Sources

📝 Notes

  • A compose.yaml declares a stack of services; docker compose up builds and starts them on a shared user-defined network → [[docker-compose]].
  • Service discovery is DNS by service name: the app connects to host db, not localhost or an IP. Compose runs an embedded DNS resolver so db:5432 resolves to the Postgres container.
  • depends_on alone only orders start, not readiness. A container can be "started" before Postgres accepts connections. Add condition: service_healthy + a healthcheck so the app waits for the DB to be truly ready → [[healthcheck]].
  • App config still comes from the environment ([[12-factor]]); Compose injects it via environment: or an env_file:. Secrets use the *_FILE pattern so values come from mounted files, not inline env → [[day-152]].
  • Named volumes persist DB data across up/down; without one, docker compose down wipes the database. down -v removes volumes too.
  • localhost inside a container is the container itself. To reach the host machine use host.docker.internal; to reach a sibling service use its service name.
  • Compose is a local/dev tool. Production orchestration (Kubernetes, ECS) reuses the same image but not this file.

💻 Code Examples

# compose.yaml — local app + datastores
services:
  app:
    build:
      context: .
      args:
        VERSION: dev
    ports:
      - "8080:8080"          # host:container
    environment:
      PORT: "8080"
      LOG_LEVEL: "debug"
      DATABASE_URL: "postgres://app:app@db:5432/app?sslmode=disable"
      REDIS_ADDR: "cache:6379"
    depends_on:
      db:
        condition: service_healthy   # wait until the DB passes its healthcheck
      cache:
        condition: service_started

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 2s
      timeout: 3s
      retries: 10
    volumes:
      - dbdata:/var/lib/postgresql/data

  cache:
    image: redis:7

volumes:
  dbdata:

The app reads DATABASE_URL/REDIS_ADDR exactly like the stdlib config loader below.

Full code: examples/month-06/config/main.go · Run: go run ./examples/month-06/config

🏋️ Exercises / Practice

Exercise Status Link
Parse the env Compose injects, fail-fast on bad values exercises/month-06/week-2/envconfig

🐛 Mistakes Made

  • App raced the DB: it crashed on the first connect because depends_on only waited for start. Added a healthcheck + condition: service_healthy.
  • Pointed the app at localhost:5432; got connection refused. Inside a container that's the container itself — changed to the service name db.

❓ Open Questions

  • Should the app still implement retry/backoff on connect even with healthchecks? (Yes — healthchecks reduce, not eliminate, startup races and mid-life blips.)

🧠 Active Recall (answer without looking)

  1. Q: What hostname does the app use to reach the Postgres service, and why not localhost?
    A

The service name db. Compose's embedded DNS resolves service names on the shared network. localhost inside a container refers to that container, not to sibling services. 2. Q: Why isn't depends_on: [db] enough to avoid connection errors at startup?

A

depends_on only orders container start, not application readiness. Postgres may not yet accept connections. Use a healthcheck plus condition: service_healthy.

🪶 Feynman Reflection

Compose is a recipe that boots several containers as one stack and gives them a private network where they find each other by name. depends_on says "start B before A"; a healthcheck says "and don't tell A it's ready until B actually answers."

🕳️ Knowledge Gaps

  • Compose profiles and override files (compose.override.yaml) for dev-vs-test variants — skim later.

✅ Summary

I can stand up a multi-service local stack with Compose, connect services by DNS name, and gate startup on real readiness via healthchecks.

⏭️ Next Steps / Prep for Tomorrow

  • Day 150: harden the runtime image — non-root, distroless, read-only filesystem.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): docker-compose local stack (day 149)