Repository navigation
v0.18.0 — relay as a container image; honest login and upgrade advice
⚠ Upgrading requires retiring the agent database
app_env gains an updated_at column (#495). Piper has no migrations, and CREATE TABLE IF NOT EXISTS is a no-op against a table that already exists — so an upgraded box hits no such column: updated_at on the first env read or write.
Upgrade the relay first, then each agent. GET /v1/github/status now returns the account's username, which the new piper login reuse path reads.
The relay's schema is unchanged — a relay upgrades in place (install the binary, restart) and keeps its database. Only agents are affected.
On each agent box — option A: migrate in place (recommended)
The change is additive, so one ALTER TABLE plus a backfill gets the existing database to the shape the new code expects. Nothing is lost — apps, env vars, deployments and running containers all survive, and the only interruption is the daemon restart the binary upgrade needs anyway. Verified on a live box serving traffic.
sudo systemctl stop piperd
sudo cp -a /var/lib/piper/piper.db /var/lib/piper/piper.db.pre-0.18
DB=/var/lib/piper/piper.db
has() { sudo sqlite3 "$DB" "PRAGMA table_info($1);" | cut -d'|' -f2 | grep -qx "$2"; }
has app_env updated_at || sudo sqlite3 "$DB" "ALTER TABLE app_env ADD COLUMN updated_at TEXT NOT NULL DEFAULT '';"
has deployments hostname || sudo sqlite3 "$DB" "ALTER TABLE deployments ADD COLUMN hostname TEXT NOT NULL DEFAULT '';"
sudo sqlite3 "$DB" "UPDATE app_env SET updated_at = COALESCE((SELECT a.created_at FROM apps a WHERE a.name=app_env.app), strftime('%Y-%m-%dT%H:%M:%SZ','now')) WHERE updated_at='';"The second ALTER is only needed coming from v0.16.0 or earlier (deployments.hostname, #479); the guard skips it otherwise.
Three things that matter:
- The backfill is not optional. An empty
updated_atfailstime.Parse(RFC3339Nano)and the client treats that as fatal, sopiper env lswould error on every migrated row.apps.created_atis already RFC3339Nano and is an honest floor — an env var cannot predate its app. - Use the
T…Zform. sqlite3's more naturaldatetime('now')yields2026-08-04 09:15:22, which does not parse. - Check file ownership afterwards.
/var/lib/piperisDynamicUserstate owned bynobody:nogroup;sqlite3run as root creates-wal/-shmowned by root. A clean exit removes them, but a lingering root-owned WAL is exactly what would stop the daemon writing.
Verify with sudo sqlite3 -header -column "$DB" "SELECT app,key,updated_at FROM app_env;" — every row must carry a timestamp — then upgrade and start.
The migration is backward-compatible: the old binary's insert names only (app, key, value), so it keeps working against the migrated table and you cannot get stranded halfway.
Option B: retire the database
If you would rather not touch SQL, retiring only piper.db also works. The box keeps its identity, so there is no re-enrollment, no new box id, and no stale row on the relay — but you lose every app definition and env var, and you must clean up stranded containers by hand:
sudo systemctl stop piperd
sudo sh -c 'mkdir -p /var/lib/piper/pre-0.18 && mv /var/lib/piper/piper.db /var/lib/piper/pre-0.18/'
# install 0.18.0 (apt upgrade, or the installer), then:
piper login # converges — prints "already enrolled as <box>"box-id, relay.json, acme_account.key, appdomains/ and caddy/ stay put. domain_config re-derives itself from relay.json on first start. Verified on a Raspberry Pi 4 upgrading from 0.16.0.
What you do lose: app definitions and env vars, which lived in the discarded database. Recover them from the retired file before re-adding — it is a normal SQLite database and still readable:
sudo python3 -c "
import sqlite3
c=sqlite3.connect('file:/var/lib/piper/pre-0.18/piper.db?mode=ro',uri=True)
for r in c.execute('SELECT name,port,repo,branch,root_dir FROM apps ORDER BY name'): print(r)
for r in c.execute('SELECT app,key,value FROM app_env ORDER BY app,key'): print(r)
"Stop your running containers before re-adding the apps. Discarding the database does not stop anything Docker is running, so the previous deployments survive as orphans the agent will never reconcile or retire — and they hold the host ports a fresh deploy of the same app wants. docker ps will show them.
The relay now ships as a container image
ghcr.io/piperbox/piper-relay:0.18.0 — one multi-arch image (linux/amd64 + linux/arm64), distroless, built from the release binary. This is the relay's packaging format, deliberately not apt or brew: its deployment targets are container platforms. Version tags publish on every tag including RCs; :latest moves only on finals. See the runbook's "Run as a container" section for volume/cert mounts, the single-replica-only constraint, and the L4-only fronting rule. (#486, closes #483)
piper login is idempotent and honest (#492)
- A second
piper loginno longer charges a browser round-trip. Both login paths try the stored credential first and settle it with one cheap authed request; only an expired, revoked, or disabled credential falls through to the full device flow.--reloginis the escape hatch for switching GitHub accounts. (#473) - The apply-wait
-racefailure is fixed, the deadline message no longer tells youpiperd did not come backabout a daemon that answered every poll, and the enroll POST gets a budget that survives a Pi on a slow uplink. (#467) - Device-flow hardening: a disabled account is refused at all three doors rather than handed an inert credential, and the in-flight
flowsmap is now bounded by expiry instead of growing for the life of the process. (#81, which stays open for its remainingFakeVerifierask) - GitHub App installs reconcile: an App link that was left stranded now heals. (#470, #474)
piper agent status stops giving wrong upgrade advice (#499)
It compared the running daemon against whatever piperd resolved to in the caller's PATH, so a box with a current system service and a stale ~/.local/bin copy was told to restart into a downgrade. It now compares against the systemd unit's ExecStart, skips the comparison entirely when systemd won't report one, and prints restart piperd to apply only on positive evidence that the disk build is newer. Git-describe and dirty builds are never ordered. A shadowing copy on your PATH gets its own explicit warning. (Closes #472)
Fixes
- Relay reloads its wildcard TLS certificate without a restart. A renewed cert on disk is now picked up through a
GetCertificatecallback that re-stats and re-parses only on change; a failed reload keeps serving the last good pair. Aserving the last good certificateline during a normal renewal is expected, not an incident. (#497, fixes #484) - Custom-domain DNS target is the box's base domain, not the dial address. A box whose relay dial address isn't the relay's public name advertised a nonsense record and its issuance gate could never flip. The mirror-image bug is also closed: a domain resolving to loopback no longer "points at" a loopback dial address. Docs updated to match. (#493, #498, fixes #434)
- Monorepo pushes only rebuild the app they touch. A push that changes nothing under an app's
root_diris skipped. A push whose file list GitHub truncated still rebuilds — a needless build beats a swallowed change. PR previews still rebuild on any change. (#490, closes #319) - Tunnel handshakes can no longer be pinned by a stalled peer, in either direction.
Dial's handshake write is bounded (#457, fixes #436),Serve's success-ack write is bounded (#487, fixes #455), and both agent callers make the handshake context-interruptible so shutdown doesn't wait out the deadlines (#488, fixes #481). - Relay heals a stranded
target_loginafter a GitHub org rename, on the token-mint miss path only. (#464, fixes #433) installation_repositorieswebhooks link the installation, so re-saving an App's repository selection works; a failed link now names the installer instead of failing silently. (#489, fixes #471)piper env lsshows how long ago each variable was last set. (#495, fixes #442)- Tests no longer pop a real browser tab at GitHub device login;
PIPER_NO_BROWSER=1suppresses the opener anywhere, which a headless box or SSH session wants anyway. (#491) - Data race on the test log buffer in
TestServeTunnelLogsRejectedHandshake. (#463, fixes #439) - The installer one-liner is now
curl -fsSL https://get.piperbox.dev/install.sh | sh. (#480)
Known issues shipped open
- #494 — token and installation lists sort RFC3339Nano timestamps lexically, so rows can come back mis-ordered.
- #485 — the relay does not speak PROXY protocol, so client IPs are lost behind an L4 ingress.
- #81 —
FakeVerifieris neither one-shot nor pruned; it is reachable in production underPIPER_RELAY_FAKE_APPROVE=1for the loopback e2e. - If you installed the
v0.18.0-rc.1debs by hand, nfpm rendered that version as0.18.0.rc.1, which outranks0.18.0under dpkg's comparison rules.apt upgradewill not move you to the final; usesudo apt install --allow-downgrades piperd=0.18.0 piper=0.18.0. The apt repo is unaffected — RCs never publish there.