Skip to content
These docs describe Tradr v0.14.0. Running an older release? Check the release notes for what changed.

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.

  1. Take a backup. Follow Back up and restore.
  2. Read the release notes at github.com/madmatt112/tradr/releases. Breaking changes are announced there.
  3. Note the version you are on. You need it to go back.
Terminal window
curl -fsS http://localhost:8080/api/health

Expected 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:

Terminal window
docker compose ps --format '{{.Service}}\t{{.Image}}'

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.

Tradr is pre-1.0, so the usual semver reading is inverted:

Bump Example What it means today
Minor 0.5.40.6.0 A breaking change. Read the release notes before upgrading.
Patch 0.5.30.5.4 Everything else — features, fixes, UI.

The full policy, and what counts as a breaking change, is in docs/versioning.md.

The shipped docker-compose.yml builds both images locally, so this is the default path.

Terminal window
git pull
docker compose up -d --build

Compose 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:

Terminal window
git fetch --tags
git checkout v0.5.4
docker compose up -d --build

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.4

Then upgrade by editing the tag and pulling:

Terminal window
docker compose pull
docker compose up -d

Expected 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.

The api runs migrations during boot, before it serves a single request.

  1. It takes a PostgreSQL advisory lock, so only one container migrates at a time.
  2. It applies any pending schema migrations, in order.
  3. It applies any pending post-migrations — index builds that cannot run inside a transaction.
  4. 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.

Terminal window
docker compose ps

Expected result: api reports Up … (healthy), postgres reports Up … (healthy), and web reports Up.

Terminal window
curl -fsS http://localhost:8080/api/health

Expected 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.

Terminal window
docker compose exec api tradr migrate --status

Expected result: exit code 0, and output ending in Schema is up to date.

Standard migrations (drizzle.__drizzle_migrations):
table exists: true
applied: 24
pending: none
Post-migrations (_post_migrations_journal):
table exists: true
applied: 1
pending: none
Advisory locks:
migrations (7064001) held: false
post-migrations (7064002) held: false
Schema 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.

Redeploy the version you came from. Set the previous tag, or check out the previous commit, and start the stack again.

Terminal window
# pinned images
docker compose pull
docker compose up -d
# built from source
git checkout v0.5.3
docker compose up -d --build

This 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.

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.

  • api sits at health: starting and web returns 502 — a migration is running. Allow the full 180 seconds, and watch it with docker compose logs -f api.
  • api restarts in a loop, naming ENCRYPTION_KEY_PREVIOUS — you are on a release through v0.5.3. Comment the line out in .env, or upgrade to v0.5.4.
  • api restarts in a loop, naming the encryption keyENCRYPTION_KEY changed, or ENCRYPTION_KEY_FINGERPRINT no longer matches it. Restore the original value.
  • tradr migrate --status exits 1 after an upgrade — the api has not finished booting, or it crashed before migrating. Read docker compose logs api.
  • docker compose pull reports there is nothing to pull — the services still carry build: 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.