Air-Gapped Installation
The release bundle is the offline variant of the Docker Compose path: the same single-host stack, but with pre-built image tarballs shipped inside the archive. Once the bundle is on the server, no internet egress is needed — not at install time and not at runtime.
You receive from your vendor:
- The tarball
abrq-dip-<VERSION>.tar.gzplus its.sha256. - A signed
license.json, issued against your customer name and expiry.
Prerequisites#
| Component | Minimum |
|---|---|
| OS | Linux x86_64. Tested on Ubuntu 22.04 LTS and RHEL 9. |
| Docker Engine | 24.0+ |
| Docker Compose plugin | v2.20+ (docker compose version — the legacy v1 docker-compose binary is not supported) |
| RAM | 4 GB free |
| Disk | 5 GB free under /var/lib/docker; ~2 GB for the install overall |
| Ports | 8080 free on the host (configurable via ABRQ_FRONTEND_PORT) |
1. Verify and extract#
shasum -a 256 abrq-dip-<VERSION>.tar.gz
Compare against the value in abrq-dip-<VERSION>.tar.gz.sha256. If the
hashes do not match, stop and contact the vendor — the file was
corrupted in transit.
tar -xzf abrq-dip-<VERSION>.tar.gz
cd abrq-dip-<VERSION>/
The extracted directory contains docker-compose.yml, .env.example,
INSTALL.md, UPGRADE.md, VERSION, images/, and license/.
2. Load the four image tarballs#
docker load -i images/abrq-dip-backend.tar
docker load -i images/abrq-dip-frontend.tar
docker load -i images/postgres-15-alpine.tar
docker load -i images/redis-7-alpine.tar
Each load prints a Loaded image: line — confirm all four before
continuing:
docker images | grep -E "abrq-|postgres:15-alpine|redis:7-alpine"
3. Configure .env#
cp .env.example .env
chmod 600 .env
Keys in the bundle's .env template:
| Key | Preset / default | Effect |
|---|---|---|
ABRQ_VERSION |
pre-filled by the release build with this bundle's version | Image tag for every app service. Leave it alone — see the gotcha below. |
POSTGRES_PASSWORD |
you set it | Password for the bundled PostgreSQL; also interpolated into the app's connection URLs. |
ABRQ_MASTER_KEY |
you set it | Fernet master key — encrypts all stored secrets. |
ABRQ_JWT_SECRET |
you set it | Session-token signing secret (32+ chars). |
ABRQ_INITIAL_ADMIN_PASSWORD |
you set it | First-boot admin password; forced change at first login. |
ABRQ_CORS_ALLOWED_ORIGINS |
you set it | The exact URL your users hit the UI at (comma-separate multiples). |
ABRQ_ENABLE_HSTS |
true |
HSTS header. Flip to false only for plain-HTTP LAN trials. |
ABRQ_ALLOW_INSECURE |
0 |
Set 1 to disable the production plain-HTTP refusal (for example TLS terminated upstream, or a LAN trial). |
ABRQ_GUNICORN_WORKERS |
2 |
API worker processes. |
ABRQ_CELERY_CONCURRENCY |
4 |
Concurrency per worker container. |
ABRQ_WORKER_REPLICAS |
2 |
Worker container count. |
ABRQ_FRONTEND_PORT |
8080 |
Host port for the web UI. |
ABRQ_AI_REQUEST_TIMEOUT_SECONDS |
120 |
Per-request timeout for the optional customer-managed AI provider. |
Secret generation commands are in choosing a deployment path — the bundle's backend image can generate the Fernet key and JWT secret offline:
docker run --rm abrq-dip-backend:<VERSION> python -c \
"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
4. Place the license#
cp /path/to/your/vendor-supplied/license.json license/license.json
The compose file mounts exactly this single file read-only into the
backend, worker, and beat containers at /var/abrq-dip/license.json
(mounting the whole directory would shadow /var/abrq-dip and break beat
state and the initial-password file — keep the single-file layout).
Hardware-bound licenses and the machine fingerprint#
The compose file also mounts the host's /etc/machine-id read-only. If
your license is hardware-bound, the runtime hashes this file (SHA-256)
and requires it to match one of the license's fingerprints. To give the
vendor your fingerprint before they mint the license, run the bundled
helper on the host that will run Abrq DIP:
sh host_fingerprint.sh
Send the printed hash to your vendor contact. Unbound licenses ignore the mount entirely — it is always mounted so a later renewal that adds binding needs no compose change. Details: installing a license.
5. Start and watch the logs#
docker compose up -d
docker compose ps
docker compose logs -f backend
Expected in the backend logs:
license_verified customer=<CUSTOMER> expires_at=<EXPIRY> days_remaining=NNN
app_started env=prod
Application startup complete.
If the license check fails the backend exits and the log names the
reason — file not found at /var/abrq-dip/license.json, invalid
signature (file altered, or the release's embedded key does not match the
key that signed your license), or expired. See
expiry and clock behaviour.
6. First login#
Open http://<HOST>:8080 (or your ABRQ_FRONTEND_PORT). The bundle's
install guide documents the initial username as abrq; the password is
your ABRQ_INITIAL_ADMIN_PASSWORD. You are forced to change the password
immediately.
7. Smoke test#
curl -fsS http://localhost:8080/api/v1/health | jq
Expected: {"status": "ok", "deps": {"framework_db": "ok", "redis": "ok"}}
(plus version fields). A dep reporting down means something inside the
stack is broken — docker compose logs framework_db redis backend will
surface the cause.
8. Take the first backup#
docker compose exec backend uv run python scripts/backup_metadata.py \
dump --out /var/abrq-dip/first-backup.json
docker compose cp backend:/var/abrq-dip/first-backup.json ./
Schedule this on your ops cadence (daily or weekly) and store copies off the host. The dump is an encrypted envelope; see backup and restore.
The version-tag gotcha: "pull access denied"#
If docker compose up fails with pull access denied for abrq-dip-backend (or -frontend), it is not an authentication
problem. The compose file pins images by ${ABRQ_VERSION}; if that
variable is unset or empty the tag resolves to :latest, which
docker load never imported (the shipped images are tagged with the
version). Compose then falls back to a registry pull and is denied.
Confirm what compose actually resolved:
docker compose config | grep 'image:'
Every app image should show :<VERSION> (for example :1.4.1), never
:latest or an empty tag. Make sure you ran cp .env.example .env (the
template pre-fills ABRQ_VERSION), or pass it inline:
ABRQ_VERSION=<VERSION> docker compose up -d
Tip.
sudodrops shell variables — keepABRQ_VERSIONin.env, not just exported in your shell.
Differences from docker-compose.prod.yml — read before relying on them#
The shipped bundle's compose file differs from the in-repo production compose in two security-relevant ways. These are stated plainly so you can compensate:
Warning. In the release bundle, Redis runs without a password (
requirepassis not set), unlikedocker-compose.prod.yml, which refuses to start withoutREDIS_PASSWORD. The bundled Redis publishes no host port, so exposure is limited to the Docker network, but any process that can reach that network reaches an unauthenticated broker.
Warning. The bundle's compose file has no host bind-mount for backups (and no ownership-fixing init step): ADR 0040 auto-backups are written inside the container filesystem and do not survive container removal. Treat the scheduled
backup_metadata.pydump +docker compose cpoff the host (step 8) as your real backup path on this deployment.
Both are known gaps in the shipped bundle, flagged to the vendor; do not assume the in-repo compose behaviour applies here.
Air-gapped Kubernetes#
For a cluster with no registry egress, the flow is: save images on a connected machine, move the tarball across, load and push into your private registry, and point the chart at it.
On the connected side, the repository ships a helper:
deploy/release/save-images.sh <VERSION>
It saves abrq-dip-backend:<VERSION>, abrq-dip-frontend:<VERSION>,
and — only needed if you keep the in-cluster subcharts enabled — the
pinned bitnamilegacy PostgreSQL and Redis images into a single
abrq-images-<VERSION>.tar.
On the air-gapped side:
docker load -i abrq-images-<VERSION>.tar
docker tag abrq-dip-backend:<VERSION> registry.example.com/abrq/abrq-dip-backend:<VERSION>
docker tag abrq-dip-frontend:<VERSION> registry.example.com/abrq/abrq-dip-frontend:<VERSION>
docker push registry.example.com/abrq/abrq-dip-backend:<VERSION>
docker push registry.example.com/abrq/abrq-dip-frontend:<VERSION>
Then set the chart's image coordinates:
image:
repository: registry.example.com/abrq/abrq-dip-backend
tag: "<VERSION>"
frontend:
image:
repository: registry.example.com/abrq/abrq-dip-frontend
tag: "<VERSION>"
The subchart dependencies are vendored as committed tarballs inside the
chart, so helm install needs no chart-repository access either.
Warning. The chart currently has no values key for image pull secrets — no template renders
imagePullSecrets, so a registry that requires authentication cannot be wired through chart values. This is a known product gap; until it is fixed, use a registry the cluster can pull from without credentials, or attach pull credentials outside the chart through your own cluster tooling.
Note.
save-images.shpins thebitnamilegacyPostgreSQL and Redis image tags in the script itself, and those pins can drift from the tags the chart's subcharts resolve. If you run the in-cluster data tier, verify after install that the running pods use the images you actually pushed (kubectl -n <NAMESPACE> get pods -o jsonpath='{..image}').
Once installed, no path in the system requires internet egress: licensing is fully offline (file-based verification, no phone-home), and the only outbound connections are the ones you configure (connectors, notification channels, an optional AI provider you host).
When a new release arrives, continue with upgrade and rollback.