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.
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.⚡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.
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.
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!
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.
--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:
Installs to /usr/local/bin/sd-deploy. Automatically available in standard system PATH for all SSH shells.
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:
3Universal CI/CD Workflow Setup
Copy and paste this production-ready workflow step into your .github/workflows/deploy.yml:
.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."
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.process.on(
server.close(() => {
process.exit(
});
});
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:
| Command | Description | Example Usage |
|---|---|---|
| sd-deploy init | Initializes 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 up | Orchestrates zero-downtime deployment, verifies target health, swaps proxy upstreams, and cleans up old containers. | sd-deploy up --tag v1.0.4 |
| sd-deploy doctor | Analyzes 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 set | Persists 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 upgrade | Manually 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 status | Reads 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:
| Flag | Default | Description |
|---|---|---|
| --config, -c | docker-compose.yml | Path to the docker-compose YAML file containing service definitions and x-safedeployer metadata. |
| --tag, -t | latest | Target image commit SHA or release tag version to deploy for the target container. |
| --state, -s | .safedeployer-state.yaml | File 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. |
| --verbose | true | Show container logs if a pre-flight health check or deployment fails. Disable with --verbose=false. |
| --tail | 100 | The number of log lines to stream when --verbose is enabled. |
| --daemon | false | Run in persistent daemon mode. |
| --serve | "" | Launches internal HTTP API & SSE telemetry server (e.g., :7474) for Enterprise dashboard syncing. |
| --local | false | Forces installation or execution in user home space ($HOME/.local/bin). |
| --no-auto-upgrade | false | Disables 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.
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: trueandinterval: "1m".
Reverse Proxy Setup (Nginx, Caddy, Traefik)
SafeDeployer natively integrates with multiple reverse proxies. Configure your provider via the provider field in your configuration.
- 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.
Set provider: "nginx". Works out of the box with standard upstream configurations.
/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.Set provider: "caddy". Uses Caddy's REST API to dynamically swap upstreams in memory.
http://localhost:2019• is_host (optional): Defaults to
false (Container). Set to true if running on host.Set provider: "traefik". Leverages the file provider for dynamic target updates.
/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.server 127.0.0.1:<DYNAMIC_PORT>; # SafeDeployer injects the active port
}
sites-available/api) should then include: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
percentageof traffic (e.g., 20) and adurationto 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, thebefore_deployhooks will run after the health check passes, just before the traffic swap. - rollback_on_after_deploy_failure (boolean): Defaults to
true. If anafter_deployscript 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.
💡 Subscribe or get started for free at https://safedeployer.com
Resorting to traditional deployment (bypassing zero-downtime safety checks).
Troubleshooting & Common Solutions
sd-deploy: command not found or sd-deploy re-installs on every pushCause: 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.
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
Exit 1 and logs show Traditional deployment failed: open docker-compose.yml: no such file or directoryCause: 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.
sd-deploy set --token "sd_live_..."
nohup sd-deploy daemon --serve :7474 > daemon.log 2>&1 &
duplicate upstream "saas_backend_upstream" in /etc/nginx/conf.d/upstream.confCause: 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.
# Simply use proxy_pass http://saas_backend_upstream; directly inside location blocks.
No such container: nginx-proxyCause: is_host: false configured when Nginx is installed directly on the VPS host OS instead of inside a Docker container.
router:
provider: "nginx"
is_host: true
config_path: "/etc/nginx/conf.d/upstream.conf"
open ./nginx/upstream.conf: no such file or directoryCause: The ./nginx folder does not exist yet on the remote server host filesystem.
mkdir -p /opt/safedeployer/backend/nginx
unable to get image ... invalid reference formatCause: 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.
sudo: a terminal is required to read the passwordSolution: 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.
open /etc/nginx/conf.d/upstream.conf: permission deniedCause: 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.
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
cannot load certificate "/etc/letsencrypt/live/.../fullchain.pem": Permission deniedCause: 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).
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
client version 1.43 is too old. Minimum supported API version is 1.44Cause: 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.
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.