Quick install: AGLedger on Docker Compose (Developer Edition)
This is the fastest way to a running AGLedger Server: clone the install repository, run one script, and notarize a record in about five minutes. It brings up the Server, a background worker, and a bundled PostgreSQL with Docker Compose, with no Kubernetes and no external database.
Every feature is enabled from the first boot. A free Developer Edition key licenses this bundled-PostgreSQL stack for production as well as evaluation; an external database is Enterprise. With no key applied the install is Unlicensed Use, which the license permits for evaluation, development and testing only, for the Evaluation Period the license sets, and the Server says so in its log on every boot and once a day after that. For a production Kubernetes deployment fronted by TLS and backed by your own PostgreSQL, see the Kubernetes install guide.
A fresh 2.0 install applies one migration,
001_consolidated.sql, the 2.0 baseline. 2.0 does not upgrade a database a 1.x release migrated: install it on a new, empty database, as this page does. Stopping, upgrading, removing below shows whatupgrade.shsays when it is pointed at a 1.x install.
Prerequisites
- Docker Engine 24.0+ and the Docker Compose v2 plugin
jq,curl, andopensslon your PATH (the installer uses them for secret generation and the Docker Hub version lookup)- 2 vCPU, 4 GB RAM, 20 GB disk minimum. The stack scales vertically until the database is the bottleneck.
- Free host ports 3001 (API) and 5432 (bundled PostgreSQL). The installer checks before it starts
anything and, if something already listens there, names the variable that moves each one
(
AGLEDGER_HOST_PORT,POSTGRES_HOST_PORT).
1. Clone and install
The deployment packaging (Compose files, scripts, and Helm chart) lives in the
agledger-ai/install repository. Clone a tagged release
and run the installer.
$ git clone --branch v2.0.0 https://github.com/agledger-ai/install.git
$ cd install
$ ./scripts/install.sh --version 2.0.0
install.sh generates the cryptographic secrets locally (POSTGRES_PASSWORD,
AGLEDGER_APP_ROLE_PASSWORD for the runtime database role, API_KEY_SECRET, a
METRICS_AUTH_TOKEN for /metrics, an Ed25519 VAULT_SIGNING_KEY, and this Server's
federation identity AGLEDGER_INSTANCE_ID), writes them to compose/.env, starts PostgreSQL,
runs migrations, mints the first platform API key, and starts the API and worker. --version
pins the release; omit it on a fresh install to take the latest stable from Docker Hub. On a host
running OpenSSL in FIPS mode the same script takes --fips, which runs the containers with the
FIPS provider active and an ES256 signing key: read FIPS 140 hosts before
choosing it.
[STEP] Checking platform
[OK] Ubuntu 24.04
[STEP] Checking prerequisites
[OK] Docker Engine 29.1.3
[OK] Docker Compose 2.40.3
[OK] RAM: ~47 GB
[OK] CPU cores: 20
[STEP] Checking host ports
[OK] Host ports available: API 3001, PostgreSQL 5432
[STEP] Resolving version
[OK] Version: 2.0.0 (requested)
[OK] Using Docker Hub: agledger/agledger
[STEP] Verifying image signature
[OK] Image signature verified (keyless, public Rekor):
agledger/agledger@sha256:6e29779624951807…
[STEP] Configuring environment
[OK] Generated POSTGRES_PASSWORD
[OK] Generated AGLEDGER_APP_ROLE_PASSWORD
[OK] Generated AGLEDGER_INSTANCE_ID (this Server's federation identity)
[OK] Generated API_KEY_SECRET
[OK] Generated METRICS_AUTH_TOKEN (bearer token for /metrics)
[OK] Wrote METRICS_TOKEN_FINGERPRINT so the bundled Prometheus picks up the /metrics token.
[OK] Generating VAULT_SIGNING_KEY (ed25519)...
[OK] Vault signing key pin: sha256:<64 hex>
[OK] Give it to anyone who verifies this install offline; it is not secret.
dist/scripts/signing-key-digest.js derives it again from VAULT_SIGNING_KEY.
[OK] Generated VAULT_SIGNING_KEY
[STEP] Running database migrations
{"migration":"001_consolidated.sql","msg":"Applied migration"}
{"role":"agledger_app","reassigned":[],"msg":"Runtime role has a login under the configured
password, and owns the queue schema"}
{"roles":["agledger_monitor"],"msg":"Took LOGIN away from roles that still accepted the
password the baseline migration creates them with. ..."}
{"count":1,"msg":"Migration complete"}
[OK] Migrations complete
[STEP] Checking runtime role privileges and connection topology
✓ Boot configuration: NODE_ENV, AGLEDGER_EXTERNAL_URL, database TLS and the parsed settings
are all accepted
✓ Runtime role privileges: Role "agledger_app" holds what the schema grants the runtime role
on all <n> tables in public: full DML on <n>, and the narrowed privileges and column grants
on the <n> append-only tables
✓ Audit chain privileges: Role "agledger_app" is held out of the UPDATE/DELETE/TRUNCATE the
schema revokes on all <n> append-only tables
✓ Default role passwords: Neither agledger_app nor agledger_monitor accepts the password the
baseline migration creates them with
✓ Connection topology: one connection string, no transaction-mode pooler detected in front of it
✓ pg-boss schema: Not yet installed; pg-boss will install it on first start (role has CREATE
on the database)
All checks passed.
[STEP] Creating platform API key
[OK] Platform API key created
[OK] Platform API key saved to .env
[STEP] Starting all services
[OK] All services started
[STEP] Running preflight checks
✓ VAULT_SIGNING_KEY: Set (Ed25519)
✓ PostgreSQL version: 18.6 (native uuidv7)
⚠ Federation signing key: AGLEDGER_FEDERATION_SIGNING_KEY unset — federation outbound will
be suppressed until it is configured
✓ Migrations: 1/1 applied — up to date
✓ pg-boss schema: pgboss schema is installed
...
All checks passed with warnings — review before deploying.
=============================================================================
AGLedger — Installation Complete
=============================================================================
Version: 2.0.0
API URL: http://localhost:3001
Signed issuer: http://localhost:3001 (iss baked into every record)
Signing: Ed25519
Health: http://localhost:3001/health/ready
Conformance: http://localhost:3001/v1/conformance
OpenAPI spec: http://localhost:3001/openapi.json
Agent guide: http://localhost:3001/llms.txt
Database: Bundled PostgreSQL
Platform API Key (SAVE THIS — shown only once):
agl_plt_<save-this-securely>
This key has full admin access. Store it securely.
⚠ Signed issuer is a localhost default
AGLEDGER_EXTERNAL_URL=http://localhost:3001 is the iss signed into every record,
receipt, and cert — and it cannot be changed for records already written.
Fine for evaluation. Before notarizing records you intend to keep, set
AGLEDGER_EXTERNAL_URL to your real https:// domain in compose/.env and restart.
<n> in the privilege lines stands for table counts, which depend on the release and grow as
date partitions are created. The one warning is expected: the federation key matters only if you
link this Server to another.
The vault signing key pin is not secret. Give it to anyone who verifies this install offline. It
stays valid across later key rotations, and dist/scripts/signing-key-digest.js derives it again
from VAULT_SIGNING_KEY.
The install is role-separated. Migrations run as agledger, the role that owns the schema, and
the API and worker connect as agledger_app, which the migration gives a login under the
generated AGLEDGER_APP_ROLE_PASSWORD. Because agledger_app does not own the tables, the
schema's revokes bind it, and preflight's Audit chain privileges line confirms it cannot update
or delete rows in the append-only tables. After migrating, the migrator takes LOGIN away from
agledger_monitor, which still accepts the password the baseline migration gives it, and the
Default role passwords line confirms neither role accepts that password.
The platform key is printed once. Save it: it is your first credential, and you use it to
provision organizations and agents. The installer also writes it to compose/.env as
PLATFORM_API_KEY so the bundled scripts can find it.
Take the installer's issuer warning seriously, including in evaluation. The
issin every signed envelope comes fromAGLEDGER_EXTERNAL_URL, and records already written keep whatever issuer was set when they were signed. Before notarizing anything you will keep or verify under your real identity, setAGLEDGER_EXTERNAL_URLincompose/.envto thehttps://URL where the Server will actually be reachable, restart the stack (docker compose up -dfromcompose/), and front the API with TLS at that hostname.
2. Confirm the Server is up and signing
The API serves on http://localhost:3001. Two unauthenticated checks prove it is healthy:
$ curl -s http://localhost:3001/health
{"status":"ok","version":"2.0.0","timestamp":"2026-09-30T07:09:43.808Z",
"signingKey":{"gate":"usable","keyId":"c338ad9d95c38cfc"}}
$ curl -s http://localhost:3001/health/ready
{"status":"ready","version":"2.0.0","timestamp":"2026-09-30T07:09:43.815Z"}
signingKey.gate is usable: this process holds its signing key and may sign with it. The
keyId is the fingerprint of the VAULT_SIGNING_KEY the installer generated for you, and the
verification key it names is published unauthenticated. Each key entry declares its COSE
algorithm, the minimum verifier version that can consume it (2.0.0 on this release), and the
signed key statements (statements) that admit it. anchoredFrom is the pin of the key the
Server signs with now: on a fresh install it equals the one the installer printed, and it moves to
the new key's digest when the signing key is rotated. The document also carries the
signature-input template an offline verifier reconstructs:
$ curl -s http://localhost:3001/v1/verification-keys
{"data":[{"keyId":"c338ad9d95c38cfc","algorithm":"Ed25519",
"publicKey":"MCowBQYDK2VwAyEAU1SCX6HdbRcxiLWKEa2z8B3iqd+zn6nuIuYNeH+98YU=",
"status":"active","activatedAt":"2026-09-30T06:31:30.071Z","retiredAt":null,
"statements":[{"kind":"genesis","cose":["<base64 COSE_Sign1>"]}],
"coseAlgorithm":-8,"minVerifierVersion":"2.0.0",
"publicKeyRaw":"U1SCX6HdbRcxiLWKEa2z8B3iqd+zn6nuIuYNeH+98YU="}],
"anchoredFrom":"sha256:c338ad9d95c38cfc8f44577a878c2bad247ee1ca1c830511662a73569c7a3adf",
"keyStatementFormat":"application/vnd.agledger.key-statement+cbor",
"envelope":"COSE_Sign1","payloadFormat":"application/vnd.in-toto+cbor",
"canonicalization":"RFC8949-CDE","coseAlgorithm":-8,"signatureAlgorithm":"Ed25519",
"signatureInputTemplate":"Sig_structure = [\"Signature1\", protected_bstr, h'', payload_bstr], ..."}
With no license key installed, every feature is enabled and /v1/admin/license reports
"tier":"unlicensed". The full response is in
step 5 of the Kubernetes install guide, and it is the same on Compose.
You can run the whole loop below without a key. To license this stack for production and clear the periodic unlicensed-install warning, get a free Developer Edition key, set it as
AGLEDGER_LICENSEincompose/.env, and rundocker compose up -dfromcompose/so the API and worker pick it up. The key validates offline at boot and never gates a feature.
3. Notarize one record
A fresh install ships with the notarize-generic-v1 contract type and a Default organization,
and zero agents: every record must be tied to a real agent identity. Create one agent, mint it a
key, and notarize.
$ export AGLEDGER_API_URL=http://localhost:3001
$ export AGLEDGER_PLATFORM_KEY=agl_plt_<from-step-1>
$ ORG=$(curl -s -H "Authorization: Bearer $AGLEDGER_PLATFORM_KEY" "$AGLEDGER_API_URL/v1/admin/orgs" \
| jq -r '.data[0].id')
# create an agent in the Default org
$ AGENT=$(curl -s -X POST -H "Authorization: Bearer $AGLEDGER_PLATFORM_KEY" -H "Content-Type: application/json" \
-d "{\"displayName\":\"Quickstart Agent\",\"orgId\":\"$ORG\"}" "$AGLEDGER_API_URL/v1/admin/agents" | jq -r '.id')
# mint an agent key (the plaintext apiKey is returned once; capture it now)
$ AGENT_KEY=$(curl -s -X POST -H "Authorization: Bearer $AGLEDGER_PLATFORM_KEY" -H "Content-Type: application/json" \
-d "{\"role\":\"agent\",\"ownerId\":\"$AGENT\",\"ownerType\":\"agent\",\"scopeProfile\":\"agent-full\",\"label\":\"quickstart-key\"}" \
"$AGLEDGER_API_URL/v1/admin/api-keys" | jq -r '.apiKey')
Now notarize. As an agent key, you do not name the org or principal: the Server resolves both from the key.
$ curl -s -X POST -H "Authorization: Bearer $AGENT_KEY" -H "Content-Type: application/json" \
-d '{"type":"notarize-generic-v1","criteria":{"summary":"About to reconcile invoice INV-4471 against PO-9921"}}' \
"$AGLEDGER_API_URL/v1/records"
The response carries the whole record; these are the fields that answer "did it work":
{
"id": "01a0de64-af41-7ef6-b344-889b51f6dedc",
"status": "RECORDED",
"type": "notarize-generic-v1",
"terminalReason": "RECORDED",
"criteria": { "summary": "About to reconcile invoice INV-4471 against PO-9921" },
"signedStatement": {
"chainPosition": 1,
"leafHash": "2258d11968e30a68572a48320a6bfae3efa90da15c91e64de701b7db143e4a18",
"previousHash": null,
"signingKeyId": "c338ad9d95c38cfc",
"signedAt": "...",
"signedCheckpointRef": null,
"url": "/v1/records/01a0de64-af41-7ef6-b344-889b51f6dedc/attestation"
},
"nextSteps": [
{ "action": "Read this record", "method": "GET",
"href": "/v1/records/01a0de64-af41-7ef6-b344-889b51f6dedc",
"description": "Fetch the recorded artifact with its full audit chain" },
{ "action": "Export audit trail", "method": "GET",
"href": "/v1/records/01a0de64-af41-7ef6-b344-889b51f6dedc/audit-export",
"description": "Export this record's audit chain for offline verification" }
]
}
The record is RECORDED and already signed: signingKeyId is the same c338ad9d95c38cfc you saw
at /v1/verification-keys, so the record is signed with your Server's key, COSE_Sign1 / Ed25519.
That is the whole loop: install, up, signing, notarized.
The bundled vault-verify.sh walks every chain the Server holds and re-checks each signature. It
opens with a warning log line while external anchoring is off, which it is by default:
$ ./scripts/vault-verify.sh
{"level":"warn","msg":"External vault anchoring is disabled (VAULT_ANCHOR_ENABLED=false). ..."}
Verifying 3 chain(s): 2 record, 1 schema...
[PASS] record 00000000-0000-0000-0000-000000000000 (2 entries)
[PASS] record 01a0de64-af41-7ef6-b344-889b51f6dedc (1 entries)
[PASS] schema:01a0de64-37d1-7ed1-ac7a-8e0de3bf2354 (4 entries)
3 chain(s) checked, 0 error(s), 0 unverifiable
record chains: 2
schema chains: 1
To verify that record offline with nothing but the published public key, follow the
quick start. The full API surface is the OpenAPI document the Server serves at
/openapi.json.
When an install stops partway
install.sh is re-runnable. It checks what it can before it changes anything, and a run that
refuses has changed nothing, so the fix is to correct what it named and run the same command
again.
A port conflict is the common one, and it is caught before the first container starts:
$ ./scripts/install.sh --version 2.0.0
[STEP] Checking host ports
[ERROR] Another process on this host is already listening on a port this install needs:
[ERROR] POSTGRES_HOST_PORT=5432 (bundled PostgreSQL)
[ERROR] Docker would fail this as 'port is already allocated' partway through starting the stack.
[ERROR] Each line names the variable that moves it. Nothing answers on these right now:
[ERROR] POSTGRES_HOST_PORT=5434 ./scripts/install.sh
[ERROR] Installing alongside an AGLedger that lives in another directory needs nothing
[ERROR] more than the port variables above; run it from THIS directory.
[ERROR] To find what holds a port: docker ps or sudo lsof -i :5432
[ERROR] Refusing to start a stack that cannot bind its ports.
[ERROR] Installation failed. Check the output above for details.
[ERROR] You can re-run this script after fixing the issue.
The suggested port is one the installer found free, so the line it prints is the command to run.
The installer writes the override to compose/.env, so later runs keep it without the variable.
Run from a directory that already holds an install, the refusal also explains how to set up a
second stack beside it: a second directory, fresh secrets, and its own COMPOSE_PROJECT_NAME.
A re-run of a working install does not move it between releases. It resolves to the version the install is already on, says so, and names the script that does move a release:
$ ./scripts/install.sh
[STEP] Resolving version
[OK] Version: 2.0.0 (the version this install is already on)
[OK] Re-running the installer does not move a version. To change releases: ./scripts/upgrade.sh <VERSION>
[OK] Using Docker Hub: agledger/agledger
What a re-run does instead is reconcile compose/.env and apply the result to the containers. It
keeps every secret it finds, fills in only what is missing, prints each change, and recreates the
containers that read a changed value, so a repair made on disk is a repair that is running. This
run followed deleting METRICS_AUTH_TOKEN and AGLEDGER_EXTERNAL_URL from compose/.env:
$ ./scripts/install.sh
[STEP] Configuring environment
[WARN] .env already exists at <install>/compose/.env. Keeping it: secrets are not regenerated.
[WARN] Its POSTGRES_PASSWORD is the one the existing compose_pgdata volume was initialized with.
[WARN] Do NOT delete .env to get a fresh configuration while that volume exists: Postgres keeps the old
[WARN] password on an already-initialized volume, and a regenerated one authenticates against nothing.
...
[OK] Reconciled existing .env (3 change(s)):
[OK] - generated METRICS_AUTH_TOKEN (bearer token for /metrics; it had none)
[OK] - wrote METRICS_TOKEN_FINGERPRINT (hands the bundled Prometheus the current /metrics token)
[OK] - added AGLEDGER_EXTERNAL_URL=http://localhost:3001 (required in production; it is the issuer signed into every record)
[STEP] Running database migrations
{"msg":"No new migrations to apply"}
...
{"count":0,"msg":"Migration complete"}
[OK] Migrations complete
[STEP] Reusing existing platform API key from .env
[OK] Platform API key retained (install is idempotent)
[STEP] Starting all services
Container compose-agledger-api-1 Recreated
Container compose-agledger-worker-1 Recreated
The containers and the volume are named compose-... because Compose takes the project name from
the compose directory unless COMPOSE_PROJECT_NAME sets one. Migrations already applied are
skipped and the count is 0. The platform key is not re-minted: the one in compose/.env is kept,
so the credential you saved on the first run stays valid.
Reaching a remote Server
The Compose stack publishes the API to 127.0.0.1:3001 on the host it runs on: loopback only,
never a network port. On your own machine that is http://localhost:3001. On a remote host or a
container, localhost:3001 is that machine's loopback, not yours: nothing on the network can
reach the API directly, by design. You reach it over an SSH tunnel, so the connection is encrypted
and key-authenticated and no port is ever exposed on the target.
# forward your local :3001 to the remote Server's loopback :3001
$ ssh -N -L 3001:localhost:3001 agl@your-remote-host
# in another shell, talk to it as if it were local:
$ curl -s http://localhost:3001/health
If the Server runs somewhere you cannot route to directly (a bridge-private container, a host
behind a bastion), hop through a jump host with -J:
$ ssh -N -J you@bastion.example -L 3001:localhost:3001 agl@10.0.0.5
$ curl -s http://localhost:3001/health
The same tunnel carries every authenticated call, so your platform key never crosses the network in the clear.
For repeatable multi-host access, the install repo ships scripts/agl-deploy.sh, a small
client-side wrapper that drives this whole flow over SSH (deploy, tunnel, status, health, logs,
upgrade, uninstall) so you don't retype the flags. It opens one SSH connection per operation and
runs the same signed installer described above; it never reimplements verification or key handling.
Point it at a host and open the tunnel:
# deploy to a fresh host (installs prerequisites, verifies the image, mints the platform key)
$ ./scripts/agl-deploy.sh -H agl@your-remote-host -i ~/.ssh/agl install
# hold a tunnel open; in another shell, curl http://localhost:3001/health
$ ./scripts/agl-deploy.sh -H agl@your-remote-host -i ~/.ssh/agl tunnel
# through a bastion that can route to a bridge-private container:
$ ./scripts/agl-deploy.sh -H agl@10.0.0.5 -J you@bastion.example -i ~/.ssh/agl tunnel
It deploys this same stack (Compose on Docker CE, bundled PostgreSQL), which a free Developer
Edition key licenses for production; for production, also set AGLEDGER_EXTERNAL_URL and front the
API with TLS. For multi-node scale, HA, or an external database, see the
Helm chart (Enterprise). Run ./scripts/agl-deploy.sh --help for the full
command list.
Stopping, upgrading, removing
$ docker compose ps # from the install/compose directory
$ ./scripts/upgrade.sh <VERSION> # backs up, then upgrades in place within 2.x
$ ./scripts/uninstall.sh # stops containers and removes volumes
uninstall.sh keeps compose/.env by default. --purge removes it too, after saving a
timestamped copy beside it (compose/.env.backup-<UTC timestamp>). Move that copy somewhere safe or
shred it deliberately: it holds VAULT_SIGNING_KEY, the Ed25519 key every record signature chains
to. A lost key cannot be regenerated, and without it a restored database can still be read and its
chain verified against the published public key, but the Server can no longer sign new records
into the same chain.
upgrade.sh is the only path that moves an install between releases. Check out the tag you are
moving to first, so the scripts and Compose files match the image, then name the version:
$ git fetch --tags && git checkout v<VERSION>
$ ./scripts/upgrade.sh <VERSION>
Within 2.x, it reads the version the install is on, checks the configuration in compose/.env,
asks Upgrade AGLedger from <current> to <target>? and waits for Continue? (y/N) when it runs at
a terminal (with no terminal on stdin it proceeds), and verifies the new image's signature. Then
it takes a backup before it touches anything, records the version and image pin in that backup so
a restore returns the install to the release it was on, stops the worker so no job runs against a
half-migrated schema, applies only the migrations the new release adds, writes the new
AGLEDGER_VERSION and signature-verified digest to .env, restarts everything, runs preflight,
and confirms /health/ready reports the new version.
If the run fails, it says whether the worker is down, and applied migrations are skipped on the
retry, so the same ./scripts/upgrade.sh <VERSION> resumes rather than starts over. Bringing the
stack back on the version .env names, without finishing the upgrade, is
docker compose up -d from the compose directory. Re-running install.sh does not upgrade: it
stays on the version recorded in compose/.env and reconciles configuration.
A 1.x install does not upgrade to 2.0. The 2.0 migration refuses a database a 1.x release
migrated, so upgrade.sh refuses a 1.x install at its version check, before it writes .env,
takes a backup or stops anything:
$ git fetch --tags && git checkout v2.0.0
$ ./scripts/upgrade.sh 2.0.0
[STEP] Checking current version
[OK] Current version: 1.8.0
[OK] Target version: 2.0.0
[ERROR] 2.0.0 does not upgrade a 1.8.0 install in place: the migration refuses a database
[ERROR] a 1.x release migrated. Nothing was changed: .env, the database and every container are as they were.
[ERROR] To run 2.0.0, install it on a new, empty database from its own checkout (a new directory,
[ERROR] so a new compose project and a new data volume), and keep this install for its data.
[ERROR] Put this checkout back on the v1.8.0 deploy/ tree before running any docker compose
[ERROR] command in it: from this tree, compose starts 2.0.0 against the 1.x database.
Do what the refusal says: git checkout v1.8.0 in that checkout, and install 2.0 by cloning the
v2.0.0 tag into a new directory and running install.sh there.
Day-2 operations covers the same refusal for restores and for Helm.
Production and external databases
This Compose path is single-node: it suits evaluation, development, and small workloads. For a
production cluster with TLS, your own managed PostgreSQL, and horizontal scaling, use the
Kubernetes install guide. To keep Compose but point at a managed PostgreSQL
(Aurora, RDS, Cloud SQL), set DATABASE_URL in compose/.env and install with
./scripts/install.sh --external-db; external-database licensing is per database instance. The
URL needs sslmode=verify-full (or require / verify-ca): outside the bundled PostgreSQL the
Server refuses to boot on a database connection without TLS.
The migration role needs superuser, so grant rds_superuser (Amazon RDS / Aurora),
cloudsqlsuperuser (Google Cloud SQL), azure_pg_admin (Azure Database), or SUPERUSER on a
self-managed server. To keep the running Server least-privilege, put the privileged URL in
DATABASE_URL_MIGRATE (used for migrations only) and leave DATABASE_URL as the DML role,
agledger_app. The baseline migration creates that role without a usable login: set
AGLEDGER_APP_ROLE_PASSWORD in compose/.env and put the same password in the agledger_app
DATABASE_URL, and every migration run gives it its login under that password and CREATE on the
database. install.sh --external-db checks the superuser grant before it pulls anything and names
the grant for your provider, and it checks the runtime role's own grants immediately after
migrating, before it starts anything, naming the role and the grant it is missing.
Why that grant is needed, what the runtime role requires (the agledger_app naming rule and the
CREATE grant pg-boss needs), and the pg_dump client-version rule for backups are in External
database and What you provide on the Kubernetes install guide. They apply to
Compose identically.
Air-gapped
Nothing in install or runtime depends on agledger.ai, Docker Hub, or npm. Outbound network access is needed only to pull images during install and upgrade, and for destinations you configure yourself, all off until you set them: webhook receivers, federation peers, your IdP's discovery and JWKS endpoints, the S3 bucket external anchors are written to, the SIEM collector, an OTLP collector, and, on a Marketplace install, the AWS License Manager entitlement check.
Mirror the image into your internal registry and name that registry with --image. The registry
host comes from that --image; a private registry that is not ECR needs a docker login first.
The bundled PostgreSQL image comes from Docker Hub whatever --image says, so carry it alongside
the release.
A mirrored image (any --image other than agledger/agledger) is not verified against public
Rekor. The installer verifies it from the release's offline-verification bundle
(agledger-2.0.0-offline-verification.tar.gz on the
install repository's release):
check that archive's own signature while you still have a network, unpack it, and point
AGLEDGER_VERIFY_BUNDLE_DIR at it. The installer then verifies with no registry and no Rekor
reachable. AGLEDGER_REQUIRE_VERIFY=true makes verification mandatory: it refuses a host with no
cosign, and it refuses a mirrored image when no bundle is given rather than installing it
unverified.
$ AGLEDGER_VERIFY_BUNDLE_DIR=$PWD/offline-verification AGLEDGER_REQUIRE_VERIFY=true \
./scripts/install.sh --image your-registry.example/agledger --version 2.0.0
What to mirror, how oras cp -r keeps the signature on the mirror, and the no-registry enclave
case are in Air-gapped install on the Kubernetes install guide and the install
repository's air-gap guide
(air-gap/README.md).