Containers & Deployment

Docker & Deployment

Docker packages your app and its environment into an image that runs the same everywhere. Multi-stage builds, caching and CI/CD turn deployment into a repeatable step.

intermediate14 min readUpdated Sep 15, 2026
Dockerfile
dockerfile
// Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Image
A read-only template
Container
A running image
Build file
Dockerfile
Layers
Cached and reused
Compose
Multi-service apps
Registry
Where images are stored

Why it matters

Why containers won the deployment story

Consistent everywhere

The image includes the OS, runtime and dependencies, so it runs the same on your laptop, CI and production.

Isolated by default

Containers share the host kernel but isolate processes, filesystems and networks, which limits blast radius.

Portable and scalable

The same image runs on any container host, which makes scaling and moving between clouds straightforward.

The big picture

The three ideas behind Docker

An image is the build, a container is the run, and a registry is how images travel between machines.

The image

Build

A layered, read-only template built from a Dockerfile.

The container

Run

A running instance of an image with its own process and filesystem.

The registry

Distribute

A store for images that CI and production pull from.

Docker at a glance

The core of Docker

Dockerfile

Instructions that build an image layer by layer.

Layers and cache

Each instruction is a cached layer, so unchanged steps are reused.

Multi-stage builds

Build in one stage and ship only the output in a smaller final image.

Compose

Define and run multi-container apps such as an app plus a database.

Registry

Push and pull images from Docker Hub, GHCR or a private registry.

CI/CD

Build, test and deploy automatically on every change.

A short history

From VMs to containers to CI/CD

  1. 2013

    Docker released

    Containers become accessible to ordinary developers.

    13
  2. 2015

    Compose and orchestration

    Compose and Kubernetes make multi-service deployments practical.

    15
  3. 2017

    Multi-stage builds

    Smaller production images become the standard practice.

    17
  4. 2020

    Containers everywhere

    Container-based CI/CD and serverless container platforms become common.

    20
  5. Today

    The deployment baseline

    Most production systems ship as container images.

    Today

The complete guide

Docker & Deployment: Everything you need to know

Why containers won

Before containers, deploying meant reproducing an environment: the right runtime version, the right system libraries, the right configuration. “It works on my machine” was a genuine problem, and servers drifted from development over time.

Docker solved it by packaging the application and its environment into a single image. The image runs identically on a laptop, in CI and in production, because it carries everything it needs. That consistency is why containers became the baseline for deployment, and why learning Docker is one of the highest-value skills for shipping software.

Images and containers

An image is a read-only template built from a Dockerfile, composed of cached layers. A container is a running instance of an image with its own writable layer, process and network. You build once and run many.

docker build -t my-app:1.0 .
docker run -p 3000:3000 my-app:1.0
docker ps
docker logs <container>

The -p flag maps a host port to a container port. Containers are ephemeral by design: you can stop, remove and recreate them freely because the image is the source of truth.

The Dockerfile

The Dockerfile is the build recipe. Each instruction creates a layer.

# Dockerfile
FROM node:22-alpine
WORKDIR /app

# install dependencies first for better caching
COPY package*.json ./
RUN npm ci --omit=dev

# then copy source
COPY . .

ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]

Order matters. Copying the dependency manifests and installing before copying source means a code change only invalidates the cheap source layer, not the expensive install. npm ci gives a reproducible install, and USER node runs as a non-root user.

Layer caching

Docker caches each layer and reuses it when the inputs are unchanged. This is why the order above is deliberate: put things that change rarely at the top and things that change often at the bottom.

# cached unless package files change
COPY package*.json ./
RUN npm ci

# invalidated on every source change
COPY . .

A .dockerignore file also matters. Without it, node_modules, .git and build output are copied into the build context, which slows the build and can leak files into the image.

node_modules
.git
dist
.env

Multi-stage builds

A multi-stage build compiles in one image and ships only the output in another.

# Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

The final image contains only the built static files and a web server, not the Node runtime, the source or the build tools. The result is smaller, faster to pull and has a much smaller attack surface. For a server app, the final stage would be a slim Node image with production dependencies only.

Compose for multi-service apps

Real applications usually need more than one process. Compose defines them together.

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: example
    volumes:
      - data:/var/lib/postgresql/data

volumes:
  data:

depends_on controls startup order, volumes persists data across container restarts, and services reach each other by name on the shared network. Compose is ideal for local development and simple single-host deployments.

Registries and CI/CD

Images travel through a registry such as Docker Hub, GitHub Container Registry or a private registry. A typical pipeline:

  1. Install dependencies and run tests.
  2. Build the image and tag it with the commit SHA.
  3. Push it to the registry.
  4. Deploy the new tag to the target environment.
docker build -t ghcr.io/me/app:$GIT_SHA .
docker push ghcr.io/me/app:$GIT_SHA

Because the image is immutable and tagged by commit, rollbacks are just deploying a previous tag. This is the core of a reliable deployment process: build once, promote the same artifact through environments, and never rebuild for production.

Production practices

  • Health checks. Expose an endpoint the platform can poll to know the container is ready.
  • Non-root user. Run as a user with the least privilege it needs.
  • Read-only filesystem where possible, with explicit writable volumes.
  • Graceful shutdown. Handle SIGTERM to finish in-flight requests.
  • Resource limits. Set CPU and memory so one container cannot starve others.
  • Pin base image versions for reproducible builds.
  • Secrets at runtime, never baked into the image.
  • Small base images such as Alpine or distroless to reduce vulnerabilities.

Best practices

  • Order Dockerfile instructions from least to most frequently changing.
  • Use multi-stage builds and ship only what is needed.
  • Add a .dockerignore for node_modules, .git, dist and .env.
  • Run as a non-root user.
  • Tag images by commit SHA, not just latest.
  • Build the image once and promote it through environments.
  • Handle SIGTERM for graceful shutdown.

Common mistakes

  • Copying node_modules into the image.
  • Running as root.
  • Baking secrets into the image or committing .env.
  • Using latest as the only tag and losing reproducibility.
  • Rebuilding the image differently for each environment.
  • Ignoring .dockerignore and creating huge, slow builds.

Where to go next

Containers make deployment predictable, and the same skills apply whether you ship to a VM, Kubernetes or a serverless container platform. Ground the runtime in the Node.js guide, manage dependencies with npm, serve it over HTTP and harden it with Web Security. Then containerise one small app and deploy it end to end.

Building the image

A multi-stage build keeps build tools out of the final image, which makes it smaller and reduces the attack surface.

Prefer
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
Avoid
FROM node:22
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
# ships dev deps, source
# and build tools

Running as a user

Run the container as a non-root user so a compromise cannot easily escalate on the host.

Prefer
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
Avoid
FROM node:22-alpine
WORKDIR /app
COPY . .
# runs as root by default
CMD ["node", "server.js"]

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Docker & Deployment?

Our interactive tutorial walks you through Docker & Deployment step by step — with quizzes and real code you can run in the browser.