|$ curl https://forge-ai.dev/api/markdown?path=docs/deploy/server
$cat docs/server-deployment-(linux/nginx).md
updated Recently·35 min read·published

Server Deployment (Linux/Nginx)

DeploymentServerIntermediate to Advanced
Introduction

Self-managed server deployment gives you full control over infrastructure, configuration, and costs. This guide covers setting up a production-grade Linux server (Ubuntu) with Nginx as a reverse proxy, PM2 for process management, Let's Encrypt for SSL, and security hardening.

While platform-as-a-service solutions offer convenience, understanding server deployment is essential for debugging production issues, optimizing performance, and managing infrastructure that PaaS platforms cannot handle.

VPS Setup (Ubuntu)

A Virtual Private Server (VPS) from providers like DigitalOcean, Linode, Vultr, or AWS EC2 gives you a root-accessible Linux machine. Start with Ubuntu 24.04 LTS.

vps-setup.sh
Bash
1# SSH into your server (replace with your IP)
2ssh root@203.0.113.10
3
4# Update system packages
5apt update && apt upgrade -y
6
7# Create a non-root user with sudo privileges
8adduser deploy
9usermod -aG sudo deploy
10
11# Copy your SSH key for passwordless login
12rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy/
13
14# Disable root SSH login (edit /etc/ssh/sshd_config):
15# PermitRootLogin no
16# PasswordAuthentication no
17systemctl restart sshd
18
19# Set hostname
20hostnamectl set-hostname app-server-01
21
22# Configure timezone
23timedatectl set-timezone UTC
24
25# Install essential packages
26apt install -y curl wget git ufw fail2ban unattended-upgrades

danger

Always disable root login and password authentication after setting up SSH keys. A server with root SSH enabled will be attacked by bots within minutes of going online. Use a non-root user with sudo for all administration.
Firewall & SSH Hardening

UFW (Uncomplicated Firewall) blocks all incoming traffic except explicitly allowed ports. SSH hardening prevents brute-force attacks.

firewall-ssh.sh
Bash
1# Configure UFW firewall
2ufw default deny incoming
3ufw default allow outgoing
4
5# Allow SSH (change port first if using custom port)
6ufw allow 22/tcp
7
8# Allow HTTP and HTTPS
9ufw allow 80/tcp
10ufw allow 443/tcp
11
12# Enable firewall
13ufw enable
14ufw status verbose
15
16# Configure Fail2ban for SSH protection
17cat > /etc/fail2ban/jail.local << 'EOF'
18[sshd]
19enabled = true
20port = 22
21maxretry = 3
22bantime = 3600
23findtime = 600
24EOF
25
26systemctl restart fail2ban
27
28# SSH hardening (/etc/ssh/sshd_config):
29# Port 2222 (change to non-standard port)
30# PermitRootLogin no
31# PubkeyAuthentication yes
32# PasswordAuthentication no
33# ClientAliveInterval 300
34# ClientAliveCountMax 2
35# AllowUsers deploy
🔥

pro tip

Consider changing the SSH port from 22 to a higher port (e.g., 2222). This dramatically reduces automated attack traffic in your logs. If you do, remember to update your UFW rules and any monitoring/backup scripts that connect via SSH.
Nginx Reverse Proxy

Nginx sits in front of your application server, handling TLS termination, static file serving, load balancing, and request routing. It is significantly more efficient than handling these concerns in application code.

nginx-config.sh
Bash
1# Install Nginx
2apt install -y nginx
3
4# Create Nginx config for your app
5cat > /etc/nginx/sites-available/my-app << 'EOF'
6server {
7 listen 80;
8 server_name example.com www.example.com;
9
10 # Redirect all HTTP to HTTPS (after SSL setup)
11 return 301 https://$server_name$request_uri;
12}
13
14server {
15 listen 443 ssl http2;
16 server_name example.com www.example.com;
17
18 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
19 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
20
21 # SSL configuration
22 ssl_protocols TLSv1.2 TLSv1.3;
23 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
24 ssl_prefer_server_ciphers on;
25 ssl_session_cache shared:SSL:10m;
26 ssl_session_timeout 10m;
27
28 # Security headers
29 add_header X-Frame-Options "DENY" always;
30 add_header X-Content-Type-Options "nosniff" always;
31 add_header Referrer-Policy "strict-origin-when-cross-origin" always;
32 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
33
34 # Gzip compression
35 gzip on;
36 gzip_types text/plain text/css application/json application/javascript;
37
38 # Static files (Next.js public folder)
39 location /_next/static/ {
40 alias /var/www/my-app/.next/static/;
41 expires max;
42 add_header Cache-Control "public, immutable";
43 }
44
45 location /public/ {
46 alias /var/www/my-app/public/;
47 expires 30d;
48 }
49
50 # Reverse proxy to Node.js app
51 location / {
52 proxy_pass http://127.0.0.1:3000;
53 proxy_http_version 1.1;
54 proxy_set_header Upgrade $http_upgrade;
55 proxy_set_header Connection 'upgrade';
56 proxy_set_header Host $host;
57 proxy_set_header X-Real-IP $remote_addr;
58 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
59 proxy_set_header X-Forwarded-Proto $scheme;
60 proxy_cache_bypass $http_upgrade;
61 }
62}
63EOF
64
65# Enable the site
66ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/
67rm /etc/nginx/sites-enabled/default
68
69# Test configuration
70nginx -t
71
72# Restart Nginx
73systemctl restart nginx

best practice

