Skip to content

Self-hosting overview

This page is the reference for operational tasks around Vesana. If you host Vesana for others, or maintain an instance at a customer's site: here are the levers.

Directory structure

On the host itself, /opt/vesana/ only contains configuration files:

/opt/vesana/
├── docker-compose.prod.yml      # compose stack definition (owned by Vesana, replaced on updates)
├── docker-compose.override.yml  # optional: your own additions (kept on updates)
├── .env                         # secrets + configuration (plain environment variables, readable by root only)
├── .env.example                 # template listing all variables
└── secrets/                     # copy of the generated secrets (root only; only .env is effective)

Plus small installer helper files (e.g. .vesana-ac-autoinstall.sh for the active checker setup) and possibly admin-nginx/, which the split container wizard writes.

Your own networks, labels, resource limits or bind mounts never go into docker-compose.prod.yml, but into docker-compose.override.yml — see Customising the stack.

Everything else — SSL certificates, backups, agent/collector binaries, uploads, icon assets, avatars — lives in named Docker volumes, not as a directory under /opt/vesana. That has the advantage that a docker compose down (without -v) can never accidentally drag along data from a host directory.

To look inside a volume or copy something out, use a throwaway container:

# Example: list a volume's contents
docker run --rm -v vesana_backup-data:/data alpine ls -la /data

# Example: copy a file out of a volume
docker run --rm -v vesana_backup-data:/src -v $(pwd):/dst alpine \
  sh -c 'cp /src/some-file /dst/'

(Volume names usually carry the compose project prefix, e.g. vesana_backup-data — check with docker volume ls if your prefix differs.)

Optional compose profiles

Optional profiles turn on additional services:

Profile Service When
Default (no profile) postgres, redis, init, init-binaries, api, receiver, worker, worker-scheduler, nginx, updater Always
ai ollama AI features with local Ollama
agent-gateway agent-gateway A separate port serving only agent/collector endpoints (so the web UI can be locked down by firewall) — see Distribution & reachability
admin-split, admin-split-port api-admin, nginx-admin Own admin container — set by the split container wizard, never by hand

Backups are no longer a profile — the built-in DR backup system (see Backup & Disaster Recovery) always runs, without a separate sidecar.

Enable:

docker compose -f docker-compose.prod.yml --profile ai up -d

Or permanently via .env:

COMPOSE_PROFILES=ai,agent-gateway

Important environment variables

The .env is generated by the setup script. Important variables:

Required

Variable Meaning
BASE_URL Public URL including https://
POSTGRES_PASSWORD Generated
REDIS_PASSWORD Generated
SECRET_KEY Signing key for login tokens, generated
FIELD_ENCRYPTION_KEY AES-256-GCM key for field encryption, generated
UPDATER_SHARED_SECRET Secret header between API and updater, generated. Empty = older installs without this protection; retrofit by setting a random value (e.g. openssl rand -hex 32) and restarting the stack
VESANA_SETUP_TOKEN One-time code for the setup wizard, generated; meaningless once setup is complete

Optional / tuning

Variable Default Meaning
WORKER_REPLICAS 1 Number of consumer worker containers
WORKER_CONCURRENCY 4 Parallelism per worker
WORKER_BATCH_SIZE 100 Batch size per DB insert pass
DB_POOL_SIZE 10 Size of the DB connection pool
DB_MAX_OVERFLOW 20 Extra connections beyond the pool, as needed
PG_SHARED_BUFFERS by RAM, see below Postgres buffer
PG_WORK_MEM 16MB Sort/hash buffer per operation
PG_EFFECTIVE_CACHE by RAM, see below Postgres planner assumption (allocates no memory)
PG_MAX_CONNECTIONS 200 Connection limit
PG_SHM_SIZE by RAM, see below Upper limit for the Postgres container's /dev/shm (parallel queries)
PG_MAINTENANCE_WORK_MEM by RAM, see below Memory cap for vacuum/index maintenance — without this, the automatic Postgres tuning logic derives the value from the host's total RAM, which can cause OOM on small machines
REDIS_MAXMEMORY 512mb Redis memory limit (noeviction policy — Redis rejects new data instead of evicting old data)
HTTP_PORT / HTTPS_PORT 80 / 443 If other services occupy those ports
AGENT_GATEWAY_PORT 8443 Only relevant with the agent-gateway profile active
AI_ENABLED false Default for the AI features as long as nothing is saved under Admin → System → AI
LICENSE_KEY empty License key (takes effect without entering it again in the UI)
VESANA_VERSION latest Image tag; the updater writes the installed version here
COMPOSE_PROJECT_DIR /opt/vesana Install directory the updater updates (set by the installer with VESANA_DIR)
VESANA_PULSE on off switches off the daily instance pulse
VESANA_NGINX_KEEPALIVE empty (on) off = nginx opens a new connection to API and receiver per request (emergency switch)

