Auto init via environment variables support, qu init for systemd
Container image / image (push) Successful in 1m38s
Container image / image (push) Successful in 1m38s
This commit is contained in:
@@ -35,6 +35,8 @@ Override the socket path with `QUPTIME_SOCKET=/run/foo.sock`.
|
||||
|
||||
## Environment variables
|
||||
|
||||
### Paths
|
||||
|
||||
| Variable | Purpose |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `QUPTIME_DIR` | Data directory. Defaults to `/etc/quptime` (root) or `$XDG_CONFIG_HOME/quptime`. |
|
||||
@@ -42,9 +44,52 @@ Override the socket path with `QUPTIME_SOCKET=/run/foo.sock`.
|
||||
| `XDG_CONFIG_HOME` | Honored when running as non-root and `QUPTIME_DIR` is unset. |
|
||||
| `XDG_RUNTIME_DIR` | Honored when running as non-root and `QUPTIME_SOCKET` is unset. |
|
||||
|
||||
### `node.yaml` field overrides
|
||||
|
||||
Every field in `node.yaml` can also be supplied via an environment
|
||||
variable. This is the recommended way to drive Docker / Compose
|
||||
deployments: drop the env vars into the compose file and the daemon
|
||||
will bootstrap on first start without a separate `qu init` step.
|
||||
|
||||
| Variable | `node.yaml` field | Notes |
|
||||
| ------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `QUPTIME_NODE_ID` | `node_id` | Pin a specific UUID. Leave unset to let `qu init` / auto-init generate one. |
|
||||
| `QUPTIME_BIND_ADDR` | `bind_addr` | Defaults to `0.0.0.0`. |
|
||||
| `QUPTIME_BIND_PORT` | `bind_port` | Integer. Defaults to `9901`. |
|
||||
| `QUPTIME_ADVERTISE` | `advertise` | `host:port` other peers use to reach this node. Required when bound to a wildcard or behind NAT. |
|
||||
| `QUPTIME_CLUSTER_SECRET` | `cluster_secret` | Pre-shared join secret. Set the same value on every node. If unset on the very first node, one is generated. |
|
||||
|
||||
Precedence is **env > file > compiled default**. Non-empty env values
|
||||
win over whatever is stored in `node.yaml` at load time, so changing a
|
||||
variable in `docker-compose.yml` and restarting the container is
|
||||
enough to roll out new bind/advertise values — no on-disk edit
|
||||
required. Empty env values are ignored (they will not clear a
|
||||
previously persisted field).
|
||||
|
||||
For `qu init` specifically, explicit command-line flags take
|
||||
precedence over env values; env values fill in only the fields the
|
||||
operator did not pass on the command line.
|
||||
|
||||
The daemon does not read any other environment variables. SMTP, Discord,
|
||||
and HTTP probe targets are configured exclusively in `cluster.yaml`.
|
||||
|
||||
## Auto-init on `qu serve`
|
||||
|
||||
If `node.yaml` does not exist when `qu serve` starts, the daemon
|
||||
bootstraps it in-place using the `QUPTIME_*` env vars above: a fresh
|
||||
UUID is generated (or `QUPTIME_NODE_ID` is honored if set), an RSA
|
||||
keypair and self-signed cert are written under `keys/`, and
|
||||
`cluster.yaml` is seeded with this node as its sole peer. If no
|
||||
`QUPTIME_CLUSTER_SECRET` was provided, a random one is generated and
|
||||
printed to stderr — copy it to every follower node's
|
||||
`QUPTIME_CLUSTER_SECRET` (or `--secret` flag) before they start.
|
||||
|
||||
This is what makes the docker-compose flow `docker compose up`-only
|
||||
on a fresh volume. To opt out (e.g. so a misconfigured deployment
|
||||
crashes loudly instead of silently generating a new identity), run
|
||||
`qu init` against the volume yourself before letting `qu serve` ever
|
||||
see it.
|
||||
|
||||
## `node.yaml` — local identity
|
||||
|
||||
Never replicated. One file per host. Generated by `qu init`.
|
||||
|
||||
+43
-14
@@ -27,6 +27,14 @@ services:
|
||||
image: git.cer.sh/axodouble/quptime:v0.1.0
|
||||
container_name: quptime
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# host:port other nodes use to reach this one. Must be reachable
|
||||
# from every peer — the loopback inside the container is useless.
|
||||
- QUPTIME_ADVERTISE=<host-ip>:9901
|
||||
# Pre-shared join secret. Omit on the very first node and read
|
||||
# the generated value out of `docker logs quptime`, then set
|
||||
# this env var on every follower before bringing them up.
|
||||
- QUPTIME_CLUSTER_SECRET=${QUPTIME_CLUSTER_SECRET:-}
|
||||
ports:
|
||||
- "9901:9901"
|
||||
volumes:
|
||||
@@ -41,17 +49,25 @@ volumes:
|
||||
quptime-data:
|
||||
```
|
||||
|
||||
You must **`qu init` before the daemon will start**. With this compose
|
||||
file:
|
||||
`qu serve` auto-initialises the data volume on first start using the
|
||||
`QUPTIME_*` env vars (see [configuration.md](../configuration.md) for
|
||||
the full list). One command brings everything up:
|
||||
|
||||
```sh
|
||||
docker compose run --rm quptime init --advertise <host-ip>:9901
|
||||
docker compose up -d
|
||||
docker compose exec quptime qu status
|
||||
```
|
||||
|
||||
`<host-ip>` must be reachable from every other node — the loopback
|
||||
address inside the container is useless to peers.
|
||||
On the very first node, capture the auto-generated cluster secret:
|
||||
|
||||
```sh
|
||||
docker compose logs quptime | grep -A1 'cluster secret'
|
||||
```
|
||||
|
||||
Copy that value into the `QUPTIME_CLUSTER_SECRET` env var of every
|
||||
follower before starting them, otherwise their join RPCs will be
|
||||
rejected. The full list of accepted env vars lives in
|
||||
[configuration.md](../configuration.md#nodeyaml-field-overrides).
|
||||
|
||||
## Three-node compose on a single host
|
||||
|
||||
@@ -69,18 +85,27 @@ services:
|
||||
alpha:
|
||||
<<: *quptime
|
||||
container_name: alpha
|
||||
environment:
|
||||
- QUPTIME_ADVERTISE=alpha:9901
|
||||
# First node: leave secret unset and read it from `docker logs`.
|
||||
ports: ["9901:9901"]
|
||||
volumes: ["alpha-data:/etc/quptime"]
|
||||
|
||||
bravo:
|
||||
<<: *quptime
|
||||
container_name: bravo
|
||||
environment:
|
||||
- QUPTIME_ADVERTISE=bravo:9901
|
||||
- QUPTIME_CLUSTER_SECRET=${SECRET}
|
||||
ports: ["9902:9901"]
|
||||
volumes: ["bravo-data:/etc/quptime"]
|
||||
|
||||
charlie:
|
||||
<<: *quptime
|
||||
container_name: charlie
|
||||
environment:
|
||||
- QUPTIME_ADVERTISE=charlie:9901
|
||||
- QUPTIME_CLUSTER_SECRET=${SECRET}
|
||||
ports: ["9903:9901"]
|
||||
volumes: ["charlie-data:/etc/quptime"]
|
||||
|
||||
@@ -93,15 +118,12 @@ volumes:
|
||||
Bootstrap:
|
||||
|
||||
```sh
|
||||
# First node: prints the secret to stdout.
|
||||
docker compose run --rm alpha init --advertise alpha:9901
|
||||
# Capture the secret (or read it back from alpha-data).
|
||||
SECRET=$(docker compose exec alpha cat /etc/quptime/node.yaml | grep cluster_secret | awk '{print $2}')
|
||||
|
||||
docker compose run --rm bravo init --advertise bravo:9901 --secret "$SECRET"
|
||||
docker compose run --rm charlie init --advertise charlie:9901 --secret "$SECRET"
|
||||
|
||||
docker compose up -d
|
||||
# 1. Start alpha first to mint the cluster secret.
|
||||
docker compose up -d alpha
|
||||
# 2. Read the secret off alpha's stdout.
|
||||
export SECRET=$(docker compose logs alpha | awk '/cluster secret/{getline; print $1}')
|
||||
# 3. Bring up the followers — they pick up the secret from $SECRET.
|
||||
docker compose up -d bravo charlie
|
||||
|
||||
# Invite from alpha. The hostnames resolve over the compose network.
|
||||
docker compose exec alpha qu node add bravo:9901
|
||||
@@ -127,6 +149,9 @@ services:
|
||||
image: git.cer.sh/axodouble/quptime:v0.1.0
|
||||
container_name: quptime
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- QUPTIME_ADVERTISE=${QUPTIME_ADVERTISE} # host:9901 reachable from peers
|
||||
- QUPTIME_CLUSTER_SECRET=${QUPTIME_CLUSTER_SECRET}
|
||||
ports:
|
||||
- "9901:9901"
|
||||
volumes:
|
||||
@@ -135,6 +160,10 @@ services:
|
||||
- NET_RAW
|
||||
```
|
||||
|
||||
Put the per-host values (`QUPTIME_ADVERTISE`, `QUPTIME_CLUSTER_SECRET`)
|
||||
in a sibling `.env` file or a config-management secret so the compose
|
||||
file itself is identical across hosts.
|
||||
|
||||
Persistence is a bind-mount under `/srv/quptime/data` so backups and
|
||||
upgrades hit a known path. See [operations.md](../operations.md) for
|
||||
the backup recipe.
|
||||
|
||||
@@ -53,12 +53,21 @@ services:
|
||||
quptime:
|
||||
image: git.cer.sh/axodouble/quptime:v0.1.0
|
||||
container_name: quptime
|
||||
environment:
|
||||
# host:port other QUptime nodes use to reach this one. Should be
|
||||
# this node's tailnet IP / MagicDNS name. Auto-init reads this on
|
||||
# first start.
|
||||
- QUPTIME_ADVERTISE=${QUPTIME_ADVERTISE}
|
||||
# Shared cluster join secret. Omit on the very first node to have
|
||||
# it generated and logged for you, then copy it into every
|
||||
# follower's .env.
|
||||
- QUPTIME_CLUSTER_SECRET=${QUPTIME_CLUSTER_SECRET:-}
|
||||
volumes:
|
||||
- quptime:/etc/quptime
|
||||
network_mode: "service:tailscale"
|
||||
depends_on: [tailscale]
|
||||
cap_add: [NET_RAW]
|
||||
# No restart directive yet — needs `qu init` first.
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
tailscale:
|
||||
@@ -67,43 +76,41 @@ volumes:
|
||||
|
||||
### One-time bootstrap
|
||||
|
||||
Each host runs the same script with different `HOST` and `TAILSCALE_AUTHKEY`:
|
||||
Each host runs the same compose file with a per-host `.env`:
|
||||
|
||||
```sh
|
||||
# .env
|
||||
# .env (alpha — the first node)
|
||||
HOST=alpha
|
||||
TAILSCALE_AUTHKEY=tskey-auth-xxxxxxxx
|
||||
QUPTIME_ADVERTISE=100.64.1.1:9901 # this node's tailnet IP
|
||||
# QUPTIME_CLUSTER_SECRET left unset — will be generated on first boot.
|
||||
```
|
||||
|
||||
Start Tailscale alone first so it gets an IP:
|
||||
Start the stack on the first host. `qu serve` auto-initialises the
|
||||
volume using the env vars above, so a single `docker compose up`
|
||||
brings everything up:
|
||||
|
||||
```sh
|
||||
docker compose up -d tailscale
|
||||
sleep 5
|
||||
TSIP=$(docker compose exec tailscale tailscale ip --4)
|
||||
echo "this node's tailnet IP: $TSIP"
|
||||
docker compose up -d
|
||||
docker compose logs quptime | grep -A1 'cluster secret'
|
||||
# Pipe the secret through your password manager.
|
||||
```
|
||||
|
||||
On the **first** host, init without `--secret`:
|
||||
On every **other** host, write the same `.env` plus the captured
|
||||
secret:
|
||||
|
||||
```sh
|
||||
docker compose run --rm quptime init --advertise "$TSIP:9901"
|
||||
# Grab the printed secret; pipe through your password manager.
|
||||
# .env (bravo, charlie, …)
|
||||
HOST=bravo
|
||||
TAILSCALE_AUTHKEY=tskey-auth-xxxxxxxx
|
||||
QUPTIME_ADVERTISE=100.64.1.2:9901
|
||||
QUPTIME_CLUSTER_SECRET=<paste from alpha>
|
||||
```
|
||||
|
||||
On every **other** host, paste the secret:
|
||||
Bring them up and invite them from the first node:
|
||||
|
||||
```sh
|
||||
docker compose run --rm quptime init \
|
||||
--advertise "$TSIP:9901" \
|
||||
--secret "$CLUSTER_SECRET"
|
||||
```
|
||||
|
||||
Then bring up `qu` on every node and invite from the first:
|
||||
|
||||
```sh
|
||||
# Each host
|
||||
docker compose up -d quptime
|
||||
docker compose up -d
|
||||
|
||||
# From alpha
|
||||
docker compose exec quptime qu node add 100.64.1.2:9901
|
||||
|
||||
+19
-4
@@ -146,15 +146,26 @@ both call this out.
|
||||
load node.yaml: open ...: no such file or directory
|
||||
```
|
||||
|
||||
Run `qu init` before `qu serve`. The daemon does not auto-init —
|
||||
silently generating identities and secrets would be a worse failure
|
||||
mode than crashing.
|
||||
`qu serve` normally auto-bootstraps a missing `node.yaml` using the
|
||||
`QUPTIME_*` env vars (see
|
||||
[configuration.md](configuration.md#auto-init-on-qu-serve)). If you
|
||||
still see this error, the most likely causes are:
|
||||
|
||||
- The data directory is read-only or owned by a different user — the
|
||||
bootstrap can't write `node.yaml`. Fix permissions on
|
||||
`$QUPTIME_DIR`.
|
||||
- Something else removed `node.yaml` mid-run (a config-management
|
||||
tool, a misconfigured volume). Re-run `qu serve` and it will
|
||||
rebuild from env, or run `qu init` manually with the flags you
|
||||
want.
|
||||
|
||||
```
|
||||
node.yaml has empty node_id — run `qu init` first
|
||||
```
|
||||
|
||||
Same fix.
|
||||
`node.yaml` exists but lacks a `node_id`. Either delete the file and
|
||||
let auto-init regenerate it, or run `qu init` against a wiped data
|
||||
dir.
|
||||
|
||||
```
|
||||
listen tcp :9901: bind: address already in use
|
||||
@@ -197,3 +208,7 @@ sudo systemctl start quptime
|
||||
|
||||
The data directory is the only state. Wipe it and you're back to a
|
||||
fresh node.
|
||||
|
||||
Under Docker (or any env-driven deploy), the explicit `qu init` step
|
||||
isn't needed — wiping the data volume and restarting the container is
|
||||
enough; `qu serve` will re-bootstrap from the `QUPTIME_*` env vars.
|
||||
|
||||
Reference in New Issue
Block a user