Building React & REST API with Nginx on Docker

A Complete Step-by-Step Guide with Security Hardening

Table of Contents

1. Architecture Overview

System Architecture
Client Browser
Nginx Container
(Port 80/443)
React App
(Static Files)
or
REST API Container
(Port 3000)

This architecture uses Docker containers to isolate services, with Nginx acting as a reverse proxy and static file server. The React application is served as static files, while API requests are proxied to a backend container.

ℹ️ Key Benefits: Containerization provides isolation, scalability, easy deployment, and consistent environments across development and production.

2. Prerequisites

1 Ubuntu Server Installation

Ensure you have Ubuntu Server 20.04 LTS or newer installed and updated.

sudo apt update && sudo apt upgrade -y
2 Install Docker
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Add your user to docker group
sudo usermod -aG docker $USER

# Start and enable Docker
sudo systemctl start docker
sudo systemctl enable docker

# Verify installation
docker --version
3 Install Docker Compose
# Download Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

# Make it executable
sudo chmod +x /usr/local/bin/docker-compose

# Verify installation
docker-compose --version
4 Install Node.js (for development)
# Install Node.js 18.x
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs

# Verify installation
node --version
npm --version

3. Project Structure

Create a well-organized project structure:

my-web-app/
├── docker-compose.yml
├── nginx/
│   ├── Dockerfile
│   ├── nginx.conf
│   └── ssl/
│       ├── cert.pem
│       └── key.pem
├── frontend/
│   ├── package.json
│   ├── public/
│   └── src/
│       ├── App.js
│       └── index.js
└── backend/
    ├── Dockerfile
    ├── package.json
    └── server.js
1 Create Project Directory
mkdir -p my-web-app/{nginx/ssl,frontend,backend}
cd my-web-app

4. Building the React Application

1 Initialize React App
cd frontend
npx create-react-app .
# Wait for installation to complete
2 Create Sample React Component

Edit frontend/src/App.js:

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchData = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/data');
      const result = await response.json();
      setData(result);
    } catch (error) {
      console.error('Error fetching data:', error);
    }
    setLoading(false);
  };

  return (
    <div className="App">
      <header className="App-header">
        <h1>React + REST API + Nginx + Docker</h1>
        <button onClick={fetchData} disabled={loading}>
          {loading ? 'Loading...' : 'Fetch Data from API'}
        </button>
        {data && (
          <div>
            <h2>API Response:</h2>
            <pre>{JSON.stringify(data, null, 2)}</pre>
          </div>
        )}
      </header>
    </div>
  );
}

export default App;
3 Build React App for Production
npm run build
# This creates a 'build' folder with optimized production files
ℹ️ The build folder will be copied to the Nginx container to serve static files.

5. Creating the REST API

1 Initialize Node.js API
cd ../backend
npm init -y
npm install express cors helmet
2 Create API Server

Create backend/server.js:

const express = require('express');
const helmet = require('helmet');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 3000;

// Security middleware
app.use(helmet());
app.use(cors());
app.use(express.json());

// Health check endpoint
app.get('/api/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// Sample data endpoint
app.get('/api/data', (req, res) => {
  res.json({
    message: 'Hello from REST API!',
    data: [
      { id: 1, name: 'Item 1', value: 100 },
      { id: 2, name: 'Item 2', value: 200 },
      { id: 3, name: 'Item 3', value: 300 }
    ],
    timestamp: new Date().toISOString()
  });
});

// Sample POST endpoint
app.post('/api/data', (req, res) => {
  const { name, value } = req.body;
  res.json({
    success: true,
    created: { id: Date.now(), name, value }
  });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`API Server running on port ${PORT}`);
});
3 Create Backend Dockerfile

Create backend/Dockerfile:

FROM node:18-alpine

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy app source
COPY . .

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

# Change ownership
RUN chown -R nodejs:nodejs /usr/src/app

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 3000

# Start app
CMD ["node", "server.js"]

6. Nginx Configuration

1 Create Nginx Configuration

Create nginx/nginx.conf:

# Main nginx configuration
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Logging
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    
    access_log /var/log/nginx/access.log main;

    # Performance optimization
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # 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;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;

    # Hide nginx version
    server_tokens off;

    # Gzip compression
    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/json application/javascript;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    # Upstream backend
    upstream api_backend {
        server backend:3000;
        keepalive 32;
    }

    server {
        listen 80;
        server_name localhost;

        # Security limits
        client_max_body_size 10M;
        client_body_buffer_size 128k;

        # Root directory for React app
        root /usr/share/nginx/html;
        index index.html;

        # API proxy
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_conn conn_limit 10;

            proxy_pass http://api_backend;
            proxy_http_version 1.1;
            
            # Proxy headers
            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;
            proxy_set_header Connection "";
            
            # Timeouts
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }

        # React app - serve static files
        location / {
            try_files $uri $uri/ /index.html;
            
            # Cache static assets
            location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
                expires 1y;
                add_header Cache-Control "public, immutable";
            }
        }

        # Deny access to hidden files
        location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
        }
    }

    # HTTPS server (uncomment and configure for production)
    # server {
    #     listen 443 ssl http2;
    #     server_name your-domain.com;
    #
    #     ssl_certificate /etc/nginx/ssl/cert.pem;
    #     ssl_certificate_key /etc/nginx/ssl/key.pem;
    #     ssl_protocols TLSv1.2 TLSv1.3;
    #     ssl_ciphers HIGH:!aNULL:!MD5;
    #     ssl_prefer_server_ciphers on;
    #
    #     # ... rest of server configuration
    # }
}
2 Create Nginx Dockerfile

Create nginx/Dockerfile:

FROM nginx:1.25-alpine

# Remove default nginx config
RUN rm /etc/nginx/nginx.conf

# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf

# Copy React build files
COPY ../frontend/build /usr/share/nginx/html

# Create non-root user (nginx user already exists)
RUN chown -R nginx:nginx /usr/share/nginx/html && \
    chown -R nginx:nginx /var/cache/nginx && \
    chown -R nginx:nginx /var/log/nginx && \
    touch /var/run/nginx.pid && \
    chown -R nginx:nginx /var/run/nginx.pid

# Switch to non-root user
USER nginx

EXPOSE 80

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

7. Docker Setup

1 Create Docker Compose File

Create docker-compose.yml in the project root:

version: '3.8'

services:
  nginx:
    build: 
      context: .
      dockerfile: nginx/Dockerfile
    container_name: web-nginx
    ports:
      - "80:80"
      - "443:443"
    networks:
      - app-network
    depends_on:
      - backend
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
      - NET_BIND_SERVICE
    read_only: true
    tmpfs:
      - /var/cache/nginx
      - /var/run
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      # Uncomment for SSL
      # - ./nginx/ssl:/etc/nginx/ssl:ro
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  backend:
    build: 
      context: ./backend
      dockerfile: Dockerfile
    container_name: api-backend
    expose:
      - "3000"
    networks:
      - app-network
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    read_only: true
    tmpfs:
      - /tmp
    environment:
      - NODE_ENV=production
      - PORT=3000
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

networks:
  app-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
2 Create .dockerignore Files

Create backend/.dockerignore:

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.DS_Store

8. Nginx Security Hardening

Security Best Practices

1 Implement Rate Limiting

Already included in the nginx.conf above. This prevents DDoS attacks and API abuse:

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req zone=api_limit burst=20 nodelay;
2 Configure SSL/TLS (Production)

Generate self-signed certificate for testing:

cd nginx/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout key.pem -out cert.pem \
  -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"

For production, use Let's Encrypt:

sudo apt install certbot
sudo certbot certonly --standalone -d your-domain.com
3 Additional Security Headers

Add to nginx.conf for enhanced security:

# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;

# Strict Transport Security (HTTPS only)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Prevent clickjacking
add_header X-Frame-Options "DENY" always;
4 Block Common Attacks

Add to server block in nginx.conf:

# Block SQL injection attempts
location ~* (union|select|insert|cast|set|declare|drop|update|md5|benchmark) {
    deny all;
}

# Block directory traversal attempts
location ~* \.\./|\.\.\\  {
    deny all;
}

# Block access to sensitive files
location ~* \.(env|log|ini|conf|bak|sql|sh)$ {
    deny all;
}
Security Feature Purpose Implementation
Rate Limiting Prevent DDoS attacks limit_req_zone directive
SSL/TLS Encrypt traffic TLS 1.2+ only
Security Headers Browser protection X-Frame-Options, CSP, HSTS
Hide Version Reduce attack surface server_tokens off

9. Docker Security Hardening

Container Security Best Practices

1 Use Non-Root Users

Already implemented in Dockerfiles. Always run containers as non-root users to limit privilege escalation attacks.

# In Dockerfile
USER nodejs  # or USER nginx
2 Implement Read-Only Filesystems

Already configured in docker-compose.yml. This prevents malicious code from writing to the filesystem:

read_only: true
tmpfs:
  - /tmp
  - /var/cache/nginx
