Skip to content

Updating

TravStats follows semantic versioning and ships through GHCR with a Docker Hub mirror for stable releases. The update flow is two commands. Migrations run automatically.

TagWhat it points atWhen to use
:X.Y.ZAn exact released version (e.g. :2.6.0)Immutable pin — you control upgrades manually
:stableSame image as the latest X.Y.Z final release”Production trunk” — promoted only after RC verification
:latestSame image as :stableDefault if you don’t pin in compose
:rc-latestThe most recent Release Candidate (e.g. 2.7.0-rc.2)Beta-test the next release on a side instance
:X.Y.Z-rc.NAn exact RC buildPin a specific RC — no auto-rollover

Images are built to GHCR — ghcr.io/abrechen2/travstats — and the final tags (:X.Y.Z, :latest, :stable) plus :rc-latest are mirrored byte-identically to Docker Hub as abrechen2/travstats, so Unraid, Synology and Portainer users can pull without switching registries. Immutable RC and beta tags stay on GHCR only.

Terminal window
cd /opt/travstats # wherever your docker-compose.prod.yml lives
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml logs -f app

That’s it. The entrypoint takes care of:

  1. Loading the persisted JWT secret from /app/data/secrets/jwt.secret.
  2. Auto-resolving any failed migrations from a previous boot (prisma migrate resolve --rolled-back … against the marker rows).
  3. Running new migrations (prisma migrate deploy).
  4. Running idempotent backfill scripts (e.g. backfillTimeSemantics.js on the v1.2.0 → v1.3.0 boundary). Subsequent boots return zero rows and finish in milliseconds.

A successful upgrade ends with the [entrypoint] TravStats is ready line and /health returning 200.

If you want full control over upgrades — recommended for production:

# docker-compose.prod.yml (excerpt)
services:
app:
image: ghcr.io/abrechen2/travstats:2.6.0

Or via .env:

Terminal window
VERSION=2.6.0

The compose file already references ${VERSION:-latest}, so setting VERSION pins the tag without touching the YAML.

Read the CHANGELOG first — every entry calls out:

  • Breaking changes (rare; major versions only)
  • Database migrations that run automatically (and what they do)
  • New required env vars (almost never — most things move into the admin UI instead)

Take a backup, especially before any minor version bump. TravStats writes backups to /app/data/backups on the interval you chose once you’ve enabled the schedule (it is off by default), but a fresh dump right before an upgrade is cheap insurance:

Terminal window
docker exec travstats-db pg_dump -U flights flights > flights-pre-upgrade.sql

2.6.0 is the largest release so far. Three things to know before you pull it:

  • 45 migrations run on the upgrade from 2.5.2. They add the lodging and places domains, two-factor and passkey tables, exchange-rate snapshots, the country-evidence tables and more, and one turns a flight’s duration into a column the database owns. On a large logbook the first boot takes noticeably longer than usual. Back up first; this is the release the paragraph above exists for.
  • Your country count moves. Countries are now counted by the evidence that proves a visit — a completed stay, a recorded place, a port call, a day on the ground — rather than by where a flight touched down. The headline rises for accounts with cruises, places or hotel stays, and falls for accounts whose countries rested on airport connections, which are kept and greyed out but left out of the headline by default. The passport page says so once, with your real before-and-after figures. Nothing was lost; see Countries & passport and the troubleshooting entry.
  • Take a fresh backup after upgrading. Backups taken before 2.6.0 carried only three of the six upload directories — trip photos, place photos and profile pictures were never in them — and cannot be repaired after the fact.

Other figures that move on the upgrade, all explained in the changelog: total flight time and hours in the air (placeholder clocks on date-only flights no longer count; estimates are labelled), airline counts (identity is now the IATA code), the top-routes list (a route is the pair, not the direction), and any total that used to add amounts across currencies.

Most TravStats migrations are pure schema and run automatically during boot. A few releases also benefit from a one-shot data backfill that populates fields the new schema introduced. The scripts are idempotent — re-running them is safe and finishes in milliseconds when there’s nothing left to do.

Pre-v1.5 versions left route_distance NULL on flights inserted manually or via Excel import — only the provider-lookup paths populated it. Result: total km, longest-flight, and the distance-tier achievements were blank for those rows.

After upgrading to v1.5+, run the backfill once. Since 2.6.2 the script is compiled into the image and runs with plain node — the container has neither tsx nor the TypeScript sources, which is why the earlier “copy the .ts file in and run it with npx tsx” recipe could not resolve its imports. Count first, then apply:

Terminal window
docker exec travstats-app node /app/backend/dist/scripts/backfillRouteDistance.js --dry-run
docker exec travstats-app node /app/backend/dist/scripts/backfillRouteDistance.js

travstats-app is the container name from the shipped compose file; use yours if you renamed it. On images before 2.6.2 the script is present only as .ts and does not run inside the container — run it from a source checkout against the database instead (DATABASE_URL=… npx tsx backend/src/scripts/backfillRouteDistance.ts).

The script Haversine-computes distance from each flight’s already- enriched departure / arrival coordinates and UPDATEs in batches of 500. It only touches rows where routeDistance IS NULL, so re-running is a no-op. --dry-run counts rows without writing, --batch-size=N tunes throughput; on a typical homelab instance, even ~200 rows backfill in well under a second.

If the new version misbehaves, roll back to the previous image tag before the database has accumulated new schema you can’t undo:

.env
VERSION=2.5.2 # the previous version
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

Prisma migrations are forward-only by design — they don’t auto-revert when you downgrade the image. The new schema stays. If the failed upgrade introduced columns or tables the older code can’t tolerate — and 2.6.0 does, with a generated column and renamed stored values — you’ll need to restore the pre-upgrade SQL dump:

Terminal window
# Stop the app first so it can't write to the rolling-back DB
docker compose -f docker-compose.prod.yml stop app
# Drop and recreate the database from the dump
docker exec -i travstats-db psql -U flights -d flights -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
docker exec -i travstats-db psql -U flights flights < flights-pre-upgrade.sql
# Bring the (older) app back up
docker compose -f docker-compose.prod.yml up -d app

In practice this is rare — most TravStats migrations are additive, and most “upgrade went wrong” cases recover by just downgrading the image tag. Treat a rollback across 2.6.0 as the exception and restore the dump.

TravStats checks GitHub Releases every six hours and shows a pulsing yellow Update badge in the header when a newer final release is available (RCs and pre-releases are filtered out). Click for the version, release date, a 600-character preview of the release notes, and a link to the full notes. “Ignore this version” hides the badge until something even newer ships.

If your instance is air-gapped or firewalled, the GitHub call fails silently — you just won’t see the badge. No errors are thrown.

Old image tags accumulate in the local Docker storage. The narrow command removes dangling images and nothing else:

Terminal window
docker image prune -f

docker system prune is the wider one: besides unused images it also deletes stopped containers, unused networks and the build cache. On a host that runs only TravStats that is usually fine, but it is not an image cleanup — read what it lists before confirming. Neither command touches a running container or a named volume.