Docker Basics
How to containerize an application, write a Dockerfile, and run containers with Docker Compose.
Overview
Docker packages your application and its dependencies into a lightweight, portable container that runs consistently across development, staging, and production. A Dockerfile is a recipe for building that container image, and Docker Compose lets you run multi-container setups with a single command.
Here is how to the essential Dockerfile instructions, image layering, and a practical Docker Compose example for a web application with a database.
When to Use
Use this recipe when:
- You want to eliminate “works on my machine” problems. See Environment Variables for managing container configuration.
- Setting up a local development environment that mirrors production. See Docker Compose Local Dev for multi-service setups.
- Preparing an application for deployment to Kubernetes, AWS ECS, or similar platforms. See Serverless Functions for function-as-a-service deployments.
- Running integration tests that depend on databases, caches, or message brokers. See Integration Testing for test isolation strategies.
Solution
Dockerfile for a Node.js App
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
USER node
CMD ["node", "dist/main.js"]
Dockerfile for a Python App
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Docker Compose (Web + Database)
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
pgdata:
Run: docker compose up --build
Explanation
- Multi-stage builds (
AS builder,AS runner) keep production images small by excluding build tools, source maps, and dev dependencies. - Layer caching: Docker caches each instruction layer. Put
COPY package*.jsonandRUN npm cibeforeCOPY . .so that dependency installation is cached unless package files change. - Non-root user:
USER node(or a custom user) reduces the attack surface if the container is compromised. .dockerignore: create one to excludenode_modules,.git, and local env files — they bloat the build context and can leak secrets.
Variants
| Goal | Approach |
|---|---|
| Smallest image | Use alpine or distroless base images |
| Fastest build | Order Dockerfile instructions from least to most frequently changed |
| Secret injection | Use BuildKit secrets (--secret) or runtime env vars, never COPY secrets |
| Health checks | Add HEALTHCHECK instruction or Docker Compose healthcheck block |
What Works
- Pin base image tags:
node:20-alpineis better thannode:latestto avoid surprise breaking changes. - One process per container: let Docker manage process lifecycle; use Compose or an orchestrator for multi-process setups.
- Use volume mounts for dev: mount source code into the container for hot-reload during development.
- Scan images: run
docker scanor Trivy to detect OS and dependency vulnerabilities in your images. - Graceful shutdown: handle
SIGTERMin your application so Docker can stop containers cleanly.
Common Mistakes
- Giant images: copying unnecessary files (logs, test data,
.git) inflates image size and build time. - Running as root: default users in base images are often root. Create and switch to a non-root user.
- Hardcoding secrets: baking database passwords into the image makes them visible to anyone who pulls it.
- Ignoring
.dockerignore: without it,COPY . .sends your entire repo — including sensitive files — to the Docker daemon. - Not handling signals: apps that ignore
SIGTERMget killed withSIGKILLafter a timeout, risking data corruption.
Performance Tips
- Use BuildKit for faster builds. Enable it with
DOCKER_BUILDKIT=1ordocker buildx:
$ DOCKER_BUILDKIT=1 docker build -t myapp:latest .
- Order Dockerfile layers from least to most changing. Dependencies change rarely; source code changes often:
# Good: deps first, code last
COPY package*.json ./
RUN npm ci
COPY . .
- Use
--targetfor partial builds. Build only the stage you need:
$ docker build --target builder -t myapp:builder .
- Use
docker compose up --buildin dev. Rebuilds only changed layers:
$ docker compose up --build --watch # Rebuild on file changes (Compose v2.22+)
- Use
docker saveanddocker loadfor offline transfer. Move images between machines without a registry:
$ docker save myapp:latest | gzip > myapp.tar.gz
$ docker load < myapp.tar.gz Frequently Asked Questions
What is the difference between a Docker image and a container?
An image is a read-only template with your code and dependencies. A container is a running instance of that image. You can run many containers from the same image.
Should I use Docker Compose in production?
Docker Compose is great for single-host production deployments and local dev. For multi-host, high-availability production workloads, use Kubernetes or a managed container service.
How do I reduce my Docker image size?
Use multi-stage builds, alpine or distroless base images, and ensure your .dockerignore excludes build artifacts and dependency caches.
Related Resources
Docker Network Isolation and Inter-Container Security
Secure inter-container communication with custom Docker networks, network segmentation, and access control policies.
RecipeImmutable Infrastructure
Build immutable infrastructure with versioned machine images and containers to eliminate configuration drift and ensure reproducible deployments.
RecipeEnvironment Variables
How to read, set, and manage environment variables securely across Python, JavaScript, and Java.
RecipeGitHub Actions CI/CD
How to build and deploy with GitHub Actions using workflows, matrices, caching, and secrets.
RecipeAnsible Playbook for Server Configuration
How to write and run Ansible playbooks for provisioning, configuring, and managing servers with idempotent tasks, roles, and inventory files.
RecipeSetup SSL Certificates with Let's Encrypt
How to obtain, install, and auto-renew SSL certificates using Certbot with Nginx, Apache, and standalone modes for HTTPS-enabled deployments.