Deploy on one VM with Docker Compose
The fastest production-shaped deployment: one virtual machine, four containers. It runs the same on an OVHcloud or Hetzner VPS, an EC2 instance, a Compute Engine or Azure VM. For several agents or a team, prefer Kubernetes with Helm.
Goal
A VS Agent reachable at https://agent.example.com, with its admin API bound to the host only, data on named volumes, TLS handled automatically.
Prerequisites
- A VM with Docker Engine and the Compose plugin, ports 80 and 443 open, 2 GB of memory.
- A DNS A or AAAA record for your hostname pointing at the VM.
- The values of What VS Agent needs to run decided.
Steps
1. Lay out the files
Create a directory with three files. The Compose file:
# VS Agent on one host: the agent, its PostgreSQL database and Redis.
# TLS and the public hostname are terminated by Caddy in front (see Caddyfile).
# Copy .env.example to .env and fill it in before `docker compose up -d`.
services:
vs-agent:
image: veranalabs/vs-agent:${VS_AGENT_VERSION:-2.0.0}
restart: unless-stopped
env_file: .env
environment:
PUBLIC_API_PORT: "3001"
ADMIN_API_PORT: "3000"
POSTGRES_HOST: postgres
POSTGRES_USER: vsagent
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
REDIS_URL: redis://redis:6379
# The admin API is reached only from this host; never publish port 3000.
ADMIN_API_TRUSTED_NETWORKS: 127.0.0.0/8,::1/128,172.16.0.0/12
volumes:
- agent-data:/root/.afj
ports:
- "127.0.0.1:3000:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: vsagent
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
POSTGRES_DB: vsagent
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vsagent"]
interval: 5s
timeout: 3s
retries: 20
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis-data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping"]
interval: 5s
timeout: 3s
retries: 20
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
depends_on:
- vs-agent
volumes:
agent-data:
postgres-data:
redis-data:
caddy-data:
The TLS proxy configuration, with your hostname:
# Replace agent.example.com with the host of PUBLIC_API_BASE_URL.
# Caddy obtains and renews the TLS certificate itself.
agent.example.com {
reverse_proxy vs-agent:3001
}
The environment, copied to .env and filled in:
# ---- identity and corporation -------------------------------------------------
# The corporation this agent acts for (its id on the registry, from the Frontend).
VERANA_CORPORATION_ID=
# BIP-39 mnemonic of the agent's own account. Generate it once, keep it secret, back it up.
VERANA_ACCOUNT_MNEMONIC=
# ---- network --------------------------------------------------------------------
VERANA_CHAIN_ID=vna-testnet-1
VERANA_RPC_ENDPOINT_URL=https://rpc.testnet.verana.network
VERANA_INDEXER_BASE_URL=https://idx.testnet.verana.network
# The ECS ecosystem you trust for identity credentials (see Environments and endpoints).
TRUSTED_ECS_ECOSYSTEM_DIDS=
# ---- the service --------------------------------------------------------------------
# Public origin of the agent. The DID is derived from it: choose it once.
PUBLIC_API_BASE_URL=https://agent.example.com
AGENT_PUBLIC_DID_METHOD=webvh
AGENT_MODE=standalone
# Claims of the ECS Service credential the agent issues to itself.
ECS_CLAIMS_SERVICE_NAME=My First Verifiable Service
ECS_CLAIMS_SERVICE_TYPE=WEB_PORTAL
ECS_CLAIMS_SERVICE_DESCRIPTION=A service made verifiable with VS Agent on the Verana testnet.
ECS_CLAIMS_SERVICE_MINIMUM_AGE_REQUIRED=0
# Placeholders served by the agent itself; replace with your own resources later.
ECS_CLAIMS_SERVICE_LOGO_URI=https://agent.example.com/vt/default/logo.svg
ECS_CLAIMS_SERVICE_TERMS_AND_CONDITIONS_URI=https://agent.example.com/vt/default/terms.html
ECS_CLAIMS_SERVICE_PRIVACY_POLICY_URI=https://agent.example.com/vt/default/privacy.html
# Claims the agent proposes for its ECS Organization credential (the validator may adjust them).
ECS_CLAIMS_ORG_NAME=Example Company Ltd
ECS_CLAIMS_ORG_LOGO_URI=https://agent.example.com/vt/default/logo.svg
ECS_CLAIMS_ORG_REGISTRY_ID=12345678
ECS_CLAIMS_ORG_ADDRESS=1 Example Street, Example City
ECS_CLAIMS_ORG_COUNTRY_CODE=CH
# ---- optional ---------------------------------------------------------------------
# EVENTS_WEBHOOK_URL=https://backend.example.com/vs-agent/events
# EVENTS_WEBHOOK_API_KEY=
# OID4VC_CONFIG_FILE_LOCATION=/config/openid4vc.json
AGENT_LOG_LEVEL=info
ADMIN_API_LOG_LEVEL=info
# ---- database ----------------------------------------------------------------------
POSTGRES_PASSWORD=change-me
2. Generate the agent's mnemonic
Any BIP-39 generator works; veranad keys add agent --keyring-backend test prints one and the matching address. Put the mnemonic in .env, store it in your secret manager, and never commit the file. Fund the address with a few test coins.
3. Start
docker compose up -d
docker compose logs -f vs-agent
The first start creates the DID and publishes the DID Document; later starts load it.
Verify
curl -s http://127.0.0.1:3000/v2/agent/health/ready # 200
curl -s http://127.0.0.1:3000/v2/agent/info | jq # did, version
curl -s https://agent.example.com/.well-known/did.json | jq .id # the same DID, from the internet
The DID Document must be reachable over HTTPS from outside the VM, otherwise nobody can resolve you.
Operations
- Upgrade: change
VS_AGENT_VERSIONin.env,docker compose pull && docker compose up -d. Read the release notes first; see Upgrade VS Agent. - Backup: the
postgres-dataandagent-datavolumes, plus.env. See Back up and restore. - Logs:
docker compose logs; levels throughAGENT_LOG_LEVELandADMIN_API_LOG_LEVEL.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Caddy cannot obtain a certificate | DNS does not point at the VM yet, or port 80 is closed | Fix DNS or the firewall, restart Caddy |
| Agent exits at start naming a variable | A required variable is missing or invalid | Compare with the .env.example |
| Agent exits saying the DID location differs | PUBLIC_API_BASE_URL changed after first start | Restore the original value |
Admin API answers 403 from your backend | The backend's address is outside ADMIN_API_TRUSTED_NETWORKS | Add its network, or switch to corporation mode |
Next
- Configuration walk-through, then the quickstart steps that make the agent trusted.
- Production hardening: Production checklist.