Docker for Developers -- From Zero to Containerized in 20 Minutes
Docker for Developers -- From Zero to Containerized in 20 Minutes
You have heard "it works on my machine" enough times. Docker solves this by packaging your app and all its dependencies into a container that runs identically everywhere -- your laptop, your colleague's laptop, staging, production.
This is not a theory guide. By the end, you will have a working Dockerized application with a database, and you will understand every line of configuration.
Installing Docker
macOS
Download Docker Desktop from the official Docker website. Install it like any other app. Once running, you will see the whale icon in your menu bar. Verify in your terminal:
> docker --version
> docker compose version
Windows
Download Docker Desktop. During installation, enable WSL 2 backend when prompted. Restart your machine, then verify:
> docker --version
> docker compose version
Linux
> curl -fsSL https://get.docker.com | sh
> sudo usermod -aG docker $USER
Log out and back in, then verify with the same commands.
Your First Dockerfile
A Dockerfile is a recipe that tells Docker how to build your application image. Here is a real one for a Node.js app:
FROM node:20-alpine
>
WORKDIR /app
>
COPY package*.json ./
RUN npm ci --only=production
>
COPY . .
>
EXPOSE 3000
>
CMD ["node", "server.js"]
What Each Line Does
- •FROM node:20-alpine -- start with a lightweight Node.js base image. Alpine images are about 50MB vs 900MB for full images.
- •WORKDIR /app -- set the working directory inside the container. All subsequent commands run from here.
- •COPY package*.json ./ -- copy package.json and package-lock.json first. This is intentional -- Docker caches layers, so if your dependencies have not changed, it skips the npm install step.
- •RUN npm ci --only=production -- install dependencies. Using npm ci instead of npm install gives you reproducible builds.
- •COPY . . -- copy the rest of your application code.
- •EXPOSE 3000 -- document which port the app listens on. This does not actually publish the port.
- •CMD -- the command to run when the container starts.
Build and Run
> docker build -t my-app .
> docker run -p 3000:3000 my-app
Your app is now running at localhost:3000 inside a container. The -p flag maps port 3000 on your machine to port 3000 in the container.
The .dockerignore File
Just like .gitignore keeps files out of your repo, .dockerignore keeps files out of your container. Create one in your project root:
node_modules
.git
.env
npm-debug.log
Dockerfile
docker-compose.yml
.dockerignore
README.md
.DS_Store
This prevents Docker from copying unnecessary files into the image, making builds faster and images smaller.
Docker Compose -- Multi-Service Apps
Real applications rarely run alone. You need a database, maybe a cache, maybe a message queue. Docker Compose lets you define and run all of these together.
Create a docker-compose.yml file:
version: "3.9"
>
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:password@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
volumes:
- .:/app
- /app/node_modules
>
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
>
cache:
image: redis:7-alpine
ports:
- "6379:6379"
>
volumes:
pgdata:
Start Everything
> docker compose up
That single command starts your app, a PostgreSQL database, and a Redis cache. They can communicate using their service names as hostnames -- your app connects to the database at db:5432, not localhost.
Useful Compose Commands
- •docker compose up -d -- start in background (detached mode)
- •docker compose down -- stop and remove all containers
- •docker compose logs -f app -- follow logs for the app service
- •docker compose exec app sh -- open a shell inside the running app container
- •docker compose build --no-cache -- rebuild images from scratch
- •docker compose ps -- see running containers and their status
Development vs Production
Your development Dockerfile and production Dockerfile should be different. Use multi-stage builds to keep production images small:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
>
# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
The final image only contains the built output and production dependencies. Build tools, source code, and devDependencies are left behind in the builder stage.
Hot Reload in Development
For development, you want code changes to reflect immediately without rebuilding the container. Use a volume mount:
services:
app:
build: .
volumes:
- .:/app
- /app/node_modules
command: npm run dev
The volume mount syncs your local files into the container. The /app/node_modules line prevents your local node_modules from overwriting the container's.
Common Patterns
Environment Variables
Never hardcode secrets in Dockerfiles. Use environment variables:
> services:
> app:
> env_file:
> - .env
Or pass them directly:
> docker run -e API_KEY=your_key my-app
Health Checks
Add health checks so Docker knows if your app is actually working:
> HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
> CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
Networking
By default, all services in a Docker Compose file share a network. They can reach each other by service name. If you need to isolate services, create custom networks:
> networks:
> frontend:
> backend:
Debugging Docker Issues
- •Container exits immediately? Check logs with docker logs container_name. Usually the app is crashing on startup.
- •Cannot connect to database? Make sure depends_on is set, and your app retries the connection on startup. depends_on only waits for the container to start, not for the database to be ready.
- •Build is slow? Check your .dockerignore. If node_modules or .git are being copied, it will take forever.
- •Out of disk space? Run docker system prune to clean up unused images, containers, and volumes.
- •Port already in use? Another service or container is using that port. Check with docker ps or lsof -i :3000.
Essential Docker Commands Cheat Sheet
- •docker ps -- list running containers
- •docker ps -a -- list all containers including stopped ones
- •docker images -- list all images
- •docker logs -f container -- follow container logs
- •docker exec -it container sh -- open a shell in a running container
- •docker stop container -- gracefully stop a container
- •docker rm container -- remove a stopped container
- •docker rmi image -- remove an image
- •docker system prune -a -- remove all unused images, containers, networks, and volumes
What Next?
Once you are comfortable with the basics:
- •Set up CI/CD -- build and push Docker images automatically with GitHub Actions
- •Learn Docker networking -- custom networks, host networking, overlay networks
- •Explore container orchestration -- Kubernetes or Docker Swarm for running containers at scale
- •Try multi-architecture builds -- build images that run on both Intel and ARM (Apple Silicon)
Docker is one of those tools that feels like overhead at first but becomes indispensable once it is part of your workflow. Start with one project, get comfortable, and gradually Dockerize everything.
For more developer guides, check out our blog and explore our free developer tools.