Skip to Content
GuidesDeploy Eneo

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 taskGo to
Install a new production instanceQuick start
Upgrade an existing instanceUpdating Eneo
Choose PostgreSQL or object storageChoose Content Storage
Configure OIDC or SSOAuthentication & OIDC
Configure model providersAI Provider Configuration
Diagnose a failed deploymentTroubleshooting

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.

Eneo System Architecture showing Client Layer, Reverse Proxy (Traefik), Frontend (SvelteKit), Backend API (FastAPI), Database (PostgreSQL with pgvector), Redis Cache/Queue, Worker (ARQ), and AI Providers

ServiceTechnologyPortRole
FrontendSvelteKit3000Web interface, SSR
BackendFastAPI (Python)8000API server, authentication
WorkerARQ-Background processing, document indexing
DatabasePostgreSQL 16 + pgvector5432Data storage, vector search
RedisRedis 76379Cache, task queue
Object contentS3-compatible endpoint8333 privateDurable original and derived bytes
db-initPython-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

RequirementMinimumRecommended
CPU2 cores4+ cores
RAM4GB8GB+
Disk20GB50GB+
NetworkPorts 80, 443 open-

Quick start

Before running docker compose up:

  • replace every your-domain.com, changeme, and CHANGEME;
  • generate the database password, JWT_SECRET, and URL_SIGNING_KEY;
  • set PUBLIC_ORIGIN in the backend and frontend files;
  • configure at least one AI provider;
  • create the proxy_tier network;
  • pin released Eneo images instead of mutable latest tags;
  • 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.template

Create 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.env

Configure 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 value
  • URL_SIGNING_KEY - paste the generated value
  • PUBLIC_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 value
  • ENEO_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.com with your actual domain (4 locations)
  • Replace your-email@domain.com with 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 -f

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

NetworkServicesPurpose
proxy_tier (external)Traefik, frontend, backend, workerIngress and outbound access for APIs, OIDC, and crawls
data_net (internal: true)db, redis, backend, worker, db-initData layer without internet egress, isolated from Traefik and frontend
module_netTraefik, backendReserved for optional module containers
object_content_net (internal: true)optional object-content, backend, workerPrivate 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/version

Environment Configuration

Backend Environment Variables

Required:

VariableDescriptionExample
JWT_SECRETToken signing key (32+ chars). Generate: openssl rand -hex 32a1b2c3d4...
URL_SIGNING_KEYURL signing key. Generate: openssl rand -hex 32e5f6g7h8...
PUBLIC_ORIGINExternal URL for callbackshttps://eneo.example.com
DATABASE_URLPostgreSQL connection stringpostgresql://eneo:password@db:5432/eneo
REDIS_URLRedis connection stringredis://redis:6379/0

Default User Setup:

VariableDefaultDescription
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_MANAGEMENTfalseEnable 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):

VariableDefaultDescription
FEDERATION_PER_TENANT_ENABLEDfalseEnable 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:

VariableDefaultDescription
UPLOAD_MAX_FILE_SIZE10485760Max file size in bytes (10MB)
MAX_IN_QUESTION1Max files per chat message
TRANSCRIPTION_MAX_FILE_SIZE10485760Max audio file size (10MB)

Can’t upload multiple files? Increase MAX_IN_QUESTION in backend env.

Crawler Settings:

VariableDefaultDescription
CRAWL_FEEDER_ENABLEDtrueEnable optimized crawling (keep true)
CRAWL_MAX_LENGTH36000Max crawl duration in seconds (~10 hours)
CLOSESPIDER_ITEMCOUNT20000Max pages per crawl

Background Worker:

VariableDefaultDescription
WORKER_MAX_JOBS15Max concurrent background jobs (should be ≤60% of DB pool size)
TENANT_WORKER_CONCURRENCY_LIMIT4Max 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:

VariableDefaultDescription
TENANT_CREDENTIALS_ENABLEDfalseEnable 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:

PathTargetDescription
/*Frontend (port 3000)All requests except API
/api/*Backend (port 8000)API endpoints
/docsBackend (port 8000)OpenAPI documentation
/openapi.jsonBackend (port 8000)OpenAPI spec
/versionBackend (port 8000)Version endpoint

Headers:

  • X-Forwarded-Proto - original protocol (https)
  • X-Forwarded-For - client IP address
  • Host - 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.

KeyEnvironment VariablePurpose
Super API KeyENEO_SUPER_API_KEYSysadmin operations: tenants, users, credentials, crawler settings
Super Duper API KeyENEO_SUPER_DUPER_API_KEYModule management: assigning modules to tenants
User API KeyGenerated per userAdmin 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.

LiteLLM Architecture showing credential flow from Sysadmin API to encrypted storage to AI providers

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.

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

Modules API Access

To manage module assignments to tenants, set a separate higher-privileged API key:

ENEO_SUPER_DUPER_API_KEY=your-other-secure-api-key

Troubleshooting

Common Issues

SymptomLikely CauseFix
Can’t login with default credentialsDEFAULT_* vars commented or db-init didn’t runUncomment vars in env_backend.env, check docker logs eneo_db_init
Can’t see Users page in AdminUSING_ACCESS_MANAGEMENT=falseSet to true, restart backend
Frontend shows wrong API URLPUBLIC_ENEO_BACKEND_URL incorrectSet to external URL (not localhost in prod)
Can’t upload multiple filesMAX_IN_QUESTION=1Increase value in backend env
Large file uploads failUPLOAD_MAX_FILE_SIZE too lowIncrease value (default 10MB = 10485760 bytes)
“Failed to decrypt credential”ENCRYPTION_KEY missing or changedSet correct key or re-enter credentials
Credentials API returns 404/errorTENANT_CREDENTIALS_ENABLED=falseSet to true, requires ENCRYPTION_KEY
OIDC redirect failsPUBLIC_ORIGIN mismatchMust match in backend, frontend, AND IdP config
Crawls not processingCRAWL_FEEDER_ENABLED=falseSet to true
Crawls/uploads not workingWorker not running or no embedding modelCheck worker is running, enable embedding model in Admin → Models
Can’t create AppsNo transcription model enabledAdmin → Models → Transcription tab, enable Whisper
JWT token invalidJWT_SECRET mismatchMust be identical in backend AND frontend
Worker jobs stuck or slowWorker overloadedReduce WORKER_MAX_JOBS or increase resources
ECONNREFUSED 127.0.0.1:8000Frontend using localhost instead of Docker serviceSet ENEO_BACKEND_SERVER_URL=http://backend:8000 (use service name, not localhost)
HTTP to HTTPS redirect brokenMissing Traefik labelEnsure traefik.enable=true label on traefik service
502 Bad GatewayDomain not replaced in docker-compose.ymlReplace your-domain.com in all 4 locations

Debug Mode

Enable detailed logging for troubleshooting:

# In env_backend.env LOGLEVEL=DEBUG

Then restart and view logs:

docker compose restart backend docker compose logs -f backend

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

Container Logs

# All services docker compose logs -f # Specific service docker compose logs -f backend # Last 100 lines docker compose logs --tail=100 backend worker

Maintenance

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

Find available versions:

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 eneo

Volume 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 /data

If 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.gz

If 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.3v1.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 ps

For 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 \ ps

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

Rollback: 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=2

Last updated on