12 Upgrades

Upgrade an instance with deploy/update.sh. The script backs up the instance, installs the new release, rebuilds, restarts and then proves the instance came up with its data intact. Run it for every upgrade.

12.1 Before you upgrade

12.2 Running an update

  1. Unpack the new release into its own directory, never over the installed one:

    mkdir /tmp/grc-release-<version>
    tar -xzf grc-release-<version>.tar.gz -C /tmp/grc-release-<version>
    
  2. From the installation directory, run the update script from the new release, so that fixes to the script itself apply:

    cd /srv/grc
    /tmp/grc-release-<version>/deploy/update.sh /tmp/grc-release-<version> <version> --profile tls
    

    The arguments are:

    ArgumentMeaning
    <release-dir>The unpacked new release. It must contain docker-compose.yml and must not be the installation directory.
    <version>The version label recorded in .deployed-commit and used to name the next rollback image.
    --profile NAMEA Compose profile to build and start with. Repeat it for several profiles. Use --profile tls with the bundled Caddy.
    --backup-dir DIRWhere the backup and row counts go. Default ~/grc-deploy-backups/<project>.
  3. Read the output. A successful run ends with update: <project> is at <version>.

  4. Delete the unpacked release: rm -rf /tmp/grc-release-<version>.

12.3 What the update does

The script runs these steps in order and stops at the first failure.

12.3.1 Plan the file changes

The image is built from the whole installation directory, so a file left behind by an earlier release (a removed page, say) would still be built and served. The script therefore deletes files that the new release no longer contains. To make that safe, it may delete only files that the previous update installed, which it recorded in .deployed-files.

Any other file in the installation directory that is not in the release stops the update before anything changes, with exit status 3:

update: refusing: these files are here, not in the release, and were not installed by the last update:
  backup-to-offsite.sh
Remove them by hand, or list them in .deploy-keep if they belong to this machine. Nothing was changed.

Remove the file, or, if it belongs to this server, list it in .deploy-keep (one path per line, relative to the installation directory, # comments allowed) and run the update again.

These are never deleted or overwritten, whether or not you list them:

The first update of an installation that has no .deployed-files yet refuses any deletion, because it cannot tell installed files from your own. Check the list it prints, remove what is stale, and run it again.

When the plan is accepted, the script prints a summary such as update: grc 1.4.0 -> 1.5.0: 1840 files, 2 to delete and the files it will delete.

12.3.2 Back up and count rows

The script runs the new release's deploy/backup.sh into the backup directory with 30 days' retention, then runs deploy/counts.sql to count the rows of every table in the application and queue schemas, saving the result as counts-before-<stamp>.txt in the same directory.

12.3.3 Tag a rollback image

If web is running, the script tags the image the running container uses as <image>:rollback-<previous version>, where <image> is the web image name the new release's docker-compose.yml configures (by default grc-platform-<COMPOSE_PROJECT_NAME>), for example grc-platform-grc:rollback-1.4.0. It tags the running container's image by its id, so the tag holds the code that is live now even if the image name has changed. The previous version is read from .deployed-commit (none on an installation that has never been updated with the script).

12.3.4 Install the release

The release's files are copied into the installation directory, skipping the protected files above, and the stale files found by the plan are deleted, along with any directories they leave empty.

12.3.5 Build and start

The script runs docker compose build and docker compose up -d with your profiles. migrate applies any new database migrations, refreshes the application role's grants and runs the bootstrap before web and worker restart. The script then waits for web to report healthy, checking every UPDATE_POLL_SECONDS seconds (default 3) up to 60 times.

12.3.6 Verify and record

  1. Runs prisma/audit-verify.ts in the web container and fails unless the audit chain verifies.
  2. Counts rows again (counts-after-<stamp>.txt) and fails if any table in the application schema lost rows or disappeared. Tables that gained rows are listed for information. Queue tables are not compared; finished jobs are cleaned up there normally.
  3. Calls /api/health inside the web container.
  4. Writes the version to .deployed-commit and the release's file list to .deployed-files.

.deployed-commit and .deployed-files are written only when every check passes, so the version on record is always the last update that verified.

12.4 When an update fails

Fails atState of the instanceWhat to do
Usage or plan (exit status 2 or 3)Unchanged.Correct the arguments or the unexpected files and run again.
Backup or row countUnchanged. The files have not been installed.Fix the cause (usually disk space or a stopped db) and run again.
Build, start, health, audit chain, row countsThe new release's files are installed; the database may have been migrated.Read the failure, then fix forward or roll back (below).

After the files have changed, a failure prints the backup directory, the stamp, and the command that returns to the previous image:

update: FAILED after the files changed: web did not become healthy (last status: unhealthy)
Backup and row counts: /home/grc/grc-deploy-backups/grc (stamp 20261016T020000Z).
To return to the previous image: docker tag grc-platform-grc:rollback-1.4.0 grc-platform-grc:latest && docker compose --profile tls up -d

Start with the logs: docker compose logs migrate for a failed start, and docker compose logs web for a health failure (see Monitoring and Troubleshooting).

12.5 Rolling back

Choose the rollback by what the failed release changed in the database.

Image rollback. If migrate applied no new migrations (its log says No pending migrations to apply.), the previous image runs against the database as it is. Run the command the script printed; it names this instance's own image:

docker tag grc-platform-grc:rollback-1.4.0 grc-platform-grc:latest
docker compose --profile tls up -d

The installation directory still holds the new release's files, so a later docker compose build or up --build would build the new release again. Before the next build, run the previous release's update.sh with the previous release directory, or the fixed new release.

Full rollback. If the failed release applied migrations, the previous image may not work with the changed database. Return both code and data to the moment before the update:

  1. Stop the application and keep the current state for investigation:

    docker compose stop web worker
    ./deploy/backup.sh /var/backups/grc-failed-update 3650
    docker compose --profile tls down
    
  2. Restore the backup the update took before installing (db-<stamp>.dump and uploads-<stamp>.tar.gz in the backup directory) onto a new directory holding the previous release and the same .env, as described in Restoring a server backup. To reuse the installation directory and project name, remove the volumes first as in Restoring in place on the same server.

  3. Start the previous release and verify it as in the restore drill.

12.6 Cleaning up after upgrades

Each update leaves a rollback image and a backup. Once a release has run well for a while (replace grc-platform-grc with your instance's image name, grc-platform-<COMPOSE_PROJECT_NAME> unless you set APP_IMAGE):

docker image ls grc-platform-grc
docker image rm grc-platform-grc:rollback-<old version>
docker image prune
docker builder prune

The update's backups in ~/grc-deploy-backups/<project> are pruned after 30 days by the next update. Remove older ones by hand if updates are infrequent.

12.7 Upgrading an installation from before October 2026

An installation started before 15 October 2026 needs one-time steps when it first moves to a newer release. Releases from 14 October 2026 connect web and worker as the separate application role: add APP_DB_PASSWORD to .env (Compose refuses to start without it) and upgrade as usual. Releases from 15 October 2026 run PostgreSQL 18 on a new volume, pgdata18, and PostgreSQL 18 cannot open the older data directory: move the data by taking a backup with the old release, restoring it with deploy/restore.sh onto the new volume, and starting the new release. The old pgdata volume is left untouched for rollback. Follow the release notes for these versions step by step.