How to Use Docker for Local Development (Complete Guide 2026)
The "works on my machine" problem has killed more sprint demos than any bug ever could. Docker eliminates it by packaging your entire runtime environment — runtime, dependencies, config — into a container that runs identically on every developer's laptop and in production.
Setting Up Docker and Your First Container

Installing Docker Desktop and Lightweight Alternatives
Docker Desktop is the default choice for Windows and macOS. Download it from the official Docker docs and follow the installer. On Linux, install the Docker Engine directly — Desktop on Linux adds overhead you don't need.
If Docker Desktop feels heavy (it uses a full Linux VM under the hood), consider these alternatives:
- OrbStack — fastest option on Apple Silicon, minimal resource footprint
- Colima — CLI-only, great for developers who live in the terminal
- Podman Desktop — daemonless, rootless containers, open source
Verify your install works:
docker run hello-world
# Hello from Docker!
# This message shows that your installation appears to be working correctly.
Running Your First Development Container
Pull a Node.js image and run it interactively. The -it flag gives you a shell inside the container. The -d flag runs it detached in the background.
# Interactive — you get a shell
docker run -it -p 3000:3000 node:20-alpine sh
# Detached — runs in background
docker run -d -p 3000:3000 --name myapp node:20-alpine
# Jump into a running container's shell
docker exec -it myapp sh
The -p 8000:3000 flag maps port 8000 on your host to port 3000 inside the container. Your app thinks it's on 3000; your browser hits 8000. Keep this mental model clear — port confusion is one of the most common beginner stumbling blocks.
Understanding Container Lifecycle and Cleanup
Containers accumulate fast. Dangling images and stopped containers will eat your disk within weeks.
# See running containers
docker ps
# See all containers including stopped
docker ps -a
# Check logs
docker logs myapp
# Stop and remove a container
docker stop myapp && docker rm myapp
# Nuclear option — remove everything unused
docker system prune -a --volumes
Run docker system prune weekly. Skipping this is how developers end up with 40GB of Docker cruft.
Volume Mounting and Live Code Reload
Bind Mounts vs. Named Volumes vs. tmpfs
This is where most Docker tutorials go vague. Here's the concrete breakdown:
- Bind mounts — map a host directory directly into the container. Use this for your source code during development.
- Named volumes — managed by Docker, persist across container restarts. Use this for database data.
- tmpfs — stored in host memory only. Use for temporary files where speed matters and persistence doesn't.
# Bind mount — your code, live
docker run -v $(pwd):/app node:20-alpine
# Named volume — database data survives restarts
docker run -v postgres_data:/var/lib/postgresql/data postgres:16
# tmpfs — fast, ephemeral scratch space
docker run --mount type=tmpfs,destination=/tmp node:20-alpine
Setting Up Live Code Reload
The wrong way: rebuild the image every time you change a file.
# BAD — your code is baked into the image layer
COPY . /app
CMD ["node", "server.js"]
The right way: mount your code as a volume and use a file watcher.
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
# No COPY . — code comes from the volume mount
CMD ["npx", "nodemon", "server.js"]
docker run -d \
-v $(pwd):/app \
-v /app/node_modules \
-p 3000:3000 \
myapp-dev
The second -v /app/node_modules line is critical — it creates an anonymous volume for node_modules so your host machine's version doesn't overwrite the container's version.
Cross-Platform Gotchas and .dockerignore
On Windows, use WSL2 and keep your project files inside the WSL2 filesystem (/home/youruser/), not on the Windows drive (/mnt/c/). Bind mounts from /mnt/c are 10x slower.
Always create a .dockerignore to keep build context small and prevent your host's node_modules or Python cache from leaking in:
node_modules
__pycache__
.git
.env
*.log
dist
.DS_Store
Multi-Container Development with Docker Compose

