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
- Read the release notes for manual steps.
- Make sure the last nightly backup succeeded and its off-site copy exists.
- Check free disk space: the update writes a new backup and builds a new image.
- Plan a short maintenance window. Users stay signed in, but the application is unavailable while the containers are recreated, typically for a minute or two after the build.
12.2 Running an update
-
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> -
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 tlsThe arguments are:
Argument Meaning <release-dir>The unpacked new release. It must contain docker-compose.ymland must not be the installation directory.<version>The version label recorded in .deployed-commitand used to name the next rollback image.--profile NAMEA Compose profile to build and start with. Repeat it for several profiles. Use --profile tlswith the bundled Caddy.--backup-dir DIRWhere the backup and row counts go. Default ~/grc-deploy-backups/<project>. -
Read the output. A successful run ends with
update: <project> is at <version>. -
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:
.env,.deploy-keep,.deployed-commit,.deployed-files.initial-admin,.demo-accountsdocker-compose.override.ymlCaddyfile.*at the top level- the
node_modules/,.next/anduploads/directories - every path listed in
.deploy-keep
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
- Runs
prisma/audit-verify.tsin thewebcontainer and fails unless the audit chain verifies. - 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. - Calls
/api/healthinside thewebcontainer. - Writes the version to
.deployed-commitand 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 at | State of the instance | What to do |
|---|---|---|
| Usage or plan (exit status 2 or 3) | Unchanged. | Correct the arguments or the unexpected files and run again. |
| Backup or row count | Unchanged. 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 counts | The 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:
-
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 -
Restore the backup the update took before installing (
db-<stamp>.dumpanduploads-<stamp>.tar.gzin 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. -
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.