Release

XNS Relayer

Self‑host an S3‑compatible storage gateway backed by a decentralized provider network.

by ScPrime · SCP Corp
Current build: v2.29.1 (149e4d63) — 2026-11-03

Already running an earlier build? See Updating to a new build — no data loss.

If you're stuck or the dashboard doesn't come up, ask for help before reaching for a clean reinstall: docker compose down -v permanently deletes your relayer's data — its database, configuration, claim, and any stored objects — and you'll re-claim from scratch. Treat it as a last resort, not a fix.

What this is

⌄

The XNS Relayer is an S3‑compatible storage gateway. You point any S3 client at it and it spreads your data across a decentralized network of independent storage providers using erasure coding — 80 data + 40 parity shards across 120 hosts, so your files survive even when a lot of those hosts don't.

Run it on your own machine, get a local S3 endpoint, and use it like you'd use any S3 bucket.

System requirements

⌄
Operating system
Win / Mac / LinuxWindows 10/11, macOS 12+, Ubuntu 22.04+
Memory
8 GB4 GB minimum
Free disk
10 GBapp + local database
Network
Ports 8888 + 9000 + 9443free on your machine

Port 8888 is the web dashboard, 9000 is the S3 API (plain HTTP), and 9443 is the encrypted S3 endpoint that turns on if you install a TLS certificate. You'll install Docker in the next step — that's the only software you need.

What throughput to expect: on the 2-vCPU reference hardware this build is tested against, a single upload stream (S3 PUT) moves roughly 5–7 MiB/s. The bottleneck is CPU, not your network — machines with more CPU headroom will see higher rates.

Know what you're about to expose

⌄

Before you install, pick the deployment profile that matches your situation. Each profile shows what every port is for, who needs to reach it, and the consequence if the wrong party reaches it. This takes one minute and saves you from guessing later.

A personal machine — your laptop, desktop, or a single-user dev box. You are the only operator and the only user.

Port What it is Who needs to reach it If the wrong party reaches it
8888 Web dashboard (management UI) You, from this machine or your LAN They see and can manage your storage — treat this like an admin panel
9000 S3 API (plain HTTP) Your S3 clients (local scripts, dev tools) They can read/write your stored objects if they have credentials
9443 S3 API (HTTPS, once a certificate is installed) Same clients, over an encrypted channel Same as port 9000 — credentials still control access, but the connection is encrypted

On a workstation, the default binding (0.0.0.0, meaning all interfaces) is usually fine — only your LAN can reach these ports. If your machine is directly on the public internet without a NAT, consider narrowing the dashboard bind (see below).

A machine shared with other users or services — a team dev server, a small office server, or a VM with multiple tenants.

Port What it is Who needs to reach it If the wrong party reaches it
8888 Web dashboard (management UI) Authorized operators on your network Other users on the same host or LAN see the admin interface — they can manage storage, view configurations, and change settings
9000 S3 API (plain HTTP) Applications and users with S3 credentials Credential-protected, but traffic is unencrypted — other users on the network can observe the data in transit
9443 S3 API (HTTPS, once a certificate is installed) Same applications, with transport encryption Same credential protection as 9000, but the connection is encrypted — recommended for shared environments

On a shared host, consider narrowing the dashboard bind to 127.0.0.1 and accessing it through an SSH tunnel or reverse proxy. This prevents other users on the LAN from reaching the management interface directly.

A dedicated server in a production or managed environment — a rack-mount, a cloud VM in a VPC, or an MSP-managed node behind a proxy. Multiple operators or tenants may share the infrastructure.

Port What it is Who needs to reach it If the wrong party reaches it
8888 Web dashboard (management UI) Designated operators only, via a controlled network path (VPN, bastion host, or reverse proxy with NO_ORIGIN_DECLARED_PROXY_CIDRS) Full administrative access to the node — storage management, configuration changes, certificate operations, and operator credential surfaces. In a multi-tenant deployment this is a cross-tenant boundary.
9000 S3 API (plain HTTP) Authorized service accounts and applications with scoped credentials Credential-protected, but unencrypted traffic on a shared network is observable. In a multi-tenant or compliance-bound environment, use port 9443 instead.
9443 S3 API (HTTPS, once a certificate is installed) Same service accounts, with TLS encryption required by policy Same credential protection as 9000 with encrypted transport. This is the expected production endpoint for enterprise and compliance-bound deployments.

On a dedicated server, bind the dashboard to 127.0.0.1 and front it with a reverse proxy or access it via SSH tunnel. Configure NO_ORIGIN_DECLARED_PROXY_CIDRS in your .env to declare the proxy's source address. Enable encrypted-only S3 (port 9443) and install a proper certificate.

Three decisions before you continue

These are optional. The defaults work for most workstation installs. Choosing "not yet" on any of them is a complete, documented outcome — nothing will prompt you again.

Bind address (BIND_ADDRESS)

Default: empty — the dashboard (8888) and S3 (9000, 9443) listen on all interfaces and are reachable from any machine on your LAN.

Cost of narrowing: setting BIND_ADDRESS=127.0.0.1: (keep the trailing colon) in your .env restricts the dashboard and S3 to localhost only. Other machines on your LAN will no longer be able to reach them without a reverse proxy or SSH tunnel. UI_PORT stays a plain port number.

Not yet? Leave the .env as-is. The dashboard stays reachable from your LAN, exactly as today. You can narrow it any time later by adding one line to .env and running docker compose up -d.

Checking both switch states from the host

The Security page in the dashboard is the primary place both switches are reported. To read the same two states from the host — before you have a browser on the box, or to confirm what the running services actually hold rather than what was saved — run:

docker exec xns-relayer sh -c 'grep -E "require_encrypted_management|refuse_unencrypted_storage_offbox" /relayer/conf/relayer_configuration.json'
docker exec xns-relayer sh -c 'grep S3GW_REFUSE_UNENCRYPTED_OFFBOX /relayer/conf/xns.env 2>/dev/null || pm2 env 0 | grep S3GW_REFUSE_UNENCRYPTED_OFFBOX'

The first command shows the saved state of both switches. The second shows what the storage service is actually enforcing — it reads its setting once at start, so this is the value in force right now.

If those two disagree, the storage service has not picked up a recent change yet. Saving the switch restarts that service for you; if you edited the configuration file by hand instead, run docker compose up -d --force-recreate.

Management-plane encrypted-only switch

Default: Off — the dashboard serves over HTTP. This is safe when only your LAN can reach it (workstation/home network).

What it does: when enabled, the dashboard refuses unencrypted HTTP connections and serves only over HTTPS. Requires a TLS certificate to be installed first.

Cost of enabling: you must have a working certificate before flipping this switch, or the dashboard becomes unreachable. On a shared or enterprise host this is recommended; on a workstation it is optional.

Not yet? Leave it off. The dashboard keeps serving over HTTP. You can enable it later from Settings once a certificate is installed.

Storage-plane encrypted-only switch

