Backup & upgrade

Back up and restore with one command, turn on the daily backup, upgrade with one command.

A backup consists of three things: the PostgreSQL database, the uploaded files (attachments and images embedded in text, the app-uploads volume) and the .env.onprem settings file, which carries the secret keys. The database alone is not enough; after a restore the tasks and pages come back but their attachments do not open. Two scripts in the package do the job for you: ./scripts/backup.sh takes a backup, ./scripts/restore.sh restores one. Run the commands in the installation directory. Upgrading is one command: ./scripts/update.sh downloads the latest package, takes a backup first and applies the update (v1.67.2 and later).

Taking a backup

./scripts/backup.sh

The backup can be taken while the app is running; pg_dump takes a consistent snapshot. Each run creates a new directory under backups/ in the installation directory, named after the date, time and installed version, for example backups/2026-10-02-023000-v1.61.0. The version is read from the installed app container, not from a package that may already be extracted on disk, so a backup taken before an upgrade carries the old version’s name. Only the user who ran the script can open the directory (mode 700). If the database container is not running, the script says it cannot take the dump and stops.

FileContents
db.sql.gzThe database (pg_dump, gzipped plain SQL)
uploads.tar.gzThe uploaded files (the app-uploads volume)
env.onpremA copy of .env.onprem; it holds secret keys, mode 600
manifest.txtDate, version, sizes, table count, host name

The script verifies the backup itself: both archives must be valid gzip, the file archive must list cleanly, the dump must end with pg_dump’s completion marker (not truncated) and it must create a reasonable number of tables (at least 20 by default). A backup is first written to a directory ending in .partial and gets its real name only after it has been verified, so any directory without that suffix is complete. If a check fails, the script exits with a non-zero code and a clear message, and removes the partial directory.

Retention: the newest 7 complete backups are kept and older ones are deleted. Only directories that match the backup name pattern and whose manifest.txt says status: ok are counted or deleted; anything else you put under backups/ is left alone.

OptionWhat it does
--keep NKeep the newest N backups. Default: MYWAY_BACKUP_KEEP in .env.onprem, else 7
--dir PATHWrite the backup to PATH instead of backups/, for example a mounted network share
--quietPrint only errors and a one-line summary (for cron)
--verify DIRCheck an existing backup without creating one
./scripts/backup.sh --keep 14
./scripts/backup.sh --dir /mnt/nas/myway-backups
./scripts/backup.sh --verify backups/2026-10-02-023000-v1.61.0

Copy the backup to another machine. A backup on the same disk does not survive a disk failure or the loss of the server, and the script reminds you of this after every backup you run by hand. Move the directories somewhere else regularly, or write straight to a mounted network share with --dir. Since a backup includes .env.onprem it carries secret keys, so protect the destination accordingly.

Daily automatic backup

The installer offers to set up a daily backup and asks "Set up a daily automatic backup? [Y/n]"; the default is yes. If you accept, ./scripts/backup.sh --quiet runs every night at 02:30, the newest 7 backups are kept under backups/ and the output goes to logs/backup.log. The answer is saved in .env.onprem as MYWAY_DAILY_BACKUP=yes or no and is not asked again on later runs. The question comes up on a first install, and once more when you upgrade an installation older than v1.61.1, since no answer is saved there yet. When the installer runs without a terminal (automation), the daily backup is turned on by default.

The result of each backup shows up under Settings > Health. When the app is running, ./scripts/backup.sh reports every backup’s result (success or error, size, table and file counts, duration) to the app; this applies to backups you take by hand as well as the nightly one, while a check with --verify is not reported. The "Backup" row in the Scheduled jobs table shows the last backup, its result and the next run. If the last backup is more than 26 hours old the row shows "Late", if it failed "Error"; if no result has arrived yet (for example the daily backup is off and no backup was taken by hand) it shows "Not run". Reporting never fails the backup: if the app cannot be reached, the script only prints a warning and the exit code is unchanged. A backup taken while the app is stopped is not reported. When you upgrade an installation older than v1.62.0, the backup taken before the update cannot be reported to the app still running the old version; the script may say so with a warning, and the backup is not affected.

SettingWhat it does
./scripts/install.sh --daily-backupTurns the daily backup on without asking
./scripts/install.sh --no-daily-backupDoes not schedule the daily backup and removes an existing one
MYWAY_BACKUP_KEEPIn .env.onprem: how many backups are kept (default 7)
MYWAY_BACKUP_REPORT=noIn .env.onprem: stops reporting the backup result to the Health screen

