Self-Hosted Installation

Install AGLedger on your own infrastructure using Docker Compose, Kubernetes (Helm), or an air-gap bundle. This guide covers installation only. For post-install account provisioning, see the Onboarding Guide. For YAML-based bulk provisioning, see the YAML Provisioning Guide.

1. Prerequisites

| Requirement | Minimum | Recommended (production) | |-------------|---------|--------------------------| | Docker Engine | 24.0+ | Latest stable | | Docker Compose | v2 | Latest stable | | RAM | 4 GB | 8 GB | | CPU | 2 cores | 4 cores | | Disk | 20 GB free | Scales with usage | | CLI tools | jq, openssl | |

Docker Hub. AGLedger images are published to Docker Hub at agledger/agledger. No special authentication is required for pulling images.

Verify image signatures (optional)

AGLedger release images are signed with cosign. To verify an image before pulling it:

cosign verify \
  --key https://agledger.ai/cosign.pub \
  agledger/agledger:0.19.17

The canonical public key is published at https://agledger.ai/cosign.pub on the same domain as these docs. Pin or mirror the key as your policy requires — for air-gapped installs, download the file and reference it by path instead of URL.

2. Quick Install (Docker Compose)

git clone https://github.com/agledger-ai/install.git
cd install
./scripts/install.sh

The installer is non-destructive. If compose/.env already exists, it skips secret generation and reuses it. To start fresh, delete compose/.env and re-run.

When install.sh finishes, the completion banner prints a platform API key (shown once) and links to the API reference at /docs and the product walkthrough at agledger.ai/how-it-works.

What install.sh does

| Step | Action | |------|--------| | 1 | Checks prerequisites (Docker, Compose, jq, openssl, curl, RAM, CPU) | | 2 | Authenticates with ECR (warns but continues if aws CLI is absent) | | 3 | Generates secrets and writes compose/.env | | 4 | Detects database mode (bundled or external) | | 5 | Pulls images, starts PostgreSQL (if bundled), runs migrations | | 6 | Creates the platform API key and saves it to .env | | 7 | Prints a completion banner with the platform key, health URLs, and links to docs |

Generated secrets

The installer generates three secrets locally. None leave your environment.

| Secret | Format | Purpose | |--------|--------|---------| | API_KEY_SECRET | 64-character hex string | HMAC-SHA256 key for API key hashing | | VAULT_SIGNING_KEY | Base64 PKCS#8 DER | Ed25519 private key for audit vault signatures | | POSTGRES_PASSWORD | 32-character alphanumeric | Bundled PostgreSQL password (ignored with external DB) |

The VAULT_SIGNING_KEY is generated by running the AGLedger image itself (generate-signing-key.js), so the image must be pullable before this step completes.

Platform API key

At the end of installation, the installer prints a platform API key (prefixed ach_pla_). This key has full admin access. Save it immediately — it is shown only once. The key is also written to compose/.env as PLATFORM_API_KEY.

If you lose the platform key, generate a new one by running the in-container init script:

cd compose
docker compose run --rm agledger-api node dist/scripts/init.js

The new key is printed once. Copy it into compose/.env as PLATFORM_API_KEY.

Installer flags

./scripts/install.sh --version 0.19.17         # Pin a specific version
./scripts/install.sh --external-db              # Skip bundled PostgreSQL
./scripts/install.sh --non-interactive          # No prompts (CI/automation)
./scripts/install.sh --with-monitoring          # Enable Jaeger, Prometheus, Grafana
./scripts/install.sh --image registry.example.com/agledger  # Pull from internal registry (air-gap)

Bundled vs. external database

The installer auto-detects which mode to use:

Verifying the installation

After the installer completes, the API is available at http://localhost:3001:

curl http://localhost:3001/health
# {"status":"ok"}

curl http://localhost:3001/health/ready
# {"status":"ok","checks":{"database":"ok","worker":"ok"}}

Local Swagger UI is disabled by default to reduce attack surface. The hosted interactive API reference at agledger.ai/api serves the same OpenAPI 3.0 spec.

3. External Database Setup

For production, use a managed PostgreSQL service: Aurora PostgreSQL, RDS, Cloud SQL, or Azure Flexible Server.

Connection string

Set DATABASE_URL in compose/.env before running install.sh:

DATABASE_URL=postgresql://agledger:<PASSWORD>@your-cluster.cluster-xxx.us-west-2.rds.amazonaws.com:5432/agledger?sslmode=require

Connection pooler warning

Do not place a connection pooler in transaction mode between AGLedger and PostgreSQL. This includes:

AGLedger uses pg-boss for background job processing, which requires LISTEN/NOTIFY. Transaction-mode poolers silently drop these notifications, causing jobs to stall without errors.

Use direct connections only.

Role separation

For tighter database security, use two connection strings:

| Variable | Role | Privileges | |----------|------|-----------| | DATABASE_URL | agledger_app | DML only (SELECT, INSERT, UPDATE, DELETE) | | DATABASE_URL_MIGRATE | Owner / postgres | DDL (CREATE, ALTER, DROP) for schema migrations |

