โ† Tailwind CSS 13 / 18 Next: Docker Compose โ†’

Docker

Module 5 ยท Infrastructure ยท โฑ ~25 min
Why this matters: JongYang runs entirely in Docker containers. The app that works on your laptop works identically on the Hetzner server because Docker packages all dependencies together. Every docker compose build and docker compose up command you run creates and starts these containers.

Containers vs Virtual Machines

A virtual machine (VM) emulates an entire computer โ€” CPU, RAM, and a full OS. Slow to start, uses a lot of resources.

A container shares the host machine's OS kernel but isolates the filesystem and processes. Result: containers start in milliseconds and use far less RAM than VMs.

Virtual Machine:                   Container:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Guest OS (Ubuntu 22.04) โ”‚        โ”‚ App + Libraries   โ”‚
โ”‚ Libraries + Runtime     โ”‚        โ”‚ (just what's needed)
โ”‚ Your Application        โ”‚        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                  โ”‚
        โ”‚ Hypervisor                 Shared Linux Kernel
        โ”‚                                    โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Host OS (Ubuntu 22.04)  โ”‚        โ”‚ Host OS           โ”‚
โ”‚ Hardware                โ”‚        โ”‚ Hardware          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  ~2-5 minutes to start               ~0.1 seconds to start

Images vs Containers

Dockerfile โ†’ (docker build) โ†’ Image โ†’ (docker run) โ†’ Container
  (blueprint)                (template)               (running process)

Like Python:
  Class definition โ†’ Class object โ†’ Instance of the class

The Backend Dockerfile

dockerfile โ€” backend/Dockerfile
FROM python:3.12-slim        # Base image: official Python 3.12 on minimal Linux

WORKDIR /app                 # All subsequent commands run from /app directory

COPY requirements.txt .      # Copy dependency list first (for caching โ€” see below)
RUN pip install --no-cache-dir -r requirements.txt  # Install Python packages

COPY . .                     # Copy all source code

EXPOSE 8000                  # Document that this container uses port 8000 (informational only)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# CMD is the default command when the container starts

Why copy requirements.txt before the source code? Docker caches each line. If requirements don't change but source code does, Docker reuses the cached pip install layer โ€” the rebuild is much faster.

The Frontend Dockerfile โ€” Multi-Stage Build

dockerfile โ€” frontend/Dockerfile
# Stage 1: deps โ€” install node_modules
FROM node:20-alpine AS deps
WORKDIR /app
RUN npm install -g npm@11     # โ† fix: local npm is v11, Docker's default is v10
COPY package*.json ./
RUN npm ci                   # clean install from lockfile

# Stage 2: builder โ€” compile the Next.js app
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules  # reuse from stage 1
COPY . .

# NEXT_PUBLIC_* vars baked in at BUILD time (not runtime)
ARG NEXT_PUBLIC_API_URL=http://localhost:8000
ARG NEXT_PUBLIC_BASE_URL=http://localhost:3000
ARG NEXT_PUBLIC_BASE_DOMAIN=localhost
ARG NEXT_PUBLIC_LINE_LOGIN_CHANNEL_ID=
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_BASE_DOMAIN=$NEXT_PUBLIC_BASE_DOMAIN
ENV NEXT_PUBLIC_LINE_LOGIN_CHANNEL_ID=$NEXT_PUBLIC_LINE_LOGIN_CHANNEL_ID

RUN npm run build            # compiles TypeScript + bundles JS

# Stage 3: runner โ€” minimal production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next ./.next          # compiled output only
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["npm", "start"]

Multi-stage builds solve the "fat image" problem. The builder stage has all dev tools. The final runner only copies what's needed to run the app โ€” the compiled output. The source code and build tools are not in the final image. Result: smaller, faster, more secure.

deps stage:       ~600MB (node_modules + npm)
builder stage:    ~800MB (source + node_modules + build output)
runner stage:     ~250MB (compiled .next + node_modules)  โ† only this ships

The npm Version Mismatch Bug

During deployment, docker compose build failed. The error: npm ci: Missing @swc/helpers@0.5.23 from lock file. Root cause:

Fix: add RUN npm install -g npm@11 at the start of the deps stage to upgrade npm inside Docker to match the local version.

Essential Docker Commands

bash โ€” commands used during this project
# Build all images defined in docker-compose.yml
docker compose build

# Build one specific service (faster)
docker compose build fastapi

# Build with no cache (forces full rebuild)
docker compose build --no-cache nextjs

# Start all containers in background
docker compose up -d

# Restart one container (uses existing image โ€” no rebuild)
docker compose restart fastapi

# Recreate a container with new image
docker compose up -d --force-recreate nextjs

# View logs
docker compose logs fastapi --tail=20

# Run a command inside a running container
docker compose exec fastapi alembic upgrade head
docker compose exec postgres psql -U booking_user -d booking_platform

# List running containers with status
docker compose ps

# Free up disk space (removes unused images and build cache)
docker system prune -af
Disk space gotcha: Docker build caches accumulate fast. When we ran out of disk space (38GB used on a 38GB server), docker system prune -af freed 6GB by removing old image layers and build cache. Run this if you're running low.

๐Ÿง  Self-Check Quiz

1. What's the difference between a Docker image and a Docker container?

An image is a read-only blueprint โ€” like a class definition or a template. It contains the OS layers, installed packages, and application code, but it's not running. A container is a running instance of an image โ€” a live process with its own isolated filesystem. Multiple containers can run from the same image simultaneously.

2. Why does the frontend Dockerfile use three stages (deps, builder, runner)?

To produce a small, secure final image. The deps stage installs all node_modules (big). The builder stage has the full source code and runs the build. The runner stage only copies the compiled output (.next, node_modules, package.json) โ€” no source code, no build tools. The final image is ~250MB instead of ~800MB. Smaller images are faster to pull and have a smaller attack surface.

3. In the backend Dockerfile, why is COPY requirements.txt . done before COPY . .?

Docker caches each layer. If requirements.txt hasn't changed, Docker reuses the cached pip install layer and skips reinstalling packages. Copying requirements.txt first means that changing source code (which happens constantly) doesn't invalidate the pip cache. If you copied everything first, any code change would trigger a full pip reinstall, making rebuilds slow.

4. What caused the npm version mismatch error and how was it fixed?

The package-lock.json was generated with npm 11.x locally. The Docker base image (node:20-alpine) ships with npm 10.8.2, which interpreted the lock file differently and complained about a missing package. Fixed by adding RUN npm install -g npm@11 to the Dockerfile's deps stage, upgrading npm inside Docker to match the local version.

5. What does docker compose up -d --force-recreate fastapi do and when do you need it?

It stops and removes the existing fastapi container, then creates a new one from the current image. The -d runs it in the background; --force-recreate ensures a new container is created even if one is already running. You need this after docker compose build fastapi โ€” the old running container still uses the old image. Without force-recreate, docker compose up -d won't restart containers that are already running.