The daily backup is written into the same crontab block as the scheduled jobs. If you run the installer with NO_CRON=1, the daily backup is not scheduled either and the installer says so with a warning at the end; in that case run ./scripts/backup.sh yourself or add it to crontab yourself.

Restore

./scripts/restore.sh backups/2026-10-02-023000-v1.61.0

Before touching anything the script verifies the backup. It then shows the backup’s date and version next to the installed version, with the table and file counts, and asks "Type RESTORE to continue:". Type RESTORE to go ahead; any other answer exits without changing anything. When the script runs without a terminal (for example from another script), confirm with --yes. After you confirm:

  1. 1Stops the app; the database container keeps running.
  2. 2Loads the dump into a new, empty database in a single transaction and, once loading succeeds, swaps it in place of the current database by renaming. If loading fails, the current database is untouched and the app is started again.
  3. 3Keeps the previous database as <database name>_pre_restore until the app is healthy, then drops it.
  4. 4Empties the uploaded files volume and extracts the archive from the backup.
  5. 5Starts the app (pending migrations apply as usual) and waits until it is healthy. Users who were signed in before the restore may have to sign in again.

.env.onprem is not restored by default; the script only lists the NAMES of the settings that differ from the backup, never their values. If you are moving the backup to a new server, add --with-env; the current file is kept as .env.onprem.before-restore-<time>. Stored GitLab access tokens are encrypted with SESSION_SECRET and GITLAB_TOKEN_KEK: if those differ from the backup, the script warns and suggests running again with --with-env or reconnecting GitLab afterwards.

# yeni sunucuya taşıma: paketi kur, yedek klasörünü sunucuya kopyala
# moving to a new server: install the package, copy the backup directory over
./scripts/install.sh
./scripts/restore.sh --with-env backups/2026-10-02-023000-v1.61.0

Restore a backup with its own version. If the backup’s version differs from the installed one, the script warns. A backup from an older version is upgraded by the migrations on start; a backup from a newer version may not start on the installed one. The safest path: install the package of the backup’s version first, restore, then upgrade as usual.

Manual backup (without the scripts)

If you cannot use the scripts, you can do the same by hand. The compose file does not have the default name, so every command passes -f docker-compose.onprem.yml and --env-file .env.onprem. Copy .env.onprem to a safe place as well.

# veritabanı / database
docker compose -f docker-compose.onprem.yml --env-file .env.onprem \
  exec -T postgres sh -c 'pg_dump --no-owner --no-privileges -U "$POSTGRES_USER" "$POSTGRES_DB"' \
  | gzip > myway-db-$(date +%F).sql.gz

# yüklenen dosyalar / uploaded files
docker run --rm -v myway-onprem_app-uploads:/data:ro postgres:16-alpine \
  tar czf - -C /data . > myway-uploads-$(date +%F).tar.gz

For a manual restore, stop the app first. The dump must be loaded into an EMPTY database; loading it on top of a populated one stops halfway with "already exists" errors. The commands below drop the current database and create it again empty, so unlike the script there is no way back if loading fails.

C="docker compose -f docker-compose.onprem.yml --env-file .env.onprem"
$C stop app
$C exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d postgres -c "DROP DATABASE \"$POSTGRES_DB\" WITH (FORCE)" -c "CREATE DATABASE \"$POSTGRES_DB\""'
gunzip -c myway-db-YYYY-MM-DD.sql.gz | \
  $C exec -T postgres sh -c 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
docker run --rm -i -v myway-onprem_app-uploads:/data postgres:16-alpine \
  sh -c 'find /data -mindepth 1 -delete && tar xzf - -C /data && chown -R 1000:1000 /data' \
  < myway-uploads-YYYY-MM-DD.tar.gz
$C up -d

The volume name has the form <project>_app-uploads, which is myway-onprem_app-uploads on a standard install. If in doubt, confirm it with docker volume ls | grep app-uploads.

Upgrade

cd myway-onprem   # kurulum dizininiz / your install directory
./scripts/update.sh

