Concluded·30 Aug 2026 · 5 min read
What a gateway backup actually restores
Ignition 8.3.8 (b2026071409) · Docker on an unprivileged Proxmox LXC · TimescaleDB 2.x on PostgreSQL 16 · gwcmd.sh backup, container -r restore
The question here is narrow and testable: when you restore a gateway backup, what comes back?
Not "does the restore succeed" — it does. I wanted the file-by-file answer, and what the restored machine does in the first two minutes of its life.
The rig
| Original | edge-1, Ignition 8.3.8, file-based config, an edge gateway for the Rotterdam site |
| Estate | gateway-network peers central-a-master and central-a-backup |
| Historian | TimescaleDB on PostgreSQL, reached over the plant segment on :5544 |
| Restore target | a clean container from the same image, no shared volumes |
| Backup method | gwcmd.sh -b, the gateway's own supported path |
Taking the backup
$ time gwcmd.sh -b /tmp/v.gwbk -y
Gateway backup saved to /tmp/v.gwbk
2.312s
12,564,181 bytes on disk, 22.7 MB uncompressed, 2,711 entries.
Searching the archive for history data returns matches, which is misleading until
you read them: they are the tag definitions for the store-and-forward metrics —
config/resources/core/ignition/tag-definition/System/Gateway/StoreAndForward/tsdb/….
The definitions of the counters, not the data. There are no history rows in the
file at all.
Two failed restores before one worked
Both failures are behaviour worth recording, because each cost a cycle.
-h 9088 -s 9043 -n restored-gwexit 1-a 192.168.x.x -h 9088 -s 9043exit 1bridge networking, -p 9088:8088healthy in 22 sThe first is a straightforward argument-validation rule: -h and -s cannot be
supplied without -a. Fine.
The second is the interesting one. With all three supplied, the gateway still came up trying to bind 8088 and 8060 — the ports from the backup, not the ports on my command line — and collided with the original on the same host network:
java.lang.Exception: The port(s) 8088, 8060 are unavailable
at CommissioningContext.maybeCheckEnvPorts(CommissioningContext.java:555)
The restore overwrites the freshly-commissioned settings with the backup's. That is correct for a real recovery, where the original is gone. For a test restore it means you cannot place the copy beside the original on a shared network stack. Bridge networking with a published port worked immediately.
The restore itself
07:50:07 container start
07:50:08 Gateway restore file staged: Gateway backup restored
07:50:10 Starting Ignition 8.3.8 (b2026071409)
07:50:29 Gateway started in 19 seconds
Twenty-two seconds cold start to running gateway, of which the restore is roughly one. The 19 seconds is ordinary software startup and would be the same on a fresh install.
File-by-file against the original
Both gateways, find . -type f under data/config/resources, sorted under a
fixed collation:
| files | |
|---|---|
original edge-1 | 1,451 |
| restored | 1,457 |
| only in original | 6 |
| only in restored | 12 |
The six are one real file — migration-log-202608070924.md, the husk left by an
earlier config.idb migration and the same husk the
drift measurements tripped over — plus five orphaned
content-addressed blobs under .resources/. The twelve are five commissioning artifacts the fresh boot wrote
before the restore landed (identity-provider/temp/, user-source/temp/) plus
seven blobs.
Category by category the picture is cleaner still:
Twenty-six resource categories with identical counts, except the two carrying
those temp/ artifacts. Database connections, gateway-network settings, tag
providers, tag groups, store-and-forward engines, security levels, the 369 images
— all present, all matching.
One thing that looked like a finding and was not. An earlier pass over the
archive listing suggested external/config-mode.json was missing from the backup.
It is present on both gateways after the restore — but dated 13 May on both, which
is the image build date. It ships with the image. The restore lands on top of a
fresh install, so a file absent from the backup can still be present afterwards,
and an archive listing cannot tell you which. That is the argument for diffing the
restored machine rather than reading the manifest.
The credential came back working
The backup carries the database password as an encrypted object:
"password": {
"data": {
"ciphertext": "g6gOxrruY_FE92CuyA",
"encrypted_key": "sQWDrUomA-5rPVxiLpcAQu82Sisbp4SuwWBel9wM0pgAOfkefX9cGg",
"iv": "-boqHvUL1dPdY2I-",
"protected": "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwiaWF0IjoxNzg2MTE2NTM5LCJ6aXAiOiJERUYifQ",
"tag": "k06PkvdS5tu7_FtVxHJB4A"
},
"type": "Embedded"
}g6gOxrruY_FE92CuyA— the password itselfsQWDrUomA-5rPVxi…— the key that unwraps it — in the same fileeyJhbGciOiJBMjU2S1ci…— A256KW + A256GCM, issued 2026-05-13Embedded— the operative wordtype: Embedded is the operative word: the wrapping key travels with the backup.
Ninety seconds into the restored gateway's life:
[s.f.s.TagHistoryDatasourceSink] Tag history sink initialized
successfully against datasource 'tsdb'
A sink cannot initialise against a datasource that will not authenticate. The credential survived the round trip and worked against production.
Proving the writes rather than trusting the log line
Counting rows in the live sqlt_data_1_2026_08 table over two 90-second windows,
changing nothing between them except whether the restored copy was running:
The caveat that matters. The original gateway was running throughout, but its
SQL Historian access level changed: Demo_Enabled -> Demo_Expired at 07:34, so it
was writing nothing. Every one of those 270 rows came from the restored copy — a
clean attribution, but it also means I did not observe two gateways writing
concurrently. The claim this test supports is "a restored copy writes to
production unprompted", not "a restored copy duplicates the original's rows". I
checked for duplicate (tagid, t_stamp) pairs in the window and found none, which
is consistent with only one writer.
Estate trust arrived pre-approved
// gateway-network-incoming/central-a-master_3007b9a9…/config.json
{ "connectionId": "…-central-a-master|3007b9a9-…", "securityStatus": "Approved" }
// gateway-network-incoming/central-a-backup_2829c77e…/config.json
{ "connectionId": "…-central-a-backup|2829c77e-…", "securityStatus": "Approved" }- 1. announces itself to the central pair
- 2. sits pending
- 3. a person looks at it
- 4. a person approves it
central-a-master → Approvedcentral-a-backup → ApprovedBoth incoming connections came back approved. A gateway joining an estate for the first time has to be approved by a person; a restored one has that approval already, because approval is a resource and resources are what a backup restores.
What the backup does not contain
The historian database is 366 MB, and it is tempting to put that number beside the 12.5 MB backup and call it an argument. It is not a fair comparison — that database is shared across several rigs:
Ignition's own tag history on this estate is the 3.4 MB slice, about 32,000 rows. The honest statement is not about the ratio. It is that the backup contains zero rows of process history at any size, and restoring the gateway recovers none of it. Historian recovery is a separate mechanism with a separate failure mode, and it wants its own rehearsal.
Verdict
Configuration restore: complete, fast, and better than I expected — one leftover log file's difference across 1,451 files.
The rest of it is the finding. The restored gateway is not an inspection copy. It holds a working production credential, it holds pre-granted estate trust, it ignores your command line in favour of the backup's own network settings, and it starts using all three within twenty-two seconds of launch. Test restores belong on a network with nowhere to go.
The argument for a general audience is in a restore is not a rehearsal.
The same gateway killed outright and timed past this finish line, to the first row landing in the historian, is in timing a recovery to the first good row.
Newsletter
New essays, by email.
SCADA, cloud, AI, and the plant floor — a short email when something new is published. No noise, unsubscribe anytime.