Skip to content

Update and back up

An update moves the gateway to a newer Portward release. Settings, saved connections and records carry over.

A backup is a copy of the gateway’s records file. It holds the answer records, reveals (each Show of a hidden value), setup and governance changes, and the saved connections in encrypted form. It doesn’t hold the gateway key, and saved connections can’t be opened without that key. Keep the key safe and apart from the backups. Each section below says where your install keeps it.

On Azure, Google Cloud and your own server, the update is portward update, run on the machine. It:

  1. Finds the newest release on the stable channel, or the version you name with --to X.Y.Z, and checks that it is signed with Portward’s release key before it downloads anything.
  2. Copies the gateway’s file to pre-update-<version>.db, next to it, named for the version it leaves.
  3. Starts the new version and waits up to 120 seconds for it to report ready.
  4. If it isn’t ready, starts the previous version again. If the new version had already changed the file’s format, the copy from step 2 replaces the file. If not, the file stays as it is and keeps anything recorded in the meantime.

--check only says whether there is an update. The command ends with one of these exit codes:

CodeMeaning
0Updated, or already on that release
1The new release wasn’t ready; the previous one runs again
2Refused, nothing changed (for example no valid signature, or the registry didn’t answer)
3The previous release isn’t ready either. Send the output to support@portward.ai
100With --check: an update is available

Automatic updates are off by default. Once you turn them on, a release installs 7 days after it reaches the stable channel, or at once if it is a security release. Updates run only inside a window you choose, which stays open 3 hours. If the new release isn’t ready, the gateway goes back to the previous one by itself. Automatic updates never install a major release, such as 2.0.0. If you allow only fixes, they also skip minor releases, such as 1.2.0. Run those updates by hand. Each cloud’s section below says where to turn them on.

On AWS, you update the CloudFormation stack instead.

Update the stack with the new template and the Gateway image parameter it names (a stack made before 1.1.0 keeps gateway:main until you change that parameter). The current template’s address is on Portward on AWS. ECS stops the old task and starts the new one. Expect the address not to answer for about 2 to 3 minutes. The records and keys carry over. If two new tasks in a row fail to start or fail their /ready/release check, ECS puts the previous version back by itself, and CloudFormation rolls the stack update back. If you set an alert email, you get an email about it.

There are no automatic updates on AWS. The stack moves to a new release only when you update it.

AWS Backup copies the records file system every day at 05:00 UTC and keeps each copy 35 days, in the vault aws/efs/automatic-backup-vault (AWS Backup → Backup vaults). Nothing to set up. The gateway key is in Secrets Manager; the stack’s KeySecret output names the secret. Keep it, because a restore needs it.

To take a copy now, for example before an update, start a backup job in AWS CloudShell or a terminal. Put your region and the stack’s RecordsFileSystem output in the first line:

Terminal window
region=us-east-2 fs=fs-0123456789abcdef0
account=$(aws sts get-caller-identity --query Account --output text)
aws backup start-backup-job --region $region --backup-vault-name aws/efs/automatic-backup-vault --resource-arn arn:aws:elasticfilesystem:$region:$account:file-system/$fs --iam-role-arn arn:aws:iam::$account:role/service-role/AWSBackupDefaultServiceRole --lifecycle DeleteAfterDays=35
aws backup describe-backup-job --region $region --backup-job-id <BackupJobId from above> --query State # COMPLETED

The role is AWS Backup’s default role. If aws iam get-role --role-name AWSBackupDefaultServiceRole says it can’t be found, your account doesn’t have it yet. To create it, take one backup in the AWS Backup console with Default role (AWS Backup → Protected resources → Create on-demand backup). That backup is the same copy, and AWS makes the role with it. The copy sits in the vault next to the daily ones, and you restore it the same way.

