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

314 lines
12 KiB
Markdown

# 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
```toml
[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.
```toml
[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.
```toml
# 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.
```toml
# 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"]
```
```toml
# 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"]
```
```toml
# 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
```yaml
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.
```bash
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
refuses to publish unless `docker buildx inspect` reports both `linux/amd64`
and `linux/arm64`, 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.