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:
Or permanently via .env:
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=defaultwhen runningsetup.sh— the.env.examplevalues 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
.envand rundocker 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.
Certificate straight from Vesana (recommended)¶
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¶
- Resources — storage, RAM, performance — sizing profiles in detail
- Distribution & reachability
- Security — hardening checklist