DevOps

Docker and Containerization for Modern Web Development

Learn to containerize your applications using Docker for consistent deployments.

Plake
Mar 18, 20264 min read783 words
Docker and Containerization for Modern Web Development

The first time Docker prints “container is running,” something quiet clicks into place — your app no longer depends on which machine it’s on. This guide is the short, opinionated version of what to know to make containers part of your everyday toolkit, not a once-a-quarter ritual.

What is Docker?

Docker packages your application together with its operating system dependencies into an image: a versioned, immutable snapshot. A container is a running instance of that image — a process tree isolated from the host with namespaces and cgroups.

The mental model that has aged well:

  • Image — a recipe.
  • Container — a meal cooked from the recipe.
  • Volume — the pantry that survives the meal.
  • Network — how dishes pass between kitchens.

If you can think in those four nouns, the rest of Docker becomes mostly grammar.

Containerization Benefits

  • Same artifact, every environment. What you tested in CI is what runs in prod.
  • Fast onboarding. New teammates run one command and have a working stack.
  • Clean rollbacks. Tag the image, pin the deploy, roll back by tag.
  • Density. Containers share the host kernel, so you can pack many services on one VM.

The trade-off is real: another layer to debug, another file to maintain, another security boundary to think about. Earn the complexity by needing it.

Dockerfile Basics

A pragmatic Dockerfile for an Astro app built with the Node adapter (output: "server", standalone mode) — multi-stage, slim runtime, sensible defaults:

# syntax=docker/dockerfile:1.7

# 1. deps — install only production dependencies in a cacheable layer
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev

# 2. build — compile the application
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

# 3. runtime — minimal image with only what we need to run
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=4321
COPY --from=deps  /app/node_modules ./node_modules
COPY --from=build /app/dist          ./dist
COPY package.json ./

EXPOSE 4321
USER node
CMD ["node", "./dist/server/entry.mjs"]

For a purely static Astro site (the default output: "static"), skip Node at runtime entirely — copy dist/ into an nginx:alpine image and serve it.

Three habits worth keeping forever:

  1. Use a .dockerignore — excluding node_modules, .git, and dist shaves seconds off every build.
  2. Order layers by churn — least-changing first, most-changing last.
  3. Always set a non-root USER in the runtime stage.

Docker Compose

Compose is for development environments and small single-host deployments. The good kind of YAML:

services:
  web:
    build: .
    ports:
      - "4321:4321"
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 5
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
# bring everything up
docker compose up --build

# tail logs from one service
docker compose logs -f web

# rebuild a single service
docker compose up -d --build web

Image Optimization

Smaller images deploy faster, scan cleaner, and cost less to store.

  • Start from alpine or distroless when you can.
  • Use multi-stage builds to leave compilers and headers behind.
  • Pin versions — node:20-alpine, not node:latest.
  • dive image-name to see exactly what each layer is paying for.
# inspect layer-by-layer cost
brew install dive
dive my-app:latest

Security Best Practices

Containers are not a security boundary on their own — they’re a shape you can put a security boundary around.

  • Run as a non-root user.
  • Scan images on every push (trivy image my-app:latest).
  • Pin all base images by digest in production.
  • Set read-only root filesystems where possible.
  • Drop Linux capabilities you don’t need.
services:
  web:
    image: my-app:abc123
    read_only: true
    tmpfs:
      - /tmp
    cap_drop: ["ALL"]

Editorial container illustration

Production Deployment

For low-traffic services, docker compose on a single VM is fine. As you grow:

  • Fly.io / Render / Railway — push a Dockerfile, get a URL.
  • ECS / Cloud Run — managed, scales to zero, handles certs.
  • Kubernetes — when you’ve earned the operational headcount.

Whatever the runtime, the deploy command should look the same in CI:

docker build -t registry.example.com/app:$(git rev-parse --short HEAD) .
docker push registry.example.com/app:$(git rev-parse --short HEAD)

The deploy is just “use this tag.” That’s the whole point.

Closing Thoughts

Treat your Dockerfile like production code: review it, version it, retire it when something simpler arrives. The best container setup is the one your team forgets is there — until the day it saves them from a frantic Friday rebuild.

Share:

About the author

The Plake Team

Writes long-form essays on craft, software, and the slow web. Curates the Sunday digest read by 88,000+ readers.

Read more

Keep reading

Related Articles

Back to AllBack to All
Deploying Astro Applications to the Cloud

Apr 1, 2026

5 min

Deploying Astro Applications to the Cloud

Git Workflow Guide for Development Teams

Mar 28, 2026

6 min

Git Workflow Guide for Development Teams