Skip to content

useteploy/teploy-cli

Repository files navigation

teploy

Zero-downtime Docker deploys to any server via SSH.
Single binary. No management server. No dependencies.

Release CI License


Why teploy?

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.4
teploy deploy

Your app is live with HTTPS, zero-downtime deploys, and automatic rollback on failure.

Install

# 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

Quickstart

# 1. Provision your server (installs Docker + Caddy)
teploy setup <your-server-ip>

# 2. Deploy
teploy deploy

No 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.

How it works

  1. Builds your app (Dockerfile auto-detection or Nixpacks)
  2. Starts a new container alongside the old one
  3. Health checks the new container
  4. Routes traffic via Caddy (automatic HTTPS)
  5. Stops the old container — zero downtime
  6. Rolls back automatically if anything fails

Features

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

Config

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.age

TOML is also supported (teploy.toml).

Commands

Core

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

App lifecycle

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

Secrets and env

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

Fleet

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

Accessories

teploy accessory list              # list running accessories
teploy accessory start <name>      # start accessory (Postgres, Redis, etc.)
teploy accessory stop / logs / upgrade / backup / restore <name>

Shared KV (config / feature flags)

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.

Previews

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

Backups

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.

Managed secrets (OpenBao provider)

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|transit hold 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.env in the app.
  • Tamper-evident audit: secret audit ship/enable forwards every secret access into observe's hash-chained audit trail — access is cryptographically verifiable, and can't be silently altered.

Auto-deploy

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 on

A 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.

Mesh access (JIT grants)

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.

Registry and templates

teploy registry login / list / remove   # manage container registry credentials
teploy template list / info / deploy    # community app templates

Multi-server deploys

app: myapp
domain: myapp.com
servers:
  - app1
  - app2
  - app3
parallel: 2
teploy 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/tags

Staged rollouts (canary + failure budget)

rollout:
  canary: 1            # or "10%" — first wave, deployed serially
  max_failures: 0      # tolerated failures after the canary passes

With 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.

Destinations

Manage multiple environments with config overlays:

# teploy.yml         — base config
# teploy.staging.yml — staging overrides (domain, server, etc.)
teploy deploy -d staging

The overlay merges on top of base config — override only what differs per environment.

CI/CD

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.

Surviving server loss

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.

Requirements

  • A server with SSH access (any Linux VPS — Hetzner, DigitalOcean, Linode, etc.)
  • That's it. teploy setup handles the rest.

Comparison

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

License

MIT

About

Zero-downtime Docker deploys to any server via SSH. Single binary, no management server.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages