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

Shipping containers and cranes at Hamburg port showcasing global trade.
Photo by Wolfgang Weiser on Pexels

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

Software developer coding on dual monitors in a well-lit modern office, focused and engaged.
Photo by Zayed Hossain on Pexels

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

Open laptop with code editor displaying, next to an orange plush toy and green plants.
Photo by Daniil Komov on Pexels

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.

Comments

Popular posts from this blog

Node.js Error Handling Best Practices 2026: Complete Guide

Python Async Await Explained With Real Examples