Always test your Nginx config with nginx -t before restarting. A syntax error will prevent Nginx from starting, taking your site offline. The http2 flag on the listen directive enables HTTP/2 for multiplexed connections and faster page loads.
Let's Encrypt SSL (Certbot)

Certbot automates SSL certificate issuance and renewal from Let's Encrypt, a free certificate authority trusted by all major browsers.

ssl-certbot.sh
Bash
1# Install Certbot
2apt install -y certbot python3-certbot-nginx
3
4# Obtain certificate (automatically modifies Nginx config)
5certbot --nginx -d example.com -d www.example.com
6
7# Test auto-renewal
8certbot renew --dry-run
9
10# Check expiration dates
11certbot certificates
12
13# Manual certificate renewal (if auto-renewal fails)
14certbot renew
15
16# Auto-renewal is configured by default via systemd timer:
17systemctl list-timers | grep certbot
18
19# Or via cron (if not using systemd):
20# 0 3 * * * /usr/bin/certbot renew --quiet

info

Certbot certificates expire after 90 days. The automatic renewal timer runs twice daily and will renew certificates expiring within 30 days. Ensure port 80 is accessible for the HTTP-01 challenge — this is required for automatic renewal.
PM2 Process Manager

PM2 is a production process manager for Node.js applications. It keeps your app alive forever, reloads without downtime, and provides a built-in load balancer for multi-core systems.

pm2-setup.sh
Bash
1# Install PM2 globally
2npm install -g pm2
3
4# Start your application
5pm2 start npm --name "my-app" -- start
6
7# Start with specific options
8pm2 start ecosystem.config.js
9
10# PM2 ecosystem file
11cat > ecosystem.config.js << 'EOF'
12module.exports = {
13 apps: [{
14 name: 'my-app',
15 script: 'npm',
16 args: 'start',
17 cwd: '/var/www/my-app',
18 instances: 'max', // Use all CPU cores
19 exec_mode: 'cluster', // Cluster mode for load balancing
20 env: {
21 NODE_ENV: 'production',
22 PORT: 3000,
23 },
24 max_memory_restart: '500M', // Restart if memory exceeds 500MB
25 log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
26 error_file: '/var/log/pm2/my-app-error.log',
27 out_file: '/var/log/pm2/my-app-out.log',
28 merge_logs: true,
29 }],
30};
31EOF
32
33# Save PM2 process list (auto-restart on reboot)
34pm2 save
35pm2 startup systemd
36
37# Useful PM2 commands
38pm2 status # List all processes
39pm2 logs my-app # View logs
40pm2 monit # Monitor CPU/memory
41pm2 reload my-app # Zero-downtime reload
42pm2 restart my-app # Restart (brief downtime)
43pm2 stop my-app # Stop process
44pm2 delete my-app # Remove from PM2
🔥

pro tip

Use ecosystem.config.js for version-controlled PM2 configuration. The clustermode spawns one process per CPU core, distributing HTTP requests across all available cores. Combined with Nginx's reverse proxy, this maximizes your server's throughput.
Zero-Downtime Deployments

Zero-downtime deployment ensures your application remains available during updates. The process: build on the server, then use PM2's reload (graceful restart) which rotates workers one at a time.

deploy.sh
Bash
1#!/bin/bash
2# deploy.sh — Zero-downtime deployment script
3
4set -e
5
6APP_DIR="/var/www/my-app"
7REPO_URL="git@github.com:myorg/my-app.git"
8BRANCH="main"
9
10echo "=== Starting deployment ==="
11
12# 1. Pull latest code
13cd $APP_DIR
14git pull origin $BRANCH
15
16# 2. Install dependencies
17npm ci --only=production
18
19# 3. Build the application
20npm run build
21
22# 4. Run database migrations (if applicable)
23# npm run migrate
24
25# 5. Reload application (zero-downtime)
26pm2 reload ecosystem.config.js
27
28# 6. Run smoke tests
29sleep 5
30curl -f http://localhost:3000/api/health
31
32# 7. Clean up old builds
33# Keep last 2 builds for rollback
34pm2 list
35
36echo "=== Deployment complete ==="

warning

Zero-downtime deployments require that the old and new versions of your app can run side-by-side during the transition. Database schema changes must be backward-compatible — never remove columns or tables that the old version still references. Use backward-compatible migrations or expand-contract patterns.
Automatic Security Updates

Unattended-upgrades automatically install critical security patches without manual intervention. This is essential for keeping your server protected against known vulnerabilities.

unattended-upgrades.sh
Bash
1# Install unattended-upgrades
2apt install -y unattended-upgrades
3
4# Configure
5dpkg-reconfigure -plow unattended-upgrades
6
7# Configuration file: /etc/apt/apt.conf.d/50unattended-upgrades
8# Key settings:
9Unattended-Upgrade::Allowed-Origins {
10 "${distro_id}:${distro_codename}-security";
11 "${distro_id}ESMApps:${distro_codename}-apps-security";
12};
13
14Unattended-Upgrade::AutoFixInterruptedDpkg "true";
15Unattended-Upgrade::MinimalSteps "true";
16Unattended-Upgrade::Mail "admin@example.com";
17Unattended-Upgrade::AutomaticReboot "false";
18Unattended-Upgrade::Remove-Unused-Dependency "true";
19
20# View auto-update log
21cat /var/log/unattended-upgrades/unattended-upgrades.log
$Blueprint — Engineering Documentation·Section ID: SERVER-01·Revision: 1.0