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 | |

ECR access. AGLedger images are hosted in a private Amazon ECR registry. You need AWS credentials with these IAM permissions:

Authenticate before pulling images:

aws ecr get-login-password --region us-west-2 \
  | docker login --username AWS --password-stdin \
    705542379002.dkr.ecr.us-west-2.amazonaws.com

If you cannot reach ECR (air-gapped network), skip to Section 6: Air-Gap Deployment.

2. Quick Install (Docker Compose)

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

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

What install.sh does

| Step | Action | |------|--------| | 1 | Checks prerequisites (Docker, Compose, jq, openssl, RAM, CPU) | | 2 | Authenticates with ECR (warns but continues if aws CLI is absent) | | 3 | Generates secrets and writes docker-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 |

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 docker-compose/.env as PLATFORM_API_KEY.

If you lose the platform key, regenerate it:

./scripts/reset-platform-key.sh

Installer flags

./install.sh --version 0.15.6          # Pin a specific version
./install.sh --external-db              # Skip bundled PostgreSQL
./install.sh --non-interactive          # No prompts (CI/automation)
./install.sh --with-monitoring          # Enable Jaeger, Prometheus, Grafana

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"}}

Swagger UI is served at http://localhost:3001/docs.

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 docker-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 docker-compose
docker compose \
  -f docker-compose.yml \
  -f docker-compose.postgres.yml \
  -f docker-compose.prod.yml \
  up -d --wait

With external database:

cd docker-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;
    }
}

Full example configurations are in examples/reverse-proxy/ in the self-hosted repository.

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 \
  705542379002.dkr.ecr.us-west-2.amazonaws.com/agledger-ai/self-hosted:<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, create an air-gap bundle on a connected machine and transfer it to the target host.

Create the bundle

On a machine with Docker and internet access:

./scripts/airgap-bundle.sh 0.15.6

This pulls all required images (AGLedger, PostgreSQL, and optional monitoring stack), saves them as tarballs, copies all deployment files, generates SHA256 checksums, and packages everything into a single archive:

agledger-airgap-0.15.6.tar.gz

Deploy on the air-gapped host

Transfer the archive to the target machine, then follow these steps:

# 1. Extract the bundle
tar -xzf agledger-airgap-0.15.6.tar.gz
cd agledger-airgap-0.15.6

# 2. Verify checksums (optional but recommended)
sha256sum -c SHA256SUMS

# 3. Load Docker images
./load-images.sh

# 4. Copy the environment template
cp docker-compose/.env.example docker-compose/.env

# 5. Edit .env — set your secrets (API_KEY_SECRET, VAULT_SIGNING_KEY)
#    Generate API_KEY_SECRET on the air-gapped host:
#      openssl rand -hex 32
#    For VAULT_SIGNING_KEY, generate before bundling or use:
#      docker run --rm <image> dist/scripts/generate-signing-key.js

# 6. Set file permissions
chmod 600 docker-compose/.env

# 7. Start services
cd docker-compose
docker compose \
  -f docker-compose.yml \
  -f docker-compose.postgres.yml \
  up -d --wait

The bundle includes the Helm chart at helm/agledger/ for Kubernetes deployments.

7. Post-Install Verification

Preflight checks

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

./scripts/preflight.sh

If the API container is running, preflight executes inside it. Otherwise, it starts a one-off container.

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 self-hosted repository 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 self-hosted v0.15.6. Source: self-hosted.