Documentation
Everything you need to deploy and operate containers with zero. From initial setup to advanced features.
Quickstart
Get your first app live in under five minutes. All you need is a Linux VPS with root access and a domain pointing to it.
1. Set up the server
On your VPS — run the install script on any Linux server (Ubuntu 22.04+ recommended):
The installer sets up Docker, prompts for your domain and email (for TLS), and starts zero. After installation, point your DNS to the server:
| Type | Name | Value | |
|---|---|---|---|
| A | example.com | Your server IP | Required |
| A | *.example.com | Your server IP | Recommended for auto-subdomains |
2. Install the CLI
On your machine — install the zero CLI locally:
3. Connect
On your machine — authentication uses SSH. If you can SSH into the server, you can use zero.
Login is per directory. zero stores a .zero/config.json file in your current working directory and automatically adds .zero/ to your .gitignore. This means you can work with multiple servers from different project directories.
4. Deploy
That's it. zero figures out the port, assigns a domain, provisions a TLS certificate, and routes traffic.
Deploying apps
Only the image is required. Everything else is inferred:
| What | How it works |
|---|---|
| Name | Last segment of the image path (ghcr.io/shipzero/demo becomes demo) |
| Port | Read from the image's EXPOSE directive, falls back to 3000 |
| Domain | <name>.<server-domain> unless --host-port is set |
| Health | TCP connection check, or HTTP GET when --health-path is set |
Deploy options
| Flag | Description | Default |
|---|---|---|
| --name | App name (overrides inferred name) | From image |
| --domain | Domain for routing and TLS | <name>.<server-domain> |
| --port | Internal container port | Auto-detect |
| --host-port | Expose directly on a host port (skips auto-domain) | - |
| --tag | Image tag to deploy | latest |
| --command | Container startup command | - |
| --volume | Volumes, comma-separated | - |
| --health-path | HTTP health check endpoint | - |
| --health-timeout | Health check timeout (e.g. 30s, 3m) | 60s |
| --env | Env vars, comma-separated (e.g. KEY=val,KEY2=val2) | - |
| --preview | Deploy as a preview environment | - |
| --ttl | Time to live for previews (e.g. 24h, 7d) | 7d |
Health checks
By default, zero checks whether the container accepts TCP connections on the detected port. For HTTP health checks, pass --health-path:
The health check runs for up to 60 seconds (configurable with --health-timeout). HTTP checks accept any response with status < 500. If the container crashes during the health check (restart loop), the deployment fails immediately.
If the health check fails, the new container is discarded and traffic stays on the previous version. For new apps, the app is removed entirely.
Host port mode
Not every app needs a domain. Use --host-port to expose a container directly on a port:
Host port mode skips domain assignment and TLS. The container is accessible at http://<server-ip>:<port>. Useful for databases, message queues, and other non-HTTP services.
Environment variables
Pass env vars inline with --env:
Or manage them separately — changes take effect on the next deploy:
Volumes
Mount persistent volumes with the --volume flag. Format: source:destination[:mode]
Private registries
Authenticate with GitHub Container Registry, Docker Hub, or any OCI-compatible registry:
Docker Compose
For multi-container apps, pass a Compose file:
| Flag | Description |
|---|---|
| --compose | Path to a docker-compose.yml file (required) |
| --service | The entry service that receives traffic (required) |
| --name | App name (required) |
| --image-prefix | Shared image prefix for tag substitution (e.g. ghcr.io/org/project) |
The Compose file is uploaded to the server. On deploy, zero pulls images, starts services, and health-checks the entry service before routing traffic.
Image prefix
When you pass --image-prefix ghcr.io/you/mystack, zero replaces the tag of every image in your Compose file that starts with that prefix. This makes --tag, webhooks, and preview deployments work for Compose apps.
Logs & metrics
myapp ██████░░░░░░░░░░░░░░ 28.3% ████████████░░░░░░░░ 312 MB / 512 MB (60.9%) 1.2 MB/s 340 KB/s
Rollback
Roll back to the previous deployment. A new container is started from the previous image and traffic swaps once healthy.
Start, stop, remove
Domains
Apps can have multiple domains. The first domain is the primary (used for preview subdomains).
The --domain flag on zero deploy sets the initial domain when creating an app. Use zero domain add for additional domains.
Deployment history
Preview deployments
Spin up a temporary version of any app. Previews get a unique subdomain (preview-<label>.<primary-domain>) and expire automatically (default: 7 days). The parent app must have at least one domain.
Expired previews are cleaned up automatically every hour. Duration format supports 24h, 7d, 14d, etc.
Webhooks
Every app gets a webhook URL and a signing secret. Push an image to your registry, zero deploys it automatically.
Getting your webhook credentials
When you deploy an app for the first time, zero shows the webhook URL and secret:
Important: Running zero webhook url rotates the secret and displays the new one. The old secret stops working immediately. Only run this when you intend to rotate.
Auto-deploy from GitHub Actions
The most common setup: your CI builds and pushes a Docker image, then notifies zero to deploy it. Add a step to your GitHub Actions workflow:
- Store the webhook secret as a GitHub repository secret (e.g.
ZERO_WEBHOOK_SECRET) - Add a deploy step after your Docker push:
Replace myapp with your app name and "latest" with the tag you pushed. For PR-based preview deploys, use "pr-${{ github.event.pull_request.number }}" as the tag.
Triggering from a script
The same request works from any environment. The body must be JSON with a tag, and the x-hub-signature-256 header must contain the HMAC-SHA256 signature of the body:
zero accepts two payload formats for the tag:
- Docker Hub:
{"push_data": {"tag": "v1.2.3"}} - GHCR:
{"package": {"package_version": {"container_metadata": {"tag": {"name": "v1.2.3"}}}}}
Tag matching and previews
When a webhook arrives, zero checks the tag against the app's tracked tag (set during deploy):
- Tag matches → triggers a deploy of the main app
- Tag doesn't match and app has a domain → creates a preview deployment automatically
- Tag doesn't match and app has no domain → ignored
If trackTag is set to any, every tag triggers a main deploy.
Secret rotation
Running zero webhook url rotates the secret. The URL stays the same, but the old secret stops working immediately. Update the secret in your registry after rotating.
Reverse proxy
No nginx. No Traefik. zero includes a built-in reverse proxy:
- Routes requests based on the
Hostheader - TLS termination with automatic certificate selection (SNI)
- WebSocket support with automatic upgrade detection (5-minute idle timeout)
- Security headers:
Strict-Transport-Security,X-Content-Type-Options,X-Frame-Options - Forwarding headers:
X-Forwarded-For,X-Real-IP,X-Forwarded-Proto - Request timeout: 60s, headers timeout: 10s
- Max body size: 100 MB (configurable via
MAX_BODY_SIZE) - Connection limits: 1024 global, 128 per IP
TLS
zero provisions and renews TLS certificates via Let's Encrypt automatically when EMAIL is set and DOMAIN is a real domain (not an IP). Certificates are provisioned on first deploy and renewed within 30 days of expiry (checked every 12 hours). HTTP requests are redirected to HTTPS.
Certificates are obtained via ACME HTTP-01 challenges. The built-in proxy handles this automatically on port 80.
Running without a domain
zero works without a domain by setting DOMAIN to your server's IP address. In this mode:
- TLS is disabled — all traffic is HTTP only
- Apps are accessible via
--host-portinstead of subdomains - Webhooks use
http://<ip>:<api-port>/webhooks/<appName> - Preview deployments are not available (they require a domain for subdomains)
This mode is functional but not recommended for production. Use a domain for HTTPS, automatic subdomains, and preview environments.
CLI reference
Aliases
Common commands have short aliases: zero ls (list), zero rm (remove), zero domain ls (domain list), zero env ls (env list), zero registry ls (registry list).
Server configuration
Configuration is stored in /opt/zero/.env:
| Variable | Description | Default |
|---|---|---|
| TOKEN | Internal auth token (do not share) | Generated |
| JWT_SECRET | Secret for signing JWT tokens | Generated |
| DOMAIN | Server domain (used for app subdomains and TLS) | Server IP |
| Let's Encrypt email (enables automatic TLS) | - | |
| API_PORT | API server port | 2020 |
| CERT_RENEW_BEFORE_DAYS | Renew certificates this many days before expiry | 30 |
| PREVIEW_TTL | Default time to live for preview deployments | 7d |
| MAX_BODY_SIZE | Maximum request body size for the reverse proxy | 100m |
Server requirements
- Linux server (Ubuntu 22.04+ recommended)
- Root access
- A domain pointing to your server (recommended for HTTPS and automatic subdomains; IP-only mode is also supported)
Upgrade
CLI and server can be upgraded independently:
You can also re-run the install script directly on the server to upgrade the server component.