Docker Basics
How to containerize an application, write a Dockerfile, and run containers with Docker Compose.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
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.
Frequently Asked Questions
Q: What is the difference between a Docker image and a container? A: 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.
Q: Should I use Docker Compose in production? A: 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.
Q: How do I reduce my Docker image size?
A: Use multi-stage builds, alpine or distroless base images, and ensure your .dockerignore excludes build artifacts and dependency caches.
Is this solution production-ready?
Yes. The code examples above show tested implementations. Adapt error handling and configuration to your specific environment before deploying.
What are the performance characteristics?
Performance depends on your data volume and infrastructure. The solutions shown prioritize clarity. For high-throughput scenarios, add caching, batching, and connection pooling as needed.
How do I debug issues with this approach?
Start with the minimal example above. Add logging at each step. Test with small inputs first, then scale up. Use your language’s debugger to step through edge cases.
.dockerignore File
Prevent unnecessary files from entering the build context:
# .dockerignore
node_modules
dist
.git
.gitignore
.env
.env.local
*.log
coverage
.vscode
.idea
Dockerfile
docker-compose*.yml
README.md
Dockerfile for a Java Spring Boot App
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradle/ gradle/
COPY gradlew build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon
COPY src/ src/
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:21-jre-alpine AS runner
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
USER 1000:1000
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Dockerfile with Health Check
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
Essential Docker Commands
# Build an image
$ docker build -t myapp:latest .
# Build with a specific platform
$ docker build --platform linux/amd64 -t myapp:latest .
# Run a container
$ docker run -d --name web -p 3000:3000 myapp:latest
# Run with environment variables
$ docker run -d --name web -p 3000:3000 \
-e DATABASE_URL=postgres://user:pass@db:5432/mydb \
-e NODE_ENV=production \
myapp:latest
# Run with a volume mount
$ docker run -d --name web -p 3000:3000 \
-v $(pwd)/data:/app/data \
myapp:latest
# List running containers
$ docker ps
# List all containers (including stopped)
$ docker ps -a
# View container logs
$ docker logs -f web
# Execute a command inside a running container
$ docker exec -it web sh
# Stop and remove a container
$ docker stop web
$ docker rm web
# Remove an image
$ docker rmi myapp:latest
# Prune unused images, containers, and networks
$ docker system prune -a
# Show disk usage
$ docker system df
Docker Networking
# Create a custom network
$ docker network create mynet
# Run containers on the same network
$ docker run -d --name db --network mynet postgres:16-alpine
$ docker run -d --name app --network mynet -p 3000:3000 myapp:latest
# Containers can reach each other by name
# app can connect to db via: postgres://user:pass@db:5432/mydb
# docker-compose.yml with custom network
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
networks:
- frontend
- backend
db:
image: postgres:16-alpine
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # No external access
Docker Volumes for Persistent Data
# Create a named volume
$ docker volume create pgdata
# Inspect a volume
$ docker volume inspect pgdata
# Remove unused volumes
$ docker volume prune
# docker-compose.yml with volumes
version: "3.9"
services:
app:
build: .
volumes:
- ./src:/app/src # Bind mount for dev
- app-data:/app/data # Named volume for persistent data
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
app-data:
Debugging Containers
# Inspect a container's configuration
$ docker inspect web
# View resource usage
$ docker stats web
# View processes inside a container
$ docker top web
# Copy files from a container to host
$ docker cp web:/app/logs ./logs
# Export a container's filesystem
$ docker export web | gzip > web.tar.gz
# View container filesystem changes
$ docker diff web
# Commit a container's changes to a new image
$ docker commit web myapp:debugged
Multi-Stage Build with Distroless
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12 AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["dist/main.js"]
Distroless images have no shell, package manager, or unnecessary tools — reducing attack surface and image size.
Docker BuildKit Secrets
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
# Mount npm token as a secret, not persisted in the image
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm ci
# Build with secrets
$ docker build --secret id=npm_token,src=$HOME/.npmrc -t myapp:latest .
Additional Best Practices
- Label your images. Add metadata for traceability:
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.version="1.2.0"
LABEL org.opencontainers.image.revision="${GIT_SHA}"
- Use
dumb-initortinifor signal handling. PID 1 in containers doesn’t handle signals properly by default:
FROM node:20-alpine
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
- Pin dependency versions. In
requirements.txtorpackage.json, pin exact versions for reproducible builds:
# requirements.txt
fastapi==0.111.0
uvicorn==0.30.1
pydantic==2.7.4
- Use
docker compose profiles. Run different service sets for dev vs test:
version: "3.9"
services:
app:
build: .
profiles: ["dev", "test"]
db:
image: postgres:16-alpine
profiles: ["dev", "prod"]
test-runner:
build: ./test
profiles: ["test"]
$ docker compose --profile dev up
$ docker compose --profile test up
Additional Common Mistakes
-
Storing data in the container’s writable layer. The writable layer is lost when the container is removed. Use volumes for anything that needs to persist.
-
Not setting
restartpolicies. Containers crash and stay down without a restart policy:
services:
app:
build: .
restart: unless-stopped # or: always, on-failure:3
- Building images as root without
USERinstruction. Many base images default to root. Always switch:
RUN addgroup -S app && adduser -S app -G app
USER app
-
Using
latesttag in production.latestis mutable — the image you pull today may differ from yesterday. Pin to specific versions:myapp:1.2.0. -
Not cleaning up Docker resources. Old images and volumes accumulate:
# Clean up everything not currently in use
$ docker system prune -a --volumes
Additional FAQ
How do I pass build arguments to a Dockerfile?
Use ARG and --build-arg:
FROM node:20-alpine
ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV
$ docker build --build-arg NODE_ENV=staging -t myapp:staging .
How do I use Docker in CI/CD?
Use Docker layer caching to speed up builds. In GitHub Actions:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myorg/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
What is the difference between CMD and ENTRYPOINT?
CMD sets the default command, which can be overridden by docker run. ENTRYPOINT sets the executable that always runs; CMD becomes arguments to it. Use both for flexible containers:
ENTRYPOINT ["node"]
CMD ["server.js"]
# docker run myapp → node server.js
# docker run myapp worker.js → node worker.js
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 Related Resources
Git Workflow
A practical branching strategy for teams: feature branches, pull requests, and clean commit history.
RecipeCall a REST API
How to make HTTP requests to a REST API and handle the JSON response in multiple languages.
RecipeEnvironment Variables
How to read, set, and manage environment variables securely across Python, JavaScript, and Java.
RecipeImmutable Infrastructure
Build immutable infrastructure with versioned machine images and containers to eliminate configuration drift and ensure reproducible deployments.
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.
RecipeCron Jobs
How to schedule and manage recurring tasks using cron syntax across Linux, Python, and Node.js.
RecipeGitHub Actions CI/CD
How to build and deploy with GitHub Actions using workflows, matrices, caching, and secrets.