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 and production docs.
Single-node Docker Compose
The repository root ships the production deployment: a Dockerfile, docker-compose.yml, .env.example, and a baked config (examples/lockwell.production.toml).
git clone https://github.com/KelpHect/lockwell.git
cd lockwell
cp .env.example .envSet the initial S3 credentials in .env. The deploy fails fast if they are unset, so there is never a default credential:
# 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.1Generate strong secrets with openssl rand -hex 32. Then bring it up:
docker compose up -d --buildThis builds the static binary (CGO_ENABLED=0, debian-slim, non-root uid 1000), runs an idempotent first-boot bootstrap (creates the root tenant, the root S3 access key, and optionally the first admin user), then starts lockwelld. Restarts never clobber your credentials. Verify:
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 lockwellThe 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.
ssh -L 9001:127.0.0.1:9001 youruser@yourhost
# then open http://localhost:9001/admin and scrape http://localhost:9001/metricsThis 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.
TLS posture
The public port binds plaintext inside the container. Lockwell expects TLS to be terminated by a reverse proxy in front of it.
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.
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.
Open only 443 to the internet:
ufw default deny incoming
ufw allow 22/tcp # SSH
ufw allow 443/tcp # HTTPS (reverse proxy)
ufw enableWhere 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.
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. :::
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).
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.binRestore 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:
docker compose stop lockwell
# lockwell metadata-restore -c /etc/lockwell/lockwell.toml --from <file> --yes
docker compose start lockwellBoth 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.
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 arollback-master-key-rewrapcompanion). - 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, andrun.
Both are CLI maintenance workflows that never print plaintext secrets. See the repository CLI reference for the exact flags and operator contract.
Updating
git pull
docker compose up -d --buildCompose recreates the container with the new image 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 docs for the full TOML reference.
Honest scope
- Single-node. This is a single-node deployment: one container, one volume, the embedded engine. There is no built-in clustering or multi-node replication. 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. Credential-free object access is only ever via a scoped, time-limited signed URL.
Next steps
- The app kit. Point your app at the public + admin endpoints.
- Data operations. The native API on the public listener.
- Tenancy and auth. The Admin API on the private listener.