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#

Which section to follow, by version and install type
You're on…How you installedFollow
0.5 or laterOne server with goose./goose update: in place, nothing else to do
0.4One server with gooseFrom 0.4: one last side-by-side upgrade, then ./goose update
0.1 to 0.5Docker / Docker Compose by handDocker, or move it to goose
0.1 to 0.5Bare Node.js (systemd)Node.js, or move it to goose
0.1 to 0.5Cloudflare WorkersCloudflare

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.

Backing up and rolling back, per install type
InstallBack upRoll 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).
DockerSnapshot the volume (Docker backups).Previous image tag in image:, restore the snapshot, docker compose up -d.
Node.jssqlite3 … ".backup …" plus files/ (Node.js backups).Point /opt/ats/current back at the old folder, restore the backup, restart.
Cloudflarenpx 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:

Terminal
./goose update --check      # installed vs. latest, and what's new; changes nothing
./goose update              # shows what's new, asks, then updates this folder in place

The 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

  1. Checks for a newer release in latest/manifest.json on this site (or the version you name with --to). Nothing newer: it says Up to date (…) and stops.
  2. Downloads and verifies the new kit: its SHA-256 must match both manifest.json and SHA256SUMS, or nothing is installed.
  3. Shows what's new since your version (from the new kit's CHANGELOG.md) and asks before going on.
  4. 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.
  5. Backs up: a regular ./goose backup (data, files, two-step registrations, .env, config/, secrets/) and the current kit as deploy/backups/kit-<old version>.tar.gz.
  6. 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 from deploy/.kit-files).
  7. Renders, starts, checks: re-renders config/, starts everything, waits until Gooseplicant and Authelia are healthy and Caddy runs, then runs doctor (a WARN is fine, a FAIL is not). Migrations, if the release has any, run as Gooseplicant starts.
  8. 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 in deploy/backups/update-failed-<version>-<id>.log.
  9. Records the run in deploy/backups/update.log, which rollback reads later.
./goose update (abridged; versions are an example)
==> 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#

OptionWhat it does
--checkCompares the installed and the latest version and shows what's new. Changes nothing, needs no terminal; exit code 10 when an update is available.
--yesDoesn'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-rollbackIf 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 codeMeaning
0Updated, or already up to date.
10--check only: an update is available.
1An error before anything changed: download, checksum, build, validation, refused or cancelled. The old version keeps running.
2The new version failed and goose rolled back: the old version runs again, with the data from right before the update.
3Failed and not rolled back (--no-rollback, or the rollback failed too). Follow the steps it printed.
a nightly check, without installing
./goose update --check > /dev/null
case $? in
  0)  ;;                                   # up to date
  10) echo "Gooseplicant update available" ;;
  *)  echo "update check failed" ;;
esac

Roll 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).

Terminal
./goose rollback

Scheduled updates#

Terminal
./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) and deploy/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.

Terminal
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 folder

