DockerContainerization with Docker
"It works on my machine" is one of the most expensive phrases in software engineering. You install Node 18, a colleague has Node 14. Your app depends on a specific version of a library that behaves differently on different OS versions. You spend days debugging a bug that only exists in production because the environments are subtly different.
Docker solves this by packaging your application and ALL of its dependencies — the exact Node version, exact library versions, exact OS utilities — into a single, portable unit called a container. The container runs identically on your MacBook, on your teammate's Windows machine, and on a Linux production server.
Containers vs Virtual Machines
You might think "that sounds like a virtual machine." It's similar, but fundamentally different in efficiency:
🖥️Virtual Machine (VM)Emulates an entire computer, including its own OS kernel. Takes minutes to start, uses gigabytes of RAM, GBs of disk. Heavy and slow to provision.
📦Docker ContainerShares the host OS kernel. Only packages the application and its dependencies. Starts in under a second, uses MBs of RAM. Lightweight and portable.
Core Docker Concepts
- Dockerfile: A script that describes how to build a container image. Each line is an instruction: start from this base, copy these files, run this command.
- Image: The built result of a Dockerfile — a read-only snapshot of the application and all its dependencies. Like a cake recipe baked into an actual cake.
- Container: A running instance of an image. You can run many containers from the same image simultaneously.
- Docker Hub / Registry: A repository of pre-built images.
node:20-alpine is a base image for Node.js apps. You can also push your own images here.
Writing Your First Dockerfile
# Dockerfile
# 1. Start from an official Node.js base image
# 'alpine' is a minimal Linux distribution — much smaller than the default
FROM node:20-alpine
# 2. Set the working directory inside the container
WORKDIR /app
# 3. Copy package files FIRST (before copying all source code)
# This is a caching optimization: if package.json didn't change,
# Docker reuses the cached layer and skips npm install
COPY package*.json ./
# 4. Install dependencies
# npm ci is faster and more deterministic than npm install in CI/CD environments
RUN npm ci --only=production
# 5. Copy the rest of the application source code
COPY . .
# 6. Tell Docker which port your app listens on (documentation only, doesn't expose it)
EXPOSE 3000
# 7. The command to run when the container starts
CMD ["node", "server.js"]
Build and run the image:
# Build the image, tag it with a name
docker build -t my-backend-api .
# Run the container
# -p 3000:3000 maps host port 3000 to container port 3000
# -e passes environment variables
# -d runs in detached mode (background)
docker run -p 3000:3000 -e JWT_SECRET=mysecret -d my-backend-api
# See running containers
docker ps
# View logs
docker logs
# Stop the container
docker stop
Docker Compose — Multi-Container Applications
Real applications aren't a single container. You need your API server, a PostgreSQL database, and a Redis instance all running and able to talk to each other. Docker Compose lets you define all of these services in a single docker-compose.yml file and start everything with one command.
# docker-compose.yml
version: '3.8'
services:
# ─── Your API Server ───
api:
build: . # Build from the Dockerfile in this directory
container_name: voidx-api
ports:
- "3000:3000" # Expose port 3000 to your host machine
environment:
- NODE_ENV=development
- PORT=3000
- DATABASE_URL=postgresql://postgres:secret@postgres:5432/voidxapp
- REDIS_URL=redis://redis:6379
- JWT_SECRET=your-dev-secret-here
depends_on:
postgres:
condition: service_healthy # Wait for postgres to be healthy before starting
redis:
condition: service_started
volumes:
- .:/app # Mount source code for live reload in development
- /app/node_modules # But don't overwrite the installed node_modules
command: npx nodemon server.js # Use nodemon in development
# ─── PostgreSQL Database ───
postgres:
image: postgres:16-alpine
container_name: voidx-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: voidxapp
ports:
- "5432:5432" # Expose for local database GUI tools
volumes:
- postgres_data:/var/lib/postgresql/data # Persist data across restarts
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
# ─── Redis Cache & Queue ───
redis:
image: redis:7-alpine
container_name: voidx-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
# Start all services
docker compose up
# Start in background
docker compose up -d
# View logs for all services
docker compose logs -f
# View logs for just the API
docker compose logs -f api
# Stop everything
docker compose down
# Stop and delete all data volumes (clean reset)
docker compose down -v
💡Why this is powerful: A new developer clones your repo and runs docker compose up. In under 2 minutes, they have a fully running local environment with a populated database, Redis, and your API — all identical to every other developer on the team. No "it works on my machine" ever again.
A .dockerignore File
Just like .gitignore, the .dockerignore file tells Docker which files to exclude when building the image. Always exclude these:
# .dockerignore
node_modules/
npm-debug.log
.env
.env.*
.git/
.gitignore
README.md
docker-compose*.yml
*.test.js
coverage/