React Deployment in 2026: Immutable Builds, Traefik, and Zero-Downtime Releases
A current, production-oriented follow-up that turns vendor guidance into operating controls, migration decisions, and verifiable release criteria.
What changed in 2026
We revisited this subject because the operating boundary has moved. The durable principles remain useful, but current versions make several old shortcuts incomplete or unsafe. This follow-up starts with the vendor documentation available on 14 July 2026, separates facts from local choices, and treats every configuration change as a controlled production intervention.
Current primary sources
How we use the update
Read the sources first, record the versions actually deployed, and define the observable result before changing a setting. Test the smallest reversible intervention in a representative environment. A successful command is not the acceptance criterion: service health, data integrity, latency, security boundaries, and rollback time are. Where the earlier guide offers a useful diagnostic sequence, we retain it below as an operating foundation; version-specific examples must still be checked against the current documentation. The configuration files and commands referenced below are in the original guide.
- GitHub Actions for Self-Hosted Deployment Pipelines
- Stacking Reverse Proxies: A Production Architecture
- From React SPA to Astro: When and Why to Migrate
Operating foundation
Building a React application locally is straightforward. Deploying it correctly to production servers is 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 how to run npm run build and copy 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:
- Disk-full 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 produces 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.
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:
- Local Development: Build and test your React app with npm run dev
- Production Build: Create optimized static files with npm run build
- Containerization: Package build files into an nginx Docker container
- Server Deployment: Upload and start the container on the production server
- Traefik Registration: Automatic routing and SSL certificate provisioning
- 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
├── package-lock.json # Locked dependency versions (npm ci)
├── 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, // Do not publish source maps in production
// Minification is on by default (Oxc minifier in Vite 8)
rolldownOptions: {
output: {
// Vite 8 (Rolldown): codeSplitting replaces the manualChunks object form
codeSplitting: {
groups: [
{ test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/, name: 'vendor' },
],
},
},
},
},
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 the exact dependency versions from package-lock.json
npm ci
# 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 (a location with its own add_header drops these)
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";
}
# Revalidate HTML on every request so new releases show up immediately
location ~* \.html$ {
expires -1;
}
# Health check endpoint for monitoring
location /health {
access_log off;
default_type text/plain;
return 200 "healthy\n";
}
}
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 (under 30MB compressed as of October 2026) 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. In our setup, adding custom HTTP routers interfered with this behavior and caused 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 can conflict with it.
Container Unhealthy Status
Symptom: docker compose ps shows the container as “unhealthy”.
Root Cause: The health check uses localhost, which resolves to IPv6 [::1], but nginx is 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 the 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:
- DNS not properly pointed to the server
- Port 80/443 not accessible from the internet
- Let’s Encrypt rate limits hit (for example, 5 certificates per exact set of hostnames or 50 per registered domain every 7 days)
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 2>&1 | 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 with minimal downtime. Recreating a single container with docker compose up still briefly interrupts traffic; true zero downtime needs a second healthy instance behind Traefik before the old one stops:
#!/bin/bash
# deploy-update.sh - rebuild and replace the container
# Recreating a single container briefly interrupts traffic (see above)
set -euo pipefail
cd /opt/my-react-app
# Pull latest code (from git or updated files)
git pull origin main
# Build new production assets from the lockfile
npm ci
npm run build
# Build the new image while the old container keeps serving
docker compose build my-react-app
# Replace the container: Compose stops the old one, then starts the new one,
# and --wait returns once the new container reports healthy
docker compose up -d --no-deps --wait my-react-app
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;
# 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
An nginx server serving static React files requires minimal resources—512MB of memory and half a CPU core is a reasonable starting limit; load-test to confirm the concurrency your traffic needs.
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 separate containers and networks per client.
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@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '24'
cache: 'npm'
- name: Build React app
run: |
npm ci
npm run build
- name: Deploy to server
uses: appleboy/[email protected]
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/[email protected]
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 --wait
GitLab CI/CD Pipeline
For GitLab users:
# .gitlab-ci.yml
stages:
- build
- deploy
build:
stage: build
image: node:24
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
deploy:
stage: deploy
image: alpine:3.23
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
# Trust the server's host key (store `ssh-keyscan your-server` output in SSH_KNOWN_HOSTS)
- mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
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 --wait"
rules:
- if: $CI_COMMIT_BRANCH == "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, above the server block: set Connection only for WebSocket upgrades
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Inside the server block. Same-origin proxying: the browser makes no
# cross-origin request, so these API calls need no CORS headers
location /api {
proxy_pass http://your-backend-service:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
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
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
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:
- Self-hosting n8n for workflow automation – Automate deployments and infrastructure tasks
- Self-hosting Windmill with Docker – Alternative workflow automation platform
- Building multi-tenant Docker stacks – Comprehensive Traefik setup for multiple applications
- tva Duplicate Pro – WordPress automation tools for content workflows
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.
From configuration to an operating decision
The practical question is not whether a platform can be configured. It is whether the team can explain ownership, detect drift, recover without improvisation, and demonstrate that the change improved the intended outcome. We therefore pair configuration with an owner, a baseline, a rollback path, and a readback window. That turns a one-off fix into an operating capability. The production record should include the reason for the change, the evidence reviewed, the exact scope, and the person authorized to stop or reverse it. After release, compare the agreed indicators with the baseline and keep the result with the change record. If the expected improvement is absent, rollback is a valid outcome rather than a failure. This discipline matters more than any individual configuration value. The same record gives the next operator a trustworthy starting point and makes later optimization a measured decision rather than another round of guesswork.