goose 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: upgrade only 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 backup upgrade made. Then delete it (move its deploy/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-only only downloads, verifies and unpacks; run ../../gooseplicant-0.7.0/deploy/goose upgrade-finish when 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:

Terminal
cd gooseplicant-0.4.0/deploy
./goose restore backups/gooseplicant-backup-<date>.tar.gz --yes
./goose up --build

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

Database migrations and the release that first shipped each
MigrationWhat it doesFirst shipped in
0001_initThe schema.0.1.0
0002_search_index_stateSearch-index bookkeeping, followed by a one-time background rebuild of the search index.0.1.0
0003_brand_themeBrand theme settings.0.1.0
0004_liteImports 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 0001 to 0004 as 0.2. Coming from 0.1, 0004 runs 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#

Terminal
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.gz

On 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#

  1. Back up

    Snapshot the ats-data volume as in Docker backups.

  2. Build the new image from the new kit
    Terminal
    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.

  3. Check the image: line

    In your docker-compose.yml it must say gooseplicant:latest (or gooseplicant:0.7.0). Coming from 0.1, it still says ats:latest: change it. The ats service name, the ats-data volume, your .env and Caddyfile stay as they are.

  4. Recreate the container
    Terminal
    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#

  1. Back up

    ats.db with SQLite's .backup, and the files/ folder: Node.js backups.

  2. Unpack next to the current version and install
    Terminal
    sudo tar -xzf gooseplicant-0.7.0.tar.gz -C /opt/ats
    cd /opt/ats/gooseplicant-0.7.0
    sudo npm ci --omit=dev
  3. Switch over and restart
    Terminal
    sudo ln -sfn /opt/ats/gooseplicant-0.7.0 /opt/ats/current
    sudo systemctl restart ats
    journalctl -u ats -f

    /etc/ats/ats.env and /var/lib/ats (your DATA_DIR) stay where they are. Coming from 0.1, the old folder is called ats-0.1.0 and the new one gooseplicant-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#

  1. Back up
    Terminal
    npx wrangler d1 export ats --remote --output ats-backup.sql

    Uploaded files stay in the ats-files R2 bucket; an upgrade doesn't touch them.

  2. Carry over your Wrangler configuration

    Copy your database_id, routes and [vars] from the old apps/cloudflare/wrangler.toml into the new one (or copy the old file over, then re-check it against the new one). Coming from 0.1, add EDITION = "full" under [vars] (leaving it out means the same). The Worker, D1 and R2 names stay ats, ats and ats-files; secrets live in Cloudflare and carry over.

  3. Install and deploy
    Terminal
    cd gooseplicant-0.7.0
    npm ci
    npm run deploy -w @ats/cloudflare

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

  1. 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, its users_database.yml. Your old PROXY_SECRET doesn't need to come along: goose generates a new one for both Caddy and Gooseplicant.

  2. Stop the old install

    So its ports and data are free: docker compose down in your deployment folder (the volume stays), or sudo systemctl stop ats for Node.js. Stop your old cloudflared too, if you ran one.

  3. Run setup with your existing accounts
    Terminal
    cd gooseplicant-0.7.0/deploy
    ./goose setup --import-users /path/to/users_database.yml

    Without Authelia before, leave out --import-users and create accounts with ./goose add-user afterwards. To keep the address staff know, give it as the app hostname (for example ats.example.com). Answer n to Start it now?. On a Mac, unpack the kit outside Desktop, Documents and Downloads if you want scheduled updates.

  4. Keep your website's secret

    Setup can't take the secrets from an answers file (--config has no key for INGEST_SECRET or PROXY_SECRET); it keeps whatever deploy/.env already holds and generates only what is missing. So replace the generated line in deploy/.env with your existing value, then re-render:

    Terminal
    # 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 new deploy/.env before the first ./goose setup works too, but setup then treats the folder as an existing install and suggests none for the intake hostname: type apply.example.com yourself.) Use a plain value like the ones openssl rand -hex 32 makes: no spaces, quotes, # or $.

  5. Copy the data into goose's volume

    The first up builds 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.

    Terminal
    ./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
  6. 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_URL on your website (./goose connect shows 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 update gets you there on one server; manual installs follow the steps above. No migrations: the 0.6 archive carries the same 0001 to 0004 as 0.5, and environment variables, .env.example, the Compose example and wrangler.toml are unchanged.
  • Settings gain positionOrder on 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 through PUT /admin/settings without it keeps the stored order; PUT /admin/sites/:id/positions/order sets 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 id unsorted by 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 positionTitle on PATCH /applications/:id and adds POST /applications/organize.

0.5#

  • No migrations, and nothing to change: environment variables, .env.example, the Compose example and wrangler.toml are the same as in 0.4.
  • One server: new ./goose update, ./goose rollback and ./goose update --schedule. ./goose upgrade still 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 sites through PUT /admin/settings now uses PUT /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 /ats base path, the X-ATS-* headers your website signs with and the ats.db database file.

0.3#

  • New, optional environment variables (Node.js and Docker): INGEST_PORT, INGEST_HOST and MAIN_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 with INGEST_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.gz and .zip (0.1 was ats-0.1.0.*), and they unpack to gooseplicant-<version>/.
  • Docker image tag: gooseplicant:<version> and gooseplicant:latest instead of ats. Change the image: line from ats:latest to gooseplicant:latest; the ats service name, the ats-data volume and your .env stay.
  • Migration 0004_lite runs once on the first start (installs older than the 0.1.0 release also get 0002 and 0003). It's quick and runs in one batch.
  • Gosling and EDITION: EDITION defaults to full, so you keep the HR app and Gosling appears alongside it at /ats/lite/, already covered by your Access policy or proxy. EDITION=lite runs only Gosling; see Set the edition.
  • Cloudflare: wrangler.toml has a new EDITION entry under [vars]. If you copy your old file over, add EDITION = "full" (or leave it out, which means the same).
  • Unchanged on purpose: the /ats base path, the X-ATS-* headers your website signs with, all other environment variables, the Worker, D1 and R2 names, and the ats.db database file. Your careers form keeps working without a change.

After upgrading#

  • Check /ats/api/health and sign in. On one server, update already ran ./goose doctor for 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.