Writing Your First docker-compose.yml
Real apps aren't a single container. They're a web server, a database, maybe a cache, possibly a queue. Docker Compose defines all of it in one file.
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://postgres:password@db:5432/devdb
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: devdb
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
Notice depends_on with condition: service_healthy. Without this, your app starts before the database is ready and you get connection errors. This is one of the most common Docker Compose bugs in beginner setups.
Managing Database Persistence and Dev Data
The named volume postgres_data keeps your database alive across docker-compose down. When you need a clean slate:
# Stop containers AND delete volumes — full reset
docker-compose down -v
# Just stop, keep data
docker-compose down
Seed your database automatically by dropping SQL files into /docker-entrypoint-initdb.d/. These run once on first container creation.
Team Onboarding and Environment Parity
Commit docker-compose.yml to version control. Never commit .env. Commit a .env.example instead.
#!/bin/bash
# scripts/setup.sh
cp .env.example .env
docker-compose pull
docker-compose up --build -d
echo "Stack is up at http://localhost:3000"
New developer joins the team? One command. No 45-minute onboarding doc. That's the entire value proposition of committing your Compose file.
Debugging, Performance, and Troubleshooting

Attaching IDE Debuggers to Containers
For Node.js, expose the inspector port and connect VS Code to it:
# In your dev Dockerfile
CMD ["node", "--inspect=0.0.0.0:9229", "server.js"]
// .vscode/launch.json
{
"type": "node",
"request": "attach",
"name": "Docker: Attach to Node",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app",
"restart": true
}
Map port 9229 in your Compose file, set your breakpoints, and VS Code connects directly to the process running inside the container. Full breakpoints, variable inspection, call stack — no compromises.
Performance Tuning by Platform
Set resource limits in Compose so one runaway container doesn't starve your whole machine:
services:
app:
deploy:
resources:
limits:
cpus: "1.5"
memory: 512M
Optimize your Dockerfile build cache by ordering instructions from least to most frequently changed. Dependencies before source code:
# RIGHT — cache-friendly layer order
COPY package*.json ./
RUN npm ci
COPY . .
# WRONG — busts cache on every code change
COPY . .
RUN npm ci
Monitor resource usage in real time with docker stats. If a container is pegging CPU, that's where you start investigating.
Troubleshooting Common Issues
| Error | Cause | Fix |
|---|---|---|
Bind for 0.0.0.0:5432 failed: port is already allocated |
Host port already in use | Change host port mapping or stop conflicting process |
ECONNREFUSED db:5432 |
DB not ready when app starts | Add healthcheck + depends_on: condition: service_healthy |
| Permission denied on volume files | UID mismatch host vs container | Add user: "1000:1000" to service or fix with chown in Dockerfile |
| Code changes not reflecting | Volume not mounted, or watcher not running | Verify -v flag, check nodemon/watchdog is in CMD |
| Disk full errors | Accumulated images and volumes | docker system prune -a --volumes |
For deeper reading on container networking internals, the Docker networking documentation covers bridge, host, and overlay modes in detail. Also check our post on moving Docker Compose configs to production when you're ready to go beyond local dev.
Frequently Asked Questions
Q: Should I use Docker for every local project?
A: No. A simple personal script or single-language project with no external dependencies doesn't benefit from Docker. The overhead is worth it when you have multiple services, a team working on the same codebase, or environment-sensitive dependencies like native libraries and specific runtime versions.
Q: Why is Docker so slow on my Mac?
A: Bind mounts on macOS go through a file system translation layer that adds latency. Try OrbStack instead of Docker Desktop — it uses a lighter VM and is significantly faster for bind mounts. Also make sure your project files aren't inside the macOS iCloud Drive folder.
Q: What's the difference between docker-compose up and docker-compose up --build?
A: up uses cached images if they exist. up --build forces a rebuild from the Dockerfile before starting. Use --build after changing your Dockerfile or installing new dependencies, otherwise your running container won't pick up the changes.
Wrap-up
Docker local development comes down to three things: containers give you environment parity, volumes give you live code reload, and Compose gives you the full stack in one command. Master those three and onboarding friction, environment bugs, and "it worked yesterday" conversations disappear. Start by converting one existing project to a Compose setup this week — even a simple two-service app will make the patterns click immediately. For scaling these patterns across services, see our guide on Docker workflows for microservices teams.
References
- How to Create a Local Development Environment with Docker Compose | DevCube
- How is local development done in docker - Reddit
- How To Use Docker To Make Local Development A Breeze - YouTube
- Local Development with Docker Compose | Heroku Dev Center
- Setting Up a Self-Contained Development Environment Using ...
- 9 Common Dockerfile Mistakes - Runnablog
Comments
Post a Comment