Skip to main content

Production deployment

Reverse proxy​

Loch S3 speaks plain HTTP. Put a TLS reverse proxy in front and keep the S3 port off the network. Example with Caddy:

s3.example.com {
# The admin API is protected by a single key: keep it off the Internet
@admin path /_loch/users /_loch/users/*
respond @admin 403
reverse_proxy s3:8080
}

Clients use https://s3.example.com with path-style URLs. The proxy must forward the Host header unchanged: it is part of the signature.

Hardened container​

The server only writes to S3_DATA_DIR. It runs with a read-only root filesystem and no capabilities:

services:
s3:
image: ghcr.io/loch-sh/s3:1.4.1
restart: unless-stopped
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
user: "100:101"
mem_limit: 128m
pids_limit: 64
environment:
S3_USERS_FILE: /etc/s3/users.json
S3_ENCRYPTION_KEY_FILE: /run/secrets/master.key
S3_ENCRYPT_BY_DEFAULT: "true"
S3_MAX_STORAGE_BYTES: "107374182400" # 100 GiB
S3_INIT_BUCKETS: backups,media
volumes:
- /srv/s3/data:/data
- /srv/s3/conf:/etc/s3:ro
- /srv/s3/secrets:/run/secrets:ro
MountModeContent
/dataread-writeBuckets, objects, metadata. One filesystem, case-sensitive (Linux)
users fileread-onlyNever written while S3_ADMIN_API_KEY is unset
master keyread-onlyMode 0400, owned by the container uid

With a read-only users file, the file must already exist: the server exits instead of bootstrapping it. Users then change only by editing the file and restarting.

Keep the PID limit above S3_MAX_BLOCKING_THREADS (32) plus the number of CPUs plus a few.

Quota​

S3_MAX_STORAGE_BYTES caps the whole data directory: objects, versions, multipart parts in progress and metadata files. A write that would cross it fails with 403 QuotaExceeded; deletes still work. Versioned buckets reserve twice the object size.

The quota counts file sizes, not disk blocks or inodes. For a hard disk limit, also give the data directory its own volume or filesystem quota.

Health and usage​

EndpointAuthAnswer
GET or HEAD /_loch/healthnone200 {"status":"ok"}, plus "quota":"ok" or "exceeded" when a quota is set; 503 if the data directory is unreachable
GET /_loch/usageSigV4, root{"used_bytes":…,"reserved_bytes":…,"limit_bytes":…}
curl --aws-sigv4 "aws:amz:us-east-1:s3" -u "$ROOT_KEY:$ROOT_SECRET" https://s3.example.com/_loch/usage

Logs​

The server logs to standard error (docker logs): startup settings, then one line per request and per response with the method, path and status. Headers and query strings are never logged. Health probes are not logged. There is no metrics endpoint.

Shutdown​

On SIGTERM, the server stops accepting connections and gives running requests S3_SHUTDOWN_GRACE seconds (8 by default, below Docker's 10-second stop timeout). An interrupted upload leaves no object.

Good to know​

  • Slow uploads are cut: no data for 60 seconds, or less than S3_MIN_UPLOAD_RATE bytes/s (4096) over 60 seconds, ends with 400 RequestTimeout.
  • At most S3_MAX_CONNECTIONS (1024) connections at once; extra ones are closed.
  • HTTP/2 connections that answer pings never time out: keep the proxy in front.
  • Every limit: Limits. Every variable: Configuration reference.