# Deployment

Lockwell runs as a **single container** backed by **one named volume**: the S3 and native APIs, the embedded BadgerDB
metadata engine, and always-on at-rest encryption, all in one process. There is no external database, message broker, or
cache to run alongside it.

This page covers the single-node Docker Compose deployment, the two-listener model, where the master key and data live,
TLS posture, and backup/restore on the embedded engine. For the authoritative, full procedures, see the repository
[deployment docs](https://github.com/RusticStack/lockwell/tree/main/docs/deployment) and
[production docs](https://github.com/RusticStack/lockwell/tree/main/docs/production).

## Single-node Docker Compose

The [`lockwell-deploy`](https://github.com/RusticStack/lockwell-deploy) repository ships everything needed to run a
node: a `Dockerfile` and `docker-compose.yml` that assemble a thin runtime image from the official prebuilt binary, an
`.env.example`, the production config (`examples/lockwell.production.toml`), and reverse-proxy examples. **No source is
compiled**: the image build only downloads and checksum-verifies the released static binary for your architecture
(`linux/amd64` or `linux/arm64`).

```bash
git clone https://github.com/RusticStack/lockwell-deploy.git
cd lockwell-deploy
cp .env.example .env
```

Set the initial S3 credentials in `.env`. The deploy **fails fast** if they are unset, so there is never a default
credential:

```ini
# Initial S3 access key, created on first boot (idempotent).
LOCKWELL_ROOT_ACCESS_KEY_ID=lockwell-admin
LOCKWELL_ROOT_SECRET_KEY=change-me-to-a-long-random-secret-min-16-chars
LOCKWELL_ROOT_TENANT=root

# Optional first admin web-UI login (separate from the S3 key above).
LOCKWELL_ADMIN_USERNAME=admin
LOCKWELL_ADMIN_PASSWORD=change-me-to-a-different-long-random-secret
LOCKWELL_ADMIN_ROLE=owner

# Published host ports. /metrics is UNAUTHENTICATED, so bind the admin port to localhost.
LOCKWELL_S3_PORT=9000
LOCKWELL_ADMIN_PORT=9001
LOCKWELL_ADMIN_BIND=127.0.0.1
```

Generate strong secrets with `openssl rand -hex 32`. Then bring it up:

```bash
docker compose up -d --build
```

The `--build` step assembles a thin runtime image from the official static binary (`debian-slim`, non-root uid 1000). It
downloads and checksum-verifies the released binary, compiling no source. Compose then runs an **idempotent first-boot
bootstrap** (creates the root tenant, the root S3 access key, and optionally the first admin user) and starts
`lockwelld`. Restarts never clobber your credentials. Verify:

```bash
curl -fsS http://127.0.0.1:9000/health   # always-200 liveness on the public port
curl -fsS http://127.0.0.1:9000/readyz   # deep readiness; pings the metadata engine + storage
docker compose logs -f lockwell
```

The bootstrap must run before the daemon because the embedded engine takes an **exclusive single-process directory
lock**. Only the daemon may hold the store open.

## The two listeners

| Port     | Bind (default)        | Surface                                                                                 |
| -------- | --------------------- | --------------------------------------------------------------------------------------- |
| **9000** | published (public)    | S3 API, the native API (`/api/v1`), bearer token mint, signed URLs, `/health` `/readyz` |
| **9001** | `127.0.0.1` (private) | Admin web UI (`/admin`), JSON Admin API (`/admin/api/v1`), Prometheus `/metrics`        |

Put a TLS-terminating reverse proxy in front of port **9000**. **Keep port 9001 private.** `/metrics` is
**unauthenticated**. Reach the admin port over an SSH tunnel or a firewalled private network, and never attach a public
domain to it.

```bash
ssh -L 9001:127.0.0.1:9001 youruser@yourhost
# then open http://localhost:9001/admin and scrape http://localhost:9001/metrics
```

This split is why the SDK takes two endpoints. The data planes (S3 + native) live on 9000; the Admin API and app-kit
provisioning live on 9001. See [Installation](/guide/installation#prerequisites).

## TLS posture

The public port binds **plaintext inside the container**. Lockwell expects TLS to be terminated by a reverse proxy in
front of it.

::: info TLS is the proxy's job by design Lockwell does not terminate TLS itself. This keeps certificate handling out of
the daemon and lets you use whatever proxy you already run. Always front the public port with a TLS-terminating proxy in
production. :::

Two object-storage concerns matter at the proxy: raise the body-size limit (large PUTs and multipart parts), and
**stream** the request body rather than buffering it to disk.

```text
s3.example.com {
  reverse_proxy 127.0.0.1:9000
  request_body { max_size 5GB }
}
```

Caddy provisions Let's Encrypt certificates automatically and streams bodies by default. With nginx, set
`client_max_body_size`, `proxy_request_buffering off`, and forward `Host` / `X-Forwarded-Proto` so SigV4 and signed URLs
see the original host. The full proxy recipes (Caddy and nginx) and firewall policy are in the
[Docker Compose deployment guide](https://github.com/RusticStack/lockwell/tree/main/docs/deployment).

Open only `443` to the internet:

```bash
ufw default deny incoming
ufw allow 22/tcp     # SSH
ufw allow 443/tcp    # HTTPS (reverse proxy)
ufw enable
```

## Where the master key and data live

The entire state (object blobs, the embedded metadata engine, and the auto-generated at-rest **master key**) lives in
the single `lockwell-data` volume mounted at `/var/lib/lockwell`. The master key is written there during the first-boot
bootstrap.

**Losing the master key makes the encrypted data unrecoverable**, so back it up out of band. For stronger separation,
mount the key from a secret on a path outside the data volume via `LOCKWELL_MASTER_KEY_FILE`.

::: warning Back up the master key separately The master key lives in the same volume as the data. A volume snapshot
alone is not key separation. Copy the key to a different location, or mount it from a secret with
`LOCKWELL_MASTER_KEY_FILE`, so a data-volume loss is recoverable. :::

## Data residency and local placement

Lockwell does not choose cloud regions or fan object bytes out to a remote storage service. Residency comes from where
you run `lockwelld`, where `storage.data_dir` lives, and where backups plus key escrow are stored. `server.region` is a
SigV4/S3 compatibility label, not a placement or replication engine.

For SaaS, pin the Lockwell node, volume/blob store, metadata store, backups, and data-encryption key custody to
EU/Portugal-approved infrastructure and expose only that endpoint to the ERP. For on-prem/server and desktop-local
installs, point `storage.data_dir` at the customer-controlled local volume and keep backups, the master key, and the
data-encryption key directory in the same customer custody boundary. There are no remote-backend or external-KMS storage
network calls by default.

## Backup and restore

Take an **online** metadata backup from the running daemon (no downtime). The CLI is an S3 client: it signs a request to
the daemon with **admin-scoped** credentials and writes the metadata to `--out` (object blobs are captured separately
via the data volume).

```bash
docker compose exec \
  -e AWS_ACCESS_KEY_ID=lockwell-admin \
  -e AWS_SECRET_ACCESS_KEY=change-me-to-a-long-random-secret-min-16-chars \
  lockwell lockwell metadata-backup \
    --endpoint http://127.0.0.1:9000 \
    --out /var/lib/lockwell/metadata-backup.bin
```

**Restore is offline.** The embedded engine takes an exclusive single-process directory lock, and the restore loads the
backup into a **fresh** data dir, so the daemon must be stopped first:

```bash
docker compose stop lockwell
# lockwell metadata-restore -c /etc/lockwell/lockwell.toml --from <file> --yes
docker compose start lockwell
```

Both work directly on the embedded engine. There is no external migration step and nothing to dump/restore from a
separate database. For the full procedure (blob backup, master-key handling, and `lockwell backup-verify`), see the
[backup-restore docs](https://github.com/RusticStack/lockwell/tree/main/docs).

## Key rewrap

Lockwell rotates encryption keys on the embedded engine without downtime for reads:

- **Access-key master rewrap** re-encrypts stored access-key secrets under a new master key
  (`lockwell access-keys rewrap-master-key`, with a backup file and a `rollback-master-key-rewrap` companion).
- **Tenant data-key rewrap** re-encrypts a tenant's objects under a fresh data key as a tracked, resumable job:
  `lockwell keys rewrap plan` (read-only), `start`, `status`, and `run`.

Both are CLI maintenance workflows that never print plaintext secrets. See the repository CLI reference for the exact
flags and operator contract.

## Updating

Bump `LOCKWELL_VERSION` in `.env` to the new release, then:

```bash
git pull
docker compose up -d --build
```

Compose rebuilds the image from the new binary and reattaches the existing `lockwell-data` volume. The metadata engine
opens **in place** with no external migration step. Watch the logs and confirm `/readyz` returns 200.

## Sizing

The embedded engine and single static binary keep the footprint small. A 1 vCPU, 1 GiB VPS comfortably runs a small
workload. The dominant memory tunable is `[metadata].block_cache_mb` (64 to 128 MiB is a good range with encryption on).
Choose a durability tier with `[metadata].sync_interval_ms` and `[storage].relaxed_durability` (STRICT by default: an
acked write survives sudden power loss). Dedup and compression are ON in the baked config, reducing physical bytes. See
the production [configuration](https://github.com/RusticStack/lockwell/tree/main/docs/production) docs for the full TOML
reference.

## Honest scope

- **Single-node recipe.** This page deploys the single-node profile: one container, one volume, the embedded engine.
  An opt-in Raft consensus mode (`[cluster.consensus]`) exists for three-node replicated authority — it passes the live
  acceptance gate but is not yet an approved release profile and is outside this recipe's scope. Single-node durability
  comes from the durability tier plus your own volume snapshots and backups.
- **TLS is your proxy's job.** Lockwell binds plaintext behind a reverse proxy. It does not terminate TLS itself.
- **No public/anonymous buckets, no SSE-KMS, no IAM/STS.** These are deliberate non-goals. See
  [the three surfaces](/guide/the-three-surfaces). Credential-free object access is only ever via a scoped, time-limited
  [signed URL](/guide/signed-urls).

## Next steps

- [The app kit](/guide/app-kit). Point your app at the public + admin endpoints.
- [Data operations](/guide/data-operations). The native API on the public listener.
- [Tenancy and auth](/guide/tenancy-and-auth). The Admin API on the private listener.
