Deployment
What you'll do: run the standalone OpenBucket container in production, with a persistent data volume, real secrets, and health checks — then put it behind TLS.
The minimal working deployment
A docker-compose.yml with one persistent volume is the fastest real deployment:
services:
openbucket:
image: ghcr.io/projectbay/openbucket:latest
restart: unless-stopped
ports:
- "9000:9000"
env_file:
- .env
environment:
DATA_DIR: /data # the mounted volume — overrides any DATA_DIR in .env
volumes:
- openbucket-data:/data
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:9000/api/admin/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
volumes:
openbucket-data:
Generate the secrets, then start it:
# 1. argon2id hash for the admin password
node scripts/hash-password.mjs 'choose-a-strong-password'
# 2. fill in .env (see the required vars below), then:
docker compose up -d
OpenBucket now listens on http://localhost:9000:
- S3 API —
http://localhost:9000(path-style) - Admin console —
http://localhost:9000/admin - Admin API —
http://localhost:9000/api/admin - Health / readiness —
/api/admin/health,/api/admin/ready
The standalone server image is published to ghcr.io/<owner>/openbucket on
every v* release tag, with :latest, :{major}.{minor}, and exact :{version}
tags (plus linux/amd64 and linux/arm64). Pin an exact version in production;
use :latest only for trials. To build locally instead, use the repo's
Dockerfile (docker compose up --build).
Required environment
OpenBucket validates its environment on boot and refuses to start if anything is missing or weak. The five required variables:
| Variable | Notes |
|---|---|
DATA_DIR | Absolute path to the data volume (no trailing slash). Holds the SQLite DB, blobs, and sse.key. |
JWT_SECRET | ≥ 32 chars, high-entropy. Signs admin JWTs. openssl rand -base64 48. |
ADMIN_PASSWORD_HASH | argon2id hash — node scripts/hash-password.mjs '<password>'. |
ROOT_ACCESS_KEY_ID | 16–32 uppercase alphanumerics. |
ROOT_SECRET_ACCESS_KEY | ≥ 32 chars, high-entropy. |
Common optional ones:
| Variable | Default | Notes |
|---|---|---|
PORT | 9000 | HTTP listen port. |
ADMIN_USERNAME | admin | Admin login. |
OPENBUCKET_REGION | us-east-1 | Region reported to clients; match it in your SDK. |
OPENBUCKET_SSE_KEY | generated | base64 of 32 bytes; else generated to <DATA_DIR>/sse.key. |
KEY_ENCRYPTION_SECRET | root secret | KEK for scoped sub-key secrets. Set it to decouple from the root key. |
SHUTDOWN_DRAIN_MS | 30000 | Grace period to drain in-flight requests on SIGTERM. |
The full, commented list lives in .env.example at the repo root (webhooks,
replication, tiering, backups, metrics, analytics, and the DoS-guard limits). See
the configuration reference for every key.
A short, all-same-character, or known-placeholder value for JWT_SECRET /
ROOT_SECRET_ACCESS_KEY is rejected at startup (not a warning — a hard refusal).
Generate real random values.
The data volume
Everything OpenBucket persists lives under DATA_DIR — see
storage layout. Two rules:
- Mount all of
DATA_DIRas one volume. The atomic write path renames fromDATA_DIR/tmpintoDATA_DIR/blobs, which is only atomic within a single filesystem. Don't bind-mount subdirectories separately. - Back it up as a unit. The SQLite DB and the blob tree must stay consistent with each other; snapshot them together (or use the built-in scheduled backups).
The container runs as a non-root user and exposes port 9000, so the volume must be writable by that user.
Behind a reverse proxy (TLS)
Terminate TLS at a proxy (nginx, Caddy, Traefik, an ALB) and forward to port 9000. OpenBucket speaks the S3 protocol, so the proxy must not buffer or rewrite request bodies, and must forward the client's scheme and host so SigV4 and presigned URLs verify:
server {
listen 443 ssl;
server_name storage.example.com;
# SigV4 signs the Host + path; presigned URLs sign the scheme too.
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Raw, unbuffered bodies — the S3 protocol needs them, and large uploads
# must not be spooled to disk by the proxy.
proxy_request_buffering off;
client_max_body_size 0; # don't cap upload size at the proxy
location / {
proxy_pass http://127.0.0.1:9000;
}
}
Set OPENBUCKET_ENDPOINT=storage.example.com so the store reports a DNS-safe
public hostname for endpoint discovery.
When you embed @openbucket/nestjs instead of running the container, everything
mounts under mountPath (default /storage) — the S3 endpoint is
http(s)://<host>/storage, the admin API is <mountPath>/api/admin, and the
console is <mountPath>/admin. Your proxy just needs to forward that path prefix
to your app. See the NestJS module reference.
Running behind a reverse proxy at a subpath
To expose OpenBucket at a subpath — https://example.com/storage/… instead
of a dedicated host or subdomain — set MOUNT_PATH
on the container. Every surface moves under the prefix in lockstep: S3 becomes
https://example.com/storage/<bucket>/<key>, the admin API
https://example.com/storage/api/admin, the console
https://example.com/storage/admin (its <base href> is rewritten to match),
and the health/metrics endpoints follow too.
docker run -d --name openbucket \
-e MOUNT_PATH=/storage \
-e DATA_DIR=/data \
--env-file .env \
-v openbucket-data:/data \
ghcr.io/projectbay/openbucket:latest
Forward the prefix as-is — do not strip it, because SigV4 signs the full request path and the store verifies over the same path:
location /storage/ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_request_buffering off;
client_max_body_size 0;
proxy_pass http://127.0.0.1:9000; # note: no trailing slash — keeps /storage/… intact
}
Point your S3 client's endpoint at https://example.com/storage with path-style
addressing, and probe health at https://example.com/storage/api/admin/health.
The prefix comes from MOUNT_PATH, not from a forwarded header — so a
misconfigured proxy can't relocate the admin API out from under its guard. Leave
MOUNT_PATH unset to keep everything at the root, exactly as before. It is the
standalone twin of OpenBucketModule.forRoot({ mountPath }).
A minimal Kubernetes sketch
OpenBucket is single-node (one writer over SQLite + a local blob tree), so run it as a single-replica Deployment (or a StatefulSet) with a persistent volume — not a horizontally-scaled Deployment.
apiVersion: apps/v1
kind: Deployment
metadata:
name: openbucket
spec:
replicas: 1 # single-node — do NOT scale out
strategy:
type: Recreate # never two pods on the one RWO volume
selector:
matchLabels: { app: openbucket }
template:
metadata:
labels: { app: openbucket }
spec:
containers:
- name: openbucket
image: ghcr.io/projectbay/openbucket:latest
ports:
- containerPort: 9000
envFrom:
- secretRef: { name: openbucket-secrets } # JWT_SECRET, ROOT_*, ADMIN_PASSWORD_HASH…
env:
- name: DATA_DIR
value: /data
volumeMounts:
- name: data
mountPath: /data
livenessProbe:
httpGet: { path: /api/admin/health, port: 9000 }
periodSeconds: 30
readinessProbe:
httpGet: { path: /api/admin/ready, port: 9000 }
periodSeconds: 10
volumes:
- name: data
persistentVolumeClaim:
claimName: openbucket-data # a ReadWriteOnce PVC
Call app.enableShutdownHooks() (the standalone image already does) so a
SIGTERM from a rolling update drains in-flight requests before the process exits.
Health checks
Two unauthenticated probes are built in — orchestrators hit them without credentials:
| Endpoint | Meaning | Response |
|---|---|---|
GET /api/admin/health | Liveness — the process is up and the event loop responds. | 200 { "status": "ok", "uptime": <s> } |
GET /api/admin/ready | Readiness — the process can serve traffic now. | 200 { "status": "ready" }, or 503 { "status": "draining" } during shutdown. |
Wire liveness to /api/admin/health and readiness to /api/admin/ready so a
draining pod is pulled from the load balancer before it stops accepting work.
(Under an embedded mountPath, these are at <mountPath>/api/admin/health and
/ready.)
Next steps
- Monitoring — probes, Prometheus metrics, structured logs, and the audit log.
- Upgrading — how to move between versions safely.
- Storage layout — what's on the data volume.
- Configuration reference — every environment variable and option.