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 - no Kubernetes, no external database.
The Server comes up as Developer Edition with no license applied: every feature is enabled, and it is free to self-host for evaluation, development, and production on the bundled PostgreSQL. For a production Kubernetes deployment fronted by TLS and backed by your own PostgreSQL, see the Kubernetes install guide.
Validated against API v1.1.0 on 2026-06-10 (Developer Edition, Docker Compose, bundled PostgreSQL 18). The install transcript was captured live at v1.0.0 on 2026-06-09 and is version-stable; the v1.0.2 release pins, image digest, and migration output shown below are verified against the v1.0.2 release artifacts.
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)- 4 GB RAM and 2 CPU cores minimum
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 v1.0.2 https://github.com/agledger-ai/install.git
$ cd install
$ ./scripts/install.sh --version 1.0.2
install.sh generates the cryptographic secrets locally (POSTGRES_PASSWORD, API_KEY_SECRET,
and an Ed25519 VAULT_SIGNING_KEY), writes them to compose/.env, starts PostgreSQL, runs
migrations, mints the first platform API key, and starts the API and worker. Pass --version to
pin a release; omit it to install the latest stable from Docker Hub.
[STEP] Checking prerequisites
[OK] Docker Engine 29.1.3
[OK] Docker Compose 2.40.3
[STEP] Resolving version
[OK] Version: 1.0.2 (requested)
[STEP] Verifying image signature
[OK] Image signature verified (keyless, public Rekor): agledger/agledger@sha256:9f0e0801…
[STEP] Configuring environment
[OK] Generated POSTGRES_PASSWORD
[OK] Generated API_KEY_SECRET
[OK] Generated VAULT_SIGNING_KEY
[STEP] Running database migrations
{"migration":"001_consolidated.sql","msg":"Applied migration"}
{"count":1,"msg":"Migration complete"}
[STEP] Creating platform API key
[OK] Platform API key created
[STEP] Starting all services
[OK] All services started
[STEP] Running preflight checks
✓ DATABASE_URL: Set
✓ API_KEY_SECRET: Set (non-default)
✓ VAULT_SIGNING_KEY: Set
✓ PostgreSQL connectivity: Connected
✓ PostgreSQL version: 18.4 (native uuidv7)
✓ Migrations: 1/1 applied — up to date
✓ pg-boss schema: pgboss schema exists
=============================================================================
AGLedger — Installation Complete
=============================================================================
Version: 1.0.2
API URL: http://localhost:3001
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.
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.
The API URL is the signed issuer - set it before you notarize records you keep. Every signed record carries an
iss: the issuer identity baked into the envelope, taken fromAGLEDGER_EXTERNAL_URLincompose/.env. A fresh install shipsAGLEDGER_EXTERNAL_URL=http://localhost:3001, so records you notarize now are signed withiss: http://localhost:3001. That is fine for evaluation, but the issuer is signed into the envelope and cannot be changed after the fact - 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_URLto 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":"1.0.2","timestamp":"2026-06-09T15:55:26.237Z"}
$ curl -s http://localhost:3001/health/ready
{"status":"ready","version":"1.0.2","timestamp":"2026-06-09T15:55:26.242Z"}
A Server is only useful if it is signing. The active verification key is published unauthenticated;
the keyId is the fingerprint of the VAULT_SIGNING_KEY the installer generated for you:
$ curl -s http://localhost:3001/v1/verification-keys
{"data":[{"keyId":"c4dd3e20388b594d","algorithm":"Ed25519",
"publicKey":"MCowBQYDK2VwAyEAo95XH8DQ6ZYqhC761LqlCq0b9wxYgHPyHs67OkQ9Frw=",
"status":"active","activatedAt":"2026-06-09","retiredAt":null}],
"envelope":"COSE_Sign1","signatureAlgorithm":"Ed25519"}
With no license applied, the Server runs as Developer Edition with every feature enabled:
$ curl -s -H "Authorization: Bearer $PLATFORM_KEY" http://localhost:3001/v1/admin/license
{"validity":"unlicensed","tier":"developer",
"features":["custom_schemas","expression_engine","delegation_chains","audit_export",
"compliance_reports","encrypted_mode","entity_references","proposals","federation"],
"source":"none"}
"validity":"unlicensed"is expected - every feature is already on, and you can run the whole loop below without a key. To clear the periodic unlicensed-install warning and tie this install to your name, get a free Developer Edition key and apply it. 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 U=http://localhost:3001
$ export PLATFORM_KEY=agl_plt_<from-step-1>
$ ORG=$(curl -s -H "Authorization: Bearer $PLATFORM_KEY" "$U/v1/admin/orgs" \
| jq -r '.data[0].id')
# create an agent in the Default org
$ AGENT=$(curl -s -X POST -H "Authorization: Bearer $PLATFORM_KEY" -H "Content-Type: application/json" \
-d "{\"name\":\"Quickstart Agent\",\"orgId\":\"$ORG\"}" "$U/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 $PLATFORM_KEY" -H "Content-Type: application/json" \
-d "{\"role\":\"agent\",\"ownerId\":\"$AGENT\",\"ownerType\":\"agent\",\"scopeProfile\":\"agent-full\",\"label\":\"quickstart-key\"}" \
"$U/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"}}' \
"$U/v1/records"
{
"id": "019ead18-c3f9-7b4d-8edf-2dcd8b99fbc7",
"status": "RECORDED",
"type": "notarize-generic-v1",
"signedStatement": {
"chainPosition": 1,
"leafHash": "ba3de4d004f03f6ef8179ad09161b0c3f0bdac37f6c4379595712e311c47efde",
"previousHash": null,
"signingKeyId": "c4dd3e20388b594d",
"signedCheckpointRef": null,
"url": "/v1/records/019ead18-c3f9-7b4d-8edf-2dcd8b99fbc7/attestation"
}
}
The record is RECORDED and already signed: signingKeyId is the same c4dd3e20388b594d 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.
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.
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 [email protected] -L 3001:localhost:3001 [email protected]
$ 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 [email protected] -J [email protected] -i ~/.ssh/agl tunnel
It deploys this same Developer Edition stack (Compose on Docker CE, bundled PostgreSQL) - free and
production-ready; for production, 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 1.0.1 # backs up, then upgrades in place
$ ./scripts/uninstall.sh # stops containers and removes volumes
uninstall.sh keeps compose/.env by default - pass --purge to remove it too.
Production and external databases
This Compose path is single-node: ideal for evaluation, development, and small workloads. For a
production cluster - TLS, your own managed PostgreSQL, 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.
Air-gapped
Nothing in install or runtime depends on agledger.ai, Docker Hub, or npm at runtime. For a
restricted network, mirror the image (agledger/agledger:1.0.2) into your internal registry and
run ./scripts/install.sh --image your-registry.example/agledger --version 1.0.2. The Server makes
no outbound calls except the webhook and federation endpoints you configure. Every release is
keyless-signed (cosign, verifiable against the GitHub Actions build workflow with no static key);
see the install repository's SECURITY.md for the verify recipe.
Validated against API v1.1.0 on 2026-06-10 (Developer Edition, Docker Compose install).