Since v1.67.2, ./scripts/update.sh in the install directory upgrades in a single step. It downloads the latest package with the LICENSE_KEY from .env.onprem and compares the package version with the installed one; if they match it prints "Already up to date." and touches nothing. If there is a new version it unpacks the package over the install directory (.env.onprem and backups/ are left alone), runs ./scripts/install.sh --backup so the installed version is backed up before the update is applied, and, when PUBLIC_URL is set, waits up to 150 seconds for /api/health to report the new version. If the backup fails, the update is not applied.

OptionWhat it does
--forceDownloads and reinstalls even if the version is the same
MYWAY_DOWNLOAD_URLDownload address (environment variable); defaults to https://www.themyway.app/api/download
ENV_FILESettings file to read (environment variable); defaults to .env.onprem

First time: the script ships with the v1.67.2 package. Update an older installation once the way described below; get the new package from the link in your purchase email or the "Download v{version} package" button under Settings > Updates (it reads like "Download v1.69.0 package"). From then on ./scripts/update.sh is enough. The script needs internet access; on an air-gapped network, update the way described below as well.

If the backup fails, the package files are already unpacked on disk but the installed version is unchanged. Running ./scripts/update.sh again then says "Already up to date."; once the problem is fixed, run ./scripts/update.sh --force or ./scripts/install.sh. On an installation that uses its own certificate (IP address or non-standard ports) the final wait may not be able to verify the certificate and can fail after 150 seconds even though the update was applied; check the version under Settings > Updates. A "Download failed" error can come from the license key, the network or an expired update entitlement; the script does not show the server’s explanation, Settings > Updates does.

If you downloaded the package yourself (an air-gapped network or the first switch), you upgrade with the same script you installed with. It keeps your existing settings (address, ports, license key, email configuration), only fills in new settings that are missing, loads the new images, applies database migrations and refreshes the scheduled jobs. A new release shows up under Settings > Updates; "Check now" looks right away.

Backup before the update: when the installer finds an existing installation, it asks "Back up before updating? [Y/n]" BEFORE loading the new images; the default is yes. If you say yes, it backs up the installed (old) version with ./scripts/backup.sh and verifies it. If the backup fails, the update is NOT applied and nothing has changed; fix the problem and run the installer again. If the database is not running, only the database is started for the backup; the app stays down. The question is not asked on a first install. When the installer runs without a terminal (automation) and no option is given, it picks the safe side: it backs up before the update and says so in its output.

OptionWhat it does
--backupOn an update, backs up without asking
--no-backupDeliberately skips the backup before the update (not recommended); the installer prints "Updating WITHOUT a backup."
--daily-backup / --no-daily-backupTurns the daily automatic backup on / off without asking
  1. 1Read the release notes; items marked "Heads-up" are behaviour changes and may affect your team.
  2. 2Keep about 1 GB of free disk space during the upgrade: while the new image is written, the old one is still on disk. The backup taken before the update also takes space under backups/ in the installation directory.
  3. 3Download the new package and extract it over the installation directory on your server.
  4. 4Run ./scripts/install.sh. The setup questions are not asked again, your stored settings are used; you answer "Back up before updating? [Y/n]" (Enter means yes). On an installation older than v1.61.1, "Set up a daily automatic backup? [Y/n]" is asked once before it.
  5. 5The installer first backs up and verifies the old version, then loads the new images. Migrations apply automatically on start; at the end the script runs a scheduled job once and reports the result. On an update the "first user to register becomes the administrator" notice is not shown; instead it says that existing users, data and settings are unchanged.
  6. 6Afterwards, docker image prune -a reclaims the space held by old images; images used by running containers are kept.
# paketi elle indirdiyseniz: kurulum dizininin üstüne çıkart, sonra
# if you downloaded the package yourself: extract it over the install directory, then
cd myway-onprem   # kurulum dizininiz / your install directory
./scripts/install.sh   # önce yedek almayı sorar (varsayılan evet) / asks to back up first (default yes)

Do NOT upgrade by hand with docker load and docker compose up -d alone. The app updates, but no backup is taken first, the scheduled jobs are not installed and new setting lines that the release needs are not added to .env.onprem: due-date notifications and review reminders silently stop running with no visible error. The installer is the path that does those jobs; ./scripts/update.sh calls it too.

Migrations are forward-only; the way back is the backup taken before the update, so answer yes to the backup question. Skipping versions is fine: the migrations in between are applied in order, you do not have to step through every release. After an upgrade, a "What’s new" box shows once on first login with what the release brought.