React & Node.js Deployment: Complete Production Guide - Deploy your React and Node.js applications to production with Docker, CI/CD, and cloud platforms....
Tutorial

React & Node.js Deployment: Complete Production Guide

Deploy your React and Node.js applications to production with Docker, CI/CD, and cloud platforms.

TechDevDex Team
12/10/2024
25 min
#React#Node.js#Deployment#Docker#CI/CD

React & Node.js Deployment: Complete Production Guide

Deploying full-stack applications can be complex. This comprehensive guide covers everything from Docker containerization to production deployment on various platforms.

Table of Contents

  1. Project Structure
  2. Docker Configuration
  3. Environment Setup
  4. Database Configuration
  5. Deployment Platforms
  6. CI/CD Pipeline
  7. Monitoring and Logging
  8. Security Best Practices
  9. Performance Optimization

Project Structure

Full-Stack Application Layout

text
my-app/
ā”œā”€ā”€ client/                 # React frontend
│   ā”œā”€ā”€ public/
│   ā”œā”€ā”€ src/
│   ā”œā”€ā”€ package.json
│   └── Dockerfile
ā”œā”€ā”€ server/                 # Node.js backend
│   ā”œā”€ā”€ src/
│   ā”œā”€ā”€ package.json
│   └── Dockerfile
ā”œā”€ā”€ docker-compose.yml      # Development
ā”œā”€ā”€ docker-compose.prod.yml # Production
ā”œā”€ā”€ nginx.conf              # Reverse proxy
└── .github/workflows/      # CI/CD

Package.json Structure

Client (React):

json
{
  "name": "my-app-client",
  "version": "1.0.0",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.8.0",
    "axios": "^1.3.0"
  }
}

Server (Node.js):

json
{
  "name": "my-app-server",
  "version": "1.0.0",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.0",
    "cors": "^2.8.5",
    "helmet": "^6.0.0",
    "mongoose": "^6.8.0",
    "jsonwebtoken": "^9.0.0"
  }
}

Docker Configuration

Client Dockerfile (React)

dockerfile
# Multi-stage build for React app
FROM node:16-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY . .

# Build the app
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy built app to nginx
COPY --from=builder /app/build /usr/share/nginx/html

# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

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

Server Dockerfile (Node.js)

dockerfile
FROM node:16-alpine

# Create app directory
WORKDIR /app

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

# Copy package files
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY . .

# Change ownership
RUN chown -R nextjs:nodejs /app
USER nextjs

EXPOSE 3000

CMD ["npm", "start"]

Docker Compose (Development)

yaml
version: '3.8'

services:
  client:
    build: ./client
    ports:
      - "3000:80"
    environment:
      - REACT_APP_API_URL=http://localhost:5000
    depends_on:
      - server

  server:
    build: ./server
    ports:
      - "5000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=mongodb://mongo:27017/myapp
    depends_on:
      - mongo
    volumes:
      - ./server:/app
      - /app/node_modules

  mongo:
    image: mongo:5.0
    ports:
      - "27017:27017"
    volumes:
      - mongo_data:/data/db

volumes:
  mongo_data:

Docker Compose (Production)

yaml
version: '3.8'

services:
  client:
    build: ./client
    restart: unless-stopped
    environment:
      - REACT_APP_API_URL=https://api.myapp.com

  server:
    build: ./server
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - mongo

  mongo:
    image: mongo:5.0
    restart: unless-stopped
    environment:
      - MONGO_INITDB_ROOT_USERNAME=${MONGO_USERNAME}
      - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
    volumes:
      - mongo_data:/data/db

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - client
      - server

volumes:
  mongo_data:

Environment Setup

Environment Variables

Client (.env):

text
REACT_APP_API_URL=http://localhost:5000
REACT_APP_ENVIRONMENT=development

Server (.env):

text
NODE_ENV=development
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-super-secret-jwt-key
CORS_ORIGIN=http://localhost:3000

Production (.env.production):

text
NODE_ENV=production
PORT=3000
DATABASE_URL=mongodb+srv://user:pass@cluster.mongodb.net/myapp
JWT_SECRET=your-production-jwt-secret
CORS_ORIGIN=https://myapp.com

Server Configuration

javascript
// server/src/config/database.js
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.DATABASE_URL, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log(`MongoDB Connected: ${conn.connection.host}`);
  } catch (error) {
    console.error('Database connection error:', error);
    process.exit(1);
  }
};

module.exports = connectDB;
javascript
// server/src/index.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const connectDB = require('./config/database');

const app = express();

// Security middleware
app.use(helmet());
app.use(cors({
  origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
  credentials: true
}));

// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: 'Something went wrong!' });
});

