React & Node.js Deployment: Complete Production Guide
Deploy your React and Node.js applications to production with Docker, CI/CD, and cloud platforms.
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
- Project Structure
- Docker Configuration
- Environment Setup
- Database Configuration
- Deployment Platforms
- CI/CD Pipeline
- Monitoring and Logging
- Security Best Practices
- Performance Optimization
Project Structure
Full-Stack Application Layout
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):
{
"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):
{
"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)
# 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)
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)
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)
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):
REACT_APP_API_URL=http://localhost:5000
REACT_APP_ENVIRONMENT=development
Server (.env):
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):
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
// 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;
// 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
// 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
// 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:
# macOS
brew tap heroku/brew && brew install heroku
# Ubuntu/Debian
curl https://cli-assets.heroku.com/install.sh | sh
Deploy to Heroku:
# 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:
# server/Procfile
web: npm start
# client/Procfile
web: npm start
2. DigitalOcean App Platform
app.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:
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:
{
"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:
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:
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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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! š