Complete Guide to Deploying a MERN Stack Application with GitHub Actions CI/CD
Introduction
Deploying a MERN (MongoDB, Express, React, Node.js) stack application can be challenging, especially when you want to automate the entire process. This comprehensive guide will walk you through setting up a production-ready deployment pipeline using GitHub Actions, complete with SSL certificates, security configurations, and best practices.
By the end of this guide, you'll have a fully automated deployment system where pushing to your main branch automatically deploys your application to your server.
Table of Contents
- Prerequisites
- Architecture Overview
- Server Setup and Configuration
- MongoDB Configuration
- Application Preparation
- Nginx Configuration
- SSL Certificate Setup
- GitHub Actions CI/CD Pipeline
- Environment Variables and Secrets
- Deployment Process
- Monitoring and Logging
- Troubleshooting
Prerequisites
Before starting, ensure you have:
- A Ubuntu/Debian server (20.04 LTS or later recommended)
- A domain name pointed to your server's IP address
- Root or sudo access to the server
- A GitHub account with a repository containing your MERN application
- Basic knowledge of Linux command line
- Node.js application with separate frontend and backend directories
Architecture Overview
Our deployment architecture consists of:
- Frontend: React application served by Nginx as static files
- Backend: Node.js/Express API running as a PM2 process
- Database: MongoDB running on the same server or external service
- Reverse Proxy: Nginx routing requests to appropriate services
- SSL: Let's Encrypt certificates for HTTPS
- CI/CD: GitHub Actions for automated deployment
Internet → Nginx (443/80) → React Static Files (/)
→ Node.js API (/api)
→ MongoDB (localhost:27017)Server Setup and Configuration
Initial Server Configuration
Connect to your server via SSH and update the system:
ssh user@your-server-ip
sudo apt update && sudo apt upgrade -yInstall Node.js and npm
Install Node.js using NodeSource repository:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node --version
npm --versionInstall PM2 Process Manager
PM2 will keep your Node.js application running and automatically restart it if it crashes:
sudo npm install -g pm2
pm2 startup systemdRun the command that PM2 outputs to enable it to start on boot.
Install Nginx
sudo apt install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginxConfigure Firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableCreate Application Directory
sudo mkdir -p /var/www/your-app-name
sudo chown -R $USER:$USER /var/www/your-app-nameMongoDB Configuration
Option 1: Install MongoDB Locally
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
sudo apt update
sudo apt install -y mongodb-org
sudo systemctl start mongod
sudo systemctl enable mongodSecure MongoDB
mongoshInside the MongoDB shell:
use admin
db.createUser({
user: "adminUser",
pwd: "StrongPasswordHere",
roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]
})Exit the shell and enable authentication:
sudo nano /etc/mongod.confAdd or modify:
security:
authorization: enabledRestart MongoDB:
sudo systemctl restart mongodOption 2: Use MongoDB Atlas
If you prefer a managed solution, sign up for MongoDB Atlas and create a cluster. Save your connection string for later use.
Application Preparation
Project Structure
Your project should have this structure:
your-repo/
├── client/ # React frontend
│ ├── src/
│ ├── public/
│ ├── package.json
│ └── vite.config.js (or other bundler config)
├── server/ # Node.js backend
│ ├── src/
│ ├── package.json
│ └── server.js
├── .github/
│ └── workflows/
│ └── deploy.yml
└── README.mdBackend Configuration
Update your server/server.js to handle production settings:
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors({
origin: process.env.CLIENT_URL || 'http://localhost:3000',
credentials: true
}));
app.use(express.json());
// MongoDB Connection
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('MongoDB connection error:', err));
// Routes
app.use('/api', require('./routes'));
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Frontend Configuration
Create a .env.production file in your client directory:
VITE_API_URL=https://yourdomain.com/apiUpdate your API calls to use this environment variable:
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api';
export const fetchData = async () => {
const response = await fetch(`${API_URL}/endpoint`);
return response.json();
};Create PM2 Ecosystem File
Create ecosystem.config.js in the server directory:
module.exports = {
apps: [{
name: 'your-app-backend',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 5000
},
error_file: '/var/www/your-app-name/logs/err.log',
out_file: '/var/www/your-app-name/logs/out.log',
log_file: '/var/www/your-app-name/logs/combined.log',
time: true
}]
};Nginx Configuration
Create Nginx Server Block
sudo nano /etc/nginx/sites-available/your-app-nameAdd the following configuration:
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL certificates (will be configured by Certbot)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# 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;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Root directory for frontend build
root /var/www/your-app-name/client/dist;
index index.html;
# 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;
# API proxy
location /api {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header 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;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Frontend - serve React app
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";
}
}
# Error pages
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}Enable the site:
sudo ln -s /etc/nginx/sites-available/your-app-name /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxSSL Certificate Setup
Install Certbot
sudo apt install -y certbot python3-certbot-nginxObtain SSL Certificate
First, temporarily configure Nginx without SSL. Edit your Nginx config to only include the HTTP (port 80) server block:
sudo nano /etc/nginx/sites-available/your-app-nameSimplify to:
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/your-app-name/client/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}Reload Nginx:
sudo nginx -t
sudo systemctl reload nginxNow obtain the certificate:
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.comFollow the prompts. Certbot will automatically modify your Nginx configuration to include SSL settings.
Auto-Renewal Setup
Certbot automatically creates a systemd timer for renewal. Verify it:
sudo systemctl status certbot.timer
sudo certbot renew --dry-runGitHub Actions CI/CD Pipeline
Setup SSH Access for GitHub Actions
On your server, create a dedicated deployment user (optional but recommended):
sudo adduser github-deploy
sudo usermod -aG sudo github-deployGenerate SSH key pair on your local machine:
ssh-keygen -t ed25519 -C "github-actions" -f github-deploy-keyCopy the public key to your server:
ssh-copy-id -i github-deploy-key.pub user@your-server-ipOr manually add it:
# On server
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# Paste the public key, save and exit
chmod 600 ~/.ssh/authorized_keysKeep the private key (github-deploy-key) for GitHub Secrets.
Create GitHub Secrets
Go to your GitHub repository → Settings → Secrets and variables → Actions → New repository secret
Add these secrets:
SSH_PRIVATE_KEY: Content of your private key fileSERVER_HOST: Your server IP or domainSERVER_USER: Your SSH user (e.g., github-deploy or your username)MONGODB_URI: Your MongoDB connection stringSERVER_PATH:/var/www/your-app-name
Create GitHub Actions Workflow
Create .github/workflows/deploy.yml:
name: Deploy MERN Application
on:
push:
branches: [ main, production ]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: |
client/package-lock.json
server/package-lock.json
- name: Install frontend dependencies
working-directory: ./client
run: npm ci
- name: Build frontend
working-directory: ./client
env:
VITE_API_URL: https://yourdomain.com/api
run: npm run build
- name: Install backend dependencies
working-directory: ./server
run: npm ci --production
- name: Setup SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Add server to known hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
- name: Create logs directory on server
run: |
ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} "mkdir -p ${{ secrets.SERVER_PATH }}/logs"
- name: Deploy frontend
run: |
rsync -avz --delete \
-e "ssh -o StrictHostKeyChecking=no" \
./client/dist/ \
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.SERVER_PATH }}/client/dist/
- name: Deploy backend
run: |
rsync -avz --delete \
--exclude 'node_modules' \
--exclude '.env' \
--exclude 'logs' \
-e "ssh -o StrictHostKeyChecking=no" \
./server/ \
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.SERVER_PATH }}/server/
- name: Install production dependencies on server
run: |
ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF'
cd ${{ secrets.SERVER_PATH }}/server
npm ci --production
EOF
- name: Create/Update .env file
run: |
ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF'
cat > ${{ secrets.SERVER_PATH }}/server/.env << 'ENVEOF'
NODE_ENV=production
PORT=5000
MONGODB_URI=${{ secrets.MONGODB_URI }}
CLIENT_URL=https://yourdomain.com
ENVEOF
EOF
- name: Restart backend with PM2
run: |
ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF'
cd ${{ secrets.SERVER_PATH }}/server
pm2 delete your-app-backend || true
pm2 start ecosystem.config.js
pm2 save
EOF
- name: Reload Nginx
run: |
ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} "sudo systemctl reload nginx"
- name: Health check
run: |
sleep 5
curl -f https://yourdomain.com/health || exit 1
- name: Deployment summary
if: success()
run: |
echo "✅ Deployment successful!"
echo "🌐 Frontend: https://yourdomain.com"
echo "🔌 Backend: https://yourdomain.com/api"
echo "⏰ Deployed at: $(date)"Allow Passwordless Sudo for Nginx Reload
On your server, configure sudo to allow nginx reload without password:
sudo visudoAdd this line at the end:
github-deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginxReplace github-deploy with your deployment user.
Environment Variables and Secrets
Server-Side Environment Variables
Create .env file on the server (this won't be in Git):
nano /var/www/your-app-name/server/.envNODE_ENV=production
PORT=5000
MONGODB_URI=mongodb://adminUser:password@localhost:27017/yourdb?authSource=admin
CLIENT_URL=https://yourdomain.com
JWT_SECRET=your-super-secret-jwt-key
SESSION_SECRET=your-session-secretImportant: Never commit .env files to Git. Add to .gitignore:
# .gitignore
.env
.env.local
.env.production
node_modules/
dist/
build/
logs/
*.logManaging Secrets in GitHub
For additional secrets needed by your application:
# Add to GitHub Secrets
JWT_SECRET
SESSION_SECRET
DATABASE_PASSWORD
API_KEYSUpdate the workflow to pass these to the server during deployment.
Deployment Process
Manual First Deployment
Before relying on GitHub Actions, do a manual deployment to verify everything works:
# On your server
cd /var/www/your-app-name
# Clone your repository
git clone https://github.com/yourusername/your-repo.git .
# Build frontend
cd client
npm install
npm run build
# Setup backend
cd ../server
npm install --production
pm2 start ecosystem.config.js
pm2 saveAutomated Deployment
Once GitHub Actions is configured:
- Make changes to your code
- Commit and push to the main branch
- GitHub Actions automatically:
- Builds the frontend
- Installs backend dependencies
- Deploys to your server
- Restarts the backend
- Reloads Nginx
Monitor the deployment in the Actions tab of your GitHub repository.
Monitoring and Logging
PM2 Monitoring
# View logs
pm2 logs your-app-backend
# Monitor resources
pm2 monit
# View detailed info
pm2 info your-app-backend
# View all processes
pm2 listNginx Logs
# Access logs
sudo tail -f /var/log/nginx/access.log
# Error logs
sudo tail -f /var/log/nginx/error.logApplication Logs
Your application logs are stored in /var/www/your-app-name/logs/:
tail -f /var/www/your-app-name/logs/combined.logSetup Log Rotation
Create log rotation configuration:
sudo nano /etc/logrotate.d/your-app/var/www/your-app-name/logs/*.log {
daily
rotate 14
compress
delaycompress
notifempty
create 0640 www-data www-data
sharedscripts
}Troubleshooting
Common Issues and Solutions
1. Deployment Fails - SSH Connection
Problem: GitHub Actions can't connect to server
Solution:
- Verify SSH key is correctly added to GitHub Secrets
- Ensure server allows SSH connections
- Check firewall rules:
sudo ufw status - Verify known_hosts step in workflow
2. Backend Not Starting
Problem: PM2 process crashes or won't start
Solution:
# Check PM2 logs
pm2 logs your-app-backend
# Common issues:
# - Missing environment variables
# - MongoDB connection failure
# - Port already in use
# Check if port 5000 is available
sudo lsof -i :5000
# Restart with verbose output
cd /var/www/your-app-name/server
node server.js3. 502 Bad Gateway
Problem: Nginx can't connect to backend
Solution:
- Verify backend is running:
pm2 list - Check backend port matches Nginx config
- Review Nginx error logs:
sudo tail -f /var/log/nginx/error.log - Test backend directly:
curl http://localhost:5000/health
4. SSL Certificate Issues
Problem: HTTPS not working or certificate errors
Solution:
# Verify certificate
sudo certbot certificates
# Renew manually
sudo certbot renew
# Check Nginx SSL config
sudo nginx -t
# Verify certificate files exist
ls -la /etc/letsencrypt/live/yourdomain.com/5. MongoDB Connection Errors
Problem: Backend can't connect to MongoDB
Solution:
# Check if MongoDB is running
sudo systemctl status mongod
# Test connection
mongosh "mongodb://adminUser:password@localhost:27017/yourdb?authSource=admin"
# Check MongoDB logs
sudo tail -f /var/log/mongodb/mongod.log
# Verify credentials in .env file
cat /var/www/your-app-name/server/.env6. Frontend Not Loading
Problem: React app shows blank page or errors
Solution:
- Check browser console for errors
- Verify API URL in frontend build
- Check Nginx serves correct directory:
/var/www/your-app-name/client/dist - Rebuild frontend:
npm run build - Verify Nginx try_files directive handles React routing
7. CORS Errors
Problem: Frontend can't communicate with backend
Solution: Update backend CORS configuration:
app.use(cors({
origin: ['https://yourdomain.com', 'https://www.yourdomain.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));Debugging Checklist
When deployment fails, check in this order:
- ✅ GitHub Actions log for error messages
- ✅ PM2 logs:
pm2 logs - ✅ Nginx error logs:
sudo tail -f /var/log/nginx/error.log - ✅ Application logs:
tail -f /var/www/your-app-name/logs/combined.log - ✅ MongoDB logs:
sudo tail -f /var/log/mongodb/mongod.log - ✅ System resources:
htoporfree -h - ✅ Disk space:
df -h - ✅ Port availability:
sudo lsof -i :5000
Best Practices
Security
- Keep Software Updated
sudo apt update && sudo apt upgrade -y- Use Strong Passwords
- Generate strong passwords for MongoDB users
- Use password managers
- Implement Rate Limiting
Install express-rate-limit:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api', limiter);- Environment Variables
- Never commit
.envfiles - Rotate secrets regularly
- Use different credentials for development and production
- Database Backups
Setup automated MongoDB backups:
# Create backup script
nano ~/backup-mongo.sh#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/var/backups/mongodb"
mkdir -p $BACKUP_DIR
mongodump --uri="mongodb://adminUser:password@localhost:27017/yourdb?authSource=admin" --out="$BACKUP_DIR/backup_$DATE"
# Keep only last 7 days
find $BACKUP_DIR -type d -mtime +7 -exec rm -rf {} +chmod +x ~/backup-mongo.sh
# Add to crontab
crontab -e
# Add: 0 2 * * * /home/user/backup-mongo.shPerformance
- Enable Caching
- Use Redis for session storage
- Implement API response caching
- Configure proper cache headers
- Optimize Database Queries
- Create proper indexes
- Use aggregation pipelines
- Limit query results
- Monitor Resources
# Install monitoring tools
sudo apt install -y htop iotop nethogsDevelopment Workflow
- Branching Strategy
main- production deploymentsdevelop- staging/testingfeature/*- feature development
- Testing Before Deployment
Add tests to your workflow:
- name: Run tests
working-directory: ./server
run: npm test
- name: Run frontend tests
working-directory: ./client
run: npm test- Rollback Strategy
Create rollback workflow:
name: Rollback Deployment
on:
workflow_dispatch:
inputs:
version:
description: 'Commit hash to rollback to'
required: true
jobs:
rollback:
runs-on: ubuntu-latest
steps:
- name: Checkout specific commit
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.version }}
# Rest of deployment steps...Conclusion
You now have a fully automated MERN stack deployment pipeline with:
- ✅ Automated CI/CD with GitHub Actions
- ✅ SSL/HTTPS encryption
- ✅ Production-ready Nginx configuration
- ✅ Process management with PM2
- ✅ Secure MongoDB setup
- ✅ Comprehensive logging
- ✅ Error handling and troubleshooting guides
Next Steps
- Setup Monitoring: Consider tools like Uptime Robot or New Relic
- Implement Analytics: Add Google Analytics or similar
- Setup Error Tracking: Use Sentry or similar services
- Configure CDN: Use Cloudflare for improved performance
- Implement CI Tests: Add unit and integration tests
- Setup Staging Environment: Create a separate staging server
- Document API: Use Swagger or similar for API documentation
Additional Resources
- GitHub Actions Documentation
- Nginx Documentation
- PM2 Documentation
- MongoDB Documentation
- Let's Encrypt Documentation
Need help? If you encounter issues not covered in this guide, check the troubleshooting section or review logs systematically. Most deployment issues can be resolved by carefully reading error messages and logs.
Happy deploying! 🚀
javascript book
If this interested you, check out my book Javascript Book