Postgres memory by RAM

On a fresh install, setup.sh derives the Postgres values in the table from the server's RAM and writes them into the .env. The values are deliberately modest because Postgres shares the server with the API, worker, receiver, Redis and possibly Ollama:

RAM PG_SHARED_BUFFERS PG_EFFECTIVE_CACHE PG_MAINTENANCE_WORK_MEM PG_SHM_SIZE
up to 2 GB 256MB 512MB 256MB 512m
4 GB 512MB 2GB 256MB 512m
8 GB 1GB 4GB 256MB 512m
16 GB 2GB 8GB 512MB 512m
32 GB 4GB 16GB 512MB 1g
64 GB and more 8GB 32GB 1GB 1g

The boundaries sit between the usual sizes (below 3, 6, 12, 24 and 48 GB) because a server reports slightly less than its nominal RAM. PG_WORK_MEM and PG_MAX_CONNECTIONS stay at 16MB and 200 regardless of RAM — both multiply per query or per connection.

PG_SHM_SIZE is the upper limit for /dev/shm in the Postgres container (not a reservation). Parallel queries keep their shared memory there; Docker's default of 64 MB is not enough even for one parallel hash join over a few million rows — the query then aborts with could not resize shared memory segment … No space left on device. The value takes effect with the compose file from October 2026 on (shm_size on the postgres service); without an entry in the .env, 512m applies there.

  • Existing installations do not change. The installer never touches an existing .env, and an update does not change these values either.
  • The size choice in the setup wizard never lowers these values, it can only raise them. Under Resources → Performance, a profile applies its values exactly as shown.
  • Install without the derivation: set VESANA_PG_SIZING=default when running setup.sh — the .env.example values apply (256MB / 512MB / 256MB / 512m).
  • When to turn it back by hand: if the server also runs other memory-hungry services (for example a large Ollama model) and RAM gets tight. Adjust the lines in the .env and run docker compose up -d — Postgres restarts briefly.

The four sizing profiles (Small/Medium/Large/XL) under Resources → Performance set WORKER_REPLICAS, WORKER_CONCURRENCY, PG_SHARED_BUFFERS, PG_EFFECTIVE_CACHE, PG_WORK_MEM, PG_MAINTENANCE_WORK_MEM, PG_MAX_CONNECTIONS, DB_POOL_SIZE/DB_MAX_OVERFLOW, and REDIS_MAXMEMORY in one go — easier for most setups than tuning individual variables by hand.

Full list of available variables in the bundled .env.example. After changes: docker compose up -d to apply.

SSL / TLS

Default: automatic self-signed certificate

On first start, Vesana automatically generates a self-signed certificate — that way the setup wizard always runs over HTTPS, even with no domain or certificate of your own at all. Your browser will show a certificate warning for this until you install a real certificate.

Admin → System → Certificate issues and renews a public certificate on its own: Let's Encrypt or your own ACME CA, validated through a DNS provider (more than 200 providers, e.g. OVH, Cloudflare, IONOS, Hetzner, RFC2136 for Windows DNS) or via port 80. A purchased certificate of your own is uploaded on the same page. No certbot, no cron job, no change to the compose file — details: TLS / certificate.

Installing your own certificate by hand

If you want to keep certbot on the host: the certificate and key live inside the container at /etc/nginx/ssl/fullchain.pem and /etc/nginx/ssl/privkey.pem — that's a named Docker volume, not a host directory. Copy your own certificate in like this:

docker run --rm \
  -v vesana_ssl:/etc/nginx/ssl \
  -v /path/to/your/cert:/src:ro \
  alpine sh -c 'cp /src/fullchain.pem /src/privkey.pem /etc/nginx/ssl/'

docker compose -f /opt/vesana/docker-compose.prod.yml restart nginx

As long as valid files exist under those two names in the volume, Vesana won't overwrite them with a new self-signed certificate on restart — renewal is then something you repeat yourself via a cron job. Vesana recognises such a certificate, never silently overwrites it, and warns through the “TLS Certificate” check before it expires.

Behind your own reverse proxy

If the server already sits behind your own nginx / Traefik / HAProxy / Nginx Proxy Manager, terminate TLS there and proxy internally to the Vesana web server container. Details on reverse-proxy specifics (SSE buffering, WebSocket upgrade): Distribution & reachability. How to attach the web server to your proxy's Docker network (also across several hosts via an overlay network) without an update undoing it, and what applies to real client addresses and the agent gateway: Customising the stack.

Updates & backups

  • Updates — GUI updater, online + offline + emergency CLI, no auto-rollback
  • Backup & restore — built-in DR system, multiple schedules, disaster drill

Next