Deploying Multiple Node.js Projects Under One Domain with Caddy
Introduction
In this guide, I’ll show you how to manage multiple Node.js applications under a single domain, each served from different subfolders, using Caddy. Whether you’re running a main application alongside a blog or managing multiple microservices, Caddy simplifies routing traffic to different Node.js applications. We’ll walk through deploying two Node.js projects under project.com and project.com/blog.
Prerequisites
Before starting, ensure you have:
- A registered domain (project.com in our example)
- A Linux server (Ubuntu/Debian recommended), see our list with recommended cloud providers
- Node.js (v16 or higher) installed
- PM2 process manager installed (npm install -g pm2
)
- Caddy 2.x installed
- Basic command line knowledge
- Root or sudo access to your server
Project Structure
First, let's set up our project structure:
/var/www/
├── main-app/
│ ├── package.json
│ ├── node_modules/
│ ├── src/
│ │ └── index.js
│ └── ...
└── blog/
├── package.json
├── node_modules/
├── src/
│ └── index.js
└── ...
Setting Up the Node.js Applications
Main Application Setup
// /var/www/main-app/src/index.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Welcome to the main application!');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Main app running on port ${PORT}`);
});
Blog Application Setup
// /var/www/blog/src/index.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Welcome to the blog!');
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Blog running on port ${PORT}`);
});
Installing Dependencies
# Main application
cd /var/www/main-app
npm init -y
npm install express
# Blog application
cd /var/www/blog
npm init -y
npm install express
Process Management with PM2
We'll use PM2 to manage our Node.js processes:
# Start main application
cd /var/www/main-app
pm2 start src/index.js --name "main-app"
# Start blog application
cd /var/www/blog
pm2 start src/index.js --name "blog-app"
# Save PM2 configuration
pm2 save
# Set up PM2 to start on system boot
pm2 startup
Caddy Configuration
Create or edit your Caddyfile at /etc/caddy/Caddyfile
:
project.com {
# Global settings
tls admin@project.com
encode gzip
# Main application
handle /* {
reverse_proxy localhost:3000
}
# Blog application
handle /blog/* {
uri strip_prefix /blog
reverse_proxy localhost:3001
}
# Handle errors
handle_errors {
respond "{http.error.status_code} {http.error.status_text}"
}
}
Let's break down the configuration:
- Domain Configuration:
-
The block starts with
project.com
to handle all requests to this domain -
Global Settings:
tls
: Enables automatic HTTPS certification-
encode gzip
: Enables compression for better performance -
Main Application Handler:
- Handles all requests to the root domain
-
Proxies to the main application running on port 3000
-
Blog Handler:
- Handles requests to
/blog
- Strips the
/blog
prefix before proxying - Proxies to the blog application running on port 3001
Security Configuration
Application Security
Add basic security middleware to both applications:
// Add to both Node.js applications
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
// Install dependencies
npm install helmet express-rate-limit
// Configure security middleware
app.use(helmet());
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
Caddy Security Headers
Add security headers in your Caddyfile:
project.com {
header {
# HSTS
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# XSS Protection
X-XSS-Protection "1; mode=block"
# Content Security Policy
Content-Security-Policy "default-src 'self'"
# Frame Options
X-Frame-Options "SAMEORIGIN"
# Remove Server header
-Server
}
# ... rest of your configuration
}
Deployment Steps
- Prepare the Server:
# Update system
sudo apt update && sudo apt upgrade -y
# Create project directories
sudo mkdir -p /var/www/{main-app,blog}
sudo chown -R $USER:$USER /var/www
- Deploy Applications:
# Deploy main app
cd /var/www/main-app
git clone [your-main-app-repo] .
npm install
npm run build # if applicable
# Deploy blog
cd /var/www/blog
git clone [your-blog-repo] .
npm install
npm run build # if applicable
- Start Services:
# Restart PM2 processes
pm2 restart all
# Validate Caddy configuration
sudo caddy validate --config /etc/caddy/Caddyfile
# Restart Caddy
sudo systemctl restart caddy
Monitoring and Maintenance
Application Monitoring
# Monitor PM2 processes
pm2 monit
# View application logs
pm2 logs main-app
pm2 logs blog-app
# Check process status
pm2 status
Caddy Monitoring
# View Caddy logs
sudo journalctl -u caddy --follow
# Check Caddy status
sudo systemctl status caddy
Possible Issues
- Connection Refused:
# Check if Node.js applications are running
pm2 status
# Check ports
sudo netstat -tulpn | grep LISTEN
- 502 Bad Gateway:
# Check application logs
pm2 logs
# Verify Caddy configuration
sudo caddy validate --config /etc/caddy/Caddyfile
- SSL Issues:
# Check Caddy SSL certificates
ls -la /var/lib/caddy/.local/share/caddy/certificates
# Verify DNS settings
dig project.com
Backup Strategy
- Application Backups:
# Create backup script
cat > backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/backup/nodejs-apps"
DATE=$(date +%Y%m%d)
mkdir -p "$BACKUP_DIR"
# Backup applications
tar -czf "$BACKUP_DIR/main-app-$DATE.tar.gz" /var/www/main-app
tar -czf "$BACKUP_DIR/blog-$DATE.tar.gz" /var/www/blog
# Backup PM2 configuration
pm2 save
cp -r ~/.pm2 "$BACKUP_DIR/pm2-$DATE"
# Backup Caddy configuration
cp /etc/caddy/Caddyfile "$BACKUP_DIR/Caddyfile-$DATE"
EOF
chmod +x backup.sh