> Markdown version of https://agledger.ai/docs/install/compose/
> Full index of this site for AI assistants: https://agledger.ai/llms.txt

# 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](/docs/install/).

> Captured against a live Developer Edition v1.4.0 Compose install on 2026-08-10 (bundled
> PostgreSQL 18.4): the image pulls and its keyless signature verifies, migrations apply, the
> health and verification-keys endpoints answer, and a notarize round-trip returns `RECORDED`.
> The `sha256:0515c4f8…` digest below is the `linux/amd64` image cosign verified against public
> Rekor on that host. Log lines show their message fields only, and the preflight's federation-key
> warnings (for unset outbound federation keys, expected on a fresh single-Server install) are
> omitted for brevity.

## Prerequisites

- Docker Engine 24.0+ and the Docker Compose v2 plugin
- `jq`, `curl`, and `openssl` on your PATH (the installer uses them for secret generation and the
  Docker Hub version lookup)
- 4 GB RAM and 2 CPU cores minimum
- 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`](https://github.com/agledger-ai/install) repository. Clone a tagged release
and run the installer.

```
$ git clone --branch v1.4.0 https://github.com/agledger-ai/install.git
$ cd install
$ ./scripts/install.sh --version 1.4.0
```

`install.sh` generates the cryptographic secrets locally (`POSTGRES_PASSWORD`, `API_KEY_SECRET`,
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. Pass `--version` to pin a release; omit it to install 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](/docs/install/fips/) 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
[STEP]  Checking host ports
[OK]    Host ports available: API 3001, PostgreSQL 5432
[STEP]  Resolving version
[OK]    Version: 1.4.0 (requested)
[OK]    Using Docker Hub: agledger/agledger
[STEP]  Verifying image signature
[OK]    Image signature verified (keyless, public Rekor): agledger/agledger@sha256:0515c4f8…
[STEP]  Configuring environment
[OK]    Generated POSTGRES_PASSWORD
[OK]    Generated AGLEDGER_INSTANCE_ID (this Server's federation identity)
[OK]    Generated API_KEY_SECRET
[OK]    Generated VAULT_SIGNING_KEY
[STEP]  Running database migrations
        {"migration":"001_consolidated.sql","msg":"Applied migration"}
        {"migration":"002_webhook_record_types.sql","msg":"Applied migration"}
        {"migration":"003_export_cap_and_checkpoint_immutability.sql","msg":"Applied migration"}
        {"migration":"004_signing_agility.sql","msg":"Applied migration"}
        {"count":4,"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 (Ed25519)
        ✓ Database SSL: SSL not configured but ALLOW_DB_WITHOUT_SSL=true
        ✓ PostgreSQL connectivity: Connected
        ✓ PostgreSQL version: 18.4 (native uuidv7)
        ✓ Migrations: 4/4 applied — up to date
        ✓ pg-boss schema: pgboss schema exists
        ✓ Connection headroom: ~40 connections (pool 20 + workers 5+10 + pg-boss ~5) within max_connections=100

=============================================================================
  AGLedger — Installation Complete
=============================================================================
  Version:       1.4.0
  API URL:       http://localhost:3001
  Signed issuer: http://localhost:3001  (iss baked into every record)
  Signing:       Ed25519
  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.
```

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 `iss` in every
> signed envelope comes from `AGLEDGER_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, set `AGLEDGER_EXTERNAL_URL` in `compose/.env` to the `https://` URL where
> the Server will actually be reachable, restart the stack (`docker compose up -d` from
> `compose/`), 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.4.0","timestamp":"2026-08-10T06:41:05.094Z"}
$ curl -s http://localhost:3001/health/ready
{"status":"ready","version":"1.4.0","timestamp":"2026-08-10T06:41:05.099Z"}
```

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. As of
v1.4.0 each key entry also declares its COSE algorithm and the minimum verifier version that can
consume it, the signing-agility surface:

```
$ curl -s http://localhost:3001/v1/verification-keys
{"data":[{"keyId":"3be5d5512168b252","algorithm":"Ed25519",
          "publicKey":"MCowBQYDK2VwAyEAuQxNIrEmqjJJ0INkNrCLStM7IOCpa2bwIu+tlMZNJbQ=",
          "status":"active","activatedAt":"2026-08-10T06:40:34.257Z","retiredAt":null,
          "coseAlgorithm":-8,"minVerifierVersion":"1.0.0",
          "publicKeyRaw":"uQxNIrEmqjJJ0INkNrCLStM7IOCpa2bwIu+tlMZNJbQ="}],
 "envelope":"COSE_Sign1","payloadFormat":"application/vnd.in-toto+cbor",
 "canonicalization":"RFC8949-CDE","coseAlgorithm":-8,"signatureAlgorithm":"Ed25519"}
