Docker
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
- Image โ A read-only blueprint. Built from a Dockerfile. Like a class definition in Python.
- Container โ A running instance of an image. Like an object created from a class. You can have many containers from one image.
Dockerfile โ (docker build) โ Image โ (docker run) โ Container (blueprint) (template) (running process) Like Python: Class definition โ Class object โ Instance of the class
The 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
# 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:
- Local machine: npm 11.x (generates
package-lock.jsonwith one algorithm) - Docker image
node:20-alpine: ships with npm 10.8.2 (interprets lock file differently) - npm 10 saw the lock file as out of sync; npm 11 accepted it fine
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
# 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
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?
2. Why does the frontend Dockerfile use three stages (deps, builder, runner)?
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 . .?
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?
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?
-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.