# Lesson 10 — Docker, docker-compose & Course Wrap-up > **New Go concepts in this lesson:** none — this lesson is entirely about > Docker/containerization, which is language-agnostic. If you've followed > the Go Basics lessons and Lessons 1–9, you already know everything Go > needs for this course. This is the last lesson — we'll containerize the whole app (API + MySQL + Redis) so it runs with one command, then do a full review of everything you've built. ## Part A — Docker basics playground A minimal example first, so the concepts aren't tangled up with our full project. ```bash mkdir ~/go-playground/docker-demo && cd ~/go-playground/docker-demo go mod init docker-demo ``` **`main.go`** ```go package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello from inside docker") }) http.ListenAndServe(":8080", nil) } ``` **`Dockerfile`** ```dockerfile # ---- Stage 1: build ---- FROM golang:1.26 AS builder WORKDIR /app COPY go.mod ./ RUN go mod download COPY . . # CGO_ENABLED=0 produces a statically-linked binary - no C libraries # needed, which lets us run it on a tiny base image in stage 2. RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/server . # ---- Stage 2: run ---- FROM alpine:3.20 WORKDIR /app COPY --from=builder /app/bin/server . EXPOSE 8080 CMD ["./server"] ``` Build and run it: ```bash docker build -t docker-demo . docker run -p 8080:8080 docker-demo curl http://localhost:8080 ``` Line by line: - **Multi-stage build** — two `FROM` lines means two separate images are involved. The first (`builder`) has the full Go toolchain (~800MB+) and compiles your binary. The second (`alpine`) is a tiny (~7MB) Linux image that only receives the *finished binary*, not the compiler, source code, or build tools. Your final shipped image ends up small with a much smaller attack surface — no compiler sitting around in production. - `FROM golang:1.26 AS builder` — `AS builder` names this stage so we can reference it later with `--from=builder`. - `WORKDIR /app` — sets the working directory inside the image for all subsequent commands, same idea as `cd`. - `COPY go.mod ./` then `RUN go mod download` **before** `COPY . .` — this ordering is deliberate and matters for build speed. Docker caches each layer; if `go.mod` hasn't changed, Docker reuses the cached `go mod download` layer instead of re-downloading every dependency on every code change. If we copied all the source first, any code edit would invalidate the cache and force a full re-download every build. - `CGO_ENABLED=0 GOOS=linux go build -o /app/bin/server .` — `CGO_ENABLED=0` disables cgo (Go code calling C code), forcing a fully static binary with no dynamic library dependencies — this is what lets it run on the minimal `alpine` image without missing shared libraries. `GOOS=linux` ensures we cross-compile for Linux even if you're building this on macOS/Windows. - `COPY --from=builder /app/bin/server .` — the actual multi-stage magic: pull just one file out of the *first* image into the *second*, discarding everything else from the builder stage. - `EXPOSE 8080` — documentation for humans/tools about which port the container listens on; doesn't actually publish the port by itself (that's `-p` on `docker run`). - `CMD ["./server"]` — the command that runs when the container starts. Now let's connect it to something else via **docker-compose**, so you see multi-container orchestration before we do it for real: **`docker-compose.yml`** ```yaml services: app: build: . ports: - "8080:8080" depends_on: - redis environment: REDIS_ADDR: redis:6379 redis: image: redis:8 ports: - "6379:6379" ``` ```bash docker compose up --build ``` - `build: .` — build the image from the `Dockerfile` in the current directory, instead of pulling a pre-built image. - `depends_on: [redis]` — tells compose to start `redis` before `app`. Note: this only controls *startup order*, not "wait until Redis is actually ready to accept connections" — a fast-starting app can still race ahead of a slow-starting dependency. - `environment: REDIS_ADDR: redis:6379` — the key insight for compose networking: **service names become hostnames**. Inside the compose network, the `app` container can reach Redis at the hostname `redis` (not `127.0.0.1`!), because compose sets up internal DNS that resolves service names to the right container's IP. This is exactly why our app reads `REDIS_ADDR` from config instead of hardcoding `127.0.0.1:6379` — it needs to be different in Docker vs. local dev. ## Part B — containerize the full project **`Dockerfile`** at the project root (same multi-stage pattern, adjusted for our module path): ```dockerfile FROM golang:1.26 AS builder WORKDIR /app COPY go.mod go.sum* ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/server ./cmd/api FROM alpine:3.20 # ca-certificates is needed for outbound HTTPS calls - our Google OAuth # token exchange and userinfo requests both need this to verify certs. RUN apk add --no-cache ca-certificates WORKDIR /app COPY --from=builder /app/bin/server . EXPOSE 8080 CMD ["./server"] ``` - `COPY go.mod go.sum* ./` — the `*` after `go.sum` means "copy it if it exists, don't fail if it doesn't" (useful before you've run `go mod tidy` the very first time). - `./cmd/api` in the build command — points at our actual entrypoint package from Lesson 1's project layout, not the project root. - `RUN apk add --no-cache ca-certificates` — Alpine's minimal base doesn't include root CA certificates by default. Without this, any outbound HTTPS call our app makes (Google's token/userinfo endpoints) would fail with a certificate verification error. **`docker-compose.yml`** — the full stack: our app, MySQL, and Redis: ```yaml services: app: build: . ports: - "8080:8080" depends_on: - mysql - redis environment: PORT: 8080 ENV: development DB_HOST: mysql DB_PORT: 3306 DB_USER: root DB_PASSWORD: devpass DB_NAME: go_simple_api REDIS_ADDR: redis:6379 GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} GOOGLE_REDIRECT_URL: http://localhost:8080/auth/google/callback ALLOWED_ORIGINS: http://localhost:3000 mysql: image: mysql:9 environment: MYSQL_ROOT_PASSWORD: devpass MYSQL_DATABASE: go_simple_api ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql redis: image: redis:8 ports: - "6379:6379" volumes: mysql_data: ``` - `DB_HOST: mysql` / `REDIS_ADDR: redis:6379` — using compose service names as hostnames, exactly as explained in Part A. This is *why* we built `config.go` to read these from env vars back in Lesson 3/6 instead of hardcoding `127.0.0.1` — the same code now works both locally and inside compose, just by changing environment variables. - `${GOOGLE_CLIENT_ID}` / `${GOOGLE_CLIENT_SECRET}` — compose substitutes these from your shell environment or a `.env` file sitting next to `docker-compose.yml` (compose auto-loads a file literally named `.env` in the same directory). - `volumes: mysql_data:/var/lib/mysql` — without this, MySQL's data directory lives *inside* the container's writable layer, destroyed when the container is removed (`docker compose down`). A **named volume** persists that data on the host, independent of the container's lifecycle. - About the `depends_on` startup-order caveat: MySQL can take a few seconds to become ready even after its container "starts." Our `database.NewMySQL` already calls `db.PingContext` with a timeout and returns an error if it fails — so if you hit a race on `docker compose up`, the cleanest fix is either restarting just the `app` service, or adding a small retry loop around the ping in `NewMySQL`. Treat that as an optional improvement rather than something required for this course. **Try the whole stack:** ```bash docker compose up --build ``` ```bash curl -X POST http://localhost:8080/register \ -H "Content-Type: application/json" \ -d '{"email":"hamid@example.com","password":"secret123"}' curl -c cookies.txt -X POST http://localhost:8080/login \ -H "Content-Type: application/json" \ -d '{"email":"hamid@example.com","password":"secret123"}' curl -b cookies.txt http://localhost:8080/me ``` Stop everything cleanly: ```bash docker compose down # stops and removes containers, keeps the volume docker compose down -v # also wipes the mysql_data volume ``` --- ## Course review — what you actually built | Concept | Where you learned it | Where it lives now | |---|---|---| | chi routing, graceful shutdown | Lesson 1 | `router/`, `cmd/api/main.go` | | Structured JSON logging (`slog`) | Lesson 2 | `logging/`, `middleware/request_logger.go` | | MySQL connection pooling | Lesson 3 | `database/mysql.go` | | Repository pattern, pointers | Lesson 4 | `models/user_repository.go` | | bcrypt, JSON request handling | Lesson 5 | `handlers/auth.go` | | Server-side sessions (scs + Redis) | Lesson 6 | `session/`, login/logout/me | | OAuth2 (Google login) | Lesson 7 | `oauth/`, `handlers/oauth_google.go` | | Context values, auth middleware | Lesson 8 | `middleware/require_auth.go` | | Rate limiting, CORS, cookie security | Lesson 9 | `router.go`, `session.go`, `config.go` | | Docker & docker-compose | Lesson 10 | `Dockerfile`, `docker-compose.yml` | ## Core Go ideas that came up repeatedly — make sure these are solid - **Pointers (`*`/`&`)** — sharing state (`*sql.DB`, `*scs.SessionManager`) vs. copying values; writing into caller variables (`rows.Scan`, `res.LastInsertId` → `b.ID`). - **Interfaces implicitly satisfied** — `*chi.Mux` and our custom handlers all satisfy `http.Handler` just by having the right method, no explicit "implements" keyword. - **Closures and the three-layer middleware pattern** — `func(deps) func(http.Handler) http.Handler`, seen in `RequestLogger` and `RequireAuth`. - **`context.Context`** — carrying request-scoped values (request ID, current user) and deadlines (timeouts) through a call chain without threading extra parameters everywhere. - **Error wrapping (`%w`) and sentinel errors** — `ErrUserNotFound`, `errors.Is`, giving callers a stable way to distinguish error *kinds* without string-matching messages. - **Dependency injection via structs** — `AuthHandler{userRepo, sessions, logger}` instead of global variables, making every handler's dependencies explicit and testable. ## Reasonable next steps, if you want to keep going - **Testing** — table-driven tests, `httptest` (you touched this in Lesson 5's Part A) for handlers, and mocking the repository via an interface instead of a concrete `*sql.DB`-backed struct. - **A real migration tool** (e.g. `golang-migrate`) instead of `CREATE TABLE IF NOT EXISTS` on every boot — versioned, reversible schema changes. - **CSRF tokens**, if you ever add a same-origin HTML form frontend, as flagged in Lesson 9. - **Refresh tokens / remember-me**, since right now a session simply expires after 24 hours with no renewal path. - **Structured error responses with error codes**, so a frontend can branch on `"error_code": "invalid_credentials"` instead of parsing message strings. - **Observability**: running Grafana Alloy to tail this container's JSON stdout logs and ship them to Loki is a natural next step, since Lesson 2 already gives you the right log shape for it. That's the course. You went from an empty folder to a real, containerized Go API with password auth, Google OAuth, Redis-backed sessions, rate limiting, and structured logging — and along the way, picked up the core Go idioms (pointers, interfaces, closures, contexts, error handling) that show up in essentially every real-world Go codebase.