```

With no license applied, the Server runs as Developer Edition with every feature enabled:

```
$ curl -s -H "Authorization: Bearer $AGLEDGER_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"],
 "checkedAt":"2026-08-10T06:40:32.507Z","source":"none","customerId":null,"customerName":null,
 "instanceId":null,"licensedThrough":null,"releaseDate":"2026-08-09","licenseId":null,
 "nextSteps":[{"action":"Check pricing","href":"https://agledger.ai/pricing"}]}
```

> `"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](/register/) 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 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 "{\"name\":\"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"
```

```json
{
  "id": "019fea67-48c8-75d1-af85-f6a38d6a7e60",
  "status": "RECORDED",
  "type": "notarize-generic-v1",
  "signedStatement": {
    "chainPosition": 1,
    "leafHash": "1d0528394bd21fe0c58aaf85a1c09a24b1c2bb06178c77d930e90e9d1803d8e6",
    "previousHash": null,
    "signingKeyId": "3be5d5512168b252",
    "signedAt": "2026-08-10T06:41:05Z",
    "signedCheckpointRef": null,
    "url": "/v1/records/019fea67-48c8-75d1-af85-f6a38d6a7e60/attestation"
  }
}
```

The record is `RECORDED` and already signed: `signingKeyId` is the same `3be5d5512168b252` 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](/docs/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 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 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](/docs/install/) (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.4.0         # backs up, then upgrades in place to the version you name
$ ./scripts/uninstall.sh             # stops containers and removes volumes
```

`uninstall.sh` keeps `compose/.env` by default - pass `--purge` to remove it too. Back up
`compose/.env` somewhere safe before a purge: it holds `VAULT_SIGNING_KEY`, the Ed25519 key every
record signature chains to. A purged 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. It takes a backup first and
writes the marker that says what to roll back to. Re-running `install.sh` does not upgrade: it stays
on the version recorded in `compose/.env` and reconciles configuration, so it is safe to run again
after fixing whatever stopped it the first time.

## 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](/docs/install/). 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 migration role needs superuser.** The schema installs an event trigger,
`agledger_block_audit_drop`, which is what stops the audit chain being dropped, and
`CREATE EVENT TRIGGER` is superuser-only in PostgreSQL. A managed database hands you an owner
role with `CREATEDB` and not this: 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.
`install.sh --external-db` checks this before it pulls anything and names the grant for your
provider.

**The runtime role has two requirements of its own.** Unless it owns the schema objects (the
single-role shape, where one role migrates and serves and ownership carries every privilege), it
must be named `agledger_app`, because the migration grants table privileges to that exact name and
a non-owner role called anything else receives none of them; if your naming standard forbids it,
make your role a member after migrating with `GRANT agledger_app TO "your_role" WITH INHERIT TRUE`
(spell out the `INHERIT` clause, which PostgreSQL 16 and later default to the member role's own
setting). And it needs `CREATE` on the database, `GRANT CREATE ON DATABASE agledger TO
agledger_app`, because pg-boss keeps its queue tables in a schema it creates on first start and
the Server starts pg-boss. `install.sh` checks both immediately after migrating, before it starts
anything, and names the role and the grant it is missing.

**Backups of an external database need a client at least as new as the server.** `pg_dump`
refuses to dump a server newer than itself, and distributions lag: Ubuntu 24.04 ships client 16
against the PostgreSQL 18 this product is validated on. `backup.sh` reconciles the two for you,
falling back to a matching client in a container when the host's is older, so the only case that
needs your attention is a host with neither an adequate client nor docker.

## 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.4.0`) into your internal registry and
run `./scripts/install.sh --image your-registry.example/agledger --version 1.4.0`. The registry
host comes from that `--image`; a private registry that is not ECR needs a `docker login` first.
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.

Set `AGLEDGER_REQUIRE_VERIFY=true` to make that verification mandatory. Without it, a host with
no cosign installed proceeds unverified after a warning, which suits an evaluation and not a
production install:

```
$ AGLEDGER_REQUIRE_VERIFY=true ./scripts/install.sh --version 1.4.0
```

---

*Re-captured live against a fresh Developer Edition v1.4.0 Compose install on 2026-08-10 (bundled PostgreSQL 18.4): every transcript above, from the installer output and the four-migration apply through preflight, health/readiness, verification-keys with the signing-agility fields, license, and the notarize round-trip to `RECORDED`, is the actual output of that run rather than a restamp.*

*Re-validated live against a Developer Edition v1.3.2 Compose install on 2026-07-13: install, health/readiness, verification-keys, license, and a notarize round-trip to `RECORDED` all re-run against the live Server.*

*Reviewed for API v1.3.3 on 2026-07-20: 1.3.3 is audit-vault chain-scan detection hardening plus opt-in verdict per-actor signatures, both backward-compatible; the install, health/readiness, and notarize flow above are unchanged, and the transcript reflects a v1.3.3 Server.*
