How to Migrate an Existing Host Nginx Setup to SafeDeployer with Zero Downtime
A step-by-step guide to upgrading your existing VPS host Nginx configuration (SSL certificates, WebSockets, and multi-environment setups) to SafeDeployer zero-downtime blue/green deployments.
A very common production architecture across web applications and SaaS backends is running Nginx directly on a VPS host machine (such as a DigitalOcean Droplet, AWS EC2, or Hetzner VPS). In this setup, host Nginx terminates SSL certificates (via Let’s Encrypt / Certbot), proxies traffic to local Docker containers, and handles WebSockets and REST APIs.
Traditionally, backend deployments on these servers require rebuilding a container or running docker compose up -d, resulting in temporary downtime, dropped WebSocket connections, and 502 Bad Gateway errors during the swap.
In this guide, we’ll walk through how to migrate your existing host Nginx setup to SafeDeployer for zero-downtime Blue/Green deployments—while preserving all your existing Let’s Encrypt SSL certificates, domain routing, and multi-environment isolation (e.g., Dev/Test and Production running side-by-side).
How the Architecture Changes
| Aspect | ❌ Traditional Hardcoded Setup | ✅ SafeDeployer Dynamic Setup |
|---|---|---|
| Proxy Target | Static host port (e.g., proxy_pass http://localhost:3001;) | Named upstream block (e.g., proxy_pass http://dev_api_upstream;) |
| Deployment Swap | Container is torn down and recreated (downtime & 502s) | New container boots in parallel on dynamic port, passes health checks, proxy hot-reloads |
| SSL / Let’s Encrypt | Bound to host Nginx | Untouched; host Nginx continues managing certificates seamlessly |
| Rollbacks | Manual container rollback | Automated instant rollback if new container fails health checks |
Step 1: Create Dedicated Upstream Files & Permissions
Because Nginx is installed directly on your host OS, SafeDeployer writes the dynamic upstream configuration directly to the host filesystem.
When managing multiple services or environments (such as Dev/Test and Production) on the same host, create separate upstream configuration files inside /etc/nginx/conf.d/ for each environment:
# 1. For Dev / Test environment
sudo touch /etc/nginx/conf.d/dev-api-upstream.conf
sudo chown $USER:$USER /etc/nginx/conf.d/dev-api-upstream.conf
sudo chmod 664 /etc/nginx/conf.d/dev-api-upstream.conf
# 2. For Production environment (if on the same VPS)
sudo touch /etc/nginx/conf.d/prod-api-upstream.conf
sudo chown $USER:$USER /etc/nginx/conf.d/prod-api-upstream.conf
sudo chmod 664 /etc/nginx/conf.d/prod-api-upstream.conf
Note on Permissions: Assigning ownership to your deployment user (
$USER:$USER) allows SafeDeployer to update upstreams during CI/CD pushes without requiring root permissions for file writes.
Seed Initial Placeholder Upstreams
To ensure nginx -t passes before your first SafeDeployer deployment runs, seed each file with your current running container port:
/etc/nginx/conf.d/dev-api-upstream.conf:
upstream dev_api_upstream {
server 127.0.0.1:3001;
}
/etc/nginx/conf.d/prod-api-upstream.conf:
upstream prod_api_upstream {
server 127.0.0.1:3000;
}
(Debian/Ubuntu /etc/nginx/nginx.conf automatically includes include /etc/nginx/conf.d/*.conf; in the global http context by default).
Step 2: Update Your Existing Nginx Site Configuration
Open your site configuration file (e.g. /etc/nginx/sites-available/test-api.my-app.com or /etc/nginx/sites-available/dev-api).
Replace all instances of proxy_pass http://localhost:3001; with proxy_pass http://dev_api_upstream;. Your SSL certificates, WebSocket upgrade headers, and custom routes remain completely untouched:
server {
server_name dev-api.my-app.com;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/api.my-app.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.my-app.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
return 444;
}
# REST API Endpoint
location /api/v1/ {
proxy_pass http://dev_api_upstream;
proxy_http_version 1.1;
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;
}
# WebSockets / Socket.io / Real-Time Relays
location /socket.io/ {
proxy_pass http://dev_api_upstream;
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_cache_bypass $http_upgrade;
}
# Custom WebSocket or Relay Endpoints
location /vynrelay/ {
proxy_pass http://dev_api_upstream;
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_cache_bypass $http_upgrade;
}
# API Documentation / Swagger
location /api/docs {
proxy_pass http://dev_api_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
if ($host = dev-api.my-app.com) {
return 301 https://$host$request_uri;
}
server_name dev-api.my-app.com;
listen 80;
return 444;
}
Validate and reload Nginx:
sudo nginx -t && sudo systemctl reload nginx
Step 3: Configure SafeDeployer in docker-compose.yml
In your project repository, add the x-safedeployer configuration block with is_host: true:
services:
my-app-test-api:
image: my-app-test-api:latest
ports:
- "127.0.0.1:3001:3001"
restart: always
x-safedeployer:
project_name: "my-app-test-api"
health_check:
path: "/api/v1/health"
timeout_seconds: 5
interval_seconds: 3
retries: 15
router:
provider: "nginx"
is_host: true # Instructs SafeDeployer to manage host Nginx
upstream_name: "dev_api_upstream" # Must match the proxy_pass target in Nginx
config_path: "/etc/nginx/conf.d/dev-api-upstream.conf" # Direct path on host OS
Step 4: Grant Passwordless Sudo Reloads for CI/CD
When deploying via automated CI/CD pipelines (such as GitHub Actions SSH actions with an ubuntu or deploy user), Nginx reload requires elevated privileges to re-read root-protected Let’s Encrypt SSL certificates (0700 permissions on /etc/letsencrypt/live).
Configure passwordless sudo Nginx reload once on your VPS:
echo "$USER ALL=(ALL) NOPASSWD: /usr/sbin/nginx -s reload, /bin/systemctl reload nginx" | sudo tee /etc/sudoers.d/safedeployer-nginx
sudo chmod 0440 /etc/sudoers.d/safedeployer-nginx
Step 5: Automate via CI/CD (GitHub Actions)
In your GitHub Actions workflow (.github/workflows/deploy.yml), invoke SafeDeployer using sd-deploy up:
name: Deploy Test API
on:
push:
branches: [ staging, dev ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS via SSH
uses: appleboy/[email protected]
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
# 1. Ensure CLI is in PATH
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
# 2. Install SafeDeployer if not already present
if ! command -v sd-deploy &> /dev/null; then
curl -fsSL https://safedeployer.com/api/install | bash
fi
cd /home/deploy/apps/test-backend
# 3. Build latest container image
docker compose -f docker-compose.test.yml build my-app-test-api
# 4. Trigger zero-downtime blue/green deployment
export SAFEDEPLOYER_API_TOKEN="${{ secrets.SAFEDEPLOYER_API_TOKEN }}"
sd-deploy up --config docker-compose.test.yml --token "${{ secrets.SAFEDEPLOYER_API_TOKEN }}"
# Clean up old dangling images
docker image prune -f
How SafeDeployer Executes Zero-Downtime Swaps
When sd-deploy up runs:
- Parallel Provisioning: SafeDeployer identifies the inactive color (
green) and starts the new container on an allocated local port (e.g.,127.0.0.1:3002). - Health Verification: SafeDeployer polls
/api/v1/healthuntil the application confirms it is fully booted and ready. - Atomic Upstream Switch: SafeDeployer overwrites
/etc/nginx/conf.d/dev-api-upstream.confwithserver 127.0.0.1:3002;and triggers a zero-downtime Nginx reload. - Graceful Teardown: The old
bluecontainer is cleanly stopped after in-flight requests finish processing. - Multi-Environment Safety: The production environment (
/etc/nginx/conf.d/prod-api-upstream.conf) remains completely isolated and untouched.
Summary
Migrating from a hardcoded host Nginx proxy to SafeDeployer takes less than 5 minutes and requires zero architectural overhauls. You keep your existing VPS setup, Let’s Encrypt certificates, and Docker Compose files, while gaining automated blue/green switches, zero downtime releases, and automatic rollback protection.
Discussion