// Connect to database
connectDB();

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Database Configuration

MongoDB with Mongoose

javascript
// server/src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true,
    minlength: 6
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  }
}, {
  timestamps: true
});

// Hash password before saving
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

Database Connection with Retry Logic

javascript
// server/src/config/database.js
const mongoose = require('mongoose');

const connectDB = async () => {
  const maxRetries = 5;
  let retryCount = 0;

  while (retryCount < maxRetries) {
    try {
      const conn = await mongoose.connect(process.env.DATABASE_URL, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        maxPoolSize: 10,
        serverSelectionTimeoutMS: 5000,
        socketTimeoutMS: 45000,
      });
      
      console.log(`MongoDB Connected: ${conn.connection.host}`);
      return;
    } catch (error) {
      retryCount++;
      console.error(`Database connection attempt ${retryCount} failed:`, error.message);
      
      if (retryCount === maxRetries) {
        console.error('Max retries reached. Exiting...');
        process.exit(1);
      }
      
      // Wait before retrying
      await new Promise(resolve => setTimeout(resolve, 5000));
    }
  }
};

module.exports = connectDB;

Deployment Platforms

1. Heroku Deployment

Install Heroku CLI:

bash
# macOS
brew tap heroku/brew && brew install heroku

# Ubuntu/Debian
curl https://cli-assets.heroku.com/install.sh | sh

Deploy to Heroku:

bash
# Login to Heroku
heroku login

# Create apps
heroku create myapp-api
heroku create myapp-client

# Set environment variables
heroku config:set NODE_ENV=production -a myapp-api
heroku config:set DATABASE_URL=mongodb+srv://... -a myapp-api

# Deploy
git subtree push --prefix server heroku main
git subtree push --prefix client heroku main

Heroku Procfile:

text
# server/Procfile
web: npm start

# client/Procfile
web: npm start

2. DigitalOcean App Platform

app.yaml:

yaml
name: myapp
services:
- name: api
  source_dir: /server
  github:
    repo: username/myapp
    branch: main
  run_command: npm start
  environment_slug: node-js
  instance_count: 1
  instance_size_slug: basic-xxs
  envs:
  - key: NODE_ENV
    value: production
  - key: DATABASE_URL
    value: ${DATABASE_URL}

- name: web
  source_dir: /client
  github:
    repo: username/myapp
    branch: main
  run_command: npm start
  environment_slug: node-js
  instance_count: 1
  instance_size_slug: basic-xxs
  envs:
  - key: REACT_APP_API_URL
    value: https://api.myapp.com

3. AWS Deployment

Docker Compose for AWS:

yaml
version: '3.8'

services:
  client:
    build: ./client
    ports:
      - "3000:80"
    environment:
      - REACT_APP_API_URL=http://server:5000

  server:
    build: ./server
    ports:
      - "5000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
    depends_on:
      - mongo

  mongo:
    image: mongo:5.0
    volumes:
      - mongo_data:/data/db

volumes:
  mongo_data:

AWS ECS Task Definition:

json
{
  "family": "myapp",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::account:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "client",
      "image": "myapp-client:latest",
      "portMappings": [
        {
          "containerPort": 80,
          "protocol": "tcp"
        }
      ],
      "essential": true
    },
    {
      "name": "server",
      "image": "myapp-server:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "environment": [
        {
          "name": "NODE_ENV",
          "value": "production"
        }
      ]
    }
  ]
}

CI/CD Pipeline

GitHub Actions

.github/workflows/deploy.yml:

yaml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '16'
        cache: 'npm'
        cache-dependency-path: |
          client/package-lock.json
          server/package-lock.json
    
    - name: Install dependencies
      run: |
        cd client && npm ci
        cd ../server && npm ci
    
    - name: Run tests
      run: |
        cd client && npm test -- --coverage --watchAll=false
        cd ../server && npm test
    
    - name: Build applications
      run: |
        cd client && npm run build
        cd ../server && npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v2
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1
    
    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
    
    - name: Build and push images
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:client ./client
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:server ./server
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:client
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:server
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: myapp
    
    - name: Deploy to ECS
      run: |
        aws ecs update-service --cluster myapp-cluster --service myapp-service --force-new-deployment

GitLab CI/CD

.gitlab-ci.yml:

yaml
stages:
  - test
  - build
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"

test:
  stage: test
  image: node:16
  script:
    - cd client && npm ci && npm test
    - cd ../server && npm ci && npm test
  only:
    - merge_requests
    - main

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE/client:$CI_COMMIT_SHA ./client
    - docker build -t $CI_REGISTRY_IMAGE/server:$CI_COMMIT_SHA ./server
    - docker push $CI_REGISTRY_IMAGE/client:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE/server:$CI_COMMIT_SHA
  only:
    - main

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache curl
  script:
    - curl -X POST -H "Content-Type: application/json" -d '{"image":"'$CI_REGISTRY_IMAGE/client:$CI_COMMIT_SHA'"}' $DEPLOY_WEBHOOK_URL
  only:
    - main

