Zero-downtime Docker deploys to any server via SSH.
Single binary. No management server. No dependencies.
Most deploy tools require either a management server (Coolify, Dokploy) or complex configuration (Kamal). Teploy is a single binary that deploys Docker containers to any server you can SSH into. Three lines of config, one command to deploy.
# teploy.yml
app: myapp
domain: myapp.com
server: 1.2.3.4teploy deployYour app is live with HTTPS, zero-downtime deploys, and automatic rollback on failure.
# macOS (Homebrew)
brew install useteploy/tap/teploy
# Windows (Scoop)
scoop bucket add teploy https://github.com/useteploy/scoop-bucket
scoop install teploy
# Download binary (macOS/Linux)
curl -fsSL https://github.com/useteploy/teploy-cli/releases/latest/download/teploy_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/').tar.gz | tar xz
sudo mv teploy /usr/local/bin/
# From source
go install github.com/useteploy/teploy/cmd/teploy@latest# 1. Provision your server (installs Docker + Caddy)
teploy setup <your-server-ip>
# 2. Deploy
teploy deployNo config file needed to start: running teploy deploy in a fresh project
offers a short interactive setup (app name, domain or raw port, server) and
writes the resulting 3-line teploy.yml before deploying — so your config is
always a real file you can read, diff, and check in, never hidden state.
Prefer to answer the questions up front? teploy init runs the same flow
standalone.
- Builds your app (Dockerfile auto-detection or Nixpacks)
- Starts a new container alongside the old one
- Health checks the new container
- Routes traffic via Caddy (automatic HTTPS)
- Stops the old container — zero downtime
- Rolls back automatically if anything fails
| Feature | Description |
|---|---|
| Zero-downtime deploys | New container starts and passes health checks before old one stops |
| Automatic HTTPS | Caddy provisions and renews TLS certificates |
| Rollback | teploy rollback reverts to the previous version instantly |
| Multi-process | Run web, worker, and scheduler from the same image |
| Accessories | Manage Postgres, Redis, etc. alongside your app |
| Environment variables | teploy env set KEY=value — stored securely on server |
| Encrypted secrets | teploy secret set KEY value — encrypted at rest |
| Preview environments | teploy preview <branch> — deploy branches to temporary URLs |
| Multi-server | Deploy to multiple servers with parallel rolling deploys |
| Load balancing | Automatic Caddy LB config across app servers |
| Maintenance mode | teploy maintenance on — instant 503 page |
| Deploy hooks | Run migrations before deploy, seed after deploy |
| Asset bridging | Shared volume prevents 404s on hashed assets during deploys |
| Destinations | teploy deploy -d staging — per-environment config overlays |
| Notifications | Webhook notifications on deploy/rollback/failure |
| Backups | teploy backup — volumes to S3 |
| Templates | One-command deploys for common self-hosted apps |
| Deploy locking | teploy lock — freeze deploys during incidents |
| Vulnerability gate | scan: true — Trivy scan blocks deploys on fixable CRITICALs |
| Host audit | setup --harden wires auditd + sudo session recording (ausearch -k teploy-exec, sudoreplay) |
| JIT mesh access | teploy network grant --ttl 2h — time-boxed keys that auto-revoke |
Minimum config is 3 lines. Everything else is optional:
app: myapp
domain: myapp.com
server: 1.2.3.4
port: 3000
build_local: true
platform: linux/amd64
stop_timeout: 30
keep_versions: 3 # auto-prune older versions after deploy (0 = keep all, default)
# `teploy pin <version>` protects a version from this prune
# Build a Dockerfile that lives in a subdirectory (monorepos). Omit both
# to use ./Dockerfile with the project root as context (the default).
# `dockerfile` is resolved relative to `context`; `context` is relative to
# this teploy.yml. Example: a Dockerfile under server/ that COPYs shared
# packages from the repo root —
# context: .
# dockerfile: server/monolith/Dockerfile
# Or build just one workspace of a monorepo —
# context: apps/api
# These apply only when Teploy builds the image (no `image:` set).
# Custom TLS cert — e.g. Cloudflare Origin Certificate behind a CF-proxied
# domain where ACME can't reach the origin. Cert + key are LOCAL file paths,
# uploaded to the server on deploy. Default is ACME (automatic HTTPS).
tls:
cert: ./certs/origin.crt
key: ./certs/origin.key
# Routing layer. Default "caddy" — Teploy writes the Caddyfile. "external"
# means you front the app with Cloudflare Tunnel / Tailscale Funnel / nginx
# / ALB / etc., and Teploy must not touch Caddy. The container still joins
# the teploy network with its app-name alias so the external thing can
# reach it. "host" publishes the app directly on a fixed host port (no
# Caddy, no proxy) — reach it at IP:port. Handy for a private/tailnet box.
ingress: caddy # "caddy" (default), "external", or "host"
# Only with ingress: host. The host IP to publish `port` on (default
# 0.0.0.0 — all interfaces). A fixed host port can't be zero-downtime
# blue/green, so host ingress deploys by recreate (stop old, start new):
# a few seconds of downtime per deploy for a stable, directly-reachable
# port. Single replica only; container rollback isn't available (the prior
# container is removed) — redeploy the previous version instead.
# bind: 0.0.0.0
# Edge hardening for Caddy-fronted apps (reverse-proxied / load-balanced).
# The lightweight, Caddy-native slice — not rate limiting (needs a custom
# Caddy build) or a full WAF (front with Cloudflare for that). Blocked
# requests get a 403. Only with ingress: caddy; rejected on static/external.
firewall:
allow_ips: ["203.0.113.0/24", "198.51.100.7"] # if set, ONLY these may connect
deny_ips: ["9.9.9.9"] # blocklist (IPs or CIDRs)
block_user_agents: ["masscan", "badbot"] # case-insensitive substring
max_body_size: 10MB # request body cap
# IP rules match the DIRECT connection (Caddy's remote_ip). That's the real
# client when Teploy's Caddy faces the internet (the default). If you front it
# with Cloudflare/another proxy, remote_ip is the PROXY's IP — use that proxy's
# own IP rules instead, or set trusted_proxies via caddy_extra.
# Put the app behind a login — the self-hostable "deployment protection".
# Caddy ingress + reverse-proxy only. Use basic_auth (bcrypt hashes; generate
# with `caddy hash-password`) and/or forward_auth to your own identity proxy
# (Authelia, oauth2-proxy, OIDC gateway).
access:
basic_auth:
alice: "$2a$14$...bcrypt-hash..."
# forward_auth:
# url: authelia:9091
# uri: /api/authz/forward-auth
# copy_headers: [Remote-User, Remote-Email, Remote-Groups]
# Record deploys/rollbacks in a teploy-observe audit trail (compliance).
# Fire-and-forget; a failed emit never fails a deploy. token is an observe
# editor+ credential. Omit the block to disable.
audit:
endpoint: https://observe.example.com
token: ${OBSERVE_AUDIT_TOKEN}
site: default
volumes:
data: /app/data
processes:
web: "npm start"
worker: "npm run worker"
# Per-process HEALTHCHECK overrides. disable: true passes --no-healthcheck
# so the container ignores the image's HEALTHCHECK — useful when a worker
# shares an image with web but has no HTTP listener for the inherited probe.
healthcheck:
worker:
disable: true
hooks:
pre_deploy: "npm run migrate"
post_deploy: "npm run seed"
# Vulnerability gate: trivy-scan the image on the server before containers
# start. HIGH+CRITICAL findings are reported; fixable CRITICALs block the
# deploy (unfixable base-image CVEs don't wedge you). DB caches server-side.
scan: true
accessories:
postgres:
image: postgres:16
port: 5432
env:
POSTGRES_PASSWORD: auto # generated once, persisted server-side
# or reference an encrypted secret (set with `teploy secret set DB_PASSWORD=...`);
# the app container receives the same secret, so both sides agree:
# POSTGRES_PASSWORD: secret:DB_PASSWORD
# Self-hosted S3 (object storage / backup target). `command:` overrides the
# image command — needed by images that want an explicit verb.
minio:
image: minio/minio:latest
port: 9000
command: server /data --console-address :9001
env:
MINIO_ROOT_USER: admin
MINIO_ROOT_PASSWORD: secret:MINIO_ROOT_PASSWORD
volumes:
data: /data
# Self-hosted push notifications (point `notifications:` webhooks at it).
ntfy:
image: binwiederhier/ntfy:latest
port: 80
command: serve
volumes:
cache: /var/cache/ntfy
assets:
path: /app/public/assets
keep_days: 14
notifications:
webhook: https://hooks.slack.com/services/xxx
# Sign webhook deliveries so the receiver can tell a real one from anyone who
# learned the URL. Sent as:
# X-Teploy-Timestamp: <unix seconds>
# X-Teploy-Signature: sha256=hex(HMAC-SHA256(secret, timestamp + "." + body))
# Identical to teploy-observe's scheme, so one verifier handles both. Signing
# the timestamp with the body lets a receiver bound replay. Applies to
# type: webhook only — Slack and ntfy never check these headers.
#
# Prefer `export TEPLOY_WEBHOOK_SECRET=...` over setting it here: teploy.yml
# is committed, and a signing secret in version control is not a secret. The
# config field below wins if both are set; neither means unsigned, as before.
# secret: ""
# Or multiple channels with per-event filters. type: ntfy posts
# push-notification-shaped messages to an ntfy topic — pair it with the
# self-hosted ntfy accessory above (http://<app>-ntfy/<topic> from other
# containers, or your public ntfy.sh topic) for deploy alerts on your phone.
# A channel without its own `secret` inherits the one above.
# channels:
# - type: ntfy
# url: https://ntfy.sh/my-deploys
# events: [deploy, rollback, health_failure]
# GitOps secrets: local env files merged into the container env at deploy.
# Encrypted files decrypt on YOUR machine (never plaintext in the repo or
# on the server): *.age via age (identity from TEPLOY_AGE_IDENTITY,
# SOPS_AGE_KEY_FILE, or ~/.config/teploy/age.txt), *.sops.* / *.enc.* via
# `sops -d` (any backend in your .sops.yaml — age, KMS, PGP). Later files
# and explicit env: keys win.
env_files:
- secrets.env.ageTOML is also supported (teploy.toml).
teploy init # generate config
teploy setup <server> [--yes] [--no-harden] # provision server (Docker + Caddy + firewall)
teploy deploy [server] # deploy app (reads teploy.yml)
teploy deploy --app X --image Y --domain Z # ad-hoc deploy without teploy.yml
teploy deploy -d staging # deploy with destination overlay
teploy rollback # revert to previous version
teploy stop / start / restart # container lifecycle
teploy logs [--tail N] [--process web] # stream container logs
teploy status # show running containers
teploy stats # CPU/RAM per container
teploy health # run health check on live app
teploy log # deploy history
teploy exec <server> <cmd> # run a command on the server (SSH)
teploy app exec -- <cmd> # run a command in the app container (migrations, etc.)
teploy validate # check config and server readiness
teploy scale <count> # multi-server deploy + LB update
teploy version / update # version info and self-update
teploy lock [--message "..."] # freeze deploys (incident/maintenance)
teploy unlock # release deploy lock
teploy maintenance on / off # toggle 503 maintenance page
teploy pin [version] # protect a version from keep_versions pruning (default: current)
teploy unpin <version> # remove a pin
teploy pins # list pinned versions
teploy env set KEY=value # set env var
teploy env get KEY # read env var
teploy env list [--reveal] # list env vars (masked by default)
teploy env unset KEY # remove env var
teploy secret set KEY <value> # encrypted secret
teploy secret get / list / rotate # secret management
teploy server add <name> <host> # add server to ~/.teploy/servers.yml
teploy server list / remove # fleet management
teploy network setup <provider> # VPN mesh (tailscale, headscale, netbird)
teploy network status / join # VPN management
teploy accessory list # list running accessories
teploy accessory start <name> # start accessory (Postgres, Redis, etc.)
teploy accessory stop / logs / upgrade / backup / restore <name>
teploy kv set flags/beta on # set a key (--ttl 300 for expiring keys)
teploy kv get flags/beta # print a value (exit 1 if unset)
teploy kv list 'flags/*' # list keys by glob pattern
teploy kv incr deploys/count # atomic counter, prints new value
teploy kv del / exists <key>
Operates on a Nucleus accessory
(--accessory, default nucleus) — a shared config/flag/counter store your
apps can also read directly over their DATABASE_URL (Consul-KV-style, but
riding the database you already run). One global keyspace: key prefixes are
convention, not isolation — share a store only between apps that trust each
other, and give untrusted apps their own instance.
teploy preview deploy <branch> # route subdomain to a pre-built image
teploy preview list # list active previews
teploy preview destroy <branch> # tear down a preview
teploy preview prune # remove expired previews
teploy backup create # backup volumes to S3
teploy backup list / restore <id>
teploy backup schedule # cron-driven backups
teploy backup prune # apply a retention policy
# Retention: keep only the newest N, and/or drop anything older than D days.
teploy backup create --bucket b --keep-last 7 # auto-prune after backup
teploy backup schedule "0 3 * * *" --bucket b --keep-last 7 # nightly, keep 7
teploy backup prune --bucket b --keep-last 3 --max-age-days 30
teploy backup prune --bucket b --accessory db --keep-last 5
--keep-last N is a floor (the newest N are always kept, even if old);
--max-age-days D deletes older backups but never below that floor. A failed
backup never triggers a prune. Both backup create and scheduled cron backups
alert on failure via the app's notifications config — create uses all
channels; the headless cron job posts to the first webhook channel (SMTP and
other channels need the CLI, which isn't on the server).
backup schedule uploads a small alert script to
/deployments/<app>/backup-alert.sh (mode 0700) and points cron at it, rather
than inlining a curl in the crontab line. That is what lets the alert be
signed: the signature covers a timestamp, date +%s contains a %, and crontab
treats % as a newline escape. If a signing secret is configured (see
notifications.secret / TEPLOY_WEBHOOK_SECRET) the alert carries the same
X-Teploy-Signature headers as every other delivery; otherwise it goes
unsigned, and backup schedule says so on its output so a verifying receiver
rejecting it doesn't look like a wrong secret.
teploy secret is provider-abstracted (like teploy network): the default
local provider is the built-in age store; openbao is a managed
OpenBao secrets manager — provisioned, auto-unsealed, and wired for
least-privilege access in one command. Select it with --provider openbao or
secret.provider: openbao in teploy.yml.
teploy secret set KEY=value # local (age) — the default provider
teploy secret set KEY=value --provider openbao
teploy secret get / list --provider openbao # same verbs, managed backend
teploy secret setup # provision + auto-unseal OpenBao (idempotent)
teploy secret setup --replicas 3 # HA: a 3-node Raft quorum
teploy secret setup --seal transit --transit-address ... --transit-token ... # off-box key
teploy secret put db password=s3cr3t host=pg # multi-field secret (openbao)
teploy secret status # seal/init status (+ Raft cluster in HA)
# Reference a secret from teploy.yml — fetched + injected at deploy:
# env: { DB_PASSWORD: "secret:db#password" } # multi-field
# env: { API_KEY: "secret:API_KEY" } # flat (defaults to the value field)
# Dynamic, short-lived, auto-revoked database credentials:
teploy secret db setup --db-accessory postgres --admin-pass <pw>
teploy secret db creds # on-demand ephemeral credentials
# Rotate an EXISTING db user's password on a schedule (static role):
teploy secret db static-role --username appuser --rotation-period 24h --admin-pass <pw>
teploy secret db static-creds --username appuser
# Ship secret-access events into the observe tamper-evident audit trail:
teploy secret audit ship # one-shot forward
teploy secret audit enable --interval 300 # timer: stream continuously
- Auto-unseal: a static-env-key seal (key held in the app's encrypted local
store) means OpenBao unseals itself on every restart — no manual ceremony.
--seal awskms|transithold the key off-box (KMS / a second OpenBao) for disk-theft/root-compromise protection; the credentials ride a 0600 env-file, never the config. - High availability:
--replicas 3(or 5) runs a Raft quorum. Nodes auto-unseal + auto-join; the cluster tolerates losing a node (reads and writes keep working), and a recovered node auto-rejoins. - Least privilege: each app gets a scoped AppRole (read-only on its own secrets); cross-app reads and writes are denied.
- Dynamic + static rotation: dynamic creds mint a new short-lived DB user
per request; static roles rotate an existing account's password on a schedule.
Set
secret: { provider: openbao, agent: true }to run an OpenBao Agent sidecar that renders auto-rotating creds to/vault/secrets/db.envin the app. - Tamper-evident audit:
secret audit ship/enableforwards every secret access into observe's hash-chained audit trail — access is cryptographically verifiable, and can't be silently altered.
teploy autodeploy setup # webhook-triggered auto-deploys
teploy autodeploy status / remove
For a monorepo where one repo holds several apps, restrict which pushes
redeploy this app with an autodeploy: block in teploy.yml:
autodeploy:
paths:
- "apps/web/**" # everything under apps/web/
- "packages/common/**" # a shared package this app depends onA push redeploys only if it touched a matching file. Patterns support a
trailing /** and path.Match globs (* within a segment, ?, exact
paths); shared code must be listed explicitly. When a push payload doesn't
carry a reliable file list (a large/truncated push, or a provider whose
shape we can't read), teploy deploys anyway rather than risk skipping a real
change.
teploy network grant --ttl 2h --tag tag:contractor # time-boxed key, auto-revokes
teploy network grants # list active keys
teploy network revoke <key-id> # cut a grant early
Mints ephemeral, tagged pre-auth keys via the Tailscale or Headscale API — "give the contractor two hours" without permanent credentials. What a tag can reach is your ACL policy's call; teploy never edits ACLs.
teploy registry login / list / remove # manage container registry credentials
teploy template list / info / deploy # community app templates
app: myapp
domain: myapp.com
servers:
- app1
- app2
- app3
parallel: 2teploy deploy # deploys to all servers in parallel
teploy scale 3 # deploy to 3 app-role servers + update LB
teploy deploy --role worker --tag region=eu # target a subset by role/tagsrollout:
canary: 1 # or "10%" — first wave, deployed serially
max_failures: 0 # tolerated failures after the canary passesWith a rollout: block, multi-server deploys stage in two waves. The canary
wave deploys first and must come up healthy — any canary failure halts the
rollout, rolls the canary back, and leaves the rest of the fleet untouched.
Then the main wave deploys with your parallel setting. max_failures: 0
(default) keeps today's all-or-nothing behavior: any failure rolls the whole
fleet back to the old version. With max_failures: N, every server is
attempted, up to N failures are tolerated — servers that succeeded keep the
new version and only they enter the load balancer — and the command still
exits non-zero with a named straggler list and the exact converge command.
A mixed-version fleet is never left silent.
Manage multiple environments with config overlays:
# teploy.yml — base config
# teploy.staging.yml — staging overrides (domain, server, etc.)
teploy deploy -d stagingThe overlay merges on top of base config — override only what differs per environment.
Teploy doesn't bundle a CI runner — it integrates with the one you run. To wire
push-to-deploy into Forgejo or GitHub Actions (build → test → teploy deploy),
or the no-secrets webhook alternative, see docs/ci-deploy.md.
For secrets scanning (catching a committed API key before it's in history
forever), drop the Gitleaks recipe into your Actions or a pre-commit hook —
see docs/secrets-scanning.md. For runtime secrets,
use teploy secret set or SOPS/age env_files:, never plaintext in the repo.
Teploy has no scheduler by design — resilience comes from topology: N+1
active servers for the stateless tier, state that outlives any one box
(managed/replicated DB, or scheduled + verify-backup-proven backups off
the server), and a five-command human-confirmed rebuild runbook. Read
docs/resilience.md before you need it.
- A server with SSH access (any Linux VPS — Hetzner, DigitalOcean, Linode, etc.)
- That's it.
teploy setuphandles the rest.
| teploy | Kamal | Coolify | Dokploy | |
|---|---|---|---|---|
| Management server required | No | No | Yes | Yes |
| Config lines to deploy | 3 | ~20 | GUI | GUI |
| Single binary | Yes | No (Ruby) | No | No |
| Auto HTTPS | Caddy | Kamal Proxy | Traefik | Traefik |
| Build from source | Dockerfile + Nixpacks | Dockerfile only | Nixpacks | Nixpacks |
| Preview environments | Yes | No | Yes | Yes |
| Maintenance mode | Yes | No | No | No |
| Templates | Yes | No | Yes | Yes |