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.
How an update works
Section titled “How an update works”On Azure, Google Cloud and your own server, the update is portward update, run on the machine. It:
- 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. - Copies the gateway’s file to
pre-update-<version>.db, next to it, named for the version it leaves. - Starts the new version and waits up to 120 seconds for it to report ready.
- 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:
| Code | Meaning |
|---|---|
| 0 | Updated, or already on that release |
| 1 | The new release wasn’t ready; the previous one runs again |
| 2 | Refused, nothing changed (for example no valid signature, or the registry didn’t answer) |
| 3 | The previous release isn’t ready either. Send the output to support@portward.ai |
| 100 | With --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
Section titled “Update”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.
Back up
Section titled “Back up”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:
region=us-east-2 fs=fs-0123456789abcdef0account=$(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=35aws backup describe-backup-job --region $region --backup-job-id <BackupJobId from above> --query State # COMPLETEDThe 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.
Restore
Section titled “Restore”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.
- 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. - Restore into a new file system. AWS Backup → Protected resources → the file system in the stack’s
RecordsFileSystemoutput → 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 namedaws-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. - 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
KeySecretoutput, 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.aiaddress, so the old stack must be gone first.
- 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 systemaws efs describe-file-system-policy --file-system-id $fs --query Policy --output text # Deny when aws:SecureTransport is falseaws efs describe-backup-policy --file-system-id $fs --query BackupPolicy.Status --output text # ENABLEDaws 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.
Update
Section titled “Update”portward-azure updateIt 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.
Back up
Section titled “Back up”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:
portward-azure backupIt 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.
Restore
Section titled “Restore”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.
- List the copies, oldest first:
Terminal window . /etc/portward/azure.envtoken=$(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>' - 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:The restore loses anything recorded after that copy was made.Terminal window name=portward/backup-2026-10-27T033000Z.db. /etc/portward/azure.envtoken=$(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.dbcd /var/lib/portward/installdocker compose stop gatewaydocker 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 gatewayrm /var/lib/portward/restore.db
Google Cloud
Section titled “Google Cloud”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.
Update
Section titled “Update”gcloud compute ssh portward-gateway --zone <zone> --tunnel-through-iap -- sudo bash /mnt/disks/portward/portward-gcp updateIt 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.
Back up
Section titled “Back up”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:
gcloud secrets versions access latest --secret portward-key --project <project> > portward-key.txtRestore
Section titled “Restore”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.
- Find the snapshot, and make a disk from it next to the machine (add
--kms-keywith the same key if you setdisk_kms_key):Terminal window gcloud compute snapshots list --filter="sourceDisk~portward-records" --sort-by=~creationTimestampgcloud 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 - 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/restoresudo mount /dev/disk/by-id/google-portward-restore /mnt/disks/restoresudo docker stop portward-gateway-1sudo rm -f /mnt/disks/portward/data/records.db-wal /mnt/disks/portward/data/records.db-shmsudo 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-1sudo umount /mnt/disks/restore - Back on your own machine, remove the extra disk:
Terminal window gcloud compute instances detach-disk portward-gateway --zone <zone> --disk portward-restoregcloud compute disks delete portward-restore --zone <zone>
Your own server
Section titled “Your own server”Run these in the install folder, where compose.yaml is.
Update
Section titled “Update”Run portward update from the gateway’s own image:
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)" updateFor 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:
- Set
auto = trueunder[updates]inportward.toml. The file explainsscope,windowandtimezone. No restart is needed: each run reads[updates]itself. - Run the command above with
update --autoevery hour, from the crontab of the user who randocker loginin 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 - 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
Section titled “Back up”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:
docker compose exec gateway portward --config /config/portward.toml backup /data/backup-$(date +%F).dbdocker compose cp gateway:/data/backup-$(date +%F).db .docker compose exec gateway rm /data/backup-$(date +%F).dbRestore
Section titled “Restore”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.
docker compose stop gatewaydocker 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 gatewaydocker compose exec gateway portward --config /config/portward.toml records verifyrecords verify confirms no answer record, reveal or setup change in the restored file was altered or removed.
Go back a version
Section titled “Go back a version”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 thechmodline on. - Google Cloud: on the machine,
Terminal window sudo docker stop portward-gateway-1sudo rm -f /mnt/disks/portward/data/records.db-wal /mnt/disks/portward/data/records.db-shmsudo cp -p /mnt/disks/portward/data/pre-update-<version>.db /mnt/disks/portward/data/records.dbsudo 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.