⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

Docker for Frontend Developers: A Practical Guide

June 15, 2022
dockerdevopsfrontenddeployment
Docker for Frontend Developers: A Practical Guide

Docker for Frontend Developers: A Practical Guide

Docker isn't just for backend developers. As a frontend developer, understanding Docker can revolutionize your workflow, ensure consistency across environments, and simplify deployments.

Why Docker for Frontend?

  • Consistent environments - Same setup across all machines
  • Easy onboarding - New developers get started quickly
  • Isolated dependencies - No more "works on my machine"
  • Simplified deployments - Ship containers, not configurations
  • CI/CD integration - Automated testing and deployment

Docker Basics

Key Concepts

Image    - A blueprint for containers (like a class)
Container - A running instance of an image (like an object)
Dockerfile - Instructions to build an image
Registry - Storage for images (Docker Hub, GitHub Registry)

Common Commands

# Build an image
docker build -t my-app .

# Run a container
docker run -p 3000:3000 my-app

# List running containers
docker ps

# Stop a container
docker stop container_id

# Remove a container
docker rm container_id

# List images
docker images

# Remove an image
docker rmi image_id

# View logs
docker logs container_id

# Execute command in container
docker exec -it container_id sh

Dockerizing a React Application

Basic Dockerfile

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

Multi-stage Build for Production

# Dockerfile
# Stage 1: Build
FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Stage 2: Production
FROM nginx:alpine

COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Nginx Configuration

# nginx.conf
events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    location / {
      try_files $uri $uri/ /index.html;
    }

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
      expires 1y;
      add_header Cache-Control "public, immutable";
    }

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
  }
}

Dockerizing a Next.js Application

Development Dockerfile

# Dockerfile.dev
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev"]

Production Dockerfile

# Dockerfile
FROM node:18-alpine AS deps

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

# Builder
FROM node:18-alpine AS builder

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run build

# Runner
FROM node:18-alpine AS runner

WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

Next.js Configuration for Standalone

// next.config.js
module.exports = {
  output: 'standalone',
}

Docker Compose for Development

Basic Docker Compose

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - CHOKIDAR_USEPOLLING=true

Full Stack Development Environment

# docker-compose.yml
version: '3.8'

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    environment:
      - API_URL=http://api:4000
    depends_on:
      - api

  api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    ports:
      - "4000:4000"
    volumes:
      - ./api:/app
      - /app/node_modules
    environment:
      - DATABASE_URL=postgres://user:password@db:5432/mydb
    depends_on:
      - db

  db:
    image: postgres:14-alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Running with Docker Compose

# Start all services
docker-compose up

# Start in background
docker-compose up -d

# Stop all services
docker-compose down

# Rebuild and start
docker-compose up --build

# View logs
docker-compose logs -f

# Scale a service
docker-compose up --scale api=3

Environment Variables

Using .env Files

# .env
REACT_APP_API_URL=https://api.example.com
REACT_APP_ENV=production
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    env_file:
      - .env

Passing Build Arguments

# Dockerfile
ARG API_URL
ENV REACT_APP_API_URL=$API_URL

RUN npm run build
# docker-compose.yml
services:
  app:
    build:
      context: .
      args:
        - API_URL=https://api.example.com

Optimizing Docker Images

Use Smaller Base Images

# Instead of node:18 (900MB+)
FROM node:18-alpine (170MB)

# Even smaller for production
FROM nginx:alpine (50MB)

Minimize Layers

# Bad - Multiple layers
RUN npm install
RUN npm run build
RUN npm run test

# Good - Single layer
RUN npm install && \
    npm run build && \
    npm run test

Leverage Cache

# Order matters for caching
# Put frequently changing files last

# Good - Dependencies cached unless package.json changes
COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

Multi-stage Builds

# Build dependencies are discarded
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

# Final image only has production files
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
# Image size: ~25MB instead of ~1GB

.dockerignore

# .dockerignore
node_modules
npm-debug.log
build
.git
.gitignore
.env.local
.env.development.local
.env.test.local
.env.production.local
README.md
.DS_Store
coverage
.idea

Health Checks

# Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# docker-compose.yml
services:
  app:
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

CI/CD Integration

GitHub Actions Example

# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: myorg/my-app:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Common Issues and Solutions

Hot Reload Not Working

# docker-compose.yml
services:
  app:
    environment:
      - CHOKIDAR_USEPOLLING=true
      - WATCHPACK_POLLING=true

Permission Issues

# Fix npm permission issues
RUN mkdir -p /app/node_modules && chown -R node:node /app
USER node

Memory Issues in Node

# Increase Node memory limit
ENV NODE_OPTIONS="--max-old-space-size=4096"

Network Issues

# Access host from container
# On Linux: host.docker.internal not available by default
docker run --add-host=host.docker.internal:host-gateway my-app

Best Practices

1. Use Specific Versions

# Bad
FROM node:latest

# Good
FROM node:18.17.0-alpine

2. Run as Non-root User

FROM node:18-alpine

RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

WORKDIR /app
COPY --chown=appuser:appgroup . .

USER appuser

CMD ["npm", "start"]

3. Scan for Vulnerabilities

# Scan image for vulnerabilities
docker scout cves my-app:latest

# Using Trivy
trivy image my-app:latest

4. Use Docker Compose Override

# docker-compose.yml - Base configuration
services:
  app:
    image: my-app

# docker-compose.override.yml - Local overrides (not committed)
services:
  app:
    volumes:
      - .:/app
    environment:
      - DEBUG=true

Debugging Containers

# Access container shell
docker exec -it container_id sh

# View container details
docker inspect container_id

# Follow logs in real-time
docker logs -f container_id

# Copy files from container
docker cp container_id:/app/logs ./logs

# Check resource usage
docker stats

Conclusion

Docker provides frontend developers with powerful tools for creating consistent, reproducible environments. By mastering these Docker concepts and patterns, you can eliminate environment-related issues and focus on building great user interfaces.

Quick Reference

  • Use multi-stage builds for smaller production images
  • Leverage Docker Compose for local development
  • Implement health checks for reliability
  • Follow security best practices
  • Optimize for caching in CI/CD pipelines

Start containerizing your frontend applications today and experience the benefits of consistent, reliable deployments!

Share:

💬 Comments