Default: Off — the S3 API serves on both :9000 (HTTP) and :9443 (HTTPS, once a certificate is installed). Both remain available.

What it does: when enabled, the S3 API refuses connections on the plain HTTP port (:9000) and serves only on :9443 (HTTPS).

Cost of enabling: every S3 client must be reconfigured to use https://<host>:9443 and trust the certificate. Any client still pointing at :9000 will stop working.

Not yet? Leave it off. Both endpoints stay available. You can enable it later from Settings once your clients are reconfigured for HTTPS.

If you use a self-signed certificate

When you install a self-signed certificate (via Settings → Certificates), here is what happens:

  • Your browser will show a certificate warning when you visit the dashboard over HTTPS. This is expected — the certificate was not issued by a public certificate authority, so the browser cannot verify it automatically.
  • Why: a self-signed certificate still encrypts the connection, but it does not prove the server's identity to a third party. The browser warns because it cannot distinguish your self-signed certificate from an intercepted connection.
  • How to verify: print the certificate's SHA-256 fingerprint from the container and compare it with the one your browser shows in the certificate details dialog:
    docker exec xns-relayer openssl x509 -in /relayer/conf/certs/public.crt -noout -fingerprint -sha256
    The Settings → Certificates page also shows the fingerprint. If both match the browser's certificate details, the connection is to your Relayer.
  • To replace the certificate later: upload a new certificate (from a public CA such as Let's Encrypt, or from your organization's internal CA) on the Settings → Certificates page. The browser warning disappears once the certificate is issued by a trusted CA. No reinstall is needed.
✓You've picked your deployment profile and decided on the three settings above (or decided "not yet" — that's fine). You know which ports are open and why. Continue to the install.

Verify actual port publication (after install)

After starting the Relayer (Step 5), confirm that Docker actually published the ports you expect:

docker port xns-relayer

On a default install you should see:

8888/tcp -> 0.0.0.0:8888
9000/tcp -> 0.0.0.0:9000
9443/tcp -> 0.0.0.0:9443

Compare the output against whatever you set in .env. If you set BIND_ADDRESS=127.0.0.1:, all three lines should show 127.0.0.1 (for example 8888/tcp -> 127.0.0.1:8888). If the output still shows the old value, remember that editing .env alone does not change port publication — you must recreate the container: docker compose up -d.

