Upgrade an instance¶
Move a running instance to a new NetRoll release, and get back to the previous one if you need to.
Prerequisites¶
- A running instance.
- A current Postgres backup. Migrations run automatically at boot and are not reversed by rolling the image back.
Steps¶
Step 1: Pin a version¶
Pin an explicit tag rather than tracking latest, so an upgrade is something you choose:
services:
app:
image: ${IMAGE_REF:-ghcr.io/nkbooth/netroll}:${IMAGE_TAG:-latest}
# .env
IMAGE_TAG=v1.4.0
Releases follow semantic versioning, derived from Conventional Commits. Check the CHANGELOG before moving across a major version.
Step 2: Back up Postgres¶
podman compose exec postgres pg_dump -U netroll netroll > netroll-$(date +%F).sql
Back up the avatar volume too if you care about uploaded avatars — they're the only user data outside Postgres.
Step 3: Set the new tag and pull¶
# .env
IMAGE_TAG=v1.5.0
podman compose pull
Step 4: Recreate the container¶
podman compose up -d
The new container applies any pending schema migrations at boot before serving traffic.
Verify¶
podman compose logs app | tail -30 # no migration or boot errors
curl -s http://localhost:8080/healthz # {"status":"ok"} once it serves and reaches the database
curl -sI https://netroll.example.com/ | head -1
Sign in, open a net, and start a session. If the app is serving pages but a specific feature
returns HTTP 503, an optional variable it depends on is unset — the QRZ endpoints do exactly
that without a KEK.
If the container exits with an index migration error¶
NetRoll builds every index after the initial schema with CREATE INDEX CONCURRENTLY, so an
upgrade never takes a write lock on your live net and session data. The initial schema's own
indexes are plain builds, which costs nothing: that migration runs once, against the empty
database you are creating. The trade-off of the concurrent form is that a build which fails
partway — a lost connection, a restart mid-build, a statement_timeout — leaves an invalid
index behind. NetRoll cannot clean that up for you: the migration is re-run verbatim on the next
boot, and it then fails against the leftover, so the container keeps exiting until you remove it.
The symptom in podman compose logs app is a boot abort naming a _v2 index:
error: while executing migration ...: relation "idx_<name>_v2" already exists
A slow migration is not a failed one — check before you drop anything
CREATE INDEX CONCURRENTLY waits for every transaction that was already open when it
started, so one long-running query elsewhere makes a perfectly healthy upgrade look
stalled. A build that is still waiting reports indisvalid = f in the detection query
below — exactly like a failed one. Dropping it aborts a build that would have
succeeded and manufactures the very leftover this section exists to remove.
Only continue once the app container has exited and no concurrent build is still running. Nothing below is safe to act on before that.
Confirm nothing is still building:
podman compose ps # the app container must be Exited, not Up
podman compose exec postgres psql -U netroll -d netroll -c \
"SELECT pid, state, query FROM pg_stat_activity
WHERE query ILIKE '%CONCURRENTLY%' AND pid <> pg_backend_pid();"
An exited app container plus zero rows here means no build is in flight, and anything the next query reports is a leftover rather than work in progress.
Find the leftovers:
podman compose exec postgres psql -U netroll -d netroll -c \
"SELECT indexrelid::regclass AS index_name, indrelid::regclass AS table_name,
indisvalid, indisready
FROM pg_index WHERE NOT indisvalid;"
indisready says how far the build got. It narrows down where a build stopped, but on its
own it does not prove that it stopped — the check above is what establishes that:
indisvalid |
indisready |
Reading |
|---|---|---|
f |
f |
Never completed its first build pass. Either it failed there, or it is still waiting for open transactions to finish. |
f |
t |
Completed the build pass and then failed — or is still busy — validating rows. |
With the app container exited and no CONCURRENTLY query running, drop each index the query
reports. This is safe to run against a live database — it takes no lock that blocks reads or
writes:
podman compose exec postgres psql -U netroll -d netroll -c \
"DROP INDEX CONCURRENTLY IF EXISTS idx_<name>_v2;"
Then start the container again. The migration re-runs from a clean slate:
podman compose up -d
If the query returns no rows, the boot failure is something other than a half-built index — read the full log rather than applying this fix.
If the container exits with an extension error¶
The initial schema creates the pg_trgm extension, which the title-search indexes are built
on, so this failure happens on a NEW instance's first boot rather than partway through an
upgrade.
The app cannot work around a database that refuses it, so the boot stops there. This is a
different failure from a half-built index: the detection query above returns no rows, there
is nothing to drop, and dropping anything would be wrong. The remedy is a privilege or a provider
setting, then a restart.
The symptom in podman compose logs app is one of two lines:
error: while executing migration ...: permission denied to create extension "pg_trgm"
The role the app connects as lacks CREATE on the database. This cannot happen on the
quickstart stack — there netroll is the superuser the image created — so you are on a database
where the app's role was created separately with fewer rights. On PostgreSQL 13 or newer,
pg_trgm is a trusted extension, so CREATE on the database is all it needs. Grant it as a
role that can (<superuser> below is whatever your server calls that role) and restart:
podman compose exec postgres psql -U <superuser> -d netroll -c \
"GRANT CREATE ON DATABASE netroll TO netroll;"
podman compose up -d
On PostgreSQL 12 or older, only a superuser can create the extension. Create it yourself once
and restart — the migration is written IF NOT EXISTS, so it then succeeds without doing
anything:
podman compose exec postgres psql -U <superuser> -d netroll -c \
"CREATE EXTENSION IF NOT EXISTS pg_trgm;"
podman compose up -d
error: while executing migration ...: could not open extension control file ".../pg_trgm.control"
The extension's files are not installed on the server at all. The official postgres image
ships them; a distribution package usually puts them in a separate contrib package; a managed
provider exposes them through its own extension allow-list. Install or enable pg_trgm there,
then restart. There is no in-app setting that turns the requirement off.
Roll back¶
Set IMAGE_TAG back to the previous version and bring the stack up again:
# .env
IMAGE_TAG=v1.4.0
podman compose up -d
Rolling the image back does not roll back schema migrations. If the release you're leaving introduced a migration the older binary can't read, restore your Postgres dump as well.
Related guides¶
- Deployment model
- Configuration reference
- Troubleshooting self-hosting
- Releases in CONTRIBUTING.md — how versions get cut