Complete Documentation Guide

SafeDeployer Documentation

Learn how to set up zero-downtime deployments, configure your docker-compose.yml file, integrate with CI/CD, and troubleshoot server environments.

1Overview & Architecture

SafeDeployer is a zero-downtime deployment orchestrator for single-server VPS instances (DigitalOcean, AWS EC2, Hetzner, GCP, Linode). It runs zero code on your VPS and requires zero server-side build steps, allowing your CI/CD runner to push pre-built Docker containers directly from GHCR or Docker Hub.

💡 Zero-Downtime Traffic Swapping: SafeDeployer parses your docker-compose.yml file, allocates dynamic host ports for the new release, verifies health checks, updates proxy upstreams (Nginx, Traefik, or Caddy), and tears down the old container only after 100% successful verification.
# How SafeDeployer Swaps Traffic
1. Active Container: backend-current (port 3001) → Traffic 100%
2. SafeDeployer boots: backend-new (port 37335) → Target Verification
3. Run Health Checks: GET /health → Status 200 OK
4. Hot-Reload Proxy: Switch Nginx/Traefik upstream to backend-new
5. Teardown: Safely stop & clean up backend-current

Quick Start

Get up and running with SafeDeployer in seconds. This section provides a rapid deployment guide for developers who want to jump straight in.

Step 1: Install SafeDeployer

Install the CLI tool and export your API token. This is required to authorize deployments.

curl -fsSL https://safedeployer.com/api/install | bash
sd-deploy set --token "your_api_token"

Step 2: Initialize Reverse Proxy

Automatically provision and configure a reverse proxy of your choice (Caddy, Nginx, or Traefik). This step sets up the proxy globally so it can be auto-injected into your deployments.

# Choose proxy: caddy, nginx, or traefik. Modes: docker or host
sd-deploy init --proxy caddy --mode docker
Migrating an existing server? If you already have Nginx installed on your VM, you can run sd-deploy init --proxy nginx --mode host. Read how to migrate from your existing host Nginx system →

Step 3: Prepare docker-compose.yml

Because you initialized the proxy globally, you do not need to manually configure the router block. SafeDeployer auto-injects it for you!

services: backend: image: ghcr.io/myorg/sd-backend:${IMAGE_TAG} ports: - "5300:5300" x-safedeployer: health_check: path: "/api/v1/health" interval_seconds: 3 retries: 20 canary: enabled: true steps: - percentage: 20 duration: "5m"
Tip: Notice that target_service is missing from the x-safedeployer block. If your compose file has only a single service, SafeDeployer automatically detects and deploys it! You only need target_service when dealing with multi-container configurations.

Canary ConfigurationEnterprise

The canary block enables progressive traffic routing. Instead of a hard cutover, SafeDeployer gradually shifts percentage-based traffic to the new version to validate stability over time.

  • percentage (integer): The amount of traffic to route to the new version in this step.
  • duration (string): Time to wait before advancing to the next step or completing the deployment (e.g., 5m, 10s).

Step 4: Deploy

Run the up command. SafeDeployer handles the zero-downtime deployment and automatically uses the proxy initialized in Step 2.

sd-deploy up
Note: If your compose file has a different name or is located in another directory, you can pass the path explicitly using the --config flag (e.g., sd-deploy up --config path/to/docker-compose.prod.yml).

2CLI Installation & Privilege Auto-Detection

Run the official 1-line installer on your remote Linux server:

curl -fsSL https://safedeployer.com/api/install | bash
Root / Passwordless Sudo

Installs to /usr/local/bin/sd-deploy. Automatically available in standard system PATH for all SSH shells.

Unprivileged Non-Root

Installs to $HOME/.local/bin/sd-deploy with zero password prompts. Falls back gracefully to /tmp if home is read-only.

Telemetry Daemon (Zero-Config)

SafeDeployer uses a background telemetry daemon to continuously stream logs, deployment events, and health metrics to your dashboard. You don't need to manually configure it! When you trigger a deployment via sd-deploy up, the CLI will automatically discover an available port and spawn the daemon in the background if it isn't already running.