Monitoring and Logging

Application Monitoring

javascript
// server/src/middleware/monitoring.js
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

module.exports = logger;

Health Check Endpoints

javascript
// server/src/routes/health.js
const express = require('express');
const mongoose = require('mongoose');
const router = express.Router();

router.get('/health', async (req, res) => {
  try {
    // Check database connection
    await mongoose.connection.db.admin().ping();
    
    res.status(200).json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
      memory: process.memoryUsage(),
      database: 'connected'
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      timestamp: new Date().toISOString(),
      error: error.message
    });
  }
});

module.exports = router;

Performance Monitoring

javascript
// server/src/middleware/performance.js
const performance = require('perf_hooks').performance;

const performanceMiddleware = (req, res, next) => {
  const start = performance.now();
  
  res.on('finish', () => {
    const duration = performance.now() - start;
    console.log(`${req.method} ${req.path} - ${res.statusCode} - ${duration.toFixed(2)}ms`);
  });
  
  next();
};

module.exports = performanceMiddleware;

Security Best Practices

1. Environment Variables

javascript
// server/src/config/env.js
const Joi = require('joi');

const envSchema = Joi.object({
  NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
  PORT: Joi.number().default(3000),
  DATABASE_URL: Joi.string().required(),
  JWT_SECRET: Joi.string().min(32).required(),
  CORS_ORIGIN: Joi.string().required(),
}).unknown();

const { error, value: envVars } = envSchema.validate(process.env);

if (error) {
  throw new Error(`Config validation error: ${error.message}`);
}

module.exports = {
  env: envVars.NODE_ENV,
  port: envVars.PORT,
  database: {
    url: envVars.DATABASE_URL,
  },
  jwt: {
    secret: envVars.JWT_SECRET,
  },
  cors: {
    origin: envVars.CORS_ORIGIN,
  },
};

2. Rate Limiting

javascript
// server/src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');

const createRateLimiter = (windowMs, max) => {
  return rateLimit({
    windowMs,
    max,
    message: 'Too many requests from this IP, please try again later.',
    standardHeaders: true,
    legacyHeaders: false,
  });
};

// General rate limiter
const generalLimiter = createRateLimiter(15 * 60 * 1000, 100); // 100 requests per 15 minutes

// Auth rate limiter
const authLimiter = createRateLimiter(15 * 60 * 1000, 5); // 5 requests per 15 minutes

module.exports = {
  generalLimiter,
  authLimiter,
};

3. Input Validation

javascript
// server/src/middleware/validation.js
const Joi = require('joi');

const validate = (schema) => {
  return (req, res, next) => {
    const { error } = schema.validate(req.body);
    if (error) {
      return res.status(400).json({
        message: 'Validation error',
        details: error.details.map(detail => detail.message)
      });
    }
    next();
  };
};

const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(6).required(),
});

module.exports = {
  validate,
  userSchema,
};

Performance Optimization

1. Caching

javascript
// server/src/middleware/cache.js
const NodeCache = require('node-cache');

const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes

const cacheMiddleware = (duration) => {
  return (req, res, next) => {
    const key = req.originalUrl;
    const cachedResponse = cache.get(key);
    
    if (cachedResponse) {
      return res.json(cachedResponse);
    }
    
    res.sendResponse = res.json;
    res.json = (body) => {
      cache.set(key, body, duration);
      res.sendResponse(body);
    };
    
    next();
  };
};

module.exports = cacheMiddleware;

2. Database Optimization

javascript
// server/src/models/User.js
const userSchema = new mongoose.Schema({
  // ... fields
}, {
  timestamps: true
});

// Indexes for better performance
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
userSchema.index({ name: 'text', email: 'text' }); // Text search

// Virtual fields
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

// Pre-save middleware
userSchema.pre('save', function(next) {
  this.updatedAt = new Date();
  next();
});

3. Frontend Optimization

javascript
// client/src/utils/lazyLoading.js
import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./HeavyComponent'));

const App = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
};

Conclusion

Deploying React and Node.js applications requires careful planning and attention to security, performance, and scalability. This guide provides a solid foundation for production deployments.

Key takeaways:

  • Use Docker for consistent environments
  • Implement CI/CD for automated deployments
  • Monitor your applications in production
  • Secure your applications with proper authentication and validation
  • Optimize for performance and scalability

Happy deploying! šŸš€