Single-tenant mode is the default deployment configuration. It serves one publisher with path-based routing – no subdomain resolution or wildcard DNS is required. This is the fastest way to get a production-ready Sales Agent running.
In this mode:
ADCP_MULTI_TENANT is false (or unset)https://adcp.yourcompany.com)/mcp, /a2a, /api/v1, /admin)Create a docker-compose.yml file with the following services:
version: "3.8"
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: adcp_sales
POSTGRES_USER: adcp
POSTGRES_PASSWORD: changeme
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U adcp -d adcp_sales"]
interval: 5s
timeout: 3s
retries: 5
db-init:
image: adcp-sales-agent:latest
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql+asyncpg://adcp:changeme@postgres:5432/adcp_sales
command: ["python", "-m", "alembic", "upgrade", "head"]
restart: "no"
salesagent:
image: adcp-sales-agent:latest
depends_on:
db-init:
condition: service_completed_successfully
ports:
- "8080:8080"
environment:
DATABASE_URL: postgresql+asyncpg://adcp:changeme@postgres:5432/adcp_sales
DATABASE_QUERY_TIMEOUT: "30"
DATABASE_CONNECT_TIMEOUT: "10"
ADCP_SALES_PORT: "8080"
ADCP_SALES_HOST: "0.0.0.0"
ENVIRONMENT: production
PRODUCTION: "true"
ADCP_MULTI_TENANT: "false"
ADCP_AUTH_TEST_MODE: "false"
CREATE_DEMO_TENANT: "true"
SKIP_MIGRATIONS: "true"
ENCRYPTION_KEY: "" # Auto-generated on first run; set explicitly for persistence
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
proxy:
image: nginx:alpine
depends_on:
salesagent:
condition: service_healthy
ports:
- "8000:8000"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
volumes:
pgdata:
CREATE_DEMO_TENANT=true on first run to automatically create a demo tenant with sample products. Set it to false after initial setup.
Create a corresponding .env file for sensitive values:
DATABASE_URL=postgresql+asyncpg://adcp:changeme@postgres:5432/adcp_sales
ENCRYPTION_KEY=your-fernet-key-here
ENVIRONMENT=production
docker compose up -d
Verify all services are healthy:
docker compose ps
curl http://localhost:8080/health
After the services are running, complete the following steps to configure your publisher instance.
Open http://localhost:8000/admin in your browser. If ADCP_AUTH_TEST_MODE=true, you can log in without OAuth credentials. For production, configure Google OAuth (see below).
If CREATE_DEMO_TENANT=true, a demo tenant is created automatically. Otherwise, create one through the Admin UI:
For production deployments, configure Google OAuth:
environment:
GAM_OAUTH_CLIENT_ID: "your-client-id.apps.googleusercontent.com"
GAM_OAUTH_CLIENT_SECRET: "your-client-secret"
Then configure SSO in the Admin UI under Settings > SSO.
For production, configure a custom domain with TLS using the nginx reverse proxy.
Create an nginx.conf that routes traffic to the Sales Agent:
events {
worker_connections 1024;
}
http {
upstream adcp_backend {
server salesagent:8080;
}
server {
listen 8000;
server_name adcp.yourcompany.com;
location / {
proxy_pass http://adcp_backend;
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;
}
# SSE support for activity stream
location /admin/activity/stream {
proxy_pass http://adcp_backend;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
}
}
}
For HTTPS, add a TLS server block:
server {
listen 443 ssl;
server_name adcp.yourcompany.com;
ssl_certificate /etc/letsencrypt/live/adcp.yourcompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/adcp.yourcompany.com/privkey.pem;
location / {
proxy_pass http://adcp_backend;
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 https;
}
}
server {
listen 80;
server_name adcp.yourcompany.com;
return 301 https://$server_name$request_uri;
}
Use pg_dump to back up the PostgreSQL database:
# One-time backup
docker compose exec postgres pg_dump -U adcp adcp_sales > backup_$(date +%Y%m%d).sql
# Restore from backup
docker compose exec -T postgres psql -U adcp adcp_sales < backup_20250101.sql
Add a cron job on the host to run daily backups:
# /etc/cron.d/adcp-backup
0 2 * * * root docker compose -f /path/to/docker-compose.yml exec -T postgres pg_dump -U adcp adcp_sales | gzip > /backups/adcp_$(date +\%Y\%m\%d).sql.gz
| Item | Method | Frequency |
|---|---|---|
| PostgreSQL database | pg_dump |
Daily |
.env file |
File copy | On change |
ENCRYPTION_KEY |
Secure vault | On change (critical – data is unrecoverable without it) |
| nginx configuration | File copy / version control | On change |
ENCRYPTION_KEY is essential for decrypting sensitive fields in the database. If you lose this key, encrypted data (API keys, OAuth credentials, webhook secrets) cannot be recovered. Store it in a secure vault or secrets manager.
To upgrade to a new version of the Sales Agent:
# Pull the latest image
docker compose pull
# Restart services (migrations run automatically via db-init)
docker compose up -d
The db-init service runs Alembic migrations automatically before the main application starts. There are 150+ migration files that bring the database schema up to date.
To verify the upgrade:
docker compose ps
curl http://localhost:8080/health
Check the logs for the failing service:
docker compose logs salesagent
docker compose logs db-init
Common causes:
postgres service is healthy before db-init runs. The depends_on condition handles this, but check docker compose logs postgres if issues persist.docker compose logs db-init for Alembic errors. A failed migration may require manual intervention.curl -v http://localhost:8080/health
If the health endpoint returns an error, the most likely cause is a database connectivity issue. Verify:
docker compose exec salesagent python -c "import asyncio; print('Python OK')"
docker compose exec postgres pg_isready -U adcp
docker compose logs proxydocker compose port proxy 8000GAM_OAUTH_CLIENT_ID and GAM_OAUTH_CLIENT_SECRET are setADCP_AUTH_TEST_MODE=true to bypass OAuthDATABASE_URL uses the correct hostname (postgres inside Docker network, localhost if accessing from host)DATABASE_CONNECT_TIMEOUT (default: 10 seconds)DATABASE_QUERY_TIMEOUT (default: 30 seconds)USE_PGBOUNCER=true