> ## Documentation Index
> Fetch the complete documentation index at: https://capy.sc/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Secrets on a VPS: DigitalOcean Droplets, EC2, Hetzner

> Run capy run on a server you manage yourself. Covers deploy tokens, headless login over SSH, and wrapping docker compose so containers never hold a plaintext .env.

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

## Option 1: Deploy token (recommended)

The same model as every managed platform. The droplet holds two environment variables and nothing else.

On your laptop, in the project directory:

```bash theme={null}
capy deploy      # pick "Docker", copy SECRETS_BLOB and PROJECT_KEY
```

On the droplet, write them to a root-owned file — not into the container, and not into the project:

```bash theme={null}
sudo install -d -m 700 /etc/capy
sudo tee /etc/capy/app.env > /dev/null <<'EOF'
SECRETS_BLOB=...
PROJECT_KEY=...
EOF
sudo chmod 600 /etc/capy/app.env
```

Bake the CLI into your image and let it be the entrypoint:

```dockerfile theme={null}
FROM node:22-slim
WORKDIR /app
COPY . .
RUN npm install -g @capysc/cli
ENTRYPOINT ["capy", "run", "--"]
CMD ["node", "server.js"]
```

Then run it:

```bash theme={null}
docker run --init --env-file /etc/capy/app.env -p 3000:3000 my-app
```

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: `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](#which-one) 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 `19420`–`19424`, so forward all five:

```bash theme={null}
ssh -L 19420:localhost:19420 -L 19421:localhost:19421 \
    -L 19422:localhost:19422 -L 19423:localhost:19423 \
    -L 19424:localhost:19424 user@your-droplet
```

Inside that SSH session:

```bash theme={null}
curl -fsSL https://raw.githubusercontent.com/capysc/capy-cli/main/install.sh | bash
mkdir -p /srv/myapp && cd /srv/myapp
capy
```

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.

This is a one-time step. Afterwards the droplet holds a session and refreshes it silently.

You now have `keep.lock` and an encrypted `.env` in `/srv/myapp`. Both are safe at rest — every value in `.env` is ciphertext.

### Wrapping the container

`capy run` cannot inject into a container that is already running, so it has to be what launches the container:

```bash theme={null}
cd /srv/myapp
capy run -- docker compose up -d
```

List the variable names your container should receive, with no values, so Compose passes them through from `capy run`'s environment:

```yaml theme={null}
services:
  app:
    build: .
    environment:
      - DATABASE_URL
      - STRIPE_SECRET_KEY
    ports:
      - "3000:3000"
```

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

<Warning>
  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.
</Warning>

## Which one

|                           | Deploy token                  | Droplet identity                 |
| ------------------------- | ----------------------------- | -------------------------------- |
| Boot depends on           | A revocable token             | A person's session staying valid |
| Audit trail               | Per deploy token              | Per person, per decryption       |
| Updating secrets          | Re-run `capy deploy`, restart | `capy`, then restart             |
| Login required on the box | No                            | Yes, once, over an SSH tunnel    |

The deciding question is what happens when the person leaves. With a droplet identity, their session ends and containers stop booting on their next restart — 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 at container start. If that call fails the container does not start, in exchange for revocation that takes effect on the very next boot rather than whenever a cached key expires. Once running, a service outage is invisible to your process.

## See also

<Columns cols={2}>
  <Card title="Docker" icon="docker" href="/docs/using/deploying/docker" horizontal>
    Entrypoint patterns, Compose, and Kubernetes.
  </Card>

  <Card title="capy run" icon="terminal" href="/docs/cli/run" horizontal>
    What each mode needs at startup.
  </Card>
</Columns>
