Deploying React Applications to Production: Complete Docker Setup with Traefik Reverse Proxy

Building a React application locally is straightforward. Deploying it to production servers correctly? That’s where most developers hit unexpected roadblocks. This guide documents a real deployment debugging session where everything appeared configured correctly—container running, Traefik labels set, DNS resolving—yet the application returned persistent 404 errors.

Today, we’ll walk through the complete process of building React applications locally and deploying them to production Docker servers with proper reverse proxy configuration, automatic SSL, and professional domain routing. This approach builds on our philosophy of self-hosted solutions—similar to how we’ve shown you can self-host n8n for workflow automation and build multi-tenant development stacks for complete operational control.

The Problem with Traditional React Deployments

Most React deployment tutorials skip the critical production details. You’ll find guides showing npm run build and copying files to nginx, but they rarely cover:

Configuration Conflicts:

  • Custom HTTP routers that override global redirects
  • Traefik label syntax errors that cause silent failures
  • IPv6 vs IPv4 binding issues in health checks
  • Missing service port mappings that produce 404 errors

Resource Management:

  • Full disk errors preventing container registration
  • Docker image bloat from unnecessary build artifacts
  • Inefficient caching strategies that slow deployments
  • Memory constraints affecting build performance

Production Readiness:

  • Proper SSL certificate automation
  • Zero-downtime deployment strategies
  • Health check configurations
  • Logging and monitoring integration

The result? Hours wasted debugging why a perfectly working local app returns mysterious 404 errors in production, even though “everything looks correct.”

The Tools We’re Using

Let’s understand what each piece does in our streamlined React deployment architecture:

Vite: Modern Build Tool

Vite provides lightning-fast development and optimized production builds. Unlike Create React App, Vite leverages native ES modules during development and rolls up highly optimized bundles for production. Your React app builds in seconds instead of minutes.

The key advantage? Vite automatically handles code splitting, tree shaking, and asset optimization. You get production-ready builds without complex webpack configurations.

Docker: Containerization for Consistency

Docker ensures your React app runs identically in development and production. The same nginx container serving your app locally will behave exactly the same on your production server—eliminating the classic “works on my machine” problem.

Think of Docker as packaging your entire application environment (React build files, nginx configuration, and runtime) into a portable container that works anywhere.

Traefik: Intelligent Reverse Proxy

Traefik acts as an intelligent traffic director, automatically routing requests to the correct containerized applications based on domain names. Instead of manually configuring complex nginx or Apache rules for every new application, Traefik reads labels from your Docker containers and sets up routing automatically.

In our multi-tenant Docker setup, we demonstrated Traefik’s power for managing multiple client environments. The same principles apply here for managing multiple React applications on a single server.

The beauty is Traefik handles SSL termination through Let’s Encrypt automatically, provides automatic service discovery, and offers detailed monitoring—all with minimal configuration.

nginx: Production Web Server

nginx serves your static React build files with exceptional performance. It’s the de facto standard for serving static content in production, handling thousands of concurrent connections efficiently while using minimal resources.

Understanding the Deployment Flow

Here’s the complete journey from local development to production:

  1. Local Development: Build and test your React app with npm run dev
  2. Production Build: Create optimized static files with npm run build
  3. Containerization: Package build files into nginx Docker container
  4. Server Deployment: Upload and start container on production server
  5. Traefik Registration: Automatic routing and SSL certificate provisioning
  6. Health Monitoring: Continuous health checks ensure availability

What makes this powerful is the automation. Once configured correctly, you can deploy updates in under 60 seconds with a single command.

Setting Up Your React Application

Project Structure for Production

Organize your React project with deployment in mind:

my-react-app/
├── src/                    # React source code
├── public/                 # Static assets
├── dist/                   # Build output (auto-generated)
├── package.json           # Dependencies
├── vite.config.ts         # Vite configuration
├── Dockerfile             # Container definition
├── nginx.conf             # nginx configuration
└── docker-compose.yml     # Deployment definition

Optimizing Your Vite Configuration

Create vite.config.ts with production-optimized settings:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    sourcemap: false, // Disable in production for security
    minify: 'terser',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
  server: {
    port: 3000,
    host: true, // Enable network access
  },
})

This configuration:

  • Separates vendor libraries for better caching
  • Minifies code for smaller file sizes
  • Disables source maps in production (prevents code exposure)
  • Optimizes chunk splitting for faster load times

Building for Production

Build your optimized production bundle:

# Install dependencies
npm install

# Create production build
npm run build

# Verify build output
ls -lh dist/

Your dist/ folder should contain:

  • index.html – Entry point
  • assets/ – Minified JS, CSS, and images
  • Static files from public/

Creating the Production Container

nginx Configuration for React

React applications use client-side routing, requiring special nginx configuration. Create nginx.conf:

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

    # Gzip compression for better performance
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript 
               application/x-javascript application/xml+rss 
               application/javascript application/json;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # SPA: Route all paths to index.html for client-side routing
    location / {
        try_files $uri $uri/ /index.html;
    }

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

    # No cache for HTML to ensure updates are immediate
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
    }

    # Health check endpoint for monitoring
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

The critical piece is try_files $uri $uri/ /index.html which ensures React Router works correctly in production—all routes get served the main index.html file.

Dockerfile for Production

Create an optimized Dockerfile:

FROM nginx:alpine

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

# Copy production build files
COPY dist/ /usr/share/nginx/html/

EXPOSE 80

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

This uses nginx:alpine for a minimal production image (only ~8MB) that contains everything needed to serve your React app.

Docker Compose Configuration

Create docker-compose.yml for easy deployment:

services:
  my-react-app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: my-react-app
    restart: unless-stopped
    networks:
      - proxy
    labels:
      # Enable Traefik
      - "traefik.enable=true"
      
      # Define routing rule
      - "traefik.http.routers.myapp.rule=Host(`app.yourdomain.com`)"
      - "traefik.http.routers.myapp.entrypoints=https"
      - "traefik.http.routers.myapp.tls=true"
      - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
      
      # Define service port
      - "traefik.http.services.myapp.loadbalancer.server.port=80"
      
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:80/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 5s

networks:
  proxy:
    external: true

Critical Configuration Notes:

The labels section is where many deployments fail. Notice what we DON’T include:

  • No separate HTTP router definition
  • No custom redirect middleware
  • No HTTP entrypoint configuration

Why? Because Traefik’s global configuration already handles HTTP→HTTPS redirects. Adding custom HTTP routers overrides this behavior and causes 404 errors—exactly the issue we solved in our debugging session.

Deploying to Your Production Server

Prerequisites on Your Server

Your production server needs:

Docker Environment:

# Verify Docker is installed
docker --version
docker compose --version

# Verify Traefik is running
docker ps | grep traefik

# Verify proxy network exists
docker network ls | grep proxy

If Traefik isn’t set up, refer to our multi-tenant Docker guide which covers comprehensive Traefik setup.

Sufficient Disk Space:

# Check available space
df -h /

# You need at least 2-5GB free for Docker operations

DNS Configuration:

  • Point app.yourdomain.com to your server’s IP address
  • Wait for DNS propagation (usually 5-60 minutes)

Uploading Your Application

Transfer your application to the server:

# From your local machine
scp -r my-react-app/ user@your-server:/opt/

# Or use rsync for efficient updates
rsync -avz --exclude 'node_modules' \
  my-react-app/ user@your-server:/opt/my-react-app/

Building and Starting the Container

SSH into your server and deploy:

# Navigate to application directory
cd /opt/my-react-app

# Build the Docker image
docker compose build

# Start the container
docker compose up -d

# Verify it's running
docker compose ps

Verifying the Deployment

Check that everything works:

# Test container health internally
docker compose exec my-react-app wget -q -O- http://127.0.0.1/health

# Check Traefik routing (wait 30 seconds for SSL)
curl -I https://app.yourdomain.com

# View container logs
docker compose logs -f

You should see HTTP/2 200 from the curl command, indicating success.

Common Deployment Failures and Solutions

404 Error Despite Correct Configuration

Symptom: Traefik returns HTTP/2 404 even though the container works internally.

Root Cause: Multiple router definitions for the same service without proper service port mapping, or custom HTTP routers overriding Traefik’s global redirects.

Solution:

# Remove these labels if present:
# - "traefik.http.middlewares.myapp-redirect.redirectscheme.scheme=https"
# - "traefik.http.routers.myapp-http.rule=Host(`app.yourdomain.com`)"
# - "traefik.http.routers.myapp-http.entrypoints=http"
# - "traefik.http.routers.myapp-http.middlewares=myapp-redirect"

# Keep only HTTPS router:
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.myapp.rule=Host(`app.yourdomain.com`)"
  - "traefik.http.routers.myapp.entrypoints=https"
  - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
  - "traefik.http.services.myapp.loadbalancer.server.port=80"

Traefik’s global HTTP→HTTPS redirect (configured in traefik.yml) handles HTTP traffic automatically. Custom per-service HTTP routers create conflicts.

Container Unhealthy Status

Symptom: docker compose ps shows container as “unhealthy”

Root Cause: Health check using localhost which resolves to IPv6 [::1], but nginx only listening on IPv4.

Solution:

healthcheck:
  # Use explicit IPv4 address instead of localhost
  test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:80/health"]

Docker Build Fails with “No Space Left”

Symptom: Build fails with disk space errors

Solution:

# Check disk usage
df -h /

# Clean Docker system
docker system prune -a -f

# Remove unused images
docker image prune -a -f

# Remove unused volumes
docker volume prune -f

If disk is truly full (>95%), you need to free space or expand your storage. Docker operations require temporary space for layer caching and building.

SSL Certificate Not Generating

Symptom: Curl shows self-signed certificate after 10+ minutes

Common Causes:

  1. DNS not properly pointed to server
  2. Port 80/443 not accessible from internet
  3. Let’s Encrypt rate limits hit (5 per domain per week)

Solution:

# Verify DNS resolution
dig app.yourdomain.com

# Test port accessibility
curl -I http://app.yourdomain.com

# Check Traefik logs for ACME errors
docker logs traefik | grep -i acme

# Restart Traefik if needed
docker restart traefik

React Router 404 Errors on Refresh

Symptom: App works on initial load but shows 404 when refreshing on routes like /about

Root Cause: Missing try_files directive in nginx configuration

Solution: Ensure your nginx.conf includes:

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

This tells nginx to serve index.html for all routes, letting React Router handle the routing client-side.

Container Starts But Traefik Can’t Reach It

Symptom: Container runs but Traefik returns “Service Unavailable”

Solution:

# Verify container is on correct network
docker network inspect proxy

# Check if container is listed
docker inspect my-react-app | grep -A 20 Networks

# Ensure proxy network exists
docker network create proxy

Optimizing for Production

Implementing Zero-Downtime Deployments

Update your application without downtime:

#!/bin/bash
# deploy-update.sh - Zero-downtime deployment

cd /opt/my-react-app

# Pull latest code (from git or updated files)
git pull origin main

# Build new production assets
npm install
npm run build

# Build new Docker image
docker compose build

# Start new container (old one still running)
docker compose up -d --no-deps --build my-react-app

# Traefik automatically routes to healthy container
# Old container is automatically stopped after new one is healthy

Advanced Health Checks

Implement comprehensive health monitoring:

healthcheck:
  test: |
    wget --no-verbose --tries=1 --spider http://127.0.0.1:80/health &&
    wget --no-verbose --tries=1 --spider http://127.0.0.1:80/ 
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 10s

This checks both the health endpoint AND the main application route, ensuring the entire app is responding correctly.

Performance Optimization

Fine-tune nginx for better performance:

# Add to nginx.conf
server {
    # ... existing config ...
    
    # Increase buffer sizes for large headers
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;
    
    # Enable keep-alive connections
    keepalive_timeout 65;
    keepalive_requests 100;
    
    # Optimize file serving
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
}

Resource Limits

Prevent resource exhaustion with container limits:

services:
  my-react-app:
    # ... existing config ...
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

nginx serving static React files requires minimal resources—512MB memory and half a CPU core handles thousands of concurrent users.

Why Self-Hosted Docker Deployments Matter

Self-hosting your React applications on Docker infrastructure gives you complete control over your deployment pipeline without vendor lock-in. You can deploy unlimited applications on your own infrastructure, customize every aspect of the deployment process, and integrate seamlessly with your existing self-hosted services.

This approach works particularly well when combined with our multi-tenant Docker architecture, where you can host multiple client applications on the same infrastructure with complete isolation.

Automation and CI/CD Integration

GitHub Actions Deployment

Automate deployments on every push:

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Build React app
        run: |
          npm ci
          npm run build
      
      - name: Deploy to server
        uses: appleboy/scp-action@v0.1.4
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "dist/,Dockerfile,nginx.conf,docker-compose.yml"
          target: "/opt/my-react-app"
      
      - name: Restart container
        uses: appleboy/ssh-action@v0.1.10
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/my-react-app
            docker compose up -d --build

GitLab CI/CD Pipeline

For GitLab users:

# .gitlab-ci.yml
stages:
  - build
  - deploy

build:
  stage: build
  image: node:18
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  script:
    - scp -r dist/ Dockerfile nginx.conf docker-compose.yml $SERVER_USER@$SERVER_HOST:/opt/my-react-app/
    - ssh $SERVER_USER@$SERVER_HOST "cd /opt/my-react-app && docker compose up -d --build"
  only:
    - main

Monitoring Your Production Deployment

Logging Strategy

Implement comprehensive logging:

# Add to docker-compose.yml
services:
  my-react-app:
    # ... existing config ...
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

View logs efficiently:

# Real-time logs
docker compose logs -f my-react-app

# Last 100 lines
docker compose logs --tail=100 my-react-app

# Logs from specific time
docker compose logs --since=2h my-react-app

# Filter for errors only
docker compose logs my-react-app | grep -i error

Metrics and Alerts

Monitor container health:

#!/bin/bash
# health-check.sh - Regular health monitoring

CONTAINER="my-react-app"
WEBHOOK_URL="your-notification-webhook"

STATUS=$(docker inspect --format='{{.State.Health.Status}}' $CONTAINER)

if [ "$STATUS" != "healthy" ]; then
    curl -X POST $WEBHOOK_URL \
      -H 'Content-Type: application/json' \
      -d "{\"text\":\"⚠️ Container $CONTAINER is $STATUS\"}"
fi

Run this via cron every 5 minutes for basic monitoring.

Connecting React to Backend Infrastructure

Your React app likely needs to communicate with backend services. This integrates naturally with self-hosted infrastructure. If you’re running n8n for workflow automation or Windmill for backend workflows, configure proper CORS and API routing in your nginx configuration:

# Add to nginx.conf for API proxying
location /api {
    proxy_pass http://your-backend-service:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

This works seamlessly when all services are part of the same Docker network, as demonstrated in our multi-tenant architecture guide.

Environment-Specific Builds

Different environments often need different configurations:

// vite.config.ts
export default defineConfig(({ mode }) => ({
  plugins: [react()],
  define: {
    'import.meta.env.VITE_API_URL': JSON.stringify(
      mode === 'production' 
        ? 'https://api.yourdomain.com'
        : 'http://localhost:3000'
    ),
  },
  build: {
    outDir: 'dist',
    sourcemap: mode !== 'production',
  },
}))

Build for different environments:

# Development build
npm run build -- --mode development

# Staging build  
npm run build -- --mode staging

# Production build
npm run build -- --mode production

The Real Value of Understanding This Setup

This deployment approach matters if you’re:

Managing Multiple Applications:

  • Deploy React apps alongside backend services on the same infrastructure
  • Use consistent deployment processes across all projects
  • Integrate with self-hosted tools like n8n and Windmill

Building for Clients:

  • Professional SSL-secured custom domains
  • Complete control over infrastructure and deployments
  • No platform limitations or vendor lock-in

Learning Infrastructure:

  • Understand Docker containerization fundamentals
  • Master Traefik reverse proxy configuration
  • Debug production deployment issues systematically

The setup documented here is based on a real debugging session—the problems described actually happened, and the solutions actually worked. This makes it more valuable than theoretical tutorials because you’re seeing the actual pitfalls and how to avoid them.

When combined with our multi-tenant Docker architecture, this forms a foundation for scalable, self-hosted application delivery that you fully control.

Related Resources

For more self-hosted infrastructure guides, check out these resources:

These guides demonstrate different aspects of building self-hosted infrastructure that gives you complete control while maintaining professional standards.

Get Professional Support

Setting up production-grade React deployments involves many infrastructure considerations. While we’ve provided comprehensive documentation, every project has unique requirements, existing infrastructure constraints, and specific performance needs.

If you’re implementing React deployment infrastructure for production use or need customization for your specific client delivery needs, we can help with:

  • Custom deployment pipelines tailored to your workflow
  • Integration with existing CI/CD systems
  • Performance optimization for high-traffic applications
  • Multi-region deployment strategies
  • Team training on Docker and Traefik best practices
  • Ongoing infrastructure management and monitoring

Contact us through tva.sg/contact to discuss your React deployment needs and get professional guidance on implementation.

Whether you’re scaling an existing agency, launching a new SaaS product, or building enterprise-grade client delivery capabilities, we’re here to help you succeed with self-hosted, containerized React deployments that maintain your independence while delivering professional results.

Scroll to Top