Skip to content

Deployment

Deploy ZERG to production with Erlang/OTP 28, nginx TLS termination, and PostgreSQL. This guide covers single-node deployment, Docker Compose, and the production checklist.

Services

ServiceTechnologyPortAccess
zerg-solErlang/OTP 2821434localhost only
zerg-mangoPython/Tornado5800localhost only
nginxTLS reverse proxy80/443public
postgresqlDatabase5432localhost only

Prerequisites

bash
apt-get install -y postgresql postgresql-contrib nginx certbot python3-pip
pip3 install --break-system-packages tornado bcrypt aiosqlite

Install Erlang 28 via asdf:

bash
mkdir -p ~/.asdf/bin
curl -sL https://github.com/asdf-vm/asdf/releases/download/v0.16.7/asdf-v0.16.7-linux-amd64.tar.gz \
  | tar xz -C ~/.asdf/bin/
export ASDF_DIR=~/.asdf
export PATH=~/.asdf/bin:$PATH
asdf plugin add erlang
asdf install erlang 28.0.2
asdf set -u erlang 28.0.2

Step-by-Step Deployment

1. PostgreSQL Setup

bash
sudo -u postgres psql -c "CREATE USER zerg WITH PASSWORD 'CHANGE_ME';"
sudo -u postgres psql -c "CREATE DATABASE zerg OWNER zerg;"
sudo -u postgres psql -c "CREATE DATABASE zerg_auth OWNER zerg;"
sudo -u postgres psql -c "ALTER USER zerg CREATEDB;"

2. Build Sol Server

bash
PATH=~/.asdf/installs/erlang/28.0.2/bin:$PATH rebar3 as prod release

3. Deploy Release

