Deploy Eneo
Deploy Eneo in production with Docker Compose. This page is the primary deployment guide: it takes you from prerequisites to a verified installation, then links to focused guides for storage, authentication, and AI providers.
This is an example configuration. You MUST customize all values before
deployment. Search for your-domain.com, changeme, and CHANGEME and
replace with your actual values. Copy-paste without modification will NOT
work.
Start here
| Your task | Go to |
|---|---|
| Install a new production instance | Quick start |
| Upgrade an existing instance | Updating Eneo |
| Choose PostgreSQL or object storage | Choose Content Storage |
| Configure OIDC or SSO | Authentication & OIDC |
| Configure model providers | AI Provider Configuration |
| Diagnose a failed deployment | Troubleshooting |
You do not need S3-compatible storage to start Eneo. The default deployment keeps bounded durable content in PostgreSQL. Add SeaweedFS, MinIO, or another compatible endpoint only when your capacity and recovery requirements call for it.
Architecture overview
Eneo uses a small service stack. Durable content defaults to bounded PostgreSQL-inline bytes; an optional private object-store byte plane can be enabled when needed.
| Service | Technology | Port | Role |
|---|---|---|---|
| Frontend | SvelteKit | 3000 | Web interface, SSR |
| Backend | FastAPI (Python) | 8000 | API server, authentication |
| Worker | ARQ | - | Background processing, document indexing |
| Database | PostgreSQL 16 + pgvector | 5432 | Data storage, vector search |
| Redis | Redis 7 | 6379 | Cache, task queue |
| Object content | S3-compatible endpoint | 8333 private | Durable original and derived bytes |
| db-init | Python | - | One-time database initialization |
Traefik is provided as an example reverse proxy, not a requirement. You can use nginx, HAProxy, Caddy, or any reverse proxy that supports the routing requirements below.
Prerequisites
Before deploying, ensure you have:
- Docker and Docker Compose v2+ installed
- Domain name with DNS pointing to your server
- SSL certificate (Let’s Encrypt via Traefik, or your own PKI/CA)
- At least one AI provider API key (see AI Provider Configuration)
System Requirements
| Requirement | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4GB | 8GB+ |
| Disk | 20GB | 50GB+ |
| Network | Ports 80, 443 open | - |
Quick start
Before running docker compose up:
- replace every
your-domain.com,changeme, andCHANGEME; - generate the database password,
JWT_SECRET, andURL_SIGNING_KEY; - set
PUBLIC_ORIGINin the backend and frontend files; - configure at least one AI provider;
- create the
proxy_tiernetwork; - pin released Eneo images instead of mutable
latesttags; - leave the optional object-store block commented unless you have chosen and prepared that storage path.
Get deployment files
# Option A: Clone the repository
git clone https://github.com/eneo-ai/eneo.git
cd eneo/docs/deployment/
# Option B: Download files directly
mkdir eneo-deployment && cd eneo-deployment
curl -O https://raw.githubusercontent.com/eneo-ai/eneo/main/docs/deployment/docker-compose.yml
curl -O https://raw.githubusercontent.com/eneo-ai/eneo/main/docs/deployment/docker-compose.object-content.yml
curl -O https://raw.githubusercontent.com/eneo-ai/eneo/main/docs/deployment/env_backend.template
curl -O https://raw.githubusercontent.com/eneo-ai/eneo/main/docs/deployment/env_frontend.template
curl -O https://raw.githubusercontent.com/eneo-ai/eneo/main/docs/deployment/env_db.template
curl -O https://raw.githubusercontent.com/eneo-ai/eneo/main/docs/deployment/.env.templateCreate environment files
cp env_backend.template env_backend.env
cp env_frontend.template env_frontend.env
cp env_db.template env_db.env
cp .env.template .env
chmod 600 .env env_backend.env env_frontend.env env_db.envConfigure required values
Generate secrets and update environment files:
# Generate JWT_SECRET (MUST match in backend AND frontend)
JWT_SECRET=$(openssl rand -hex 32)
echo "JWT_SECRET=$JWT_SECRET"
# Generate URL_SIGNING_KEY
echo "URL_SIGNING_KEY=$(openssl rand -hex 32)"
# Generate database password
echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)"Edit env_backend.env with your values:
JWT_SECRET- paste the generated valueURL_SIGNING_KEY- paste the generated valuePUBLIC_ORIGIN- your external URL (e.g.,https://eneo.example.com)- At least one AI provider key (e.g.,
OPENAI_API_KEY)
Edit env_frontend.env:
JWT_SECRET- MUST match backend valueENEO_BACKEND_URL- backend URL for SSR (e.g.,http://backend:8000)PUBLIC_ENEO_BACKEND_URL- external API URL (e.g.,https://eneo.example.com/api)PUBLIC_ORIGIN- your external URL (e.g.,https://eneo.example.com)ORIGIN- same as PUBLIC_ORIGIN
Edit docker-compose.yml:
- Replace
your-domain.comwith your actual domain (4 locations) - Replace
your-email@domain.comwith your email for Let’s Encrypt
The default .env needs no object-store values. Eneo uses bounded
PostgreSQL-inline content. To enable the optional SeaweedFS profile or an
external compatible endpoint, follow
Object Content Storage before deployment.
Create Docker network and deploy
# Create external network for Traefik
docker network create proxy_tier
# Start all services
docker compose up -d
# Check status
docker compose ps
# View logs
docker compose logs -fVerify and login
Navigate to https://your-domain.com
Default credentials: user@example.com / Password1!
Change this password immediately after first login.
Network Isolation
The Docker Compose stack uses four networks:
| Network | Services | Purpose |
|---|---|---|
proxy_tier (external) | Traefik, frontend, backend, worker | Ingress and outbound access for APIs, OIDC, and crawls |
data_net (internal: true) | db, redis, backend, worker, db-init | Data layer without internet egress, isolated from Traefik and frontend |
module_net | Traefik, backend | Reserved for optional module containers |
object_content_net (internal: true) | optional object-content, backend, worker | Private S3-compatible byte plane when enabled; never routed publicly |
PostgreSQL and Redis are only attached to data_net. They are not reachable from frontend or Traefik containers, and they have no outbound internet access.
Existing installations: Older deployment files put every service on
proxy_tier. Running docker compose up -d with the current template
recreates containers with the new network membership while keeping PostgreSQL
and Redis volumes intact. External backup jobs or admin tools that connected
to db:5432 or redis:6379 over proxy_tier must run through docker exec
or attach to eneo_data_net.
Verify the isolation after upgrading:
# Should fail: frontend can no longer resolve the database
docker exec eneo_frontend getent hosts db
# Should succeed
docker exec eneo_backend python -c "import socket; socket.getaddrinfo('db', 5432)"
curl -fsS https://your-domain.com/versionEnvironment Configuration
Backend
Backend Environment Variables
Required:
| Variable | Description | Example |
|---|---|---|
JWT_SECRET | Token signing key (32+ chars). Generate: openssl rand -hex 32 | a1b2c3d4... |
URL_SIGNING_KEY | URL signing key. Generate: openssl rand -hex 32 | e5f6g7h8... |
PUBLIC_ORIGIN | External URL for callbacks | https://eneo.example.com |
DATABASE_URL | PostgreSQL connection string | postgresql://eneo:password@db:5432/eneo |
REDIS_URL | Redis connection string | redis://redis:6379/0 |
Default User Setup:
| Variable | Default | Description |
|---|---|---|
DEFAULT_TENANT_NAME | - | Name for initial tenant |
DEFAULT_USER_EMAIL | - | Email for initial admin user |
DEFAULT_USER_PASSWORD | - | Password for initial admin user |
USING_ACCESS_MANAGEMENT | false | Enable Users page in Admin panel |
OIDC Authentication (Enterprise SSO):
Glossary: IdP = Identity Provider (Entra ID, MobilityGuard, Auth0). OIDC = OpenID Connect protocol.
Choose one mode:
- Single-Tenant via env vars (
FEDERATION_PER_TENANT_ENABLED=false): Configure OIDC in frontend env. Requires restart to change settings. Simplest for single-organization deployments. - Federation via API (
FEDERATION_PER_TENANT_ENABLED=true): Configure IdP via sysadmin API. Changes take effect immediately (no restart). Use for multi-tenant deployments OR single-tenant with dynamic configuration.
Multi-Tenant Federation (API-managed OIDC):
| Variable | Default | Description |
|---|---|---|
FEDERATION_PER_TENANT_ENABLED | false | Enable per-tenant IdP configuration via API |
ENCRYPTION_KEY | - | 44-char base64 Fernet key (required for credentials & federation) |
ENEO_SUPER_API_KEY | - | Super admin API access |
Generating ENCRYPTION_KEY:
ENCRYPTION_KEY is required when enabling TENANT_CREDENTIALS_ENABLED=true or FEDERATION_PER_TENANT_ENABLED=true.
# Development (with uv)
uv run python -m eneo.cli.generate_encryption_key
# Production (Docker)
docker compose run --rm backend python -m eneo.cli.generate_encryption_key
# Alternative (Python)
python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'Backup your ENCRYPTION_KEY securely. Without it, encrypted credentials and federation secrets cannot be decrypted. If lost, you must re-enter all tenant API keys and reconfigure all IdP settings.
Upload Limits:
| Variable | Default | Description |
|---|---|---|
UPLOAD_MAX_FILE_SIZE | 10485760 | Max file size in bytes (10MB) |
MAX_IN_QUESTION | 1 | Max files per chat message |
TRANSCRIPTION_MAX_FILE_SIZE | 10485760 | Max audio file size (10MB) |
Can’t upload multiple files? Increase MAX_IN_QUESTION in backend env.
Crawler Settings:
| Variable | Default | Description |
|---|---|---|
CRAWL_FEEDER_ENABLED | true | Enable optimized crawling (keep true) |
CRAWL_MAX_LENGTH | 36000 | Max crawl duration in seconds (~10 hours) |
CLOSESPIDER_ITEMCOUNT | 20000 | Max pages per crawl |
Background Worker:
| Variable | Default | Description |
|---|---|---|
WORKER_MAX_JOBS | 15 | Max concurrent background jobs (should be ≤60% of DB pool size) |
TENANT_WORKER_CONCURRENCY_LIMIT | 4 | Max concurrent jobs per tenant (prevents one tenant monopolizing workers) |
High CPU usage on worker? Reduce WORKER_MAX_JOBS to limit concurrent document processing.
Dynamic Credential Management:
| Variable | Default | Description |
|---|---|---|
TENANT_CREDENTIALS_ENABLED | false | Enable API-based credential management (Fernet encrypted) |
Recommended even for single-tenant deployments. When TENANT_CREDENTIALS_ENABLED=true, you can add, update, or rotate AI provider API keys via the Credentials API without restarting the backend. Credentials are encrypted with Fernet (AES-128-CBC + HMAC). Requires ENCRYPTION_KEY to be set.
Integrations:
Setting up SharePoint? SharePoint integration requires SHAREPOINT_WEBHOOK_CLIENT_STATE for webhook validation. See the SharePoint Integration Guide for complete setup instructions including Azure AD app registration.
Object Content Storage
When object-store rows exist, PostgreSQL and that endpoint form one recovery unit. Inline-only bytes are already part of PostgreSQL backups.
The reference stack defaults to bounded PostgreSQL-inline content. An optional Compose profile runs private, Eneo-built SeaweedFS. Deployments that need an existing storage platform or multi-node availability can instead use another compatible endpoint, including MinIO. Both object-store paths use the same contract and fail closed when configuration or the database/bucket binding does not match.
Follow Object Content Storage for the image digest, credentials, MinIO policy, TLS, readiness, backup, and troubleshooting steps. See Object Content Architecture for the control-plane boundary, lifecycle, reconciliation, and image supply chain.
Reverse Proxy Requirements
Any reverse proxy (Traefik, nginx, HAProxy, Caddy) must meet these requirements:
Routing:
| Path | Target | Description |
|---|---|---|
/* | Frontend (port 3000) | All requests except API |
/api/* | Backend (port 8000) | API endpoints |
/docs | Backend (port 8000) | OpenAPI documentation |
/openapi.json | Backend (port 8000) | OpenAPI spec |
/version | Backend (port 8000) | Version endpoint |
Headers:
X-Forwarded-Proto- original protocol (https)X-Forwarded-For- client IP addressHost- original host header
Requirements:
- WebSocket support (for real-time features)
- TLS termination with valid certificate
Using nginx Instead of Traefik
upstream frontend {
server frontend:3000;
}
upstream backend {
server backend:8000;
}
server {
listen 443 ssl http2;
server_name eneo.example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# API routes to backend
location /api/ {
proxy_pass http://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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /docs {
proxy_pass http://backend;
proxy_set_header Host $host;
}
location /openapi.json {
proxy_pass http://backend;
}
location /version {
proxy_pass http://backend;
}
# Everything else to frontend
location / {
proxy_pass http://frontend;
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;
}
}API Key Hierarchy
Eneo uses a three-tier API key system for different administrative tasks.
| Key | Environment Variable | Purpose |
|---|---|---|
| Super API Key | ENEO_SUPER_API_KEY | Sysadmin operations: tenants, users, credentials, crawler settings |
| Super Duper API Key | ENEO_SUPER_DUPER_API_KEY | Module management: assigning modules to tenants |
| User API Key | Generated per user | Admin operations within a tenant via /api/v1/admin/ |
Service Account Pattern: We recommend creating a dedicated admin account (service account) for automated user provisioning via the admin API endpoints, rather than using personal accounts.
Credentials API
Manage AI provider credentials per tenant using the sysadmin API.
Prerequisite: Set TENANT_CREDENTIALS_ENABLED=true and ENCRYPTION_KEY
in backend env before using these endpoints. See Per-Tenant
Credentials in the Backend configuration.
Set Credentials
curl -X PUT "https://eneo.example.com/api/v1/sysadmin/tenants/{tenant_id}/credentials/{provider}" \
-H "X-API-Key: your-super-api-key" \
-H "Content-Type: application/json" \
-d '{"api_key": "sk-..."}'Supported providers: openai, anthropic, azure, google, mistral, vllm
Credentials are encrypted with Fernet (AES-128-CBC + HMAC). Backup your
ENCRYPTION_KEY - if lost, all credentials must be re-entered.
Crawler Settings API
Configure crawling behavior per tenant.
Set Settings
curl -X PUT "https://eneo.example.com/api/v1/sysadmin/tenants/{tenant_id}/crawler-settings" \
-H "X-API-Key: your-super-api-key" \
-H "Content-Type: application/json" \
-d '{"crawl_feeder_enabled": true, "closespider_itemcount": 20000}'Enabling Features
After deployment, some features require additional configuration in the Admin panel.
Crawler & Document Upload
To use the web crawler or upload documents for processing, the worker service must be running and at least one embedding model must be enabled in Admin → Models → Embeddings.
Apps (Voice/Audio Features)
Can’t create Apps? This feature requires a transcription model. Go to Admin → Models → Transcription tab and enable a model like Whisper.
Sysadmin API Access
To use system administration endpoints (tenants, credentials, crawler settings), set the API key in env_backend.env:
ENEO_SUPER_API_KEY=your-secure-api-keyModules API Access
To manage module assignments to tenants, set a separate higher-privileged API key:
ENEO_SUPER_DUPER_API_KEY=your-other-secure-api-keyTroubleshooting
Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| Can’t login with default credentials | DEFAULT_* vars commented or db-init didn’t run | Uncomment vars in env_backend.env, check docker logs eneo_db_init |
| Can’t see Users page in Admin | USING_ACCESS_MANAGEMENT=false | Set to true, restart backend |
| Frontend shows wrong API URL | PUBLIC_ENEO_BACKEND_URL incorrect | Set to external URL (not localhost in prod) |
| Can’t upload multiple files | MAX_IN_QUESTION=1 | Increase value in backend env |
| Large file uploads fail | UPLOAD_MAX_FILE_SIZE too low | Increase value (default 10MB = 10485760 bytes) |
| “Failed to decrypt credential” | ENCRYPTION_KEY missing or changed | Set correct key or re-enter credentials |
| Credentials API returns 404/error | TENANT_CREDENTIALS_ENABLED=false | Set to true, requires ENCRYPTION_KEY |
| OIDC redirect fails | PUBLIC_ORIGIN mismatch | Must match in backend, frontend, AND IdP config |
| Crawls not processing | CRAWL_FEEDER_ENABLED=false | Set to true |
| Crawls/uploads not working | Worker not running or no embedding model | Check worker is running, enable embedding model in Admin → Models |
| Can’t create Apps | No transcription model enabled | Admin → Models → Transcription tab, enable Whisper |
| JWT token invalid | JWT_SECRET mismatch | Must be identical in backend AND frontend |
| Worker jobs stuck or slow | Worker overloaded | Reduce WORKER_MAX_JOBS or increase resources |
ECONNREFUSED 127.0.0.1:8000 | Frontend using localhost instead of Docker service | Set ENEO_BACKEND_SERVER_URL=http://backend:8000 (use service name, not localhost) |
| HTTP to HTTPS redirect broken | Missing Traefik label | Ensure traefik.enable=true label on traefik service |
| 502 Bad Gateway | Domain not replaced in docker-compose.yml | Replace your-domain.com in all 4 locations |
Debug Mode
Enable detailed logging for troubleshooting:
# In env_backend.env
LOGLEVEL=DEBUGThen restart and view logs:
docker compose restart backend
docker compose logs -f backendService Health Checks
# Check all services are running
docker compose ps
# Test backend API
curl https://your-domain.com/api/health
# Check database
docker compose exec db psql -U eneo -c "SELECT version()"
# Check Redis
docker compose exec redis redis-cli ping
# View db-init logs (for first-run issues)
docker logs eneo_db_initContainer Logs
# All services
docker compose logs -f
# Specific service
docker compose logs -f backend
# Last 100 lines
docker compose logs --tail=100 backend workerMaintenance
Version Pinning (Recommended)
Production deployments should pin to specific version tags instead of
using latest. This gives you control over when updates are applied and makes
rollbacks straightforward.
The example docker-compose.yml uses :latest tags for simplicity, but for production you should pin versions:
# Default (not recommended for production)
frontend:
image: ghcr.io/eneo-ai/eneo-frontend:latest
# Production (pin to specific version)
frontend:
image: ghcr.io/eneo-ai/eneo-frontend:v1.2.3
backend:
image: ghcr.io/eneo-ai/eneo-backend:v1.2.3
worker:
image: ghcr.io/eneo-ai/eneo-backend:v1.2.3Find available versions:
- GitHub Releases: https://github.com/eneo-ai/eneo/releases
- Container Registry: https://github.com/orgs/eneo-ai/packages
Always run the latest released version to get bug fixes and security patches. Check the release notes before upgrading.
Backups
Take one paired recovery point when object-store rows exist. Inline-only control and bytes are both included in the PostgreSQL backup.
Database backup with pg_dump (recommended):
# Create timestamped database backup
docker compose exec -T db pg_dump -U eneo eneo | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
# Restore from backup (if needed)
gunzip -c backup_20250108.sql.gz | docker compose exec -T db psql -U eneo eneoVolume backup (for complete recovery):
# Stop services first
docker compose down
# Backup PostgreSQL data volume
docker run --rm -v eneo_eneo_postgres_data:/data -v $(pwd):/backup ubuntu \
tar czf /backup/postgres_volume_$(date +%Y%m%d).tar.gz /data
# Backup Redis data volume (optional)
docker run --rm -v eneo_eneo_redis_data:/data -v $(pwd):/backup ubuntu \
tar czf /backup/redis_volume_$(date +%Y%m%d).tar.gz /dataIf object-store rows exist, also archive eneo_eneo_object_content_data while
backend, worker, and the object-content service are stopped, or take the
matching versioned snapshot in your external provider. Follow the
object-content backup and restore
procedure, including
checksum and skew-recovery checks.
Updating Eneo
Back up your data
# Database backup (required)
docker compose exec -T db pg_dump -U eneo eneo | gzip > backup_$(date +%Y%m%d).sql.gzIf object-store rows exist, take the matching endpoint snapshot before resuming writes. Inline-only content needs no second backup artifact.
Update version tags (if pinned)
Edit docker-compose.yml and update version tags (e.g., v1.2.3 → v1.2.4).
Review network changes
If your current deployment predates the isolated-network template, recreating
the containers changes their network membership. External backup jobs or admin
tools that connected to db:5432 or redis:6379 over proxy_tier must run
through docker exec or attach to eneo_data_net.
Before the first recreate, choose the Compose invocation that matches the
deployment and keep it in both the deployment and rollback runbooks. Use plain
docker compose for PostgreSQL-inline content or an external S3-compatible
endpoint. If the retained .env points to
http://object-content:8333, use the bundled-store overlay and profile on
every command.
Pull and deploy
For PostgreSQL-inline content or an external endpoint:
docker compose pull
docker compose up -d
docker compose psFor the bundled store:
docker compose \
--profile object-content \
-f docker-compose.yml \
-f docker-compose.object-content.yml \
config --quiet
docker compose \
--profile object-content \
-f docker-compose.yml \
-f docker-compose.object-content.yml \
pull
docker compose \
--profile object-content \
-f docker-compose.yml \
-f docker-compose.object-content.yml \
up -d
docker compose \
--profile object-content \
-f docker-compose.yml \
-f docker-compose.object-content.yml \
psWhen any object-store endpoint is configured, verify its explicit readiness code. HTTP 200 alone can also represent an intentionally degraded object store while inline content remains available:
curl -fsS https://eneo.example.eu/api/readyz \
| jq -e '.detail.object_content.code == "ready"'Clean up old images
docker image prune -aRollback: Revert pinned version tags, stop writes, and restore the paired
backup. PostgreSQL-inline and external-endpoint deployments use plain
docker compose. A bundled-store deployment must use the overlay and profile
for stop, restore, and up; otherwise the store and its private network are
omitted.
docker compose \
--profile object-content \
-f docker-compose.yml \
-f docker-compose.object-content.yml \
stop backend worker object-content
# Restore the matching PostgreSQL dump and object-store snapshot as described
# in the linked procedure, then start the pinned versions:
docker compose \
--profile object-content \
-f docker-compose.yml \
-f docker-compose.object-content.yml \
up -d
curl -fsS https://eneo.example.eu/api/readyz \
| jq -e '.detail.object_content.code == "ready"'Restore the matching object-store snapshot before reopening traffic. See Back up and restore for skew recovery.
Scaling
# Scale backend instances (with load balancer)
docker compose up -d --scale backend=3 --scale worker=2Related Documentation
- AI Provider Configuration - Configure AI models and providers
- Multi-Tenant OIDC Federation - Enterprise SSO setup
- Single-Tenant OIDC - Simple SSO setup
- SharePoint Integration - Connect SharePoint and OneDrive
- Authentication Architecture - Technical authentication details
- System Architecture - Complete architecture overview
- Object Content Storage - Configure SeaweedFS, MinIO, backups, and readiness
- Release SBOMs and attestations - Verify shipped image digests and provenance