Docker for Frontend Developers - A Practical Introduction

Because “Works on My Machine” Isn’t a Deployment Strategy

You don’t need to be a DevOps wizard to use Docker. Let’s containerize your frontend app and never say “works on my machine” again.

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.