Upgrade an instance
An upgrade is a restart with newer code. Tradr keeps all its state in Postgres, so the containers are replaceable and the volume is not. Migrations run by themselves when the api boots.
Back up first, every time. Migrations are forward-only, so an upgrade changes the schema in a way you cannot undo by starting the old container again.
Before you start
Section titled “Before you start”- Take a backup. Follow Back up and restore.
- Read the release notes at github.com/madmatt112/tradr/releases. Breaking changes are announced there.
- Note the version you are on. You need it to go back.
curl -fsS http://localhost:8080/api/healthExpected result: {"status":"ok","version":"v0.5.4"} on a pinned release.
The release workflow bakes the version into the api image, and /api/health
reports it. A locally built image has no version to report. The field is
then omitted and you get {"status":"ok"}. Check out a release tag if you want a
version you can name.
Whichever image you run, docker compose ps shows the tag it came from:
docker compose ps --format '{{.Service}}\t{{.Image}}'Open tabs during an upgrade
Section titled “Open tabs during an upgrade”A running tab does not need a manual reload after an upgrade. On an image that
carries APP_VERSION, each tab checks the served version about every five
minutes while it is visible. It also checks again as soon as you return to the
tab. When the served version differs from the version the tab started with,
Tradr shows an update prompt. Select Reload to move to the new version. You
lose anything unsaved on the page.
A part of a page can fail to load after an upgrade, because its code file no longer exists on the new version. Tradr reloads the tab one time by itself to recover. If the reload does not recover the page, Tradr shows a prompt with a Reload button instead.
A locally built image has no APP_VERSION and reports no version (see the note
above). A tab on such an image does not check for updates and shows no prompt.
The value localdev is reserved. A tab treats localdev as an unset version,
and also shows no prompt.
The container carries a file named /_headers. This file is a Cloudflare Pages
cache policy for the hosted deployment only. The container does not serve the
file, and a self-hosted stack does not use it. You can ignore it.
Which version bump matters
Section titled “Which version bump matters”Tradr is pre-1.0, so the usual semver reading is inverted:
| Bump | Example | What it means today |
|---|---|---|
| Minor | 0.5.4 → 0.6.0 |
A breaking change. Read the release notes before upgrading. |
| Patch | 0.5.3 → 0.5.4 |
Everything else — features, fixes, UI. |
The full policy, and what counts as a breaking change, is in
docs/versioning.md.
Path 1 — rebuild from source
Section titled “Path 1 — rebuild from source”The shipped docker-compose.yml builds both images locally, so this is the
default path.
git pulldocker compose up -d --buildCompose rebuilds api and web, then recreates the two containers. postgres
keeps running, and the pgdata volume is untouched.
Expected result: Compose reports Container tradr-api-1 Started and
Container tradr-web-1 Started. With a warm build cache this takes seconds.
This path tracks whatever is on main. Check out a release tag first if you want
a specific version:
git fetch --tagsgit checkout v0.5.4docker compose up -d --buildPath 2 — pinned images
Section titled “Path 2 — pinned images”Each release publishes multi-arch images to the GitHub Container Registry. Pin
them if you want to choose your upgrade window rather than track main.
Replace each service’s build: block in docker-compose.yml with an image:
services: api: image: ghcr.io/madmatt112/tradr-api:0.5.4 web: image: ghcr.io/madmatt112/tradr-web:0.5.4Then upgrade by editing the tag and pulling:
docker compose pulldocker compose up -dExpected result: Compose pulls both images and recreates the containers. Your data survives — the volume is not part of the image.
Pin an exact version. :latest moves on every release, which turns an upgrade
into something that happens to you.
What happens on restart
Section titled “What happens on restart”The api runs migrations during boot, before it serves a single request.
- It takes a PostgreSQL advisory lock, so only one container migrates at a time.
- It applies any pending schema migrations, in order.
- It applies any pending post-migrations — index builds that cannot run inside a transaction.
- It starts serving.
The api healthcheck allows a 180-second start_period for this. A first-run
index build on a large positions table can take minutes, and the grace window
keeps Docker from killing the container part-way through.
During the window docker compose ps reports health: starting, and requests
through web may return 502 Bad Gateway. Both are expected. restart: unless-stopped does not interrupt a migration in flight.
Details are in Database & migrations.
Verify the upgrade
Section titled “Verify the upgrade”docker compose psExpected result: api reports Up … (healthy), postgres reports
Up … (healthy), and web reports Up.
curl -fsS http://localhost:8080/api/healthExpected result: {"status":"ok","version":"v0.5.4"}, naming the version you
moved to. A 503 with {"status":"error"} means the api is up but cannot
reach the database.
docker compose exec api tradr migrate --statusExpected result: exit code 0, and output ending in Schema is up to date.
Standard migrations (drizzle.__drizzle_migrations): table exists: true applied: 24 pending: nonePost-migrations (_post_migrations_journal): table exists: true applied: 1 pending: noneAdvisory locks: migrations (7064001) held: false post-migrations (7064002) held: falseSchema is up to date.Exit code 1 means migrations are pending. Exit code 2 means the command cannot reach the database.
Then log in and open a position you already had. That checks the whole path, which
/api/health does not.
Go back to the previous version
Section titled “Go back to the previous version”Redeploy the version you came from. Set the previous tag, or check out the previous commit, and start the stack again.
# pinned imagesdocker compose pulldocker compose up -d
# built from sourcegit checkout v0.5.3docker compose up -d --buildThis works because the schema stays ahead of the code and the older code still runs against it. Contributors are required to add before they remove, so a release adds columns and a later release drops them.
A rollback does not undo the migration. There are no down-migrations. What you are doing is running older code against a newer schema.
Two limits follow:
- A release that drops something ends the range. Once a release removes a column an earlier version still reads, you cannot go back past it. Release notes say so when it happens.
- A rollback does not restore data. Rows written by the newer version stay. Restore a backup if you need the data back as well as the code.
Upgrading a large instance
Section titled “Upgrading a large instance”SKIP_POST_MIGRATIONS=true stops the api from building indexes during boot, so a
large instance starts serving without waiting.
Leave it false on a Compose instance. The published image ships no command to
apply the work it skips, so the indexes stay missing and queries stay slow. The
setting exists for operators who run migrations out of band from a source checkout.
Troubleshooting
Section titled “Troubleshooting”apisits athealth: startingandwebreturns 502 — a migration is running. Allow the full 180 seconds, and watch it withdocker compose logs -f api.apirestarts in a loop, namingENCRYPTION_KEY_PREVIOUS— you are on a release through v0.5.3. Comment the line out in.env, or upgrade to v0.5.4.apirestarts in a loop, naming the encryption key —ENCRYPTION_KEYchanged, orENCRYPTION_KEY_FINGERPRINTno longer matches it. Restore the original value.tradr migrate --statusexits 1 after an upgrade — the api has not finished booting, or it crashed before migrating. Readdocker compose logs api.docker compose pullreports there is nothing to pull — the services still carrybuild:blocks. Use path 1, or pin images as in path 2.- The corner badge still shows the old version — the browser is serving a cached SPA. Reload without cache.
Next steps
Section titled “Next steps”- Back up and restore — do this before each upgrade.
- Database & migrations — what runs, in what order, and under which lock.
- Install with Docker Compose — the stack this page assumes.