Docker for Local Development: It's Not as Scary as You Think
Docker has a reputation for being complicated. It’s not. Well, it can be, but for local development you only need like 10% of its features. Let’s learn just that 10%.
Why Docker for Local Dev?
The problem:
- “Works on my machine” syndrome
- Complex setup instructions
- Conflicting versions of tools
- Different OS behaviors
The solution:
- Consistent environment for everyone
- One command to start everything
- No polluting your local machine
- Easy to reset/rebuild
The Basics You Need
Docker Concepts (Simplified)
Image: The blueprint (like a class) Container: A running instance (like an object) Dockerfile: Instructions to build an image docker-compose.yml: Config for multiple containers
That’s it. That’s all you need to know to start.
Your First Dockerfile
Let’s say you have a Node.js app:
# Start from Node image
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy app files
COPY . .
# Expose port
EXPOSE 3000
# Start app
CMD ["npm", "start"]
Build it:
docker build -t my-app .
Run it:
docker run -p 3000:3000 my-app
Your app is now running in a container!
Docker Compose: The Real MVP
For most projects, you need multiple services (app, database, cache). Docker Compose handles this:
docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres-data:
Start everything:
docker-compose up
Boom. App, database, and Redis all running with one command.
The Commands I Use Daily
# Start services
docker-compose up # Run in foreground
docker-compose up -d # Run in background
# Stop services
docker-compose down # Stop and remove containers
docker-compose down -v # Also remove volumes (fresh start)
# View logs
docker-compose logs # All services
docker-compose logs app # Specific service
docker-compose logs -f # Follow logs
# Run commands in container
docker-compose exec app npm install
docker-compose exec db psql -U user mydb
# Rebuild after changes
docker-compose build
docker-compose up --build # Build and start
That’s 90% of what I use.
Hot Reload / File Watching
Mount your code as a volume:
services:
app:
volumes:
- .:/app # Mount current directory
- /app/node_modules # Don't mount node_modules
Now changes to your code instantly reflect in the container!
Environment Variables
Option 1: In docker-compose.yml
environment:
- NODE_ENV=development
- API_KEY=secret123
Option 2: .env file
NODE_ENV=development
API_KEY=secret123
env_file:
- .env
Docker Compose automatically reads .env files.
Common Patterns
Pattern 1: Database Migrations
services:
migrate:
build: .
command: npm run migrate
depends_on:
- db
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
Run with:
docker-compose run migrate
Pattern 2: Multiple Environments
docker-compose.override.yml (auto-loaded):
# Development overrides
services:
app:
build:
target: development
volumes:
- .:/app
docker-compose.prod.yml:
# Production config
services:
app:
build:
target: production
restart: always
Use with:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up
Pattern 3: Running Tests
services:
test:
build: .
command: npm test
environment:
- NODE_ENV=test
depends_on:
- db
docker-compose run test
Debugging Inside Containers
Method 1: Shell access
docker-compose exec app sh
# Now you're inside the container
Method 2: VS Code Remote Container
Install “Remote - Containers” extension, then attach to running container. Full debugging support!
Common Issues & Solutions
Issue: “Port already in use”
Something’s running on that port locally.
# Find and kill it
lsof -ti:3000 | xargs kill
# Or change the port in docker-compose.yml
ports:
- "3001:3000" # Map to different local port
Issue: “Changes not reflecting”
Check your volumes are mounted correctly and your app watches for changes.
For Node:
"scripts": {
"start": "nodemon app.js" // Use nodemon
}
Issue: “Container keeps restarting”
Check logs:
docker-compose logs app
Usually it’s a missing environment variable or failed database connection.
Issue: “Out of disk space”
Docker accumulates images and volumes. Clean up:
docker system prune -a # Remove everything unused
docker volume prune # Remove unused volumes
Multi-Stage Builds (Bonus)
Make your production images smaller:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm install --production
CMD ["node", "dist/index.js"]
Development uses the full builder stage, production only copies the built files.
My .dockerignore
node_modules
npm-debug.log
.git
.env
.DS_Store
dist
coverage
Like .gitignore but for Docker. Keeps images small and builds fast.
When NOT to Use Docker
- Simple scripts that don’t need dependencies
- Projects with a single language and no external services
- When the setup is already simple
Don’t Docker-ize everything just because you can.
Tools I Use
- Lazydocker - TUI for managing containers
- Docker Desktop - GUI for Mac/Windows
- VS Code Remote Containers - Develop inside containers
Conclusion
Docker isn’t magic, and you don’t need to understand everything to use it effectively. Start simple:
- Write a Dockerfile
- Create a docker-compose.yml
- Run
docker-compose up
The rest you’ll learn as you need it.
Now go containerize something! 🐳