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
| Service | Technology | Port | Access |
|---|---|---|---|
| zerg-sol | Erlang/OTP 28 | 21434 | localhost only |
| zerg-mango | Python/Tornado | 5800 | localhost only |
| nginx | TLS reverse proxy | 80/443 | public |
| postgresql | Database | 5432 | localhost only |
Prerequisites
bash
apt-get install -y postgresql postgresql-contrib nginx certbot python3-pip
pip3 install --break-system-packages tornado bcrypt aiosqliteInstall 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.2Step-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 release3. 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.args4. Deploy Luna Agent
bash
cp client/build/luna /opt/zerg/luna/luna
chmod +x /opt/zerg/luna/luna5. 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-mangoOr run manually:
bash
cd /opt/zerg/mango
python3 scripts/bootstrap_admin.py --autoThe 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_DOMAIN10. 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.target12. 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-sol13. Verify
bash
curl -sk https://YOUR_DOMAIN/health
curl -sk https://api.YOUR_DOMAIN/healthDocker Compose Stack
A production-like Docker Compose stack is available in infra/:
bash
cd deployment/infra
docker compose --profile mango up -dSupported profiles:
| Profile | Services |
|---|---|
| default | Sol, ZMQ gateway |
mango | + Mango auth service |
monitoring | + Grafana, Tempo |
Environment Variables
| Variable | Default | Description |
|---|---|---|
SOL_PLUGIN_DIR | priv/plugins | Plugin directory path |
SOL_CLUSTER_COOKIE | (generated) | Erlang distribution cookie |
SOL_CONTAINER_ENGINE | podman | Container runtime |
SOL_PROMETHEUS_ENABLED | false | Enable /metrics endpoint |
SOL_PGVECTOR_ENABLED | false | Enable pgvector search |
SOL_GRAFANA_URL | "" | Grafana URL for alerting |
SOL_WORKER_HEARTBEAT_TIMEOUT_MS | 15000 | Worker heartbeat timeout |
SOL_WORKER_HEARTBEAT_CHECK_MS | 10000 | Heartbeat check interval |
Production Checklist
- [ ] Change default passwords and secrets
- [ ] Enable TLS via nginx reverse proxy
- [ ] Set
auth_enabledtotrue - [ ] Configure firewall rules (allow 80/443 only)
- [ ] Bootstrap admin RBAC
- [ ] Set
SOL_PROMETHEUS_ENABLED=truefor 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-solMaintenance 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.