bash
mkdir -p /opt/zerg/{bin,etc,log,data,luna,limon,mango,plugins}
cp -a _build/prod/rel/sol/* /opt/zerg/
cp sys.config /opt/zerg/releases/{VERSION}/sys.config
cp vm.args /opt/zerg/releases/{VERSION}/vm.args

4. Deploy Luna Agent

bash
cp client/build/luna /opt/zerg/luna/luna
chmod +x /opt/zerg/luna/luna

5. Deploy Mango Auth

bash
cp -a mango/* /opt/zerg/mango/

6. Deploy Limon Dashboard

bash
cp -a limon/dist/* /opt/zerg/limon/

7. Create Admin User

python
import bcrypt, json, uuid, sqlite3, time
db = sqlite3.connect('/opt/zerg/mango/data/mango.db')
pw_hash = bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode()
user_uuid = str(uuid.uuid4())
db.execute(
    'INSERT INTO users (uuid, account, password, email, role, data, created_at) VALUES (?,?,?,?,?,?,?)',
    (user_uuid, 'admin', pw_hash, 'admin@example.com', 'admin',
     json.dumps({"account":"admin","email":"admin@example.com","role":"admin","nickname":"Admin"}), time.time()))
db.commit()
db.close()

8. Bootstrap Admin RBAC

Mango's setup_db() auto-seeds admin RBAC on startup. Restart Mango:

bash
systemctl restart zerg-mango

Or run manually:

bash
cd /opt/zerg/mango
python3 scripts/bootstrap_admin.py --auto

The RBAC chain is: admin user -> system-admin team -> admin role -> permissions: ["*"]

9. SSL Certificates

bash
certbot certonly --webroot -w /var/www/html -d YOUR_DOMAIN -d www.YOUR_DOMAIN \
  --non-interactive --agree-tos --email admin@YOUR_DOMAIN
certbot certonly --webroot -w /var/www/html -d api.YOUR_DOMAIN \
  --non-interactive --agree-tos --email admin@YOUR_DOMAIN

10. nginx Configuration

nginx
server {
    listen 443 ssl http2;
    server_name YOUR_DOMAIN;

    ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    root /opt/zerg/limon;
    index index.html;

    location / { try_files $uri $uri/ /index.html =404; }
    location /api/ { proxy_pass http://127.0.0.1:21434; proxy_buffering off; }
    location /v1/  { proxy_pass http://127.0.0.1:21434; proxy_buffering off; }
    location /events { proxy_pass http://127.0.0.1:21434; proxy_buffering off; proxy_read_timeout 86400s; }
    location /auth/ { proxy_pass http://127.0.0.1:5800; }
    location /health { proxy_pass http://127.0.0.1:21434; }
    location /metrics { proxy_pass http://127.0.0.1:21434; }
}

server {
    listen 443 ssl http2;
    server_name api.YOUR_DOMAIN;
    ssl_certificate /etc/letsencrypt/live/api.YOUR_DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.YOUR_DOMAIN/privkey.pem;

    location / { proxy_pass http://127.0.0.1:21434; proxy_buffering off; }
    location /events { proxy_pass http://127.0.0.1:21434; proxy_buffering off; proxy_read_timeout 86400s; }
    location /auth/ { proxy_pass http://127.0.0.1:5800; }
}

11. systemd Units

ini
[Unit]
After=network.target postgresql.service zerg-mango.service

[Service]
Type=forking
User=zerg
WorkingDirectory=/opt/zerg
Environment=HOME=/opt/zerg
ExecStartPre=/bin/mkdir -p /opt/zerg/log /opt/zerg/data/mnesia_backups
ExecStart=/opt/zerg/bin/sol start
ExecStop=/opt/zerg/bin/sol stop
PIDFile=/opt/zerg/running_pid
Restart=on-failure
LimitNOFILE=65536
PrivateTmp=true

[Install]
WantedBy=multi-user.target

12. Enable and Start

bash
useradd -r -m -d /opt/zerg -s /bin/bash zerg
chown -R zerg:zerg /opt/zerg/
systemctl daemon-reload
systemctl enable zerg-sol zerg-mango nginx
systemctl start zerg-mango
systemctl start zerg-sol

13. Verify

bash
curl -sk https://YOUR_DOMAIN/health
curl -sk https://api.YOUR_DOMAIN/health

Docker Compose Stack

A production-like Docker Compose stack is available in infra/:

bash
cd deployment/infra
docker compose --profile mango up -d

Supported profiles:

ProfileServices
defaultSol, ZMQ gateway
mango+ Mango auth service
monitoring+ Grafana, Tempo

Environment Variables

VariableDefaultDescription
SOL_PLUGIN_DIRpriv/pluginsPlugin directory path
SOL_CLUSTER_COOKIE(generated)Erlang distribution cookie
SOL_CONTAINER_ENGINEpodmanContainer runtime
SOL_PROMETHEUS_ENABLEDfalseEnable /metrics endpoint
SOL_PGVECTOR_ENABLEDfalseEnable pgvector search
SOL_GRAFANA_URL""Grafana URL for alerting
SOL_WORKER_HEARTBEAT_TIMEOUT_MS15000Worker heartbeat timeout
SOL_WORKER_HEARTBEAT_CHECK_MS10000Heartbeat check interval

Production Checklist

  • [ ] Change default passwords and secrets
  • [ ] Enable TLS via nginx reverse proxy
  • [ ] Set auth_enabled to true
  • [ ] Configure firewall rules (allow 80/443 only)
  • [ ] Bootstrap admin RBAC
  • [ ] Set SOL_PROMETHEUS_ENABLED=true for monitoring
  • [ ] Configure PostgreSQL for memory/embeddings
  • [ ] Verify health endpoints respond
  • [ ] Test admin-gated endpoints return 200
  • [ ] Enable automatic certificate renewal (certbot renew)

Rollback

bash
systemctl stop zerg-sol
cp -a /opt/zerg/releases/{VERSION} /opt/zerg/releases/{VERSION}.bak
systemctl start zerg-sol

Maintenance Mode

Enable maintenance mode to gracefully drain traffic during deployments:

bash
curl -X POST https://api.YOUR_DOMAIN/api/v1/infra/maintenance/enable \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "deploying v{VERSION}"}'

curl -X POST https://api.YOUR_DOMAIN/api/v1/infra/maintenance/disable \
  -H "Authorization: Bearer $TOKEN"

Health and auth endpoints remain accessible during maintenance mode.

Released under the MIT License.