Roberto Tomé

ROBERTO TOMÉ

Docker 101: Complete Guide to Deploying Your React App
Tutorials

Docker 101: Complete Guide to Deploying Your React App

Docker 101: Complete Guide to Deploying Your React App

Overview

In this tutorial I will guide you through containerizing and deploying your React application using Docker. Whether you’re new to Docker or containerization, you’ll learn everything needed to successfully deploy your React app in a production-ready environment.
 

What is Docker and Why Use It?

Docker is a containerization platform that packages applications with all their dependencies into lightweight, portable containers. For React developers, Docker solves the “it works on my machine” problem by ensuring consistent environments across development, testing, and production.

Key Benefits for React Apps:

  • Consistency: Same environment everywhere
  • Portability: Deploy anywhere Docker runs
  • Isolation: Clean, separated environments
  • Scalability: Easy horizontal scaling
  • Version Control: Immutable infrastructure
     

Prerequisites and Setup

Before starting, ensure you have:

  • Docker Desktop installed on your system
  • Node.js and npm for React development
  • Basic command line knowledge
  • A React project (we’ll create one if needed)

Verify Docker Installation:

docker --version

This should display your Docker version, confirming successful installation.
 

Project Structure and Setup

Creating Your React Application

First, let’s create a new React application using Create React App:

npm create vite@latest my-react-docker-app -- --template react
cd my-react-docker-app

Your project structure should look like this:

my-react-docker-app/
├── public/
├── src/
├── package.json
├── package-lock.json
├── Dockerfile (we'll create this)
├── .dockerignore (we'll create this)
└── nginx.conf (for production)

Understanding the Development vs Production Approach

There are two main approaches to dockerizing React apps:

  1. Development Container: Runs npm start with hot reload
  2. Production Container: Builds static files and serves with Nginx

We’ll cover both approaches, focusing primarily on production deployment.
 

Creating Your First Dockerfile

Development Dockerfile

Create a Dockerfile in your project root:

# Development Dockerfile
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy source code
COPY . .

# Expose port 3000
EXPOSE 3000

# Start development server
CMD ["npm", "start"]

Explanation of Each Step:

  • FROM node:18-alpine: Uses lightweight Alpine Linux with Node.js 18
  • WORKDIR /app: Sets working directory inside container
  • COPY package*.json ./: Copies package files for dependency installation
  • RUN npm install: Installs project dependencies
  • COPY . .: Copies all source code
  • EXPOSE 3000: Documents that app uses port 3000
  • CMD ["npm", "start"]: Command to run when container starts

Production Dockerfile with Multi-Stage Build

For production, we’ll use a multi-stage build approach that creates smaller, optimized images:

# Multi-stage production Dockerfile

# Stage 1: Build the React application
FROM node:18-alpine AS build

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Stage 2: Serve with Nginx
FROM nginx:alpine

# Copy built files from build stage
COPY --from=build /app/build /usr/share/nginx/html

# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Expose port 80
EXPOSE 80

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

Why Multi-Stage Builds?

  • Smaller Images: Final image only contains production assets
  • Better Security: No build tools in production image
  • Improved Performance: Faster deployments and reduced attack surface
  • Clean Separation: Build environment separate from runtime
     

Understanding .dockerignore

Create a .dockerignore file to exclude unnecessary files from your Docker build context:

# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Production build
build
dist

# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# IDE and editor files
.vscode
.idea
*.swp
*.swo

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Git
.git
.gitignore

# Docker files
Dockerfile
.dockerignore
docker-compose.yml

# Testing
coverage
.nyc_output

# Logs
logs
*.log

Benefits of .dockerignore:

  • Reduces build context size
  • Faster build times
  • Prevents sensitive files from entering image
  • Cleaner final images
     

Nginx Configuration for Production

Create an nginx.conf file for serving your React application:

server {
    listen 80;
    server_name localhost;
    
    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    
    # Handle client-side routing
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    
    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
}

Key Features:

  • Client-side routing support: try_files directive handles React Router
  • Static asset caching: Optimizes performance
  • Security headers: Basic security hardening
  • Gzip compression: Reduces bandwidth usage
     

Building and Running Your Docker Container

Building the Development Image

# Build development image
docker build -t my-react-app:dev .

# Run development container
docker run -p 3000:3000 my-react-app:dev

Building the Production Image

# Build production image
docker build -t my-react-app:prod .

# Run production container
docker run -p 8080:80 my-react-app:prod

Understanding the Commands:

  • -t my-react-app:prod: Tags the image with name and version
  • -p 8080:80: Maps host port 8080 to container port 80
  • .: Uses current directory as build context

Verifying Your Deployment

After running the container, visit:

  • Development: http://localhost:3000
  • Production: http://localhost:8080

You should see your React application running successfully.
 

Docker Compose for Multi-Container Setup

For more complex applications, use Docker Compose to manage multiple services:

Create docker-compose.yml:

version: '3.8'

services:
  frontend:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    environment:
      - NODE_ENV=production
    
  # Example: Add a backend service
  backend:
    image: node:18-alpine
    working_dir: /app
    command: ["node", "server.js"]
    ports:
      - "5000:5000"
    volumes:
      - ./backend:/app

Running with Docker Compose:

# Start all services
docker-compose up

# Start in detached mode
docker-compose up -d

# Stop all services
docker-compose down


 

Common Pitfalls and How to Avoid Them

1. Large Image Sizes

Problem: Including unnecessary files and dependencies

Solution:

  • Use multi-stage builds
  • Choose appropriate base images (Alpine variants)
  • Properly configure .dockerignore
  • Remove build dependencies in production

2. Security Vulnerabilities

Problem: Running containers as root, hardcoded secrets

Solution:

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# Switch to non-root user
USER nextjs

3. Inefficient Layer Caching

Problem: Poor Dockerfile structure causing cache misses

Solution:

# Good: Copy package files first
COPY package*.json ./
RUN npm install

# Then copy source code
COPY . .

4. Environment Variable Management

Problem: Hardcoded configurations

Solution:

# Use environment variables
ENV REACT_APP_API_URL=https://api.example.com
ENV NODE_ENV=production

5. Port Conflicts and Network Issues

Problem: Port already in use or networking misconfiguration

Solution:

  • Check available ports: netstat -an | grep :3000
  • Use different port mappings: -p 3001:3000
  • Verify container networking
     

Best Practices and Security Guidelines

Image Optimization

  1. Use Multi-Stage Builds: Separate build and runtime environments
  2. Choose Minimal Base Images: Prefer Alpine variants
  3. Layer Optimization: Group related commands to minimize layers
  4. Regular Updates: Keep base images and dependencies current

Security Best Practices

  1. Non-Root Users: Always run containers as non-privileged users
  2. Secret Management: Use Docker secrets or environment variables
  3. Image Scanning: Regularly scan for vulnerabilities
  4. Resource Limits: Set CPU and memory constraints
# Security example
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs && \
    adduser -S reactuser -u 1001 -G nodejs
USER reactuser

Performance Optimization

  1. Efficient Caching: Structure Dockerfile for optimal layer caching
  2. Minimal Dependencies: Only install required packages
  3. Health Checks: Implement container health monitoring
# Health check example
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost/ || exit 1


 

Troubleshooting Common Issues

Build Failures

Issue: “COPY failed: no such file or directory”

Solution: Verify file paths and ensure files exist in build context

Container Won’t Start

Issue: Container exits immediately

Solution:

# Check container logs
docker logs <container-id>

# Run container interactively
docker run -it my-react-app:prod sh

Port Binding Issues

Issue: “Port already in use”

Solution:

# Find process using port
lsof -i :3000

# Use different port
docker run -p 3001:3000 my-react-app:dev

Permission Denied Errors

Issue: File permission problems

Solution: Check file permissions and Docker Desktop settings
 

Advanced Topics

Environment-Specific Builds

Create different Dockerfiles for different environments:

# Dockerfile.development
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
# Dockerfile.production  
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Container Orchestration

For production deployments, consider:

  • Kubernetes: Enterprise-grade orchestration
  • Docker Swarm: Simpler alternative to Kubernetes
  • Cloud Services: AWS ECS, Azure Container Instances, Google Cloud Run
     

Final Working Deployment

Your complete setup should include:

  1. Dockerfile (production-ready with multi-stage build)
  2. docker-compose.yml (for local development)
  3. .dockerignore (optimized for React projects)
  4. nginx.conf (production web server configuration)

Final Build and Deploy Commands:

# Build production image
docker build -t my-react-app:latest .

# Run production container
docker run -d -p 8080:80 --name react-app my-react-app:latest

# Verify deployment
curl http://localhost:8080

Your React application is now successfully containerized and running in Docker!

This containerized approach ensures your React application will run consistently across any environment that supports Docker, making deployments reliable and scalable.

Tags:

Software Development React Docker Deployment

Share this post:

Docker 101: Complete Guide to Deploying Your React App