Upgrading
Get to 0.6 from any version. Upgrades replace the code and keep your data and configuration; database migrations run by themselves when the new version starts. From 0.5 on, a one-server install updates itself in place with one command.
Find your path#
| You're on… | How you installed | Follow |
|---|---|---|
| 0.5 or later | One server with goose | ./goose update: in place, nothing else to do |
| 0.4 | One server with goose | From 0.4: one last side-by-side upgrade, then ./goose update |
| 0.1 to 0.5 | Docker / Docker Compose by hand | Docker, or move it to goose |
| 0.1 to 0.5 | Bare Node.js (systemd) | Node.js, or move it to goose |
| 0.1 to 0.5 | Cloudflare Workers | Cloudflare |
Not sure which version you run? One server: ./goose version in the kit's deploy/ folder. By hand: the kit folder's name (ats-0.1.0, gooseplicant-0.3.0, …) or the "version" line in its package.json. Then skim the notes per version for everything between yours and 0.7.0, or the changelog.
Backups first, always#
There are no down-migrations: the backup taken right before an upgrade is your way back. Keep it until the new version has run for a while.
| Install | Back up | Roll back |
|---|---|---|
| One server, 0.5+ | ./goose update backs up by itself (data, two-step registrations, settings, secrets, and the old kit). Any time: ./goose backup. | Automatic when the new version doesn't come up; afterwards ./goose rollback. |
| One server, from 0.4 | ./goose upgrade backs up by itself, into the 0.4 folder's deploy/backups/. | In the 0.4 folder: ./goose restore that backup, then ./goose up --build (details). |
| Docker | Snapshot the volume (Docker backups). | Previous image tag in image:, restore the snapshot, docker compose up -d. |
| Node.js | sqlite3 … ".backup …" plus files/ (Node.js backups). | Point /opt/ats/current back at the old folder, restore the backup, restart. |
| Cloudflare | npx wrangler d1 export ats --remote --output ats-backup.sql; files stay in R2. | Deploy the old kit again; restore D1 from the export or with D1's point-in-time restore. |
One server on 0.5 or later: ./goose update#
In the kit's deploy/ folder, the one you ran ./goose setup in:
./goose update --check # installed vs. latest, and what's new; changes nothing
./goose update # shows what's new, asks, then updates this folder in placeThe folder keeps its name: gooseplicant-0.5.0/ may well run 0.5.3 one day. Your settings, secrets, backups and the files you added stay where they are.
goose does this
- Checks for a newer release in
latest/manifest.jsonon this site (or the version you name with--to). Nothing newer: it saysUp to date (…)and stops. - Downloads and verifies the new kit: its SHA-256 must match both
manifest.jsonandSHA256SUMS, or nothing is installed. - Shows what's new since your version (from the new kit's
CHANGELOG.md) and asks before going on. - Builds in a staging copy first: the new kit's own goose, running from
deploy/backups/, builds the new images and validates the new configuration beside the running version. A failure here changes nothing; the old version keeps running. - Backs up: a regular
./goose backup(data, files, two-step registrations,.env,config/,secrets/) and the current kit asdeploy/backups/kit-<old version>.tar.gz. - Switches in place: every file of the new kit is copied over the folder, and files only the old kit had are removed. Never touched:
deploy/.env,deploy/config/,deploy/secrets/,deploy/backups/and anything you added yourself (goose knows the kit's own files fromdeploy/.kit-files). - Renders, starts, checks: re-renders
config/, starts everything, waits until Gooseplicant and Authelia are healthy and Caddy runs, then runsdoctor(aWARNis fine, aFAILis not). Migrations, if the release has any, run as Gooseplicant starts. - Rolls back by itself if anything from the switch on fails: the old kit's files, the backup from step 5 (data included, since migrations only go forward), the old images, start,
doctor. The failed version's container logs stay indeploy/backups/update-failed-<version>-<id>.log. - Records the run in
deploy/backups/update.log, whichrollbackreads later.
==> Update 0.5.0 → 0.5.1 (https://gooseplicant.goosenotes.dev/downloads)
ok downloaded gooseplicant-0.5.1.tar.gz, sha256 verified (manifest.json + SHA256SUMS)
What's new since 0.5.0:
## 0.5.1 — …
- …
? Update 0.5.0 → 0.5.1 in place (/home/dana/gooseplicant-0.5.0)? A backup comes first; if 0.5.1 doesn't come up, goose rolls back by itself. (y/N): y
==> Building 0.5.1 beside the running 0.5.0 (nothing in /home/dana/gooseplicant-0.5.0 changes yet)
==> Backup before the switch
==> Replacing the kit in /home/dana/gooseplicant-0.5.0 (settings, secrets, config, backups and your own files stay)
…
==> Updated 0.5.0 → 0.5.1 in place (/home/dana/gooseplicant-0.5.0)
backup /home/dana/gooseplicant-0.5.0/deploy/backups/gooseplicant-backup-20261020-091502.tar.gz (data + settings before the update)
previous /home/dana/gooseplicant-0.5.0/deploy/backups/kit-0.5.0.tar.gz
undo ./goose rollback (back to 0.5.0 and that backup: later changes are lost)Options#
| Option | What it does |
|---|---|
--check | Compares the installed and the latest version and shows what's new. Changes nothing, needs no terminal; exit code 10 when an update is available. |
--yes | Doesn't ask. Without a terminal (cron, CI, a script) update refuses unless you pass it. |
--to <version> | A specific version instead of the latest. It must be newer than the installed one; anything else is Up to date. |
--no-rollback | If the new version fails, leave it as it is for you to inspect (exit 3); then ./goose rollback. |
Exit codes, for scripts and monitoring#
| Exit code | Meaning |
|---|---|
0 | Updated, or already up to date. |
10 | --check only: an update is available. |
1 | An error before anything changed: download, checksum, build, validation, refused or cancelled. The old version keeps running. |
2 | The new version failed and goose rolled back: the old version runs again, with the data from right before the update. |
3 | Failed and not rolled back (--no-rollback, or the rollback failed too). Follow the steps it printed. |
./goose update --check > /dev/null
case $? in
0) ;; # up to date
10) echo "Gooseplicant update available" ;;
*) echo "update check failed" ;;
esacRoll back: ./goose rollback#
Goes back to the kit and the backup from before the last update, as recorded in update.log. It shows when that backup was taken and asks first, because everything that changed since is lost: applications that arrived, edits, new users and two-step registrations. --yes skips the question. It's one step of history: a rollback can't be rolled back, and after the side-by-side upgrade there is nothing to roll back here (the old version is still in its own folder).
./goose rollbackScheduled updates#
./goose update --schedule weekly # Sundays at 03:17 local time; or: daily
./goose update --schedule status # what's scheduled, and the last update
./goose update --schedule off # remove it--schedule weekly (or daily) shows the line it adds to your user's crontab and asks; --yes adds it without asking. The line runs deploy/goose update --yes at 03:17 with a PATH that finds Docker and appends its output to deploy/backups/update.log. Scheduled runs never prompt, back up and roll back like any update. off removes only goose's own line. Without a crontab command, goose prints a systemd timer and a launchd job to set up yourself instead.
On a Mac, cron can't read the Desktop, Documents or Downloads folders unless /usr/sbin/cron has Full Disk Access, so a scheduled update of a kit in ~/Downloads fails quietly. Put the kit somewhere else before ./goose setup, for example ~/gooseplicant/, or give cron Full Disk Access. goose warns you when you schedule from one of those folders.
You do this
Nothing, as long as updates go through. Only when one fails:
- Exit 2, "failed and was rolled back": the old version runs again with its data. Read
deploy/backups/update-failed-<version>-<id>.log(the failed containers' logs) anddeploy/backups/update.log, then try again later, or ask for help with that log. - Exit 3: follow the steps goose printed; usually
./goose rollback --yes. - Exit 1: nothing changed. The message says why (no network, a checksum mismatch, a build error).
One server on 0.4: one last side-by-side upgrade#
0.4's goose doesn't have update yet, only upgrade, which installs the new version in a new folder next to the old one. Use it once; from then on you update in place.
cd gooseplicant-0.4.0/deploy
./goose upgrade 0.7.0 # backs up, unpacks ../gooseplicant-0.7.0, carries over your settings, rebuilds, doctor
cd ../../gooseplicant-0.7.0/deploy
./goose update --check # from now on: ./goose update, in this foldergoose does this
0.4's upgrade downloads the new kit and checks its SHA-256 against manifest.json and SHA256SUMS, asks, backs up into the 0.4 folder's deploy/backups/, unpacks the new version next to the old one and copies deploy/.env, config/ and secrets/ across. Then the new kit's own upgrade-finish takes over: it fixes the file permissions older kits unpacked too tightly, records the kit's files in deploy/.kit-files (so update knows exactly what it may replace later), re-renders the configuration, rebuilds, starts and runs doctor.
- Why the folder changes once:
upgradeonly knows side by side. From 0.5 on, updates happen in place, so this is the last time the folder name changes. - The old folder: both folders use the same Docker volumes, so the data is already where 0.5 runs. Keep
gooseplicant-0.4.0/until you're happy with the new version: it holds the backupupgrademade. Then delete it (move itsdeploy/backups/somewhere safe first if you want to keep that backup). - Backups on a schedule? Point your cron job or timer at the new folder's
deploy/goose. - Prefer to look before you switch?
./goose upgrade 0.7.0 --files-onlyonly downloads, verifies and unpacks; run../../gooseplicant-0.7.0/deploy/goose upgrade-finishwhen you're ready.
Back to 0.4#
./goose rollback in the new folder doesn't apply here (no update was recorded). Restore the backup upgrade made, from the old folder:
cd gooseplicant-0.4.0/deploy
./goose restore backups/gooseplicant-backup-<date>.tar.gz --yes
./goose up --buildManual installs: Docker, Node.js, Cloudflare#
You can go straight from any version to 0.7.0: there is no need to stop at each version in between. The steps are the same from 0.1, 0.2, 0.3, 0.4 or 0.5; the few extra things to know when coming from an older version are in the notes per version.
Migrations run by themselves#
Every kit ships the full, append-only list in packages/core/src/db/migrations. On start, Gooseplicant records which ones have run in a _migrations table and applies only the missing ones, in order:
| Migration | What it does | First shipped in |
|---|---|---|
0001_init | The schema. | 0.1.0 |
0002_search_index_state | Search-index bookkeeping, followed by a one-time background rebuild of the search index. | 0.1.0 |
0003_brand_theme | Brand theme settings. | 0.1.0 |
0004_lite | Imports without an email address, position folders, export grants. | 0.2.0 |
- 0.3, 0.4, 0.5 and 0.6 add no migrations: their archives carry the same
0001to0004as 0.2. Coming from 0.1,0004runs once on the first start; from 0.2 or later the database needs nothing. - Each migration and its bookkeeping row are applied in one batch, so a failure leaves no half-applied schema. If two instances race, the second sees the work already done.
- Node.js / Docker: they run at process start, before the port opens. If one fails, the process exits with the error.
- Cloudflare: they run on the first request of each Worker isolate.
- There are no down-migrations. To go back after a newer version has started, restore your backup.
Get the new kit#
curl -LO https://gooseplicant.goosenotes.dev/downloads/0.7.0/gooseplicant-0.7.0.tar.gz
curl -LO https://gooseplicant.goosenotes.dev/downloads/0.7.0/SHA256SUMS
grep ' gooseplicant-0.7.0.tar.gz$' SHA256SUMS | shasum -a 256 -c # prints "gooseplicant-0.7.0.tar.gz: OK"
tar -xzf gooseplicant-0.7.0.tar.gzOn Linux without shasum, use sha256sum -c the same way. Compare the new kit's apps/node/.env.example (or apps/cloudflare/wrangler.toml) with what you run, in case a release adds a setting you want.
Docker#
- Back up
Snapshot the
ats-datavolume as in Docker backups. - Build the new image from the new kit
cd gooseplicant-0.7.0 docker build -f apps/node/Dockerfile -t gooseplicant:0.7.0 -t gooseplicant:latest .The version tag keeps each build around, so going back is a one-line change.
- Check the
image:lineIn your
docker-compose.ymlit must saygooseplicant:latest(orgooseplicant:0.7.0). Coming from 0.1, it still saysats:latest: change it. Theatsservice name, theats-datavolume, your.envandCaddyfilestay as they are. - Recreate the container
cd ~/ats-deploy docker compose up -d docker compose logs -f ats # watch it start (and apply 0004, coming from 0.1)
Roll back: put the previous version's tag in image: (rebuild it from the old kit if you only ever tagged latest), restore the snapshot, docker compose up -d.
Node.js#
- Back up
ats.dbwith SQLite's.backup, and thefiles/folder: Node.js backups. - Unpack next to the current version and install
sudo tar -xzf gooseplicant-0.7.0.tar.gz -C /opt/ats cd /opt/ats/gooseplicant-0.7.0 sudo npm ci --omit=dev - Switch over and restart
sudo ln -sfn /opt/ats/gooseplicant-0.7.0 /opt/ats/current sudo systemctl restart ats journalctl -u ats -f/etc/ats/ats.envand/var/lib/ats(yourDATA_DIR) stay where they are. Coming from 0.1, the old folder is calledats-0.1.0and the new onegooseplicant-0.7.0; nothing else about the paths changes.
Kept your .env inside the app folder? Copy apps/node/.env from the old folder to the new one before restarting, and make sure DATA_DIR is an absolute path, or the new folder starts with an empty ./data.
Roll back: sudo ln -sfn /opt/ats/<old folder> /opt/ats/current, restore the backup into DATA_DIR, sudo systemctl restart ats. Delete old folders in /opt/ats once you no longer need them.
Cloudflare#
- Back up
npx wrangler d1 export ats --remote --output ats-backup.sqlUploaded files stay in the
ats-filesR2 bucket; an upgrade doesn't touch them. - Carry over your Wrangler configuration
Copy your
database_id,routesand[vars]from the oldapps/cloudflare/wrangler.tomlinto the new one (or copy the old file over, then re-check it against the new one). Coming from 0.1, addEDITION = "full"under[vars](leaving it out means the same). The Worker, D1 and R2 names stayats,atsandats-files; secrets live in Cloudflare and carry over. - Install and deploy
cd gooseplicant-0.7.0 npm ci npm run deploy -w @ats/cloudflareMigrations run on the first request to each new isolate. Keep the D1 export until you're happy with the new version.
Roll back: run npm run deploy -w @ats/cloudflare from the old kit's folder, and restore D1 from your export or with D1's point-in-time restore if a migration ran (coming from 0.1).
Recommended: move a manual install to goose#
For a Docker or Node.js install on a machine you'd like goose to manage. You get HTTPS, sign-in with two-step, the intake tunnel and, from then on, ./goose update with backups and automatic rollback. Your applications, files, Gooseplicant users and settings come along; your website keeps its secret.
- Back up, and note what to keep
Take a backup as above. Note your
INGEST_SECRET(your website signs with it), the hostname staff use, and, if you ran Authelia, itsusers_database.yml. Your oldPROXY_SECRETdoesn't need to come along: goose generates a new one for both Caddy and Gooseplicant. - Stop the old install
So its ports and data are free:
docker compose downin your deployment folder (the volume stays), orsudo systemctl stop atsfor Node.js. Stop your oldcloudflaredtoo, if you ran one. - Run setup with your existing accounts
cd gooseplicant-0.7.0/deploy ./goose setup --import-users /path/to/users_database.ymlWithout Authelia before, leave out
--import-usersand create accounts with./goose add-userafterwards. To keep the address staff know, give it as the app hostname (for exampleats.example.com). AnswerntoStart it now?. On a Mac, unpack the kit outside Desktop, Documents and Downloads if you want scheduled updates. - Keep your website's secret
Setup can't take the secrets from an answers file (
--confighas no key forINGEST_SECRETorPROXY_SECRET); it keeps whateverdeploy/.envalready holds and generates only what is missing. So replace the generated line indeploy/.envwith your existing value, then re-render:# deploy/.env: change the INGEST_SECRET= line to your existing secret, e.g. # INGEST_SECRET=<the 64 hex characters your website already uses> ./goose render(Writing
INGEST_SECRET=…into a newdeploy/.envbefore the first./goose setupworks too, but setup then treats the folder as an existing install and suggestsnonefor the intake hostname: typeapply.example.comyourself.) Use a plain value like the onesopenssl rand -hex 32makes: no spaces, quotes,#or$. - Copy the data into goose's volume
The first
upbuilds the images and creates an empty database; then the old data replaces it. Use your old volume's name (docker volume ls; Compose prefixes it with the folder name), or for Node.js your data folder, like-v /var/lib/ats:/from:ro. Missing migrations run when Gooseplicant starts again../goose up ./goose down docker run --rm --user 0 --entrypoint sh \ -v ats-deploy_ats-data:/from:ro -v gooseplicant_gooseplicant-data:/to \ gooseplicant:0.7.0 -c 'rm -rf /to/ats.db /to/ats.db-wal /to/ats.db-shm /to/files && cp -a /from/. /to/ && chown -R node:node /to' ./goose up ./goose doctor - Sign in, and tell your team
Two-step registrations from an old Authelia don't carry over: everyone registers their device again at their first sign-in. Their Gooseplicant roles and sites are in the database, so they keep them as long as the email matches. If the intake address changed, update
ATS_URLon your website (./goose connectshows it).
Notes per version#
Everything between your version and 0.7.0 applies. The short list of features is in the changelog.
0.6#
- From 0.5,
./goose updategets you there on one server; manual installs follow the steps above. No migrations: the 0.6 archive carries the same0001to0004as 0.5, and environment variables,.env.example, the Compose example andwrangler.tomlare unchanged. - Settings gain
positionOrderon each location, the order of its position folders (Ordering positions). It's filled in automatically, empty at first, so position folders keep their old order (most used first) until an admin drags them. A script that saves settings throughPUT /admin/settingswithout it keeps the stored order;PUT /admin/sites/:id/positions/ordersets it. - The built-in Unsorted location (id
unsorted) holds imports filed with Sort later. It's created the first time it's used. Rare case: if you created a location with the idunsortedby hand before 0.6, it becomes the built-in one: shown as Unsorted, listed last, visible only to admins and users with all locations (users assigned to just that location lose it), and your website can no longer post to it. To keep it as an ordinary location, before upgrading create a location with another id, move its applicants there and point your website form at the new id. - Filing in Gosling: the new Needs sorting folder, Set location… / Set position… (p), editing location and position in the applicant pane, and dragging applicants onto folders. See Gosling. The Lite API now also accepts
positionTitleonPATCH /applications/:idand addsPOST /applications/organize.
0.5#
- No migrations, and nothing to change: environment variables,
.env.example, the Compose example andwrangler.tomlare the same as in 0.4. - One server: new
./goose update,./goose rollbackand./goose update --schedule../goose upgradestill works, side by side, but there's no reason to use it any more. - Location order is now set by dragging. Admins reorder locations in Gosling's left bar, and that order applies everywhere: in Gosling's tree and in every site picker, for everyone. Saving Settings → Sites (or Gosling's Admin → Locations) no longer changes the order of existing locations, so a form opened before someone reordered can't put the old order back; new locations go to the end. A script that reordered
sitesthroughPUT /admin/settingsnow usesPUT /admin/sites/order. - New for admins: deleting many applicants at once (
POST /candidates/erase), each erased and audited exactly like a single delete. See Gosling and the security model. - Lists refresh themselves: an open inbox or Gosling tab checks for new applications once a minute while it's visible (a small API request). Nothing to configure.
0.4#
- The one-server kit: the
deploy/folder with./goose, which sets up Caddy (automatic HTTPS through Cloudflare DNS), Authelia (local accounts, two-step sign-in) and the intake Tunnel. See Set up on one server. Optional: hand-made Docker, Node.js and Cloudflare installs upgrade as above. - No migrations, and Gooseplicant and Gosling themselves work exactly as in 0.3. Unchanged: environment variables, the
/atsbase path, theX-ATS-*headers your website signs with and theats.dbdatabase file.
0.3#
- New, optional environment variables (Node.js and Docker):
INGEST_PORT,INGEST_HOSTandMAIN_INGEST, for an intake-only port (Public intake, private app). Leave them unset and nothing changes. See Configuration. - Docker: the image declares
EXPOSE 3000 3001; 3001 is used only withINGEST_PORT=3001. Your Compose file keeps working unchanged. - No migrations. Gosling got a new mark (a goose peeking out of a folder); nothing you configure changes.
0.2#
- New names. The product is Gooseplicant now, the archives are
gooseplicant-<version>.tar.gzand.zip(0.1 wasats-0.1.0.*), and they unpack togooseplicant-<version>/. - Docker image tag:
gooseplicant:<version>andgooseplicant:latestinstead ofats. Change theimage:line fromats:latesttogooseplicant:latest; theatsservice name, theats-datavolume and your.envstay. - Migration
0004_literuns once on the first start (installs older than the 0.1.0 release also get0002and0003). It's quick and runs in one batch. - Gosling and
EDITION:EDITIONdefaults tofull, so you keep the HR app and Gosling appears alongside it at/ats/lite/, already covered by your Access policy or proxy.EDITION=literuns only Gosling; see Set the edition. - Cloudflare:
wrangler.tomlhas a newEDITIONentry under[vars]. If you copy your old file over, addEDITION = "full"(or leave it out, which means the same). - Unchanged on purpose: the
/atsbase path, theX-ATS-*headers your website signs with, all other environment variables, the Worker, D1 and R2 names, and theats.dbdatabase file. Your careers form keeps working without a change.
After upgrading#
- Check
/ats/api/healthand sign in. On one server,updatealready ran./goose doctorfor you. - Send a test submission with the curl recipe if the release touched intake.
- Keep the pre-upgrade backup for a while, then delete old kit folders and backups you no longer need.