Production Deployment
For a long-running server (a VPS behind a reverse proxy), use the production Compose file that ships with Hrida Terminal: docker-compose-terminal-prod.yaml. It comes with health checks, log rotation, resource limits and hardened security defaults, and reads all per-server settings from a single .env file.
What you get
| Health check | Curls /health every 30 seconds; Compose reports the container as healthy |
| Log rotation | Container logs capped at 3 × 10 MB |
| Resource limits | 2 CPUs / 4 GB RAM by default, with soft reservations |
| Fork-bomb guard | Process count capped at 2048 (TERMINAL_PIDS_LIMIT) |
| Clean shutdown | 30-second grace period so terminal and notebook sessions exit cleanly |
| Hardening | no-new-privileges on by default (see multi-user mode) |
| Shared network | Joins the external hridaai-network, so it can reach the Hrida.ai stack and its Postgres |
1. Put the files on the server
sudo mkdir -p /opt/hrida-terminal && cd /opt/hrida-terminal
# copy docker-compose-terminal-prod.yaml and .env.example here
cp .env.example .env.envCompose only reads a file named exactly .env, in the folder you run docker compose from, to fill in ${VAR} values in the Compose file. A file called terminal.env or similar is silently ignored for these values.
2. Create the shared network
Skip this if the Hrida.ai stack already created it:
docker network create hridaai-network 2>/dev/null || true3. Fill in .env
Only set what differs on your server. Everything else falls back to the defaults in the Compose file.
HRIDA_TERMINAL_API_KEY=<openssl rand -base64 48>
HRIDA_TERMINAL_TAG=v1.0.0
HRIDA_TERMINAL_CORS_ALLOWED_ORIGINS=https://terminal.example.com
TERMINAL_BIND_ADDR=127.0.0.1
TERMINAL_HOST_PORT=8086
HRIDA_TERMINAL_INFO=Production terminal (shared sandbox)| Variable | What to set |
|---|---|
HRIDA_TERMINAL_API_KEY | A strong key. Leave blank to auto-generate one and read it from the logs |
HRIDA_TERMINAL_TAG | Pin a release instead of latest |
HRIDA_TERMINAL_CORS_ALLOWED_ORIGINS | Your real front-end origin(s), comma-separated |
TERMINAL_BIND_ADDR | 127.0.0.1 if your reverse proxy runs on the same host |
TERMINAL_HOST_PORT | Port published on the host (default 8086) |
HRIDA_TERMINAL_INFO | A short description of this environment, shown to the AI |
Lock the file down:
chmod 600 .envCheck that Compose actually reads it:
docker compose -f docker-compose-terminal-prod.yaml config | grep -E 'API_KEY|CORS|published'4. Start and verify
docker compose -f docker-compose-terminal-prod.yaml pull
docker compose -f docker-compose-terminal-prod.yaml up -d
docker compose -f docker-compose-terminal-prod.yaml ps # running + healthy
curl -fsS http://127.0.0.1:8086/health5. Reverse proxy
TLS ends at your proxy; the container speaks plain HTTP. Terminal and notebook sessions stream over WebSockets, so forward the upgrade headers and turn buffering off:
location / {
proxy_pass http://127.0.0.1:8086;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 3600s;
}If the proxy runs as a container, attach it to hridaai-network and route to hrida-terminal:8000 instead.
Multi-user mode in production
Multi-user mode creates a Linux account for each Hrida.ai user with sudo useradd. The production Compose file enables Docker's no-new-privileges hardening by default, and that blocks sudo. To use multi-user mode, turn it off in .env:
HRIDA_TERMINAL_MULTI_USER=true
HRIDA_TERMINAL_NO_NEW_PRIVILEGES=falseThen recreate the container. A restart is not enough, because security options are fixed when the container is created:
docker compose -f docker-compose-terminal-prod.yaml up -d --force-recreate
docker inspect hrida-terminal --format '{{.HostConfig.SecurityOpt}}'
# expected: [no-new-privileges:false]If multi-user mode is on and no-new-privileges is still active, Hrida Terminal refuses to start and logs:
RuntimeError: HRIDA_TERMINAL_MULTI_USER requires sudo, but the container runs
with no-new-privileges, which blocks sudo from elevating. ...
See Troubleshooting if the value doesn't take effect.
Resource limits
| Variable | Default | Purpose |
|---|---|---|
TERMINAL_CPU_LIMIT / TERMINAL_MEM_LIMIT | 2 / 4G | Hard ceilings |
TERMINAL_CPU_RESERVATION / TERMINAL_MEM_RESERVATION | 0.5 / 512M | Soft reservations |
TERMINAL_PIDS_LIMIT | 2048 | Max processes. Raise for many parallel sessions |
Shared Postgres access
To let code running in the terminal reach the Hrida.ai Postgres database, uncomment the DB_HOST, DB_PORT, DB_NAME, DB_USER and DB_PASS lines in the Compose file and set the values in .env. They are passed into terminal sessions as environment variables; the terminal server itself does not use them.
Updating
# change HRIDA_TERMINAL_TAG (or any value) in .env, then:
docker compose -f docker-compose-terminal-prod.yaml pull
docker compose -f docker-compose-terminal-prod.yaml up -dWhen you upgrade, copy the latest docker-compose-terminal-prod.yaml to the server as well, not just the image. New settings (such as HRIDA_TERMINAL_NO_NEW_PRIVILEGES) only work if the Compose file on the server references them.
Back up the hrida-home volume (session files and process logs) together with .env.
OpenShift
An OpenShift-compatible image is built from Dockerfile.openshift. It runs as an arbitrary non-root UID (group 0, as OpenShift requires) and has no sudo, so some features are not available:
| Feature | OpenShift image |
|---|---|
| Command execution, files, terminals, notebooks | ✔ |
HRIDA_TERMINAL_API_KEY_FILE (secrets) | ✔ |
| Multi-user mode | ✘ The container exits with an error if enabled |
Egress filtering (HRIDA_TERMINAL_ALLOWED_DOMAINS) | ✘ Ignored with a warning |
Startup package installs (HRIDA_TERMINAL_PACKAGES, _PIP_PACKAGES, _NPM_PACKAGES) | ✘ Ignored with a warning |
For per-user isolation on OpenShift, run one instance per user or team.
Troubleshooting
| Symptom | Fix |
|---|---|
Values in .env have no effect | The file isn't named exactly .env, or you ran Compose from another folder. Check with docker compose ... config. |
.env changed but the container didn't | Run up -d --force-recreate. docker restart and the restart: unless-stopped policy reuse the old settings. |
Multi-user: startup error about no-new-privileges | Set HRIDA_TERMINAL_NO_NEW_PRIVILEGES=false and recreate. If docker compose config still shows no-new-privileges:true, the Compose file on the server is an older copy with true hard-coded. Replace it with the current version, or change the line to - no-new-privileges:false. |
network hridaai-network ... could not be found | docker network create hridaai-network |
Container exits: both API_KEY and API_KEY_FILE are set | Set only one of them |
All requests return 401 | Wrong or missing bearer token. If auto-generated, read it from docker compose logs |
| Browser blocked by CORS | Add the exact front-end origin to HRIDA_TERMINAL_CORS_ALLOWED_ORIGINS |
| Terminal stream disconnects behind the proxy | Forward Upgrade/Connection, set proxy_buffering off and a long proxy_read_timeout |
Sessions fail with resource temporarily unavailable | Raise TERMINAL_PIDS_LIMIT |
Container OOMKilled | Raise TERMINAL_MEM_LIMIT |
env_file / required parse error | Upgrade to Docker Compose v2.24 or newer |