Skip to main content
A VPS you manage yourself — a DigitalOcean droplet, an EC2 instance, a Hetzner box — differs from a managed platform in one way that matters: there is no dashboard to paste secrets into, and no build step that runs on your behalf. You own the whole boot path. Two approaches work. Pick based on whether the box should hold a deploy token or a person’s identity. The same model as every managed platform. The droplet holds two environment variables and nothing else. On your laptop, in the project directory:
On the droplet, write them to a root-owned file — not into the container, and not into the project:
Bake the CLI into your image and let it be the entrypoint:
Then run it:
Use --init so the container gets a real PID 1. capy run forwards SIGINT, SIGTERM, and SIGHUP to your process, but it is not a process reaper — pair it with Docker’s init if your app spawns children. Neither value in that file is a secret on its own. SECRETS_BLOB is inert without a live call to the Capy service, the service refuses revoked tokens, and no plaintext ever touches the droplet’s disk. To rotate: re-run capy deploy, replace the file, restart the container. To cut access: run capy deploy list to find the token’s id prefix, then capy deploy revoke <deployId>.

Option 2: Droplet-resident identity

Sometimes you want the droplet to be a member of the org — every decryption attributed to a person, secrets updated with capy the same way as on a laptop. This trades availability for auditability. Read the tradeoff before choosing it.

Logging in without a browser

capy signs in through your browser with a callback on localhost. A headless droplet has no browser, so forward the callback ports over SSH and use the browser you already have. The CLI takes the first free port in the range 1942019424, so forward all five:
Signing in is not enough on its own. A fresh droplet holds no copy of your organization’s master key, and the service cannot hand it over — so capy stops with “no encryption key on this device” until you move one across yourself. Mint a transport code on your laptop first:
Then, inside that SSH session:
capy redeem authenticates you and then installs your wrapped master key. During that sign-in the CLI prints an authentication URL and tries to open a browser, which fails on a headless box — that is expected. Paste the URL into your laptop’s browser instead. The redirect lands on your laptop’s localhost, the tunnel carries it to the droplet, and the CLI completes the flow. You have five minutes before it times out. See switching computers for the rest of the transport flow. This is a one-time step. Afterwards the droplet holds a session and refreshes it silently. That last capy asks which project the directory belongs to — pick yours rather than the New project default. Capy pulls the project’s development branch and writes keep.lock plus an encrypted .env into /srv/myapp; run capy checkout <branch> if the droplet should track a different branch. Both files are safe at rest — every value in .env is a capy:… snippet, never the full plaintext.

Wrapping the container

capy run cannot inject into a container that is already running, so it has to be what launches the container:
List the variable names your container should receive, with no values, so Compose passes them through from capy run’s environment:
Variable names are not secrets, and listing them is an explicit statement of what the container is allowed to see. Nothing plaintext is written to disk on the host or baked into the image. Deploying an update becomes two commands — capy to pull the latest secrets, then capy run -- docker compose up -d.
You may be tempted to bind-mount ~/.capy into the container and run capy run as the container’s entrypoint instead. That works, but it hands the container your full org identity — a container compromise becomes an account compromise. Keeping the CLI on the host means the container only ever receives its own variables.

Which one

The deciding question is what happens when the person leaves. With a droplet identity, their session ends and the next capy run on the box fails — you cannot deploy or restart the app through Capy until someone else’s identity is on the droplet. Production is coupled to an individual’s account. Deploy tokens have no such coupling, which is why they are the default recommendation for anything load-bearing. Both modes call the Capy service once, when capy run starts: inside the container on every container start with a deploy token, on the host at each deploy with a droplet identity. If that call fails, nothing launches — in exchange, revocation takes effect on the very next capy run rather than whenever a cached key expires. Once your process is running, a service outage is invisible to it.

See also

Docker

Entrypoint patterns, Compose, and Kubernetes.

capy run

What each mode needs at startup.
Last modified on August 11, 2026