Manual fleet installation¶
How to set up a fleet by hand, when the automated fleet installation does not fit.
Take this path for external managed PostgreSQL, Redis or S3, a private network you
already run, or hosts you want to configure yourself. The three steps below are what
./logstotal deploy otherwise does for you. Commands run on your workstation unless a step
says otherwise.
Manual step 1: Private network¶
Every machine must reach PostgreSQL, Redis and S3. With managed services on public
endpoints you may not need a private network. With the bundled services, set one up
first, so every host has a stable private address before you write the .env files in
step 2.
Warning
A hub-and-spoke network is a single point of failure. With Wireconf or a manual
WireGuard hub, all worker traffic goes through the control plane's endpoint. If the hub
goes down, workers cannot reach Redis, PostgreSQL or S3 and the fleet stops. Either run
Tailscale (NAT traversal and redundant relays), put the control plane behind highly
available ingress, or accept the risk and fail over quickly. Monitor wg show handshake
times alongside the worker heartbeats.
| Option | Best for | Trade-offs |
|---|---|---|
| Wireconf (WireGuard) | Full control, no third-party service | You manage the keys; the hub needs a public IP or port forwarding |
| Tailscale | Fast setup, NAT traversal, access control lists | Requires a Tailscale account |
| Manual WireGuard | Custom topologies, existing WireGuard infrastructure | Most configuration effort |
Option A: Wireconf¶
Wireconf sets up a WireGuard hub-and-spoke network over SSH. The first host in its inventory becomes the hub (the control plane); the others become peers (the workers).
git clone https://github.com/wagga40/Wireconf.git && cd Wireconf
chmod +x wireconf
./wireconf init # creates inventory and wireconf.env
Edit inventory — the control plane on the first line, workers below:
root@cp.example.com no
root@worker-1.example.com no
root@worker-2.example.com no
If the control plane's public IP differs from its name in the inventory, set
WG_HUB_ENDPOINT in wireconf.env. Then:
./wireconf plan # check SSH, OS and tools, show the address layout
./wireconf apply # generate keys, upload configurations, bring the tunnels up
./wireconf verify # confirm handshakes and hub-to-peer pings
The hub gets 10.200.0.1, the first peer 10.200.0.2, and so on. ./wireconf status
shows tunnel health at any time.
Option B: Tailscale¶
Tailscale is WireGuard with automatic NAT traversal and an admin console for access control. On each machine (Ubuntu or Debian):
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
Each machine gets a stable 100.x.y.z address. Use
Tailscale ACLs to limit which machines can reach the
database and Redis ports.
Option C: Manual WireGuard¶
Follow WireGuard's quick start to build a
hub-and-spoke wg0: the control plane as hub, each worker's AllowedIPs a /32, and
PersistentKeepalive = 25 on the workers so NAT keeps the tunnel open. Check it with
ping <hub-ip> from a worker.
You now have a private address for each host — the control plane (for example
10.200.0.1) and each worker (10.200.0.2, 10.200.0.3, …). Use them wherever step 2
shows an example address.
Manual step 2: Control plane and worker env files¶
Generate both files instead of writing them by hand:
./logstotal deploy:env-scaffold # writes deploy-envs/control-plane.env and deploy-envs/worker.env
The templates are for the bundled services (PostgreSQL, Garage S3 and Redis on the control
plane, exposed to the workers). They come with SECRET_KEY, ADMIN_PASSWORD,
POSTGRES_PASSWORD, REDIS_PASSWORD and the S3 keys already generated, and with example
addresses: 10.0.0.1 for the control plane, 10.0.0.2 for the first worker. The command
refuses to overwrite an existing scaffold, because a new one would rotate every secret.
For HTTPS in front of the control plane, run ./logstotal deploy:env-scaffold:proxy
instead. It writes the same two files and adds the Caddy settings to control-plane.env
(proxy profile, DOMAIN, PROXY_TLS, ACME_EMAIL, WEB_PORT, COOKIE_INSECURE=false,
ENABLE_HSTS=true). See HTTPS with Caddy for the TLS modes.
Then edit both files:
- Replace the example addresses with the real ones from step 1.
- Set
ADMIN_EMAILon the control plane, andDOMAINandACME_EMAILif you used:proxy. - Give each worker its own
WORKER_NAMEandWORKER_IP. - Add
GARAGE_RPC_SECRETandGARAGE_ADMIN_TOKENtocontrol-plane.env:./logstotal gen-secretsprints a fresh pair. Left empty, Garage starts with built-in defaults and warns.
Important
Each worker's DATABASE_URL, REDIS_URL and S3_ENDPOINT must point at the same
addresses the control plane publishes through POSTGRES_EXPOSE, REDIS_EXPOSE and
GARAGE_EXPOSE. If they disagree, jobs stay pending or fail right after a worker picks
them up.
On the control plane, POSTGRES_PASSWORD together with postgres in
COMPOSE_PROFILES is what switches it from SQLite to PostgreSQL; the scaffold sets both.
See Docker Compose profiles.
Control-plane .env¶
To write the file by hand for the bundled services:
# Generate with: ./logstotal gen-secrets
SECRET_KEY=<64-char-hex>
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=<strong-password>
# postgres + s3 (Garage) + workers (exposes Redis, PostgreSQL and Garage to the workers)
COMPOSE_PROFILES=postgres,s3,workers
POSTGRES_PASSWORD=<strong-password>
# S3 storage — required for a fleet, because workers do not share a filesystem
STORAGE_BACKEND=s3
S3_BUCKET=logstotal
S3_ACCESS_KEY=<GK-prefixed-key>
S3_SECRET_KEY=<64-char-hex>
S3_REGION=logstotal
GARAGE_RPC_SECRET=<64-char-hex>
GARAGE_ADMIN_TOKEN=<token>
# The `workers` profile publishes Redis, PostgreSQL and Garage on these addresses.
# Use the control plane's private address, so only the private network reaches them.
REDIS_PASSWORD=<strong-password>
REDIS_EXPOSE=10.0.0.1:6379
POSTGRES_EXPOSE=10.0.0.1:5432
GARAGE_EXPOSE=10.0.0.1:3900
# Plain HTTP until Caddy is in front; set to false (or remove) once it is.
COOKIE_INSECURE=true
./logstotal docker:up on the control plane then starts PostgreSQL, Redis, Garage, the
relays for the three of them, the web service and a local worker.
Worker .env¶
Every worker's .env points at the control plane. The values marked "same as the control
plane" must match it exactly:
SECRET_KEY=<same as the control plane>
WORKER_NAME=worker-eu-west-1 # the name shown on /admin/workers
WORKER_IP=10.0.0.2 # the address shown on /admin/workers
DATABASE_URL=postgresql+asyncpg://logstotal:<postgres-password>@10.0.0.1:5432/logstotal
SYNC_DATABASE_URL=postgresql+psycopg2://logstotal:<postgres-password>@10.0.0.1:5432/logstotal
REDIS_URL=redis://:<redis-password>@10.0.0.1:6379
STORAGE_BACKEND=s3
S3_ENDPOINT=http://10.0.0.1:3900
S3_BUCKET=logstotal
S3_ACCESS_KEY=<same as the control plane>
S3_SECRET_KEY=<same as the control plane>
S3_REGION=logstotal
# Worker threads — docker-compose.worker.yml defaults to 4
HUEY_WORKERS=4
A worker host runs docker-compose.worker.yml, started with ./logstotal docker:worker-up
from the install directory. How workers register, and how to cap each one's concurrent
jobs, is in Remote worker details.
External managed services¶
With managed PostgreSQL, Redis and S3 (for example AWS RDS, ElastiCache, S3, Cloudflare R2
or Backblaze B2), point DATABASE_URL, REDIS_URL and the S3_* variables at them on
every host. The control plane then needs neither the s3 nor the workers profile:
COMPOSE_PROFILES=proxy # only if you want Caddy for HTTPS
DATABASE_URL=postgresql+asyncpg://user:pass@your-pg-host:5432/logstotal
REDIS_URL=redis://:password@your-redis-host:6379
STORAGE_BACKEND=s3
S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
S3_BUCKET=logstotal
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=...
S3_REGION=<provider-region>
Manual step 3: Distribute and deploy¶
Copy each file to its host as .env. The scaffold prints these commands when it finishes:
ssh root@cp mkdir -p /opt/logstotal
ssh root@worker mkdir -p /opt/logstotal
scp deploy-envs/control-plane.env root@cp:/opt/logstotal/.env
scp deploy-envs/worker.env root@worker:/opt/logstotal/.env
Then, in deploy.env on your workstation, list the hosts — the control plane first,
then the workers — and tell the deploy not to build a private network, because you built
one in step 1:
DEPLOY_HOSTS=cp.example.com,worker-1.example.com,worker-2.example.com
DEPLOY_VPN=none
With Tailscale, also set DEPLOY_CP_ADDRESS to the control plane's 100.x address. Then:
./logstotal deploy:preflight # SSH, tools and a .env on every host
./logstotal deploy
./logstotal deploy keeps the .env already on each host. It starts the control plane with
./logstotal docker:up and each worker with ./logstotal docker:worker-up, and finishes by
checking /health; ./logstotal deploy:smoke repeats that check at any time. Its settings
are described in SSH deploy in the fleet
reference.