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:
- Bundled PostgreSQL (default). If
DATABASE_URLis not set in.env, the installer starts a PostgreSQL 18 container alongside AGLedger. Suitable for evaluation and small deployments. - External database. If
DATABASE_URLis set in.env(or--external-dbis passed), the installer skips the bundled PostgreSQL container. See Section 3 for requirements.
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:
- AWS RDS Proxy
- PgBouncer in transaction mode
- Cloud SQL managed pooling
- Azure built-in PgBouncer
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
- Provision your first enterprise account. Follow the Self-Hosted Onboarding Guide to create an enterprise, register agents, and run your first mandate.
- Bulk provisioning. Use the YAML Provisioning Guide to provision enterprises, agents, and contract schemas from a single YAML file.
- Upgrade. Run
./scripts/upgrade.sh <version>to upgrade to a new release. See the Operations Guide for details. - Backup and restore. See the Operations Guide for PostgreSQL backup procedures.
- Configuration. See the Configuration Reference for all environment variables, licensing, and Helm chart values.
- TLS. See the TLS Configuration guide for database and ingress TLS setup.
- Version planning. See the Version Matrix for supported Node/PG/K8s versions and upstream EOL dates.
Validated against AGLedger v0.19.17 (Helm chart + Docker image) on 2026-04-20. Source: agledger-ai/install.