A restore never changes the file system the gateway runs on. AWS Backup restores into a new file system, and the stack moves to it. Anything recorded after the backup you pick isn’t in it; it stays on the old file system, which is kept.

  1. Allow restores from the vault. EFS makes the vault with a policy that refuses them. AWS Backup → Backup vaults → aws/efs/automatic-backup-vault → Access policy → Edit: delete "backup:StartRestoreJob", from the denied actions, then Save. You can put it back afterwards.
  2. Restore into a new file system. AWS Backup → Protected resources → the file system in the stack’s RecordsFileSystem output → choose a recovery point → Restore. Choose Full restore, Restore to a new file system, Regional, General purpose, Enable encryption (the default key is fine) and the default restore role. When the restore job is Completed, its created resource is the new file system (fs-...). AWS Backup puts the files in a folder named aws-backup-restore_<time> at its root; the stack moves them into place. A restored file system has no policies. Before anything mounts it, the stack gives it what it gives a new one: TLS only and daily backups. If encryption was off, the stack stops with “isn’t encrypted”. Restore again with encryption on.
  3. Point a stack at it.
    • The same stack, if it still exists: Update stack, keep the template, set Restored file system to the new id, and keep Key of the backed-up stack as it is. It is empty on a stack that made its own key. On a stack that was itself started from a backup, it stays unchanged. There it holds the key secret of the stack the backup came from. Emptying it makes a new key, which opens none of the saved connections. The restore task moves the files into place while the gateway still runs on the old file system. ECS then stops the gateway and starts it on the restored records. The address doesn’t answer for about 2 to 3 minutes. If the move or the new task fails, CloudFormation puts the stack back on the old file system.
    • A new stack, if the old one was deleted: create it as in step 3 of Portward on AWS, with the same license key and sign-in settings, plus Restored file system (the new id) and Key of the backed-up stack: the old stack’s KeySecret output, or in Secrets Manager the ARN of the secret whose description is “Portward gateway key”. That key opens the saved connections in the restored records. Two gateways can’t use the same <name>.gw.portward.ai address, so the old stack must be gone first.
  4. Check it. The stack’s Logs output has a restore/... log stream: “Moved the records restored in aws-backup-restore_… into place.” If the stack runs the restore task again later, it says the records are in place. If the file system holds no Portward records, holds more than one restore with records, or has a half-moved one, the log says so. The stack then fails at RestoredRecords, so the gateway doesn’t start on it. CloudFormation rolls the update back, or the create fails. Restore again into a new file system. The same log group has the policy check’s line, “SUCCESS fs-… TLS only, daily backups”. To see the policies yourself (AWS CloudShell or a terminal):
    Terminal window
    fs=fs-0123456789abcdef0 # the restored file system
    aws efs describe-file-system-policy --file-system-id $fs --query Policy --output text # Deny when aws:SecureTransport is false
    aws efs describe-backup-policy --file-system-id $fs --query BackupPolicy.Status --output text # ENABLED
    aws efs describe-file-systems --file-system-id $fs --query 'FileSystems[0].Encrypted' # true

From then on, keep Restored file system and Key of the backed-up stack as they are. If you empty the first, the stack starts over on a new, empty file system. If you empty the second, the stack makes a new key. The old file system is no longer part of the stack but is kept, with its backups; delete it in the EFS console when you no longer need it (its backups stay until they expire).

Run each command below on the VM: in the portal, open VM → Run command → RunShellScript.

Terminal window
portward-azure update

It runs portward update as described above. portward-azure update --check only says whether there is an update, and portward-azure update --to X.Y.Z picks a version. A restart of the VM starts the version already there and pulls nothing.

Automatic updates: set auto = true under [updates] in /var/lib/portward/install/portward.toml (no restart needed). scope is "patch" (fixes to the running version, 1.1.0 to 1.1.1) or "all" (minor releases too), and window and timezone say when (default Sunday 03:00 UTC). The VM runs portward-azure update --auto every hour (portward-update.timer); journalctl -u portward-update says what each run decided.

The VM backs up every night at about 03:30 UTC to the storage account’s backups container (the deployment’s backupLocation output), in a folder named after the VM. The storage account deletes each copy after 30 days by default (the deployment’s backup setting). To take one now:

Terminal window
portward-azure backup

It runs portward backup while the gateway keeps running and uploads the copy as the VM’s own identity. The gateway key is in the Key Vault as portward-key. Never delete it. Without it, the gateway can’t open the saved connections.

