Air-gapped deployment
Deploy the full stack in an environment with no internet access at deploy time. Images arrive as OCI archives via your approved transfer mechanism (USB, signed upload, bastion scp, etc.).
When to use this path
- The target environment has no outbound internet access.
- Org policy requires images to be transferred through an approved offline channel.
- You need verifiable integrity (checksums + signatures) for every artifact.
How it works
For every approved release, CI publishes each image as an OCI archive (.tar) to the GitHub Release for that tag, alongside a SHA256 checksum and an SBOM. You download the bundle, verify it, load the images into the target Docker daemon, and deploy with the standard prod compose — pointing APP_IMAGE_* at the loaded image names.
On the internet-connected machine
1. Download the release bundle
TAG=v1.0.0 # replace with the tag you wantgh release download "${TAG}" --repo yorch/ai-agents-observability \ --pattern "*.tar" \ --pattern "*.sha256" \ --pattern "SHA256SUMS-images" \ --pattern "sbom-*.json"Or download from the GitHub Releases page in a browser.
2. Verify checksums
sha256sum -c SHA256SUMS-imagesEach .tar should report OK. Do not proceed if any file fails.
3. Verify image signatures (optional, if cosign is available)
The images are signed with cosign keyless signing via GitHub OIDC. To verify:
# Release builds execute the workflow definition from main while checking out TAG.cosign verify ghcr.io/yorch/ai-agents-observability/web:${TAG} \ --certificate-identity "https://github.com/yorch/ai-agents-observability/.github/workflows/docker.yml@refs/heads/main" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com"For offline verification, export the cosign bundle during the online step and verify with --bundle on the air-gapped machine. See the cosign docs for details.
4. Transfer to the air-gapped environment
Transfer the .tar files, SHA256SUMS-images, and SBOMs via your approved transfer mechanism.
On the air-gapped machine
1. Load images into Docker
docker load -i web-v1.0.0.tardocker load -i ingest-v1.0.0.tardocker load -i github-app-v1.0.0.tardocker load -i migrations-runner-v1.0.0.tarNote the image names Docker reports after each load (they’ll be the original GHCR refs, e.g. ghcr.io/yorch/ai-agents-observability/web:v1.0.0).
2. Clone the repo (if not already present)
You need the Compose files and .env.production.example. If git is unavailable,
transfer the repo archive alongside the images.
git clone https://github.com/yorch/ai-agents-observability.git # on the connected machine# transfer the clone to the air-gapped machinecd ai-agents-observabilitygit checkout v1.0.03. Configure environment
cp .env.production.example .env.production# Fill every required value in .env.production.bun run gen:keys -- --env-file=.env.production # if Bun is availableSet the APP_IMAGE_* variables to the loaded image names. If you loaded the images with their original GHCR refs, the defaults in docker-compose.prod.yml already match — but set them explicitly for clarity:
APP_IMAGE_WEB=ghcr.io/yorch/ai-agents-observability/web:v1.0.0APP_IMAGE_INGEST=ghcr.io/yorch/ai-agents-observability/ingest:v1.0.0APP_IMAGE_GITHUB=ghcr.io/yorch/ai-agents-observability/github-app:v1.0.0APP_IMAGE_MIGRATIONS=ghcr.io/yorch/ai-agents-observability/migrations-runner:v1.0.04. Deploy
APP_ENV_FILE=.env.production docker compose --env-file .env.production \ -f docker-compose.infra.yml -f docker-compose.prod.yml up -dThe prod compose has pull_policy: always by default. Since the images are already loaded locally and there’s no outbound network, set pull_policy: never via an override or edit the compose file. Alternatively, use the build-from-source overlay with pull_policy: never already set (but you’d need to build, not load):
# Simplest: create a one-line overridecat > docker-compose.airgap.yml <<'EOF'services: web: pull_policy: never ingest: pull_policy: never github-app: pull_policy: never migrations: pull_policy: neverEOF
APP_ENV_FILE=.env.production docker compose --env-file .env.production \ -f docker-compose.infra.yml -f docker-compose.prod.yml \ -f docker-compose.airgap.yml up -d5. Verify
curl -sf http://localhost:3000/health && echo "web OK"curl -sf http://localhost:4000/health && echo "ingest OK"Updating
There is no auto-update. To update:
- On the connected machine: download the new tag’s release bundle.
- Transfer to the air-gapped machine.
docker loadthe new images.- Update
APP_IMAGE_*in.env.productionto the new tag. docker compose ... up -d(the migration runner handles schema changes).
Tradeoffs
- No auto-update by design. Every update is a deliberate, verified transfer.
- Bundle size. Four images at ~200 MB each = ~800 MB compressed. For very large images, consider splitting the transfer or using a different mechanism.
- Base images for infra. The
docker-compose.infra.ymlfile referencestimescale/timescaledb,quay.io/minio/minio, etc. These also need to be available offline. Either pre-load them on the air-gapped Docker daemon, or mirror them to a local registry. - Signature verification offline. Cosign keyless signatures reference the online transparency log. For fully offline verification, export the cosign bundle during the online step and use
cosign verify --bundleon the air-gapped machine.