Managing the Daemon

If you need to manually start, stop, or check the status of the daemon, you can use the built-in management commands. Note that you must set your API token before starting it manually:

sd-deploy set --token "your_api_token_here" # Start the background daemon explicitly sd-deploy daemon start # Check daemon health, port, and PID sd-deploy daemon status # Gracefully terminate the daemon sd-deploy daemon stop

3Universal CI/CD Workflow Setup

Copy and paste this production-ready workflow step into your .github/workflows/deploy.yml:

- name: Trigger Remote Zero-Downtime Deployment
uses: appleboy/[email protected]
with:
host: ${{ secrets.DROPLET_IP }}
username: ${{ secrets.DROPLET_USER }}
key: ${{ secrets.DROPLET_SSH_KEY }}
script: |
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
sd-deploy set --token "$${{ secrets.SAFEDEPLOYER_API_TOKEN }}"
# 1. Auto-install SafeDeployer on remote server if missing
if ! command -v sd-deploy &> /dev/null; then
curl -fsSL https://safedeployer.com/api/install | bash
fi
# 2. Pull pre-built image and execute zero-downtime deployment
cd /opt/safedeployer/backend
REPO_LOWER=$(echo "$${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
docker pull ghcr.io/$${REPO_LOWER}:$${{ github.sha }}
sd-deploy up --config docker-compose.yml --tag $${{ github.sha }}
Why export PATH is inside the if-block: Non-interactive SSH shells don't load .bashrc in memory during execution. Putting export PATH inside the first-time installation block ensures that newly downloaded binaries are immediately executable in the running shell process without requiring a manual server re-login. On subsequent deployments, the entire block is skipped automatically!

Graceful Shutdown Guide

To guarantee that no requests are droppedduring a deployment, your application must fulfill the "Graceful Shutdown Contract."

When SafeDeployer is ready to decommission your old container, it sends a SIGTERM signal and waits for a grace period. Your app should finish active requests and then exit cleanly. SafeDeployer already routed all new traffic to the new container, so you don't need to worry about manually rejecting new connections.
const
express
= require(
'express'
);
const
app
= express();

const
server
= app.listen(
3000
);

// Listen for the SIGTERM signal

process.on(
'SIGTERM'
, () => {
  server.close(() => {
    
// Clean up DB, WebSockets, etc.

    process.exit(
0
);
  });
});

Handling WebSockets

WebSockets are persistent. When your app receives SIGTERM, it should send a close frame (e.g., 1001 Going Away) to all connected clients. The frontend must implement automatic reconnection so it seamlessly routes to the new container.

CLI Commands Reference

SafeDeployer provides CLI commands for orchestrating zero-downtime deployments and inspecting state:

CommandDescriptionExample Usage
sd-deploy initInitializes the SafeDeployer environment by provisioning a reverse proxy. Accepts --proxy (caddy, nginx, traefik, none) and --mode (docker, host).sd-deploy init --proxy caddy --mode host
sd-deploy upOrchestrates zero-downtime deployment, verifies target health, swaps proxy upstreams, and cleans up old containers.sd-deploy up --tag v1.0.4
sd-deploy doctorAnalyzes your docker-compose.yml file to ensure your x-safedeployer configuration and target services are correctly formatted.sd-deploy doctor -c docker-compose.prod.yml
sd-deploy setPersists configuration flags globally (such as the API token) so you do not need to export environment variables on every session.sd-deploy set --token <token>
sd-deploy daemon [start|stop|status]Manages the background telemetry daemon. The daemon monitors system health and streams telemetry to your dashboard. Use `start` to manually spin it up, `stop` to terminate, or `status` to view its auto-assigned internal port and PID.sd-deploy daemon status
sd-deploy upgradeManually checks GitHub for a newer version of the CLI and automatically downloads and installs it. Note: sd-deploy up runs this in the background automatically.sd-deploy upgrade
sd-deploy statusReads the local .safedeployer-state.yaml file to output the currently active deployment environment (blue or green) and the active image tag version. (Note: This shows the application's deployment status, not the telemetry daemon's status).sd-deploy status

CLI Flags & Options Reference

All available flag options accepted by sd-deploy up:

FlagDefaultDescription
--config, -cdocker-compose.ymlPath to the docker-compose YAML file containing service definitions and x-safedeployer metadata.
--tag, -tlatestTarget image commit SHA or release tag version to deploy for the target container.
--state, -s.safedeployer-state.yamlFile path on host disk where active color, ports, and version state metadata are tracked.
--token""API token for Enterprise features (telemetry, canaries). Can also be provided via SAFEDEPLOYER_API_TOKEN.
--target""Override target service defined in x-safedeployer.
--verbosetrueShow container logs if a pre-flight health check or deployment fails. Disable with --verbose=false.
--tail100The number of log lines to stream when --verbose is enabled.
--daemonfalseRun in persistent daemon mode.
--serve""Launches internal HTTP API & SSE telemetry server (e.g., :7474) for Enterprise dashboard syncing.
--localfalseForces installation or execution in user home space ($HOME/.local/bin).
--no-auto-upgradefalseDisables the automatic background check and upgrade to the latest CLI version. Can also be set via export SAFEDEPLOYER_AUTO_UPGRADE=false.
--version, -v-Prints the CLI version and build tag details.

docker-compose.yml Configuration Schema

Add an x-safedeployer top-level extension block to your docker-compose.yml file:

Building Locally (No Docker Registry)

If you are building your images directly on your server instead of pushing them to a registry, you must still include an explicit image: property for your target service so SafeDeployer knows which image to use. You must also run docker compose build to pre-build the image before running sd-deploy up.

services:
backend:
image: ghcr.io/myorg/sd-backend:$${IMAGE_TAG}
ports:
- "5300:5300"
x-safedeployer:
health_check:
path: "/api/v1/health"
interval_seconds: 3
retries: 20
polling:
enabled: true
interval: "15s"
router:
provider: "nginx" # Other options include "caddy", "traefik"
is_host: true # true = Host Nginx | false = Containerized Nginx
upstream_name: "saas_backend_upstream"
config_path: "/etc/nginx/conf.d/upstream.conf"
analytics: # Premium feature
enabled: true
provider: "datadog"
datadog_api_key: "your_api_key"
canary: # Enterprise feature
enabled: true
steps:
- percentage: 20
  duration: "5m"
hooks: # Available on all plans
before_deploy: ["npm run db:migrate"]
after_deploy: ["npm run cache:sync"]

Health Check Configuration

The health_check block defines how SafeDeployer determines if your new application version is successfully running before shifting traffic.

  • path (string): The internal API endpoint that SafeDeployer will continuously poll (e.g., /api/v1/health). The endpoint must return an HTTP 2xx status code.
  • interval_seconds (integer): Time to wait between each health check ping during deployment.
  • retries (integer): The maximum number of consecutive failed attempts before SafeDeployer marks the deployment as failed and initiates an automatic rollback.
  • polling (object): Configuration for the post-deployment background daemon polling loop. By default it is enabled with an interval of 15 seconds. Example: enabled: true and interval: "1m".
Note on Logs:Since SafeDeployer actively polls your endpoint post-deployment, you might see continuous HTTP requests in your application logs. SafeDeployer cannot suppress these from the outside, so it is highly recommended to sanitize your logs by filtering out the health check route within your application's logger configuration.

Reverse Proxy Setup (Nginx, Caddy, Traefik)

SafeDeployer natively integrates with multiple reverse proxies. Configure your provider via the provider field in your configuration.

Where do these values come from?
  • upstream_name (optional): An arbitrary identifier chosen by you. SafeDeployer uses this name to generate the proxy configuration. You must reference this exact name in your main proxy routing rules.
  • admin_url: The endpoint where Caddy's REST API is running. By default, Caddy exposes this locally on http://localhost:2019.
  • dynamic_config_path: The absolute file path that your Traefik instance is configured to watch for file-based configuration updates.
Nginx (Host or Container)

Set provider: "nginx". Works out of the box with standard upstream configurations.

config_path (optional): Defaults to /etc/nginx/conf.d/upstream.conf
is_host (optional): Defaults to false (Container). Set to true if running on host.
upstream_name (optional): Defaults to "<project_name>-upstream".
nginx_container_name (optional): Defaults to "nginx" in Container mode.
Caddy (API-Driven)

Set provider: "caddy". Uses Caddy's REST API to dynamically swap upstreams in memory.

admin_url (optional): Defaults to http://localhost:2019
is_host (optional): Defaults to false (Container). Set to true if running on host.
Traefik (File Provider)

Set provider: "traefik". Leverages the file provider for dynamic target updates.

dynamic_config_path (optional): Defaults to /etc/traefik/dynamic_conf.yml
upstream_name (optional): Defaults to "<project_name>-upstream".
is_host (optional): Defaults to false (Container). Set to true if running on host.
Auto-generated upstream.conf
upstream saas_backend_upstream {
  server 127.0.0.1:<DYNAMIC_PORT>; # SafeDeployer injects the active port
}
Your site config (e.g. sites-available/api) should then include:
location / {
  proxy_pass http://saas_backend_upstream;
}

Automatic Rollbacks

SafeDeployer has built-in automatic rollbacks when a deployment fails its post-deployment health checks. Because SafeDeployer spins up the new version of your app alongside the old one, the old container is never dismantled until the new container has fully passed its health checks and traffic has been successfully shifted.

If a health check fails (e.g., your app fails to boot or returns a 500 status), SafeDeployer aborts the deployment and instantly routes any shifted traffic back to the original, untouched container. The failed new container is then safely removed, leaving your active environment completely unaffected with zero downtime.

Analytics Configuration (Premium)

The analytics block enables deployment telemetry logging and tracking.

  • enabled (boolean): Enable or disable analytics events.
  • provider (string): The observability platform to send metrics to (e.g., "datadog").
  • datadog_api_key (string): Your Datadog API key for authentication.

Canary Configuration (Enterprise)

The canary block lets you implement gradual, safe rollouts based on time and performance metrics, catching issues before they affect 100% of your users.

  • enabled (boolean): Enable gradual canary rollouts.
  • steps (array): An array defining the rollout schedule. Each step requires a percentage of traffic (e.g., 20) and a duration to wait (e.g., "5m").

Deployment Hooks

The hooks block allows you to execute arbitrary shell scripts (e.g. database migrations, cache synchronization) securely inside the running container before or after traffic shifting occurs. Hooks run on-premise on your own infrastructure and are available on all plans, including the free tier.

  • before_deploy (array): A list of shell commands to execute inside the newly provisioned container before routing traffic. By default, this executes before the pre-flight health checks.
  • after_deploy (array): Commands to execute after traffic has been 100% shifted to the new version.
  • before_deploy_after_health_check (boolean): If set to true, the before_deploy hooks will run after the health check passes, just before the traffic swap.
  • rollback_on_after_deploy_failure (boolean): Defaults to true. If an after_deploy script fails, SafeDeployer will automatically rollback traffic to the previous version.

Licensing & Unauthenticated Fallback Behavior

SafeDeployer features (both zero-downtime swapping and Enterprise gradual rollout features) require an active subscription token (SAFEDEPLOYER_API_TOKEN).

Dynamic Variable Injection During Fallback

If no token is set or subscription has expired, sd-deploy bypasses zero-downtime swapping and falls back to traditional docker compose up -d. SafeDeployer automatically scans your docker-compose.yml file using dynamic regex extraction and populates any image variable placeholders (e.g. $${IMAGE_TAG}, $${TAG}, $${VERSION}, or custom variable names) with your --tag value.

⚠️ Safe deployment features disabled: No active subscription detected.
💡 Subscribe or get started for free at https://safedeployer.com
Resorting to traditional deployment (bypassing zero-downtime safety checks).

Troubleshooting & Common Solutions

Issue: sd-deploy: command not found or sd-deploy re-installs on every push

Cause: Non-interactive SSH sessions (e.g. appleboy/ssh-action) do not load interactive ~/.bashrc files. If export PATH is placed inside the if ! command -v block, future SSH sessions will not find $HOME/.local/bin in their PATH and will re-trigger the installer on every workflow push.

# Solution: Place export PATH at the TOP of your SSH script block before checking command -v
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"

if ! command -v sd-deploy &> /dev/null; then
  curl -fsSL https://safedeployer.com/api/install | bash
fi
Issue: Daemon crashes with Exit 1 and logs show Traditional deployment failed: open docker-compose.yml: no such file or directory

Cause: You ran sd-deploy daemon without exporting a valid SAFEDEPLOYER_API_TOKEN (e.g., using the placeholder your_api_token_here). When the CLI fails to authenticate, it falls back to basic unauthenticated mode. In this mode, the daemon is bypassed and it immediately tries to run a traditional deployment in the current directory. Since there is no docker-compose.yml present, it fails and exits.

# Solution: Export a valid API token from your dashboard before running the daemon
sd-deploy set --token "sd_live_..."
nohup sd-deploy daemon --serve :7474 > daemon.log 2>&1 &
Issue: duplicate upstream "saas_backend_upstream" in /etc/nginx/conf.d/upstream.conf

Cause: Manually adding include /etc/nginx/conf.d/upstream.conf; inside a site config file (e.g. sites-available/default) when /etc/nginx/nginx.conf already loads include /etc/nginx/conf.d/*.conf; globally.

# Solution: Remove duplicate include line from site config file
# Simply use proxy_pass http://saas_backend_upstream; directly inside location blocks.
Issue: No such container: nginx-proxy

Cause: is_host: false configured when Nginx is installed directly on the VPS host OS instead of inside a Docker container.

# Solution: Set is_host: true in docker-compose.yml under x-safedeployer.router
router:
  provider: "nginx"
  is_host: true
  config_path: "/etc/nginx/conf.d/upstream.conf"
Issue: open ./nginx/upstream.conf: no such file or directory

Cause: The ./nginx folder does not exist yet on the remote server host filesystem.

# Solution: Ensure directory creation in your CI step or server setup
mkdir -p /opt/safedeployer/backend/nginx
Issue: unable to get image ... invalid reference format

Cause: Standard docker compose fallback had unpopulated tag environment variables.

Solution: SafeDeployer v1.0.0-beta dynamically scans docker-compose.yml for all custom variable placeholders and populates them automatically. Ensure you are running v1.0.0-beta or higher.

Issue: sudo: a terminal is required to read the password

Solution: SafeDeployer's installer automatically detects non-interactive TTY shells and passwordless sudo capabilities (`sudo -n true`). If sudo requires a password, it automatically falls back to $HOME/.local/bin with zero password prompts.

Issue: open /etc/nginx/conf.d/upstream.conf: permission denied

Cause: Non-root deployment user (e.g. ubuntu or CI deploy user) does not have write permissions to root-owned /etc/nginx/conf.d/upstream.conf.

# Solution: Grant write permissions on the file to your deployment user once on the VPS
sudo touch /etc/nginx/conf.d/upstream.conf
sudo chown $USER:$USER /etc/nginx/conf.d/upstream.conf
sudo chmod 664 /etc/nginx/conf.d/upstream.conf
Issue: cannot load certificate "/etc/letsencrypt/live/.../fullchain.pem": Permission denied

Cause: When a non-root deployment user triggers Nginx reload without root privileges, Nginx cannot re-read root-protected Let's Encrypt SSL certificates (0700 permission on /etc/letsencrypt/live).

# Solution: Grant passwordless sudo Nginx reload privileges to your deployment user once on the 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
Issue: client version 1.43 is too old. Minimum supported API version is 1.44

Cause: The Docker Daemon on your server was upgraded and now requires API version 1.44+, but you are running an outdated sd-deploy CLI binary compiled against an older SDK.

# Solution: Upgrade your sd-deploy binary by re-running the install script on your server
curl -fsSL https://safedeployer.com/api/install | bash

Need help with your deployment setup?

Our team is available to assist with custom proxy routing, multi-container layouts, or enterprise plan activation.