✓The output matches your .env settings (or the defaults if you haven't changed anything)? Your host is publishing exactly what you intended.

Let your AI assistant install it

⌄

Prefer not to copy‑paste commands? If you use Claude, it can do the whole setup for you — check your system, register your account, start the Relayer, and verify storage — just by talking to it. This is the fastest way to get running. The manual steps below still work if you'd rather drive it yourself.

Claude Desktop — add this to your claude_desktop_config.json (Settings → Developer → Edit Config), then fully quit and reopen Claude Desktop:

claude_desktop_config.json
{
  "mcpServers": {
    "relayer": {
      "command": "npx",
      "args": ["@xns-cloud/relayer-mcp@latest"]
    }
  }
}

Claude Code — one command instead of editing JSON:

claude mcp add --scope user relayer -- npx @xns-cloud/relayer-mcp@latest

Then just tell Claude:

“Install the XNS Relayer for me.”

Claude installs the current public release build by default — no compose file to track down and no registry login. It handles the rest; you click one email‑verification link and complete one browser sign‑in.

The assistant installs the same release build this guide covers — agentic and manual installs land on the identical image.

✓You still need Docker Desktop (Step 1) and Node.js 20+ (npx fetches the installer). Claude checks for Docker first and tells you exactly what to install if it's missing.

Optional: the xns command-line tool

⌄

Prefer the terminal? The xns CLI is a single self-contained binary that talks to your Relayer from the command line — handy for scripting and quick checks. It's optional; the dashboard and your S3 client cover everything the CLI does.

Download the build for your platform from the release file server, then make it executable:

Linux (x86-64)

curl -fL https://releases.xns.tech/xns-cli/release/linux-amd64/xns -o xns
chmod +x xns

macOS (Apple Silicon)

curl -fL https://releases.xns.tech/xns-cli/release/darwin-arm64/xns -o xns
chmod +x xns

Step 1 of 2 — Verify the download. Each binary has a SHA-256 checksum beside it. Download it and compare:

curl -fL https://releases.xns.tech/xns-cli/release/linux-amd64/xns.sha256 -o xns.sha256
sha256sum -c xns.sha256
✓The checksum command prints xns: OK? The binary is intact. If it fails, re-download — do not run an unverified binary.

Then confirm it runs:

./xns version
✓It prints a version string? The CLI is installed. Move it onto your PATH (e.g. sudo mv xns /usr/local/bin/) to run xns from anywhere.

Step 1 Install Docker Desktop

⌄

The Relayer runs inside Docker. If you've never used Docker, no problem — you install it once and never think about it again. Pick your operating system:

  • Download Docker Desktop for Windows from docker.com.
  • Run the installer, accept the defaults, and restart if it asks you to.
  • Launch Docker Desktop and wait until the whale icon says it's running.
  • Download Docker Desktop for Mac from docker.com — pick the Apple Silicon or Intel build to match your Mac.
  • Open the .dmg and drag Docker into Applications.
  • Launch it from Applications and wait until it says running.
M1+ Apple Silicon Macs (M1/M2/M3/M4) — everything runs natively. The release images — the Relayer and the Prometheus + Grafana monitoring sidecars — all ship for both arm64 and amd64, so Docker pulls the right one automatically. No Rosetta or platform overrides needed.
  • Install Docker Engine for your distro — follow the official guide at docs.docker.com/engine/install (Ubuntu users: the apt repository steps).
  • Make sure the docker compose plugin is included (it is, in current installs).
  • Optional: add yourself to the docker group so you don't need sudo.

Verify it worked

Open a terminal (PowerShell on Windows, Terminal on Mac/Linux) and run:

docker --version
✓If it prints a version number, you're good. If it says "command not found," Docker Desktop isn't running yet — open it and wait for the running indicator.

Step 2 No login needed

⌄

This is a public build — there is nothing to log into. Skip straight to the next step.

✓No credentials are needed to pull the images.

Step 3 Create a folder for the Relayer

⌄

This folder holds your one config file. Create it and move into it:

mkdir -p ~/xns-relayer
cd ~/xns-relayer

On Windows PowerShell the same two commands work as‑is.

Step 4 Save the compose file and its .env

⌄

This step saves two files side by side: the compose file and a small .env next to it. Create a file named docker-compose.yml inside ~/xns-relayer and paste in exactly this:

docker-compose.yml
services:
  relayer:
    container_name: xns-relayer       # keep this name — Prometheus scrapes it by name
    image: releases.xns.tech/xns-relayer:release-149e4d63
    pull_policy: always         # `up -d` fetches the current moving-tag image, never a stale cache
    restart: unless-stopped
    privileged: true            # needed for the Relayer's disk + storage management
    # Audit-mode plumbing — turns the audit file sink on only in loki mode (the
    # default). Driven by RELAYER_AUDIT_MODE from your .env; do not edit.
    command: >
      sh -c '
      if [ "${RELAYER_AUDIT_MODE:-loki}" = "loki" ]; then
        export S3GW_AUDIT_NDJSON_PATH=/var/log/relayer-audit/audit.ndjson;
      fi;
      exec /xns/xns.sh
      '
    ports:
      - "${BIND_ADDRESS:-}${UI_PORT:-8888}:8888"   # web dashboard
      - "${BIND_ADDRESS:-}${S3_PORT:-9000}:9000"   # S3 API (plain HTTP, always on)
      - "${BIND_ADDRESS:-}${S3_TLS_PORT:-9443}:9443"   # S3 API (HTTPS, active once you install a cert)
    environment:
      - NODE_ENV=production
      - XNS_RELEASE_CHANNEL=release
      # Webhook mode only — set both in your .env; empty = disabled.
      - S3GW_AUDIT_WEBHOOK_URL=${S3GW_AUDIT_WEBHOOK_URL:-}
      - S3GW_AUDIT_WEBHOOK_SECRET=${S3GW_AUDIT_WEBHOOK_SECRET:-}
    volumes:
      - relayer_data:/relayer     # all data + database + config live here
      - ${LEGACY_DATA_PATH:-./data}:/relayer-legacy-check:ro   # B-4: lets the relayer refuse to start over a stranded bind-mount DB (see "Upgrading"); harmless on fresh installs
      - /mnt:/mnt:rslave          # host-disk passthrough for Cloud Sync (no-op if /mnt is empty)
      - audit_logs:/var/log/relayer-audit   # audit-log landing (loki mode); harmless when audit is off
      - relayer_logs:/relayer/log   # E3: app logs on their own volume so alloy can tail them without seeing /relayer/conf secrets
    networks:
      default:
        ipv4_address: 172.28.0.10   # pinned — the Grafana auth proxy trusts this IP; do not change

  # Audit-egress sidecars — ON by default. Your .env (Step 4, next block) switches
  # them via RELAYER_AUDIT_MODE; never edit these entries and never set
  # COMPOSE_PROFILES directly.
  loki:
    image: releases.xns.tech/relayer-loki:release-149e4d63
    pull_policy: always
    restart: unless-stopped
    profiles: ["loki"]
    command:
      - -config.file=/etc/loki/loki.yml
    volumes:
      - loki_data:/loki           # no host port — reached only inside the compose network
    deploy:
      resources:
        limits:
          memory: 256M

  alloy:
    image: releases.xns.tech/relayer-alloy:release-149e4d63
    pull_policy: always
    restart: unless-stopped
    profiles: ["loki"]
    command:
      - run
      - --server.http.listen-addr=0.0.0.0:12345
      - /etc/alloy/config.alloy
    environment:
      - S3GW_AUDIT_LOKI_PUSH_URL=${S3GW_AUDIT_LOKI_PUSH_URL:-}   # optional BYO-Loki push target; empty = on-box Loki
    depends_on:
      - loki
    volumes:
      - audit_logs:/var/log/relayer-audit:ro   # tails the audit log read-only
      - relayer_logs:/relayer/log:ro   # E3: tails the app logs; dedicated volume = Alloy never sees /relayer/conf secrets
    deploy:
      resources:
        limits:
          memory: 128M

  audit-logrotate:
    image: releases.xns.tech/relayer-audit-logrotate:release-149e4d63
    pull_policy: always
    restart: unless-stopped
    profiles: ["loki"]
    volumes:
      - audit_logs:/var/log/relayer-audit   # rotates audit.ndjson in place (~600 MB cap)
    deploy:
      resources:
        limits:
          memory: 32M

  # Monitoring stack — powers the dashboards under Monitoring in the web UI.
  prometheus:
    container_name: prometheus    # keep — the docs' `docker exec prometheus` commands rely on it
    image: releases.xns.tech/relayer-prometheus:release-149e4d63
    pull_policy: always
    restart: unless-stopped
    # The first two flags must stay; the retention pair reads your .env.
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=${PROMETHEUS_RETENTION_TIME:-15d}
      - --storage.tsdb.retention.size=${PROMETHEUS_RETENTION_SIZE:-2GB}
    # Alert-threshold dials — all optional, defaults built in. See "Alerts" below.
    environment:
      - ALERT_DISK_FREE_BYTES=${ALERT_DISK_FREE_BYTES:-}
      - ALERT_DISK_FREE_FOR=${ALERT_DISK_FREE_FOR:-}
      - ALERT_S3_UNREACHABLE_FOR=${ALERT_S3_UNREACHABLE_FOR:-}
      - ALERT_S3_ERROR_RATE=${ALERT_S3_ERROR_RATE:-}
      - ALERT_S3_ERROR_RATE_FOR=${ALERT_S3_ERROR_RATE_FOR:-}
      - ALERT_S3_P95_LATENCY_SECONDS=${ALERT_S3_P95_LATENCY_SECONDS:-}
      - ALERT_S3_P95_LATENCY_FOR=${ALERT_S3_P95_LATENCY_FOR:-}
      - ALERT_HOSTIO_DOWN_FOR=${ALERT_HOSTIO_DOWN_FOR:-}
      - ALERT_GATEWAY_DOWN_FOR=${ALERT_GATEWAY_DOWN_FOR:-}
      - ALERT_CRASH_LOOP_FOR=${ALERT_CRASH_LOOP_FOR:-}
      - ALERT_CACHE_PRESSURE_RATIO=${ALERT_CACHE_PRESSURE_RATIO:-}
      - ALERT_CACHE_PRESSURE_FOR=${ALERT_CACHE_PRESSURE_FOR:-}
      - ALERT_BRIDGE_STALE_SECONDS=${ALERT_BRIDGE_STALE_SECONDS:-}
      - ALERT_BRIDGE_STALE_FOR=${ALERT_BRIDGE_STALE_FOR:-}
      - ALERT_TARGET_DOWN_FOR=${ALERT_TARGET_DOWN_FOR:-}
      - ALERT_AUDIT_SIEM_DEAD_FOR=${ALERT_AUDIT_SIEM_DEAD_FOR:-}
    volumes:
      - prometheus_data:/prometheus
    deploy:
      resources:
        limits:
          memory: ${PROMETHEUS_MEMORY_LIMIT:-512M}

  alertmanager:
    # Delivers firing alerts to your webhook/email and sends the Watchdog
    # heartbeat — see "Alerts" below. Runs fine unconfigured (no delivery).
    container_name: alertmanager   # keep — the docs' `docker exec alertmanager` commands rely on it
    image: releases.xns.tech/relayer-alertmanager:release-149e4d63
    pull_policy: always
    restart: unless-stopped
    environment:
      - ALERTMANAGER_WEBHOOK_URL=${ALERTMANAGER_WEBHOOK_URL:-}
      - ALERTMANAGER_GROUP_WAIT=${ALERTMANAGER_GROUP_WAIT:-}
      - ALERTMANAGER_REPEAT_INTERVAL=${ALERTMANAGER_REPEAT_INTERVAL:-}
      - ALERTMANAGER_SMTP_SMARTHOST=${ALERTMANAGER_SMTP_SMARTHOST:-}
      - ALERTMANAGER_SMTP_FROM=${ALERTMANAGER_SMTP_FROM:-}
      - ALERTMANAGER_SMTP_TO=${ALERTMANAGER_SMTP_TO:-}
      - ALERTMANAGER_SMTP_AUTH_USERNAME=${ALERTMANAGER_SMTP_AUTH_USERNAME:-}
      - ALERTMANAGER_SMTP_AUTH_PASSWORD=${ALERTMANAGER_SMTP_AUTH_PASSWORD:-}
      - WATCHDOG_HEARTBEAT_URL=${WATCHDOG_HEARTBEAT_URL:-}
      - WATCHDOG_REPEAT_INTERVAL=${WATCHDOG_REPEAT_INTERVAL:-}
    volumes:
      - alertmanager_data:/alertmanager
    deploy:
      resources:
        limits:
          memory: ${ALERTMANAGER_MEMORY_LIMIT:-128M}

  grafana:
    image: releases.xns.tech/relayer-grafana:release-149e4d63
    pull_policy: always
    restart: unless-stopped
    environment:
      # Auth-proxy mode: relayer-ui /grafana proxy injects X-WEBAUTH-USER; anon
      # off (fail-closed). WHITELIST pins trust to the relayer static IP
      # (172.28.0.10, the networks:/ipam pin below) so no sibling container can
      # forge the header.
      - GF_AUTH_ANONYMOUS_ENABLED=false
      - GF_AUTH_PROXY_ENABLED=true
      - GF_AUTH_PROXY_HEADER_NAME=X-WEBAUTH-USER
      - GF_AUTH_PROXY_HEADER_PROPERTY=username
      - GF_AUTH_PROXY_AUTO_SIGN_UP=true
      - GF_AUTH_PROXY_WHITELIST=172.28.0.10
      # Admin for the auto-provisioned proxy user (identity-bound). Grafana's
      # default is Viewer (read-only) — omitting this makes dashboards RO.
      - GF_USERS_AUTO_ASSIGN_ORG_ROLE=Admin
      - GF_SERVER_SERVE_FROM_SUB_PATH=true
      - GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s/grafana
    volumes:
      - grafana_data:/var/lib/grafana
    deploy:
      resources:
        limits:
          memory: ${GRAFANA_MEMORY_LIMIT:-256M}

  node-exporter:
    image: prom/node-exporter:v1.11.1
    restart: unless-stopped
    command:
      - --path.rootfs=/host
    volumes:
      - /:/host:ro,rslave     # read-only host metrics (rslave: required on real Linux hosts)

volumes:
  relayer_data:
  relayer_logs:   # E3: dedicated app-log volume (relayer writes, alloy tails ro) — keeps /relayer/conf out of the log shipper
  prometheus_data:
  grafana_data:
  alertmanager_data:
  audit_logs:
  loki_data:

networks:
  # Fixed private subnet so the relayer's pinned IP above is stable — the
  # Grafana auth proxy whitelists it. Chosen not to collide with home LANs.
  default:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/24
          gateway: 172.28.0.1

Now create the second file, named .env, in the same folder — it carries the audit-logging switch the compose file reads:

.env
# Audit logging mode — loki (default) | webhook | off. See "Audit logging" below.
#RELAYER_AUDIT_MODE=off
#S3GW_AUDIT_WEBHOOK_URL=
#S3GW_AUDIT_WEBHOOK_SECRET=

# Internal plumbing — never edit this line and never set COMPOSE_PROFILES yourself.
COMPOSE_PROFILES=${RELAYER_AUDIT_MODE:-loki}

# Need different ports? Uncomment and change if 8888/9000/9443 are taken.
#UI_PORT=8888
#S3_PORT=9000
#S3_TLS_PORT=9443
# Localhost only? Uncomment to publish all three ports on 127.0.0.1 (keep the trailing colon).
#BIND_ADDRESS=127.0.0.1:

# ---- Alerts (all optional — see the "Alerts" section below) ----
# Webhook that receives every firing alert (JSON POST):
#ALERTMANAGER_WEBHOOK_URL=
# Email as a second receiver — set all three; auth optional:
#ALERTMANAGER_SMTP_SMARTHOST=smtp.example.com:587
#[email protected]
#[email protected]
#ALERTMANAGER_SMTP_AUTH_USERNAME=
#ALERTMANAGER_SMTP_AUTH_PASSWORD=
# Off-box heartbeat (Healthchecks.io / Cronitor ping URL) — tells you when the
# monitoring itself dies. Never the same URL as the webhook above:
#WATCHDOG_HEARTBEAT_URL=
#WATCHDOG_REPEAT_INTERVAL=5m
# How often a still-firing alert re-notifies / how long the first one waits:
#ALERTMANAGER_GROUP_WAIT=30s
#ALERTMANAGER_REPEAT_INTERVAL=5m

# ---- Alert thresholds (defaults shown; *_FOR = how long before it fires) ----
#ALERT_DISK_FREE_BYTES=5368709120
#ALERT_DISK_FREE_FOR=5m
#ALERT_S3_UNREACHABLE_FOR=60s
#ALERT_S3_ERROR_RATE=0.01
#ALERT_S3_ERROR_RATE_FOR=2m
#ALERT_S3_P95_LATENCY_SECONDS=2.0
#ALERT_S3_P95_LATENCY_FOR=2m
#ALERT_HOSTIO_DOWN_FOR=60s
#ALERT_GATEWAY_DOWN_FOR=60s
#ALERT_CRASH_LOOP_FOR=2m
#ALERT_CACHE_PRESSURE_RATIO=0.85
#ALERT_CACHE_PRESSURE_FOR=5m
#ALERT_BRIDGE_STALE_SECONDS=120
#ALERT_BRIDGE_STALE_FOR=60s
#ALERT_TARGET_DOWN_FOR=2m
# How long a CONFIGURED SIEM webhook must fail continuously before the
# AuditSIEMFeedDead alert fires. No effect when no webhook is set.
#ALERT_AUDIT_SIEM_DEAD_FOR=15m

# ---- Monitoring footprint ----
# WARNING: the first time you LOWER a retention value on an existing install,
# metrics history above the new ceiling is pruned once, permanently.
#PROMETHEUS_RETENTION_TIME=15d
#PROMETHEUS_RETENTION_SIZE=2GB
# Loki (audit-log) retention is currently fixed at 14 days inside the image —
# this key is reserved and has no effect yet.
#LOKI_RETENTION=14d
#PROMETHEUS_MEMORY_LIMIT=512M
#GRAFANA_MEMORY_LIMIT=256M
#ALERTMANAGER_MEMORY_LIMIT=128M

Audit logging. Out of the box the Relayer keeps an audit trail of every S3 request and feeds it into a small on-box log stack (three extra containers: Loki, Alloy and a log rotator) — open Monitoring in the dashboard to see it. That default costs about 710 MB of disk and 416 MB of RAM, and it manages its own growth (the log file is capped around 600 MB and Loki keeps 14 days). One value in your .env switches the behavior — uncomment RELAYER_AUDIT_MODE and set it to:

  • loki — the default described above; leaving the line commented out means exactly this.
  • webhook — no extra containers. The Relayer pushes signed audit events straight to your own SIEM or log collector: also uncomment S3GW_AUDIT_WEBHOOK_URL (your HTTPS endpoint) and S3GW_AUDIT_WEBHOOK_SECRET (deliveries are HMAC-signed with it). The secret lives only in your .env — run chmod 600 .env so only your user can read it, and know that anyone who can reach the Docker socket on this machine can read it too (docker inspect shows container environment values).
  • off — no audit trail at all; smallest footprint.

Change the mode any time: edit .env, then run docker compose --profile "*" down followed by docker compose up -d (your data is safe — it lives in the named volumes). The --profile "*" down matters: a plain up -d or down leaves the previous mode's audit containers running. Always use RELAYER_AUDIT_MODE — never set COMPOSE_PROFILES directly (an exported COMPOSE_PROFILES in your shell silently overrides the switch).

Everything the Relayer stores — your local database, config, and logs — lives in a Docker‑managed volume called relayer_data, so it survives restarts and updates.

Want the data on a specific disk? Replace the named volume with a folder of your choice by changing only the left side of the relayer's volume line:

    volumes:
      - /mnt/bigdisk/relayer-data:/relayer   # your folder : container path

The right side must stay exactly /relayer — that's where the Relayer looks for its data inside the container. If you change the right side instead, the Relayer starts with a fresh, empty data directory and your buckets and settings seem to vanish (they're safe, just not mounted). Moving an existing install? Stop the container first (docker compose down), move the whole data folder intact, update the left side, then docker compose up -d.

Step 5 Start the Relayer

⌄

From inside ~/xns-relayer, run:

docker compose up -d
  • The first run downloads the image (a few hundred MB) — give it a minute or two.
  • When it's done, open http://localhost:8888 in your browser.
  • You should land on the XNS Relayer dashboard.
▦ The dashboard on first launch
✓Dashboard loads at http://localhost:8888? You're running. Point any S3 client at http://localhost:9000 to start putting objects. If you later install a TLS certificate (Settings → Certificates), http://localhost:9000 keeps working unchanged and an encrypted endpoint also becomes available at https://localhost:9443 — the Certificates page shows both.

Alerts — get told when something breaks (optional)

⌄

The stack you just started ships ten alert rules already watching the box (disk space, S3 errors and latency, dead services, crash loops and more) plus a bundled Alertmanager that can deliver them to you. Out of the box nothing is delivered — the rules evaluate, but until you set a receiver in your .env they have nowhere to go.

1. Webhook — the primary receiver

Uncomment ALERTMANAGER_WEBHOOK_URL in your .env and point it at anything that accepts a JSON POST (a chat-ops bridge, PagerDuty/Opsgenie webhook ingestion, your own endpoint), then recreate the alertmanager so it picks the value up — restarting is not enough, the config is rendered when the container starts:

docker compose up -d --force-recreate alertmanager
✓Every firing rule now POSTs a notification to your URL. Repeats are suppressed: however often a rule keeps firing, you get one message when it starts and then at most one per ALERTMANAGER_REPEAT_INTERVAL (default 5 minutes — raise it in .env if that's chatty for your channel).

2. Email — an optional second receiver

Uncomment and fill the three ALERTMANAGER_SMTP_* lines (smarthost, from, to — all three together; the AUTH pair only if your relay needs a login), recreate the alertmanager the same way, and the same notifications also go out by mail.

3. Heartbeat — know when the watcher itself dies

If Prometheus or the Alertmanager crashes, no alert can tell you — the messenger is dead. The fix is a dead-man's-switch: the stack sends a check-in every few minutes to an off-box service that pages you when the check-ins stop. Create a free check at Healthchecks.io (or a Cronitor heartbeat monitor), give it an expected period of 5 minutes plus a few minutes' grace, paste its ping URL into WATCHDOG_HEARTBEAT_URL, and recreate the alertmanager. Don't reuse your webhook URL here — a receiver that only reacts to messages it receives can never tell you about silence.

Tuning thresholds and footprint

Every dial lives in the same .env: the ALERT_* lines change when each rule fires (recreate prometheus to apply those), and the retention/memory lines at the bottom bound how much history and RAM the monitoring stack uses. A bad value fails the container start with the validator's message as the last line of docker logs alertmanager (or docker logs prometheus) instead of silently breaking the monitoring — if a container is missing after a change, check those logs, fix the value, recreate again. Mind the retention warning in the .env: lowering retention on an existing install prunes the over-ceiling history once, permanently.

Searching your logs

You don't need to SSH in and grep to chase a fault. In the Relayer dashboard, Monitoring → Log Search runs one search across both your application logs (hostio, gateway, s3gateway, and the Relayer UI process) and the audit trail together — type a term once and matching lines from every service and the audit events come back in one list, newest first. It's additive to the richer Audit Log page, not a replacement. A provisioned Relayer Log Search Grafana dashboard shows recent activity at a glance without writing a query.

Updating to a new build

⌄

Every Relayer image in the compose file is pinned to one build (release-<build>), so your install only changes when you change that file. The dashboard shows a banner when a new build is available. To update, from ~/xns-relayer, keep your current file and fetch the new one:

cp docker-compose.yml docker-compose.yml.previous
curl -fsSL -o docker-compose.yml https://releases.xns.tech/relayer/release/docker-compose.yml
docker compose up -d

To go back to the build you ran before:

cp docker-compose.yml.previous docker-compose.yml
docker compose up -d

Your settings live in .env, which neither step touches, and your data in relayer_data is untouched. If you edited the compose file itself, carry those edits into the new file.

Automatic error reporting

⌄

The Relayer reports crashes and errors back to the dev team automatically — nothing for you to set up or configure. If the UI crashes, the server throws, or the S3 gateway returns a 5xx, we see the stack trace, tagged with the exact build you're running.

Still report it in Discord, though. The automatic report tells us what broke; your message tells us what you were doing when it broke — and that's the half that actually helps us fix it.

Upgrading & moving your data

⌄

By default your buckets, objects, database and config all live in a Docker named volume called relayer_data (the relayer_data:/relayer line in the compose file). A normal update keeps that volume, so your data carries across new versions automatically.

Switching from an older bind-mount install? Read this first.

If you previously ran the Relayer with a bind-mount — a line like - ./data:/relayer or - /your/path:/relayer — your data lives in that host folder, not in the relayer_data named volume. If you just adopt the named-volume compose, the Relayer would start against an empty volume and your buckets would look gone. They are not lost — the old data is safe in your folder, just no longer connected.

To prevent silent data loss, the Relayer now refuses to start when it detects an existing database in the old folder while the named volume is empty, and prints migration steps in the logs. Pick one of these:

  • Keep your data where it is (simplest). In the compose file, point the volume back at your bind-mount instead of the named volume:
        volumes:
          - /your/path:/relayer        # your existing data folder
    then docker compose up -d.
  • Move your data into the named volume. With the stack stopped, copy the old folder's contents into the relayer_data volume, then start normally. Ask your AI assistant (or us in Discord) for the exact copy command for your setup.

Used a custom bind-mount path (not ./data)? Point the safety check at it by adding one line to a .env file next to your compose: LEGACY_DATA_PATH=/your/old/path. On a clean first-time install none of this applies — the check is a no-op, and an empty data folder appearing next to the compose is harmless.

Troubleshooting

⌄
"port is already allocated" / "address already in use"
Something else on your machine is using port 8888, 9000, or 9443. Uncomment the port lines in the .env file you saved in Step 4 and pick free numbers, then run docker compose up -d again. A common one: Portainer uses 9443 as its default HTTPS port — if you run Portainer, set S3_TLS_PORT to something free (e.g. 9444) in your .env.
Upgrading from an earlier build? Internal port defaults changed (GATEWAY / HOSTIO)
Port defaults changed in this release: GATEWAY moved 34554 → 5500 and HOSTIO moved 37809 → 3480 (both now sit below the Linux ephemeral port range, eliminating a boot-ordering race that could crash-loop storage services on restart). Fresh installs: no action — the new defaults apply automatically. Existing installs that persisted the old port values in device-local configuration must update GATEWAY_PORT to 5500 and HOSTIO_PORT to 3480 (or any values below 32768) in their device config and restart. Symptom of the old race: bind: address already in use in container logs with storage stuck down.
"unauthorized" or "denied" when it pulls the image
This build is public, so a pull never needs credentials. If you see this, you likely have a saved login for releases.xns.tech that the registry no longer accepts — run docker logout releases.xns.tech and pull again.
"permission denied" / "cannot connect to the Docker daemon"
Docker Desktop isn't running. Open it and wait for the running indicator, then retry. On Linux, you may need sudo or to add yourself to the docker group.
The container won't start, or keeps restarting
Grab the logs and paste them in the Discord channel:
docker compose logs --tail=100
Using the encrypted (HTTPS) S3 endpoint after installing a certificate

Installing a certificate no longer changes your existing endpoint — http://<host>:9000 keeps working unchanged. It additionally enables an encrypted endpoint at https://<host>:9443. To switch a client to TLS, point it at https://<host>:9443.

For self-signed certificates: download the Relayer's public certificate and tell your client to trust it explicitly — do not disable certificate verification.

First, copy the certificate out of the container (host-local, no network hop):

docker cp xns-relayer:/relayer/conf/certs/public.crt relayer-ca.crt

AWS CLI / SDKs — set the AWS_CA_BUNDLE environment variable or pass --ca-bundle per command:

# Option A: environment variable (applies to all AWS CLI / SDK calls)
export AWS_CA_BUNDLE=/path/to/relayer-ca.crt

# Option B: per-command flag
aws s3 ls --endpoint-url https://<host>:9443 --ca-bundle /path/to/relayer-ca.crt

rclone — set ca_cert in the remote config instead of disabling verification:

[myrelayer]
type = s3
provider = Other
endpoint = https://<host>:9443
ca_cert = /path/to/relayer-ca.crt

Python (boto3) — pass verify with the certificate path:

import boto3
s3 = boto3.client('s3',
    endpoint_url='https://<host>:9443',
    verify='/path/to/relayer-ca.crt'
)

The Settings → Certificates page shows the exact endpoint and config snippets after a cert is installed.

Can't reach the dashboard
Make sure you're using http://localhost:8888 — that's http, not https — and that docker compose ps shows the container as running.
The Relayer is using too much CPU / my machine gets hot
You can cap how much CPU the Relayer is allowed to use. Add a cpus limit to the relayer service only in your docker-compose.yml — for example cpus: 1.0 limits it to one core:
docker-compose.yml (relayer service)
  relayer:
    container_name: xns-relayer
    image: releases.xns.tech/xns-relayer:release-149e4d63
    cpus: 1.0            # cap the Relayer at one CPU core
    # ...rest of the relayer service unchanged...
Then run docker compose up -d to apply it. Use a fraction (cpus: 0.5) to give it half a core, or a higher number to allow more. Only put cpus on the relayer service — not on the monitoring services, and not on any other container — otherwise you'll throttle the wrong thing. A too-low limit will make the Relayer feel sluggish; remove the line to lift the cap.

Version history

⌄

Every shipped release build — expand a version to see what changed.

v2.29.12026-10-30FixedAddedTestsChangedRemovedSecurity⌄
Fixed
  • Lifecycle expiry is metered once, by the sweep
  • Storage already over-metered is corrected once
  • The costlog writer no longer delays start-up
  • The costlog `method_name` type gains `LifecycleExpire` and `LifecycleExpireCorrection`
  • Restoring a backup while the setup check or a cache relocation was running showed "Restore failed — check connectivity a
  • Choosing HTTP Only on the Certificates page took the S3 gateway down: S3 on :9000 and :9443 stopped answering and stayed
  • When the bucket list failed to load (rate limit, network or server error), File Manager showed "0 BUCKETS" and "No bucke
  • A prefix-only lifecycle rule lost its prefix when Bucket Settings read it back, because the gateway returns that rule wi
  • Adding or deleting a lifecycle rule failed with "Each lifecycle Filter.Tag must include a non-empty string Key" whenever
  • Bucket Settings offered Abort Incomplete Multipart on a tag-scoped rule, which the gateway refuses, and showed the refus
  • A backup restore whose database step failed kept going: it wrote the config, HostIO keys and Samba files from the backup
  • Nothing opened the Relayer Bug Report dialog: its `submitBug` listener in `MainTheme.vue` had no sender, so users could
  • `POST /api/v1/system/bug-report` never answered when the Discord send failed or building the report threw, so the reques
  • A takeover the console refused (for example one that would give an account two consumed free tiers) still moved the box:
  • The bucket summary, the tenant dashboard and the tenant usage view said "Billed on the network total at $4.00 per TB per
  • On Provider Profiles, choosing a continent or an XNS Region in the pre-select filled Data View, but the banner still rea
  • Restart All and Apply Changes on `/configuration` now show the please-wait overlay while the services restart, as Backup
  • Escape now does what a dialog's Cancel or close control does in the dialogs that ignored it: backup restore, the page mo
  • The share groups list showed the next day as "Created" for a group made in the evening west of UTC, and "Created Invalid
  • The design-system base reset now loads (`client/src/main.js`). Buttons, inputs, selects and text areas now also inherit
  • A restore no longer leaves the HostIO encryption keys and the HostIO secret out of step
  • HostIO, Gateway and S3Gateway start again after an owner reset
  • A cache wipe can no longer silently delete uploads that have not reached providers
  • The image is built on a Node 22 that still gets security fixes
  • The dashboard no longer shows a speed that disagrees with other readings
  • A UI restart no longer restarts HOSTIO over the derived upload values
  • Stop keeps HOSTIO stopped during a restart the UI started
  • Issuing a Let's Encrypt certificate saves the SSL mode and answers the page
  • The Cloudflare token no longer reaches the S3 gateway's environment
  • Let's Encrypt certificates renew again
  • A bucket-scoped read role can read Object Lock settings
  • Pages no longer sign the owner out while an upload fills the line
  • A failed key fetch logs its reason
  • The start-up card no longer hangs silently on "Checking authentication"
  • Reconnect on the credential-revoked banner opens the first-access dialog
  • Provider Profiles no longer hangs on "Please wait." when a region zone has no name
  • Provider Profiles loads before the first provider fetch has succeeded
  • Network Overview and Provider Profiles download the provider catalog once per poll, not twice
  • Settings saves no longer fail with "Could not save configuration." when the provider list was never fetched
  • The object detail panel no longer logs about 20 errors on a bucket without Object Lock
  • The Tags tab of a deleted file says the file was deleted
  • Creating a bucket no longer reports auto-sharing as enabled when it is off
  • A failed upload no longer leaves a partial file in the upload spool
  • The share users list shows "Created —" for a user with no creation date
  • Attach Policy reports a failed request and no longer reports a false success
  • Throughput chart y-axis labels are no longer cut off
  • The Not Found page is styled again
  • The Transport choice card no longer covers the theme toggle
  • The sidebar reads as one gray surface in light theme
  • A certificate and key that do not match no longer take the dashboard down
  • Two operators editing the same bucket's grants no longer silently overwrite each other
  • Certificate upload accepts EC private keys and keeps the intermediate chain
  • A certificate installed after start reaches the S3 HTTPS port without a container restart
  • The share users list shows the day a user was created in the browser's own time zone
  • Server test coverage can be measured again
  • demo-backend's provider cache test TP-33 no longer fails under load
  • demo-backend's single-upload memory test TP-35 no longer fails on normal RSS noise
  • Choosing an SSL mode on the Certificates page saves again
  • SSL mode changes are refused on hosts that manage their own certificate (`SSL_MANAGED_EXTERNALLY`)
  • "No SSL" is refused while "Encrypted-only storage" is on
  • `GET /api/v1/config/ssl` answers 200 `{ success: false, installed: false }` when no certificate is installed, not 405
Added
  • The sidebar now has a Support entry under Account that opens the XNS Console ticket desk (`https://console.xns.tech/my-t
  • The Speed tab asks the owner two setup questions
  • While HostIO runs, the Speed tab says "Saving restarts HostIO. Uploads in progress are interrupted." beside Save answers
  • `POST /api/v1/system/setup-answers` (sign-in and the box owner) writes the answers to the stored `HostIO` section and re
  • First boot sizes the cache from free disk space
  • `GET /api/v1/system/cache` carries HostIO's `autotune_stats` block under `HostIO`, and none while HOSTIO is stopped. The
  • The bridge exporter carries HostIO's upload autotune
  • A setup check on a fresh install checks that the sign-in and provider hosts answer
  • The check's result is stored as `SetupCheck` in `relayer_configuration.json` and served on the sign-in-free `GET /api/v1
  • `POST /api/v1/system/setup-check` re-runs the check (409 while a check runs or a cache relocation holds its lock). It ne
  • A setup banner on every page shows only when the check found a problem (sign-in or provider host unreachable), with the
  • Configure has a Speed tab
  • The NOC Network card has an Upload row with the learned upload speed, updated at each 30 s poll.
  • Tenants can pick a `Backup` key template for immutable backups
Tests
  • The TP-35 control in `demo-backend/s3/__tests__/memory.test.js` that proves a buffering PutObject fails the 96 MiB bound
  • `confirmInert.race.demo.test.js` and `driveSourceConfirm.demo.test.js` now allow 20 s per test instead of vitest's 5 s d
Changed
  • The setup banner speaks to the owner's answers
  • Checkboxes take their check and dash marks from `@scpcorp/design-system` 0.3.3 (`client/package.json`, both lockfiles).
  • `PUT /api/v1/config` answers 409 `settings_changed` when the body carries a `Version` other than the stored one, so a pa
  • Upload workers, download workers, uploads in flight and the sector upload timeout are owner overrides: empty by default
  • One-time upgrade pass (stored `Version` below 3.79.0)
  • The Workload select offers "Standard" and "Bulk archive ingest". Bulk archive ingest no longer sets `HOSTIO_UPLOAD_WORKE
  • Settings pages that end in a setup check reload the server-derived HostIO values, keeping unsaved edits.
  • CI: the non-gating `s3_tool_latest` job is gone from merge request pipelines. It warned on nearly every merge request wh
  • `@scpcorp/design-system` 0.3.2 (from 0.3.0). In the light theme the sidebar is now gray (`--ds-bg-sidebar` `#dde1e8`, wa
  • In light, the readers that lost their edge against the gray get one, in their own component styles: a 1px `--ds-text-dim
  • Inherited from design-system 0.3.1: dialog titles have no default margin (`.ds-modal__title { margin: 0 }`). Tailwind's
Removed
  • The Persona preset bar in Advanced Settings and the "Slow Upload Connection" preset.
  • Upload profiles are gone
  • The UI no longer derives a simultaneous-upload ceiling or an upload cap: the start-up reconcile that wrote them and the
  • The Relayer no longer runs an internet speed test
Security
  • A compressed request body with trailing bytes no longer crashes the server
  • Image builds no longer print the npm registry token
  • The Relayer API accepts only access tokens
  • Browser sign-in sends a PKCE challenge
  • The HostIO secret is generated with a secure random source
v2.25.02026-10-09AddedChangedFixed⌄
Added
  • `HOSTIO_MAX_UPLOAD_MBPS` (`--max-upload-mbps`, default `0` = no cap) limits how fast hostio sends sector data to hosts
Changed
  • The gateway request-rate defaults now match what customer boxes run
Fixed
  • A tenant who runs out of funds is now blocked, and one who tops up is unblocked, within one poll interval instead of at the next costlog restart
v2.19.02026-08-17ChangedFixed⌄
Changed
  • The packer's flush defaults are now 97% / 10m (were 75% / 1m), so rows ship nearly full by default
Fixed
  • A box whose storage account and owner account are the same name no longer gets stuck restarting forever after an upgrade
v2.9.02026-07-18AddedSecurityVerified (no code change)ChangedTests⌄
Added
  • The S3 Gateway's multi-tenant conformance gaps close, and the published compatibility number becomes reproducible instead of hollow
  • Every storage snapshot tick now lays down the box's aggregate row byte-identically PLUS one row per tenant — the source data finally tells the per-tenant truth
Security
  • s3gateway: `s3:*` no longer authorizes non-s3 actions — closes a namespace-crossing privilege escalation exposed by the IAM self-service reversal
Verified (no code change)
  • mt-reshape E-A1 (iam-deconflation) — relayer regression verification
Changed
  • mt-reshape E-A2 (one quota dial): the tenant byte quota lives on the cost-center, not on `s3_tenants`
Tests
  • mt-reshape E-A2 (S6 nested-ID): heal-package regex regression against `{ccid}_cusN_tenM`
v2.7.02026-07-13AddedFixed⌄
Added
  • s3gateway: runtime Sentry kill-switch — silence error reporting without a rebuild
  • monitoring: alert delivery spine — firing rules now reach the operator instead of evaluating into a void
  • monitoring: Watchdog dead-man's-switch — the operator is told when the monitoring stack itself dies
  • monitoring: alert thresholds are `.env`-tunable — threshold changes are a config edit + recreate, never a source change or rebuild
  • docs: observability & alerting operator runbook
  • s3gateway: audit-pipeline health metrics registered unconditionally + NDJSON write-health counters
  • s3gateway: WORM-defeat attempt detection — counters + ≤60s trip-wire alert
  • monitoring: application logs now reach Loki — the operator can search every service's logs, not just audit
  • hostio + s3gateway: cache-hit-rate and per-bucket activity are now instrumented — the two series the AI-engineer overview screen needs
Fixed
  • monitoring: WORM-defeat alerts missed their ≤60s detection SLA on the default Alertmanager route
  • s3gateway: valid credentials were rejected with `InvalidAccessKeyId` under high request concurrency
v2.6.52026-07-07Fixed⌄
Fixed
  • s3gateway: a configured rate limit never throttled the box owner's account or any of its service-account/IAM keys
  • s3gateway: service-account create/delete and credential rotation left no audit trail
  • s3gateway: clients uploading an object could see the connection reset instead of the real error response
  • s3gateway: running out of physical disk returned a generic internal error instead of a storage-full response
  • s3gateway: newly created users and access keys could be assigned a cost center that doesn't exist, leaving them unable to perform any storage operation; such requests are now rejected up front with a clear error
  • s3gateway: deleting a bucket and immediately recreating it under the same name could fail until a brief background cleanup finished
v2.1.02026-06-19Added⌄
Added
  • audit landing: `S3GW_AUDIT_LOKI_PUSH_URL` lets a bring-your-own-Loki admin redirect audit logs to an external Loki with one env var — no UI, no rebuild of behavior
v2.0.02026-06-16FixedAddedChangedSecurity⌄
Fixed
  • s3gateway: conditional writes (`If-Match`/`If-None-Match`) on a missing object no longer 500 (NS1)
  • gateway + s3gateway: streaming write errors no longer destroyed by TCP resets; internal-hop outages now retryable 503s (beta-soak vae report, 2026-06-10)
  • hostio: delete-during-upload no longer crash-loops HOSTIO (beta-soak 500 storms)
  • s3gateway: DeleteBucket no longer `500`s on large buckets, and previously wedged buckets heal themselves (BUG #685, Sentry `RELAYER-BACKEND-8A`)
  • s3gateway: large/slow uploads no longer fail with a 60s `500 InternalError` (UploadPart / PutObject body-read wall)
  • s3gateway: 5xx Sentry events are now self-diagnosing and no longer fan out per object key
  • s3gateway: delimited `ListObjects` (V1+V2) no longer returns phantom empty folders after a versioned delete
  • s3gateway: `AssumeRoleWithWebIdentity` now resolves multi-audience tokens correctly (E1 SF-2)
  • s3gateway: `PutBucketPolicy` now rejects `NotPrincipal` on `Allow` statements with 400 `MalformedPolicy` (NS2-A)
  • s3gateway: temp (STS) credentials now enforce `X-Amz-Security-Token` on every signing path (epic E0, closes a credential-replay hole)
  • s3gateway: bucket-policy `Principal` is now enforced (closes a privilege escalation, epic E3)
  • Monitoring stack now survives install/upgrade/reboot (observability rework)
  • s3gateway: bootstrap owner credential is now seeded with `is_owner=true` (RC-1)
  • s3gateway: UploadPartCopy returns `InvalidRange` (416) for out-of-bounds copy range
  • gateway: versioned GET of small multipart-completed objects no longer returns HostIO fileID as content
  • hostio/cacherepo: removed diagnostic logging
Added
  • hostio: read-only `POST /v1/hostio/evaluate` endpoint (epic EP-E1)
  • all services: per-service structured logging bootstrap + support-grade operational stream (epic E9)
  • s3gateway: `AccessLogWorker` — bucket-logging access-log delivery (epic E4)
  • s3gateway admin: per-bucket live-object stats endpoint `GET /admin/v1/bucket-stats` (epic E10)
  • hostio: bounded upload host-switch loop (epic E3, D2)
  • hostio + s3gateway: staging load-shed → S3 503 SlowDown with Retry-After (epic E3, D3)
  • gateway + s3gateway: client-disconnect classified as a clean disconnect, not a 500 (epic E3, D4)
  • hostio: Go-side backstop clamp on `SimultaneousUploads` (epic E3, D1, §7 D9/D10)
  • docs: version → commit index + 2.0.0 promotion note
  • s3gateway: webhook audit sink — signed NDJSON audit export to a customer SIEM (epic E2)
  • s3gateway: AWS IAM query-protocol dialect — 16 user-plane management verbs for Terraform/Vault (epic E1)
  • s3gateway: per-request audit-record enrichment — one record answers both "who/allowed?" and "how big/how long/what client/what error?" (epic E3)
  • s3gateway: pluggable audit sinks + opt-in Loki/Alloy audit landing (epic E1)
  • s3gateway: cross-account `AssumeRole` — persisted roles, trust policies & `ExternalId` confused-deputy guard (epic E2)
  • s3gateway: `AssumeRoleWithWebIdentity` — keyless OIDC federation for ephemeral workloads (epic E1)
  • s3gateway: dual-listen — `:9000` is now permanently plain HTTP, HTTPS is additive on `:9443` (SP1)
  • s3gateway: admin/metrics `:9101` decoupled from the S3 TLS cert — now always plain HTTP (SP1)
  • s3gateway: in-account canonical-ID unification — IAM/SA/STS delegation now works end-to-end (epic E1, authz-model-fix-b)
  • s3gateway: Sentry error reporting (epic A2)
  • Observability dashboards + baseline alerts (E3)
  • Monitoring sidecar stack (E2)
Changed
  • s3gateway: reword four stale `:9000` source comments
Security
  • SEC-1: drop `127.0.0.1:9101:9101` host publish from `docker-compose.relayer.yml`. SEC-2: set `KEYCLOAK_AUDIENCE=relayer`

Stopping & removing

⌄

Stop, but keep your data

docker compose down

Stop and wipe everything for a clean slate

docker compose down -v

The -v flag deletes the relayer_data volume — your local database, config, and any stored objects. Use it when you want to start completely fresh.