3 Drop Linux Capabilities

Minimize container privileges:

cap_drop:
  - ALL
cap_add:
  - NET_BIND_SERVICE  # Only add what's needed
4 Enable Docker Security Features
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# Scan images for vulnerabilities
docker scan backend:latest

# Use AppArmor/SELinux profiles
security_opt:
  - no-new-privileges:true
  - apparmor=docker-default
5 Configure Docker Daemon Security

Edit /etc/docker/daemon.json:

{
  "icc": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "userns-remap": "default",
  "live-restore": true,
  "userland-proxy": false,
  "no-new-privileges": true
}

Restart Docker after changes:

sudo systemctl restart docker
6 Regular Updates and Scanning
# Update base images regularly
docker pull node:18-alpine
docker pull nginx:1.25-alpine

# Scan for vulnerabilities using Trivy
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt update
sudo apt install trivy

# Scan images
trivy image nginx:1.25-alpine
trivy image node:18-alpine
⚠️ Important: Never store secrets in Dockerfiles or environment variables. Use Docker secrets or external secret management tools like HashiCorp Vault.

Network Security

7 Configure UFW Firewall
# Enable UFW
sudo ufw enable

# Allow SSH
sudo ufw allow ssh

# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Deny all other incoming traffic
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Configure Docker to work with UFW
# Edit /etc/default/docker and add:
DOCKER_OPTS="--iptables=false"

# Restart Docker
sudo systemctl restart docker
8 Isolate Container Networks

Use custom bridge networks (already implemented in docker-compose.yml) to isolate containers:

networks:
  app-network:
    driver: bridge
    internal: false  # Set to true for fully isolated network

10. Deployment & Testing

1 Build React Application
cd frontend
npm run build
cd ..
2 Build and Start Containers
# Build images
docker-compose build

# Start services
docker-compose up -d

# View logs
docker-compose logs -f

# Check container status
docker-compose ps
3 Test the Application
# Test React app
curl http://localhost/

# Test API health check
curl http://localhost/api/health

# Test API data endpoint
curl http://localhost/api/data

# Test from browser
# Open: http://your-server-ip/
4 Monitor and Maintain
# View container logs
docker logs web-nginx
docker logs api-backend

# Monitor resource usage
docker stats

# Check nginx configuration
docker exec web-nginx nginx -t

# Reload nginx configuration
docker exec web-nginx nginx -s reload

# View nginx access logs
docker exec web-nginx tail -f /var/log/nginx/access.log
5 Backup and Restore
# Backup Docker volumes
docker run --rm -v my-web-app_data:/data -v $(pwd):/backup \
  alpine tar czf /backup/backup.tar.gz /data

# Backup configurations
tar czf config-backup.tar.gz nginx/ docker-compose.yml

# Stop containers
docker-compose down

# Restore from backup
tar xzf config-backup.tar.gz

# Restart services
docker-compose up -d
Deployment Complete! Your React app and REST API are now running securely on Docker with Nginx.

Useful Commands Reference

Command Description
docker-compose up -d Start all services in background
docker-compose down Stop and remove all containers
docker-compose restart Restart all services
docker-compose logs -f [service] View real-time logs
docker-compose exec [service] sh Access container shell
docker system prune -a Clean up unused Docker resources

Security Checklist

✓ Pre-Deployment Security Checklist

  • ☑ All containers run as non-root users
  • ☑ Read-only filesystems enabled where possible
  • ☑ SSL/TLS configured with strong ciphers
  • ☑ Security headers implemented in Nginx
  • ☑ Rate limiting configured
  • ☑ Firewall (UFW) properly configured
  • ☑ Docker daemon security settings applied
  • ☑ Images scanned for vulnerabilities
  • ☑ Logging enabled for all containers
  • ☑ Sensitive data not in environment variables
  • ☑ Regular backup strategy in place
  • ☑ Update schedule for base images

Troubleshooting Common Issues

Container Won't Start

# Check logs
docker-compose logs [service-name]

# Verify configuration
docker-compose config

# Check port conflicts
sudo netstat -tulpn | grep :80

502 Bad Gateway

Usually means Nginx can't reach the backend:

# Check backend is running
docker-compose ps

# Check backend logs
docker-compose logs backend

# Verify network connectivity
docker-compose exec nginx ping backend

Permission Denied Errors

# Check file ownership
ls -la nginx/ frontend/ backend/

# Fix permissions
sudo chown -R $USER:$USER .

# Rebuild containers
docker-compose build --no-cache