The Problem Docker Solves
Your React app works on your laptop but crashes on staging. Different Node versions, environment variables, and OS configurations cause chaos.
Docker ensures your app runs the same everywhere by packaging your code, runtime, dependencies, and environment together.
Your First Dockerfile
Create a Dockerfile in your project root:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Build and run it:
docker build -t my-react-app .
docker run -p 3000:3000 my-react-app
Layer Caching: Build 10x Faster
Docker builds in layers. Each layer is cached. This matters:
# ❌ Bad: Reinstalls deps every code change
COPY . .
RUN npm install
# ✅ Good: Only reinstalls when package.json changes
COPY package*.json ./
RUN npm install
COPY . .
Multi-Stage Builds for Production
Build in Node, serve with Nginx:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Result: ~23MB production image vs ~450MB dev image.
Environment Variables
Use at runtime, not in Dockerfile:
docker run -p 3000:3000 \
-e API_URL=https://api.example.com \
my-react-app
Or with a file:
docker run -p 3000:3000 --env-file .env.production my-react-app
Docker Compose for Multiple Services
Define frontend, backend, and database in one file:
version: '3.8'
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
- API_URL=http://backend:5000
backend:
build: ./backend
ports:
- "5000:5000"
db:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=pass
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Start everything: docker-compose up
Hot Reload in Development
Mount your local code:
frontend:
build: ./frontend
ports:
- "3000:3000"
volumes:
- ./frontend:/app
- /app/node_modules
environment:
- CHOKIDAR_USEPOLLING=true
Changes trigger hot reload instantly.
Essential Commands
docker ps # List containers
docker logs <container-id> # View logs
docker exec -it <id> sh # Shell into container
docker build -t app . # Build image
docker system prune -a # Clean up everything
Production Checklist
- Use multi-stage builds (smaller images)
- Run as non-root user
- Don’t include dev dependencies
- Use specific image tags (not
latest) - Set resource limits
- Add health checks
- Use environment variables for config
Common Issues
Changes not reflecting? Make sure you’re mounting volumes in dev mode.
Port already in use? Use different port: docker run -p 3001:3000 app
Permission errors? Run as non-root user in Dockerfile.
Slow builds on Mac/Windows? Use named volumes for node_modules.
.dockerignore
Like .gitignore:
node_modules
.git
.env
dist
build
*.md
Docker isn’t magic—it’s just consistency. Start simple, iterate, and you’ll wonder how you ever deployed without it.
No more “works on my machine.” Now it works everywhere.