Skip to main content
The offline delivery forms run with no outbound network access at all: model assets are served from inside the deployment and usage is recorded to a local signed ledger rather than a billing service. Getting there takes one preparation step — moving the artifacts across the air gap — and one verification step. Everything below runs on a connected staging machine first, then on the disconnected side. Only the staging machine needs registry access.

Mirror container images

On the connected machine, derive the image list from the chart you are about to install, so the list always matches the release:
Review images.txt, then pull each image and give it the name it will have in your registry. The references are pinned by digest, which docker save does not accept as a name and which does not survive the transfer, so the rename happens here, on the connected side:
zstd keeps the transfer archive small; gzip works too if zstd is not available on both sides. For the All-in-One image the same pattern applies with a single reference:
Transfer fish-audio-images.tar.zst, mirrored.txt, the chart archive from ./transfer/, and your values file across the air gap by whatever means your policy allows.

Load on the disconnected side

The images arrive already named for your registry, so push them:
Then point the chart at your mirror. For each component in your values file, set the image repository to the mirrored path, set the tag to the one in mirrored.txt, and clear the pinned digest — the chart pins images by digest, and a digest does not survive docker save, so leaving one in place makes the pull fail against your own registry. Some components carry more than one digest field; clear each of them. The bundled Redis splits its reference into a registry and a repository field rather than one path, so set both. Confirm nothing still refers to an external host before you install:
Install from the local chart archive exactly as described in Kubernetes deployment, substituting the archive path for the chart reference. The chart archive is self-contained, so no chart repositories are contacted during the install. For the All-in-One container, the loaded image is all you need — continue with Run.

Model assets offline

No delivery form downloads model weights at runtime. Two things still come from you rather than from the network:
  • Reference voices. Stage the archives yourself, as described in Kubernetes deployment or All-in-One. This behavior is identical online and offline.
  • Timestamp alignment. The forced aligner is not part of the offline bundle. Contact Fish Audio if your air-gapped deployment needs word or segment timings.

Offline usage accounting

With no billing service to call, the deployment records every charge to a local, signed, append-only ledger on persistent storage. No request is ever rejected for billing reasons, and the record is tamper-evident and independently verifiable.

How tokens behave

  • Any non-empty Authorization: Bearer token is accepted. Empty or missing still returns 401.
  • The token is recorded verbatim as the billing identity, so choose a stable, distinct value per tenant. Tenants that share a token cannot be told apart in the ledger.

Disk layout

Files are grouped one directory per UTC day, so a day archives by copying a single directory:
Each file is written by exactly one process, so replicas never interleave into the same file even though they share the directory. Every file is an independent hash chain: a new day, a restart, or a new replica starts a new file.

Record format

Each line is one record: a shared envelope plus a payload chosen by event_type (process_started, heartbeat, billing_event, process_stopping). Charges accumulate in Redis per token, product, and backend, and a background task flushes them roughly once a minute as one aggregated billing_event per bucket. A failed write is retried rather than dropped. When a file is finished it is sealed into a matching signed manifest carrying the last record’s hash and signature, the record counts, and the file’s totals. Sealing is automatic: yesterday’s file is sealed shortly after the UTC day rolls over, and a graceful shutdown seals the current one. Today’s in-progress file has no manifest yet.

Verify the ledger

Verification needs only the public key that Fish Audio supplies, plus jq and openssl:
Verified OK confirms the record is authentic. Recompute the hash itself from the record body and compare it to the record_hash field:
A full audit additionally checks that seq is contiguous, that each prev_hash links the previous record, that the manifest signature verifies, and that the manifest totals match the event file. Ask Fish Audio for the verification script that runs all of these over a ledger directory.
The ledger is tamper-evident, not tamper-proof. Signature and hash-chain checks reliably detect accidental corruption, and tampering by anyone holding the files but not the image. They do not by themselves stop an operator who holds the image, because the key that signs the records ships inside it. If your audit requirements call for stronger guarantees, such as an independent write-once anchor or a separately administered audit sink, raise it with Fish Audio so it can be designed into the deployment.

Retention and reconciliation

  • The ledger lives on persistent shared storage and survives restarts and rescheduling.
  • Nothing prunes it. Archive completed day directories to your own storage on your retention schedule, and never delete the live directory out from under a running instance.
  • Usage is reconciled afterwards from the signed day directories, on the cadence set in your agreement. Copy whole directories, including the manifests, so the totals can be verified independently.

Prove there is no egress

Configuration review is not proof. Demonstrate it.

Kubernetes

Turn off Redis’s own allow-all egress first. The chart installs a NetworkPolicy for Redis that permits all outbound traffic, and NetworkPolicies are additive — a namespace-wide deny does not override it, so those pods stay unrestricted whatever else you apply:
That leaves Redis with DNS and in-cluster traffic, which is all it needs. Then deny external egress for the namespace, keeping in-cluster traffic and DNS, and confirm the deployment still generates audio.
deny-external-egress.yaml
Re-run the smoke test. It must still return playable audio. This proves nothing unless your CNI actually enforces NetworkPolicy, so confirm enforcement with a deliberate control: exec into a pod in the namespace and check that an outbound request fails. Use an inference or API pod for that check, not a Redis one — if you skipped the value above, a Redis pod still reaches the internet and the control reads as “my CNI ignores NetworkPolicy” when it does not.

All-in-One

The strongest proof is a container that never had a network interface, on a fresh volume, so nothing could have been fetched even during the first compile:
With --network none no host port can be published, so drive the request from inside the container once the cold start finishes:
The appliance ships a single model and serves it by default, so the model header is optional. Its name is in the model registry inside the container at /opt/edge-api/models.toml. A 200 with a non-trivial audio file is the proof. A weaker but faster variant disconnects an already-warmed container from every Docker network and repeats the request; it demonstrates that the running service survives losing the network, but not that the cold start never needed it. Capture the output of whichever check you run as deployment evidence.