Files
archive-clients/docs/deployment-and-usage.md
T

12 KiB

Configuration, deployment, and usage

Configuration rules

Both daemons parse strict TOML: unknown keys, duplicate keys, invalid types, unsafe relative paths, and missing required secrets fail startup. Effective precedence is command line, environment, TOML, then documented default. Secret fields support a direct environment value or a *_file path, never both. Environment interpolation is explicit ${NAME} syntax; an unset value fails.

--print-effective-config emits a validated, fully resolved configuration with all secrets redacted. Durations accept a documented unit string. Paths shown to the APIs and paths visible inside the daemon are configured as pairs and tested for consistent mapping before registration.

Important defaults:

  • registration deadline 10s;
  • heartbeat interval 15s, offline timeout 45s;
  • unacknowledged command retry timeout 15s, three total attempts;
  • reconnect 1s initial, 60s cap, jitter enabled;
  • route policy on_demand, setup timeout 30m;
  • stall warning 30m, with no automatic failure timeout;
  • one active data job per client and route;
  • Telegram coalesce window 5s, UI session TTL 24h;
  • tombstone retention 30d;
  • database backup interval 6h, retention 12/14/8.

Control configuration example

[archive_control]
enabled = true
listen = "127.0.0.1:8765"
websocket_path = "/archive_control"
shared_token_file = "/run/secrets/archive_control_token"
database = "/var/lib/mogic/archive-control.db"
backup_dir = "/var/backups/mogic/archive-control"
registration_timeout = "10s"
heartbeat_interval = "15s"
offline_timeout = "45s"
command_ack_timeout = "15s" # Delay before resending the same command ID.
command_max_attempts = 3 # Total sends, including the initial attempt.
stall_after = "30m" # Warning state only; jobs continue waiting.
route_policy = "on_demand" # Alternative: eager_mesh.
route_setup_timeout = "30m"
# Default: allow all eligible jobs; disk/network/service capacity is the limit.
enforce_concurrency_limits = false
# Used only when enforce_concurrency_limits is true.
max_active_per_client = 1
max_active_per_route = 1
max_envelope_bytes = 1048576
inventory_chunk_bytes = 524288

[archive_control.backup]
interval = "6h"
recent = 12
daily = 14
weekly = 8

[archive_control.telegram]
progress_coalesce = "5s"
session_ttl = "24h"
tombstone_retention = "30d"
# Existing username allowlist remains authoritative when no ID is pinned.
admin_user_ids = { "@alice" = 123456789 }

The test-only HTTP adapter has a separate test_http.enabled flag that is false by default and omitted from production Compose configuration.

[archive_control.test_http]
enabled = false # Enable only in the isolated local E2E control stack.
listen = "127.0.0.1:0" # Non-loopback binds are rejected.
max_body_bytes = 1048576

Client configuration examples

All nodes use the same image. The examples show two active cache and two active archive nodes. Values are illustrative; credentials are mounted secrets.

# cache-1.toml
client_id = "cache-1"
display_name = "Cache 1"
role = "cache"
control_endpoint = "ws://control:8765/archive_control"
shared_token_file = "/run/secrets/archive_control_token"
state_db = "/var/lib/archive-control/client.db"
backup_dir = "/var/backups/archive-control"

[connection]
registration_timeout = "10s" # First response must arrive within this window.
heartbeat_interval = "15s" # Server may negotiate a different effective value.
offline_timeout = "45s"
outbound_enqueue_timeout = "15s" # Reconnect rather than wedging if sends stop draining.
reconnect_initial = "1s"
reconnect_max = "60s" # Retry forever, never wait longer than this.
reconnect_reset_after = "60s"
reconnect_jitter = true

[jobs]
stall_after = "30m" # Warning only; no automatic job failure.
verification_timeout = "30m" # Stopped qB full-recheck deadline.
poll_interval = "1s" # Active qB/Syncthing observation interval.
# Worst-case copy fallback must leave this many bytes free.
free_space_reserve_bytes = 33554432 # 32 MiB; payload bytes are added for copy paths.

[backup]
interval = "6h"
recent = 12
daily = 14
weekly = 8

[qbittorrent]
endpoint = "http://qbittorrent:8080"
username = "${QB_USER}"
password_file = "/run/secrets/qb_password"
api_root = "/downloads"
local_root = "/data/qb"
# Optional only when a nested qB path uses a different client-visible mount.
# local_path_overrides = { "/downloads/fast" = "/data/qb-fast" }

[syncthing]
endpoint = "http://syncthing:8384"
api_key_file = "/run/secrets/syncthing_api_key"
api_root = "/sync"
local_root = "/data/sync"
advertised_addresses = ["dynamic"]

When an existing Syncthing folder is physically nested in the qB data root, use a local_path_overrides entry to map that exact Syncthing API path through the same client bind mount. This enables hardlinks without creating two Docker mount boundaries for the same host files. Do not add a second bind mount for the nested folder: Linux treats it as a distinct mount even when it has the same st_dev, and the client correctly falls back to copy-only capacity accounting. The override key is the normalized Syncthing folder path beneath api_root, not its folder ID. See the production deployment README for the required compose and override pattern.

qBittorrent content is resolved differently: every existing torrent keeps its qB-reported save_path. The client maps that path beneath qbittorrent.api_root to its own mount before a source, existing-target, permission, or eviction operation. Thus /downloads/Downloading naturally maps below /data/qb; there is no migration or per-resource configuration. Add a qB local_path_overrides entry only when that nested API prefix is a separate client mount.

The remaining node examples omit optional [connection], [jobs], and [backup] tables and therefore use these same defaults; deployments may override them per node.

# cache-2.toml: same service layout, distinct stable identity/state volumes.
client_id = "cache-2"
display_name = "Cache 2"
role = "cache"
control_endpoint = "ws://control:8765/archive_control"
shared_token_file = "/run/secrets/archive_control_token"
state_db = "/var/lib/archive-control/client.db"
backup_dir = "/var/backups/archive-control"

[qbittorrent]
endpoint = "http://qbittorrent:8080"
username = "${QB_USER}"
password_file = "/run/secrets/qb_password"
api_root = "/downloads"
local_root = "/data/qb"

[syncthing]
endpoint = "http://syncthing:8384"
api_key_file = "/run/secrets/syncthing_api_key"
api_root = "/sync"
local_root = "/data/sync"
advertised_addresses = ["dynamic"]
# archive-1.toml
client_id = "archive-1"
display_name = "Archive 1"
role = "archive"
control_endpoint = "ws://control:8765/archive_control"
shared_token_file = "/run/secrets/archive_control_token"
state_db = "/var/lib/archive-control/client.db"
backup_dir = "/var/backups/archive-control"

[qbittorrent]
endpoint = "http://qbittorrent:8080"
username = "${QB_USER}"
password_file = "/run/secrets/qb_password"
api_root = "/archive"
local_root = "/data/archive"

[syncthing]
endpoint = "http://syncthing:8384"
api_key_file = "/run/secrets/syncthing_api_key"
api_root = "/sync"
local_root = "/data/sync"
advertised_addresses = ["dynamic"]
# archive-2.toml
client_id = "archive-2"
display_name = "Archive 2"
role = "archive"
control_endpoint = "ws://control:8765/archive_control"
shared_token_file = "/run/secrets/archive_control_token"
state_db = "/var/lib/archive-control/client.db"
backup_dir = "/var/backups/archive-control"

[qbittorrent]
endpoint = "http://qbittorrent:8080"
username = "${QB_USER}"
password_file = "/run/secrets/qb_password"
api_root = "/archive"
local_root = "/data/archive"

[syncthing]
endpoint = "http://syncthing:8384"
api_key_file = "/run/secrets/syncthing_api_key"
api_root = "/sync"
local_root = "/data/sync"
advertised_addresses = ["dynamic"]

No explicit route matrix is configured. Clients discover existing compatible folders, report their Syncthing device ID, and control creates missing cache/archive routes according to policy.

Client Compose example

services:
  archive-client:
    image: sodium/archive-clients:v0.1.14
    user: "1001:1001"
    restart: unless-stopped
    command: ["archive-client", "--config", "/etc/archive-control/client.toml"]
    environment:
      QB_USER: admin
    volumes:
      - ./client.toml:/etc/archive-control/client.toml:ro
      - ./secrets:/run/secrets:ro
      - ./state:/var/lib/archive-control
      - ./backups:/var/backups/archive-control
      - ./qb-data:/data/qb
      - ./sync-data:/data/sync

Archive nodes mount /data/archive instead of /data/qb. Mounts must line up with the configured local roots and the service containers' API-visible roots. The daemon runs unprivileged; directory ownership is prepared by the operator, and mismatch causes fail-fast startup.

Image and release policy

The production client image contains application source and dependencies; it does not bind-mount source code. A multi-stage Dockerfile uses a slim Python base and removes build-only packages/caches. Buildx publishes sodium/archive-clients:<semver> for linux/amd64 and linux/arm64, records the multi-platform digest, and may update latest. Deployments pin the immutable semantic tag or digest.

Generated protobuf Python bindings are committed in each consumer with the exact archive-control-proto tag/commit recorded. Release order is proto, control consumer, client consumer, E2E, then image publication.

Reproducible multi-platform Buildx lifecycle

The named Buildx builders are local acceleration/cache only; they are not a deployment dependency and can be removed after publication. Use the checked-in publisher: it installs only the non-native binfmt handler, creates a temporary rootless BuildKit builder, verifies both platforms, publishes the index, displays its digest, then removes both temporary resources.

scripts/publish-image.sh vX.Y.Z
scripts/publish-image.sh vX.Y.Z --also-latest

The publisher deliberately uses moby/buildkit:rootless with --oci-worker-no-process-sandbox. On nested Docker hosts, the default OCI sandbox can fail while masking /proc/acpi for an emulated build; rootless BuildKit confines that compatibility setting to the disposable builder. It waits briefly for the new worker to observe binfmt, then refuses to publish unless docker buildx inspect reports both linux/amd64 and linux/arm64. On capability failure it prints that inspection output and removes the builder and binfmt handler on success, failure, or interruption. Retain the displayed manifest digest in release notes and deploy the immutable tag or digest.

Operator usage

  1. Prepare local qB, sync, state, backup, config, and secret mounts with the container UID/GID (default example 1001:1001).
  2. Run a config validation/effective-config command before starting.
  3. Start clients. Confirm service health, sparse/root probes, and registration.
  4. Allow route discovery/provisioning to complete when first needed.
  5. Use Telegram Archive/Unarchive; review the exact source, target, and selection before confirming.
  6. Use Evict Cache separately only when its fresh coverage check is enabled.
  7. Treat orange Cleanup Required as a committed transfer needing staging cleanup; do not manually remove the target placement.
  8. Verify database backups periodically with the offline tooling.

The relevant runtime must be stopped before restore. The control maintenance entry point and the client's archive-client-backup command support list, verify <backup>, and restore <backup>; restore verifies the copy, preserves the replaced database and SQLite sidecars with a timestamped suspect name, and installs the selected database atomically.