When DATABASE_URL_MIGRATE is set, the migration runner uses it instead of DATABASE_URL. If unset, both operations use DATABASE_URL (which must then have DDL privileges).

Pool sizing

Adjust DATABASE_POOL_MAX based on your database's connection limits. AGLedger's total connection usage is approximately pool * 2 + 5 (API + Worker + pg-boss overhead).

| Database | Max connections | Recommended DATABASE_POOL_MAX | |----------|----------------|----------------------------------| | Aurora Serverless 0.5 ACU | ~90 | 10 | | Aurora Serverless 2 ACU | ~50 | 15 | | RDS db.t3.medium (4 GB) | ~420 | 20 (default) |

SSL certificates

The AGLedger Docker image bundles the AWS RDS/Aurora global CA bundle at /etc/ssl/certs/rds-global-bundle.pem. To enable sslmode=verify-full with Aurora or RDS, set:

NODE_EXTRA_CA_CERTS=/etc/ssl/certs/rds-global-bundle.pem

For Cloud SQL or private CAs, mount your certificate into the container and override the path:

NODE_EXTRA_CA_CERTS=/certs/your-ca.pem

For the bundled PostgreSQL (no TLS), set ALLOW_DB_WITHOUT_SSL=true.

4. Production Hardening

Production compose overlay

Apply docker-compose.prod.yml for production deployments. It adds restart policies, read-only filesystems, resource limits, and log rotation.

With bundled PostgreSQL:

cd compose
docker compose \
  -f docker-compose.yml \
  -f docker-compose.postgres.yml \
  -f docker-compose.prod.yml \
  up -d --wait

With external database:

cd compose
docker compose \
  -f docker-compose.yml \
  -f docker-compose.prod.yml \
  up -d --wait

The production overlay applies the following to the API and Worker containers:

| Setting | Value | |---------|-------| | Restart policy | unless-stopped | | Filesystem | Read-only (read_only: true) | | Temp space | tmpfs at /tmp (64 MB) | | CPU limit | 1.0 core | | Memory limit | 512 MB | | Memory reservation | 256 MB | | Log rotation | json-file driver, 50 MB per file, 5 files max |

Reverse proxy

In production, place AGLedger behind a TLS-terminating reverse proxy. Set TRUST_PROXY=true in .env so AGLedger reads X-Forwarded-For and X-Forwarded-Proto headers correctly.

Caddy (automatic HTTPS via Let's Encrypt):

agledger.example.com {
    reverse_proxy localhost:3001

    request_body {
        max_size 1MiB
    }

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
        -Server
    }
}

nginx:

upstream agledger {
    server 127.0.0.1:3001;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name agledger.example.com;

    ssl_certificate /etc/ssl/certs/agledger.crt;
    ssl_certificate_key /etc/ssl/private/agledger.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 1m;

    location / {
        proxy_pass http://agledger;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        proxy_connect_timeout 10s;
        proxy_read_timeout 35s;
        proxy_send_timeout 10s;
    }
}

Port bindings

All Docker Compose service ports bind to 127.0.0.1 (localhost only) by default:

| Service | Container port | Host port | |---------|---------------|-----------| | API | 3000 | 3001 | | Worker health | 3001 | 3002 | | PostgreSQL (bundled) | 5432 | 5432 |

5. Kubernetes / Helm Install

Basic install

helm install agledger helm/agledger/ \
  --namespace agledger \
  --create-namespace \
  --set database.externalUrl="postgresql://agledger:<PASSWORD>@your-db-host:5432/agledger?sslmode=require" \
  --set secrets.apiKeySecret="<YOUR_64_CHAR_HEX>" \
  --set secrets.vaultSigningKey="<YOUR_BASE64_ED25519_KEY>" \
  --set secrets.licenseKey="<YOUR_LICENSE_PEM>"

Generate the required secrets before installing:

# API_KEY_SECRET (64-char hex)
openssl rand -hex 32

# VAULT_SIGNING_KEY (Ed25519 — use the AGLedger image)
docker run --rm \
  agledger/agledger:<version> \
  dist/scripts/generate-signing-key.js

Using an existing Kubernetes Secret

Instead of passing secrets inline, create a Secret and reference it:

secrets:
  existingSecret: agledger-secrets   # Name of your pre-created Secret

The Secret must contain these keys:

| Key | Required | |-----|----------| | DATABASE_URL | Yes | | API_KEY_SECRET | Yes | | VAULT_SIGNING_KEY | Yes | | WEBHOOK_ENCRYPTION_KEY | No | | DATABASE_URL_MIGRATE | No | | AGLEDGER_LICENSE_KEY | No |

Production values

Apply values-production.yaml for production deployments:

helm install agledger helm/agledger/ \
  --namespace agledger \
  --create-namespace \
  --values helm/agledger/values-production.yaml \
  --values your-secrets.yaml

The production preset configures:

| Setting | Value | |---------|-------| | API replicas | 2 | | Worker replicas | 2 | | HPA | Enabled (2-10 replicas, 80% CPU target) | | PodDisruptionBudget | minAvailable: 1 | | Topology spread | 1 pod per availability zone | | API memory | 512 Mi request / 1 Gi limit | | Worker autoscaling | Enabled (2-10 replicas) | | Network policy | Enabled | | Trust proxy | true | | Log level | warn |

License delivery

Two options for providing an enterprise license on Kubernetes:

Option A: Inline in Secret (simpler)

secrets:
  licenseKey: |
    -----BEGIN LICENSE FILE-----
    <your license PEM content>
    -----END LICENSE FILE-----

Option B: Mounted file (preferred for key management)

Create a Kubernetes Secret containing the license PEM file, then reference it:

license:
  keyFile:
    enabled: true
    secretName: agledger-license        # K8s Secret name
    secretKey: license.pem              # Key within the Secret
    mountPath: /run/secrets/agledger-license.pem

If no license is configured, AGLedger runs in Starter mode. All core features are enabled. License problems fail open — the server always starts.

Bundled PostgreSQL (dev/CI only)

The Helm chart can deploy a bundled PostgreSQL for development or CI. This uses an ephemeral emptyDir volume — data does not survive pod restarts.

postgres:
  bundled:
    enabled: true

database:
  externalUrl: ""   # Leave empty when using bundled postgres

Do not use bundled PostgreSQL in production.

Ingress

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: api.agledger.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: agledger-tls
      hosts:
        - api.agledger.example.com

Set config.trustProxy: true when using an ingress controller.

EKS image pull

EKS nodes pull from ECR via their IAM instance role. Attach the AmazonEC2ContainerRegistryReadOnly managed policy to the node IAM role. No imagePullSecrets are needed.

6. Air-Gap Deployment

For networks with no internet access, pull the AGLedger image on an internet-connected machine, transfer it to your internal registry, then run install.sh --image against that registry.

On a machine with internet access

Pull and save the image:

VERSION=0.19.17
docker pull agledger/agledger:${VERSION}
docker save agledger/agledger:${VERSION} | gzip > agledger-${VERSION}.tar.gz

Grab the install repo (scripts, compose files, Helm chart):

git clone --depth 1 https://github.com/agledger-ai/install.git
tar czf install.tar.gz install/

Transfer both agledger-${VERSION}.tar.gz and install.tar.gz to the air-gapped environment.

Inside the air-gapped environment

Load the image and push it to your internal registry:

docker load < agledger-0.19.17.tar.gz
docker tag agledger/agledger:0.19.17 registry.internal.example.com/agledger:0.19.17
docker push registry.internal.example.com/agledger:0.19.17

Run the installer against your registry:

tar xzf install.tar.gz
cd install
./scripts/install.sh \
  --image registry.internal.example.com/agledger \
  --version 0.19.17 \
  --non-interactive

Helm chart (air-gapped Kubernetes)

The Helm chart is published to OCI at oci://registry-1.docker.io/agledger/agledger-chart. To air-gap it:

helm pull oci://registry-1.docker.io/agledger/agledger-chart --version 0.19.17
# transfer agledger-chart-0.19.17.tgz to the air-gapped host
helm install agledger agledger-chart-0.19.17.tgz \
  --set image.repository=registry.internal.example.com/agledger \
  --set image.tag=0.19.17 \
  --values your-values.yaml

Telemetry

Developer Edition sends an anonymous heartbeat to telemetry.agledger.ai every 48 hours. In a restricted-network environment this call will fail and log a warning — it has no effect on runtime behavior. Set AGLEDGER_TELEMETRY=false in .env to disable it cleanly. Enterprise licenses disable it automatically.

7. Post-Install Verification

Preflight checks

Run the built-in preflight script to verify database connectivity, migrations, and configuration:

cd compose
docker compose exec agledger-api node dist/scripts/preflight.js

If the API container is not yet running, start a one-off container instead:

docker compose run --rm agledger-api node dist/scripts/preflight.js

Health endpoints

| Endpoint | Purpose | |----------|---------| | GET /health | Basic liveness check. Returns {"status":"ok"} if the process is running. | | GET /health/ready | Readiness check. Verifies database connectivity and worker health. | | GET /conformance | Returns supported API version and feature set. |

# Liveness
curl -s http://localhost:3001/health | jq .

# Readiness (database + worker)
curl -s http://localhost:3001/health/ready | jq .

Smoke test

The agledger-ai/install repo includes a smoke test that validates health, conformance, and optionally runs a full mandate lifecycle:

# Health checks only
./tests/smoke-test.sh http://localhost:3001

# Full lifecycle (reads PLATFORM_API_KEY from .env automatically)
./tests/smoke-test.sh http://localhost:3001

8. Next Steps


Validated against AGLedger v0.19.17 (Helm chart + Docker image) on 2026-04-20. Source: agledger-ai/install.