The storage account only lets the VM’s own network in, so the portal can’t list the copies. Find and fetch them from the VM, through Run command.

  1. List the copies, oldest first:
    Terminal window
    . /etc/portward/azure.env
    token=$(curl -fsS -H Metadata:true --get http://169.254.169.254/metadata/identity/oauth2/token \
    --data-urlencode api-version=2018-02-01 --data-urlencode resource=https://storage.azure.com/ \
    --data-urlencode client_id=$IDENTITY_CLIENT_ID | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
    curl -fsS -H "Authorization: Bearer $token" -H "x-ms-version: 2023-11-03" \
    "https://$BACKUP_ACCOUNT.blob.core.windows.net/$BACKUP_CONTAINER?restype=container&comp=list&prefix=$(hostname)/" \
    | grep -o '<Name>[^<]*</Name>'
  2. Download the one to restore, stop the gateway, put the copy in place and start it again. Put the name from the list in name:
    Terminal window
    name=portward/backup-2026-10-27T033000Z.db
    . /etc/portward/azure.env
    token=$(curl -fsS -H Metadata:true --get http://169.254.169.254/metadata/identity/oauth2/token \
    --data-urlencode api-version=2018-02-01 --data-urlencode resource=https://storage.azure.com/ \
    --data-urlencode client_id=$IDENTITY_CLIENT_ID | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
    curl -fsS -H "Authorization: Bearer $token" -H "x-ms-version: 2023-11-03" -o /var/lib/portward/restore.db \
    "https://$BACKUP_ACCOUNT.blob.core.windows.net/$BACKUP_CONTAINER/$name"
    chmod 644 /var/lib/portward/restore.db
    cd /var/lib/portward/install
    docker compose stop gateway
    docker compose run --rm --no-deps -v /var/lib/portward/restore.db:/restore.db:ro --entrypoint sh gateway \
    -c 'rm -f /data/records.db-wal /data/records.db-shm && cp /restore.db /data/records.db'
    docker compose start gateway
    rm /var/lib/portward/restore.db
    The restore loses anything recorded after that copy was made.

The commands below run on the machine over SSH, which needs allow_iap_ssh = true in the module’s values. Use your own zone, and project if it isn’t gcloud’s default. They use the default deployment name, portward: every resource’s name starts with it (portward-gateway, portward-records), so put yours in its place if you changed it.

Terminal window
gcloud compute ssh portward-gateway --zone <zone> --tunnel-through-iap -- sudo bash /mnt/disks/portward/portward-gcp update

It runs portward update as described above. Add --check at the end to only ask whether there is an update, or --to X.Y.Z for a version. A restart or reset starts the version the machine already runs and pulls nothing. Setting gateway_image to another image and applying starts that one, when it is the same release as the one the machine runs or newer. If gateway_image names a release older than the one running, the machine skips it, because the newer version may have changed the gateway’s file. The startup log says why. This can happen after a module update that still pins an earlier release than the one you updated to. To go back on purpose, use --to X.Y.Z, which returns to the newer release if the older one isn’t ready.

Automatic updates: set auto_update = "patch" (fixes to the running version, 1.1.0 to 1.1.1) or "all" (minor releases too, 1.1 to 1.2) and auto_update_window (default "Sun 03:00", UTC) and apply. The machine then runs portward-gcp update --auto every hour (a systemd timer), and its log (portward-update unit) says what each run decided. To stop them, set auto_update = "off" and apply. The machine removes the timer at its next boot.

Google Cloud takes a snapshot of the records disk every day at 03:00 UTC (backup_time_utc) and keeps each one 14 days (backup_retention_days), even after the disk is deleted. Back up the gateway key once, apart from the snapshots, and keep that file somewhere safe. The snapshots can’t open saved connections without it:

Terminal window
gcloud secrets versions access latest --secret portward-key --project <project> > portward-key.txt

This puts the gateway’s file from a snapshot back on the running machine. The disk, the machine and Terraform’s state stay as they are.

  1. Find the snapshot, and make a disk from it next to the machine (add --kms-key with the same key if you set disk_kms_key):
    Terminal window
    gcloud compute snapshots list --filter="sourceDisk~portward-records" --sort-by=~creationTimestamp
    gcloud compute disks create portward-restore --zone <zone> --source-snapshot <snapshot>
    gcloud compute instances attach-disk portward-gateway --zone <zone> --disk portward-restore --device-name portward-restore
  2. On the machine (gcloud compute ssh portward-gateway --zone <zone> --tunnel-through-iap), stop the gateway, copy the file in, and start it again. If the gateway was running when the snapshot was taken, its last changes may be in a second file beside it, records.db-wal. The commands copy that file too when it is there:
    Terminal window
    sudo mkdir -p /mnt/disks/restore
    sudo mount /dev/disk/by-id/google-portward-restore /mnt/disks/restore
    sudo docker stop portward-gateway-1
    sudo rm -f /mnt/disks/portward/data/records.db-wal /mnt/disks/portward/data/records.db-shm
    sudo cp -p /mnt/disks/restore/data/records.db /mnt/disks/portward/data/records.db
    [ -f /mnt/disks/restore/data/records.db-wal ] && sudo cp -p /mnt/disks/restore/data/records.db-wal /mnt/disks/portward/data/
    sudo docker start portward-gateway-1
    sudo umount /mnt/disks/restore
  3. Back on your own machine, remove the extra disk:
    Terminal window
    gcloud compute instances detach-disk portward-gateway --zone <zone> --disk portward-restore
    gcloud compute disks delete portward-restore --zone <zone>

Run these in the install folder, where compose.yaml is.

Run portward update from the gateway’s own image:

Terminal window
docker run --rm --user 0:0 -e HOME=/tmp -e DOCKER_CONFIG=/docker -v "$HOME/.docker/config.json:/docker/config.json:ro" \
-v /var/run/docker.sock:/var/run/docker.sock -v "$PWD:$PWD" -w "$PWD" --entrypoint portward \
"$(sed -n '/^ gateway:$/{n;s/^ image: //p;q;}' compose.yaml)" update

For that one run it uses this server’s Docker and the registry login from step 2 of the own-server install, which it reads from $HOME/.docker/config.json. If your Docker keeps logins in a credential helper (a credsStore line in that file), the file holds no password, and the update can’t pull the new image. In that case, write to support@portward.ai.

End it with update --check to only ask whether there is an update, or update --to X.Y.Z for a version. A run leaves .portward-update.lock in the install folder, so two runs never overlap; leave the file there. compose.yaml pins the gateway image by version and digest, so a restart or docker compose pull never changes the gateway. In files written by init 1.1.0, Caddy’s image is caddy:2, so docker compose pull fetches the newest Caddy 2 release, and the next docker compose up -d starts it. The relay client’s image, fatedier/frpc:v0.71.1, is pinned by its version tag.

Automatic updates:

  1. Set auto = true under [updates] in portward.toml. The file explains scope, window and timezone. No restart is needed: each run reads [updates] itself.
  2. Run the command above with update --auto every hour, from the crontab of the user who ran docker login in step 2, because the command reads that user’s $HOME/.docker/config.json. As that user, crontab -e, and add this as one line, with your install folder’s full path in place of /path/to/install:
    7 * * * * cd /path/to/install && docker run --rm --user 0:0 -e HOME=/tmp -e DOCKER_CONFIG=/docker -v "$HOME/.docker/config.json:/docker/config.json:ro" -v /var/run/docker.sock:/var/run/docker.sock -v "$PWD:$PWD" -w "$PWD" --entrypoint portward "$(sed -n '/^ gateway:$/{n;s/^ image: //p;q;}' compose.yaml)" update --auto >> update.log 2>&1
  3. Keep the install folder and the registry login on the server, or the hourly run fails.

Each run does nothing unless [updates] allows it. Outside the window a run prints nothing and exits 0; inside it, the run writes what it decided to update.log. update --check shows whether there is an update at all. To see a run’s decision now, set window to the current hour (for example "daily 14:00" at 14:xx in timezone), run the line by hand, and set it back.

Back up .env once: copy it off the server to where you keep secrets, such as a password manager or secrets vault, readable only by the people who run the gateway. It holds the gateway key, so keep it apart from the data backups. Then, regularly, copy the gateway’s file while it runs and move the copy off the server. That file, records.db, is in the data volume. This is the data backup that init’s next steps ask for. backup reminds you to “keep it with the key”. A restore needs the gateway key too, which is the .env you backed up once. Keep both, in separate places:

Terminal window
docker compose exec gateway portward --config /config/portward.toml backup /data/backup-$(date +%F).db
docker compose cp gateway:/data/backup-$(date +%F).db .
docker compose exec gateway rm /data/backup-$(date +%F).db

Stop the gateway, put the copy in place, and start it again. The example restores backup-2026-10-27.db from the install folder: use your backup’s file name.

Terminal window
docker compose stop gateway
docker compose run --rm --no-deps -v "$PWD/backup-2026-10-27.db:/restore.db:ro" --entrypoint sh gateway \
-c 'rm -f /data/records.db-wal /data/records.db-shm && cp /restore.db /data/records.db'
docker compose start gateway
docker compose exec gateway portward --config /config/portward.toml records verify

records verify confirms no answer record, reveal or setup change in the restored file was altered or removed.

From 1.1.0, a gateway won’t start on a file a newer version has changed. It logs one line naming both file versions and leaves the file as it is. Either start the newer version again, or put back the file as it was before the update. On Azure, Google Cloud and your own server, the update kept it as pre-update-<version>.db, beside the gateway’s file, named for the version it left. Only an update that installed a new version makes this copy; one that answered “nothing to do” leaves none.

  • Your own server: copy it out with docker compose cp gateway:/data/pre-update-<version>.db ., then restore that file as in Your own server.
  • Azure: through Run command, copy it out with cd /var/lib/portward/install && docker compose cp gateway:/data/pre-update-<version>.db /var/lib/portward/restore.db, then run the Azure restore’s step 2 from the chmod line on.
  • Google Cloud: on the machine,
    Terminal window
    sudo docker stop portward-gateway-1
    sudo rm -f /mnt/disks/portward/data/records.db-wal /mnt/disks/portward/data/records.db-shm
    sudo cp -p /mnt/disks/portward/data/pre-update-<version>.db /mnt/disks/portward/data/records.db
    sudo docker start portward-gateway-1
  • AWS: restore a backup from before the update.

A backup made after the update won’t work, because it’s at the newer version too.