Installation & Deployment Guide
May 21, 2026 · View on GitHub
usulnet - Docker Management Platform This guide covers all methods to install and run usulnet in production.
Table of Contents
- System Requirements
- Installation with Docker Compose (Recommended)
- Installation with Standalone Binary
- First Access & Initial Setup
- HTTPS / TLS Configuration
- Multi-Host Setup (Master + Agents)
- Recon Module (Opt-In)
- Capability Requirements (v26.5.1 modules)
- Upgrading
- Uninstalling
- Troubleshooting
System Requirements
Minimum Requirements
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4 cores |
| RAM | 2 GB | 4 GB |
| Disk | 20 GB | 50 GB+ |
| OS | Linux (amd64/arm64) | Ubuntu 22.04+, Debian 12+, RHEL 9+ |
Software Requirements
| Software | Version | Required For |
|---|---|---|
| Docker Engine | 24.0+ | Container runtime |
| Docker Compose | v2.20+ | Orchestration (Docker Compose method) |
| Go | 1.25+ | Building from source only |
Network Requirements
| Port | Protocol | Purpose |
|---|---|---|
| 8080 | TCP | HTTP web interface |
| 7443 | TCP | HTTPS web interface (auto-TLS) |
| 4222 | TCP | NATS (internal, agent communication) |
| 5432 | TCP | PostgreSQL (internal) |
| 6379 | TCP | Redis (internal) |
Note: Ports 4222, 5432, and 6379 are only used internally between containers and are not exposed to the host by default.
Installation with Docker Compose
This is the recommended method for production deployments.
Step 1: Clone or Download
# Option A: Clone the repository
git clone https://github.com/fr4nsys/usulnet.git
cd usulnet
# Option B: Download only the compose file
mkdir usulnet && cd usulnet
curl -LO https://raw.githubusercontent.com/fr4nsys/usulnet/main/docker-compose.yml
curl -LO https://raw.githubusercontent.com/fr4nsys/usulnet/main/deploy/.env.example
Step 2: Configure Environment Variables
cp deploy/.env.example .env
Edit .env and change the following values:
# REQUIRED: Generate a secure database password
DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
# Optional: Customize ports
USULNET_HTTP_PORT=8080
USULNET_HTTPS_PORT=7443
# Optional: Set operation mode
# standalone = single host (default)
# master = multi-host control plane
USULNET_MODE=standalone
Environment Variable Reference:
| Variable | Default | Description |
|---|---|---|
DB_PASSWORD | none | Required. PostgreSQL password |
DB_USER | usulnet | PostgreSQL username |
DB_NAME | usulnet | PostgreSQL database name |
USULNET_VERSION | latest | Docker image tag |
USULNET_HTTP_PORT | 8080 | HTTP port on host |
USULNET_HTTPS_PORT | 7443 | HTTPS port on host |
USULNET_MODE | standalone | Operation mode: standalone or master |
HOST_TERMINAL_ENABLED | true | Allow web terminal to Docker host |
HOST_TERMINAL_USER | nobody | User for host terminal sessions |
AGENT_TOKEN | change-me | Token for agent authentication (multi-host only) |
Step 3: Start the Stack
docker compose up -d
This starts the following services:
- usulnet - Main application server
- postgres - PostgreSQL 16 database
- redis - Redis 8 cache and session store
- nats - NATS 2.12 message broker (JetStream)
- guacd - Apache Guacamole daemon (RDP/VNC gateway)
Step 4: Verify the Installation
Wait for all services to become healthy:
# Check service status
docker compose ps
# Verify health endpoint
curl -sf http://localhost:8080/health
Expected health response:
{
"status": "healthy",
"checks": {
"postgresql": {"status": "up"},
"redis": {"status": "up"},
"nats": {"status": "up"},
"docker": {"status": "up"}
}
}
Step 5: Access the Web Interface
Open your browser and navigate to:
- HTTP:
http://localhost:8080 - HTTPS:
https://localhost:7443(self-signed certificate)
Default credentials:
Username: admin
Password: usulnet
Important: Change the default password immediately after first login.
Installation with Standalone Binary
For environments where Docker Compose is not desired, you can run usulnet as a standalone binary. You must provide PostgreSQL, Redis, and NATS separately.
Step 1: Install Prerequisites
Ensure the following services are running and accessible:
- PostgreSQL 16+
- Redis 8+
- NATS 2.12+ (with JetStream enabled)
- Docker Engine 24+
Step 2: Download the Binary
Download the latest release from the GitHub Releases page:
# Linux amd64
curl -LO https://github.com/fr4nsys/usulnet/releases/latest/download/usulnet-linux-amd64
chmod +x usulnet-linux-amd64
sudo mv usulnet-linux-amd64 /usr/local/bin/usulnet
# Linux arm64
curl -LO https://github.com/fr4nsys/usulnet/releases/latest/download/usulnet-linux-arm64
chmod +x usulnet-linux-arm64
sudo mv usulnet-linux-arm64 /usr/local/bin/usulnet
Step 3: Create Configuration File
Create config.yaml:
mode: "standalone"
server:
host: "0.0.0.0"
port: 8080
https_port: 7443
tls:
enabled: true
auto_tls: true
database:
url: "postgres://usulnet:YOUR_PASSWORD@localhost:5432/usulnet?sslmode=disable"
max_open_conns: 25
max_idle_conns: 10
redis:
url: "redis://localhost:6379"
nats:
url: "nats://localhost:4222"
jetstream:
enabled: true
security:
jwt_secret: "GENERATE_64_HEX_CHARS"
config_encryption_key: "GENERATE_64_HEX_CHARS"
password_min_length: 8
storage:
type: "local"
path: "/var/lib/usulnet/data"
trivy:
enabled: true
cache_dir: "/var/lib/usulnet/trivy"
severity: "CRITICAL,HIGH,MEDIUM"
logging:
level: "info"
format: "json"
Generate the required secrets:
# Generate JWT secret (64 hex characters)
openssl rand -hex 32
# Generate encryption key (64 hex characters)
openssl rand -hex 32
Step 4: Create Database
# Connect to PostgreSQL and create the database
psql -U postgres -c "CREATE USER usulnet WITH PASSWORD 'YOUR_PASSWORD';"
psql -U postgres -c "CREATE DATABASE usulnet OWNER usulnet;"
Step 5: Run Database Migrations
usulnet migrate up
To verify the schema, run usulnet migrate status — it lists every
migration (001_foundation through 056_marketplace) with its applied
state.

Step 6: Start the Server
# Run in foreground
usulnet serve --config config.yaml
# Or run as a systemd service (see below)
Optional: Systemd Service
Create /etc/systemd/system/usulnet.service:
[Unit]
Description=usulnet Docker Management Platform
After=network.target postgresql.service redis.service nats.service
Requires=docker.service
[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/local/bin/usulnet serve --config /etc/usulnet/config.yaml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now usulnet
sudo systemctl status usulnet
First Access & Initial Setup
-
Open the web interface at
http://localhost:8080(or your configured URL)
-
Log in with the default credentials:
admin/usulnet -
Change the admin password immediately via the profile page
-
Configure system settings in Admin > Settings:
- Set the platform name and base URL
- Configure email/SMTP for notifications (optional)
- Configure backup storage (optional)
- Set up LDAP authentication (built-in, no separate license)
HTTPS / TLS Configuration
Auto-Generated Self-Signed Certificate (Default)
usulnet automatically generates a self-signed TLS certificate when auto_tls: true is set. Access the platform via https://localhost:7443. Browsers will show a certificate warning.
Let's Encrypt (Recommended for Production)
For production, place usulnet behind a reverse proxy (Caddy, Nginx, Traefik) with Let's Encrypt:
# Example with Caddy as reverse proxy
# Caddyfile
yourdomain.com {
reverse_proxy localhost:8080
}
Custom Certificate
To use your own certificate:
# config.yaml
server:
tls:
enabled: true
auto_tls: false
cert_file: "/path/to/cert.pem"
key_file: "/path/to/key.pem"
With Docker Compose, mount the certificates:
volumes:
- ./certs/cert.pem:/app/certs/cert.pem:ro
- ./certs/key.pem:/app/certs/key.pem:ro
Optional: TLS for In-Cluster Postgres / Redis / NATS
By default, the application talks to its Postgres, Redis, and NATS
containers over plain TCP on the private usulnet-backend Docker
network. The network is not exposed to the host, so on a single-box
install the traffic never leaves the loopback bridge.
When that hop needs to be encrypted (multi-host overlays, defence-in- depth, regulatory requirements), turn on local-services TLS:
USULNET_TLS_LOCAL_SERVICES=true docker compose up -d
What changes when the flag is true:
- The Postgres / Redis / NATS entrypoint scripts under
deploy/tls/generate a self-signed ECDSA P-256 server certificate (valid 3650 days) on first boot and persist it in the data volume. - Postgres starts with
ssl=onand accepts TLS on 5432. - Redis disables plain-TCP listener and binds TLS on 6379. The
application connects with
rediss://. - NATS appends a
tls{}block tonats-server.confand accepts TLS on 4222. - The application detects the flag at boot via
server.tls.local_services(Viper key) orUSULNET_TLS_LOCAL_SERVICES=trueand rewrites the Postgres, Redis, and NATS connection settings automatically — no config changes needed inconfig.yaml. Self-signed certificates are accepted withskip-verifyby default; mount your own CA bundle and flipredis.tls_skip_verify: false/nats.tls.skip_verify: falseto opt back intoverify-full. - Defaults stay plain TCP. With the env var unset or
false, the compose entrypoint scripts execute the upstream binaries with no changes — there is no on-disk PKI material and no scheme rewrite.
You can also pre-generate the certs on the host (useful for audit, mTLS, or mounting your own CA):
USULNET_TLS_LOCAL_SERVICES=true make dev-certs
The certs land under deploy/tls/certs/{postgres,redis,nats}/ and are
gitignored. The make dev-certs target is a no-op when the env var is
unset.
Multi-Host Setup (Master + Agents)
usulnet supports managing multiple Docker hosts through a master-agent architecture using NATS JetStream.
Configure the Master
Set the mode to master on the control plane host:
# .env
USULNET_MODE=master
AGENT_TOKEN=your-secure-agent-token
docker compose up -d
Deploy an Agent on a Remote Host
On each remote host you want to manage:
docker run -d \
--name usulnet-agent \
--restart unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock \
usulnet/usulnet-agent:latest \
--gateway nats://MASTER_IP:4222 \
--token your-secure-agent-token
Or use the agent profile in Docker Compose:
docker compose --profile agent up -d
Pre-flight validation. Before restarting an agent against an
updated config, run validate-config to surface typos or missing
required fields without bringing the agent up:
# Binary install
usulnet-agent validate-config --config /etc/usulnet-agent/config.yaml
# Docker install (one-shot validation against a mounted config)
docker run --rm \
-v /etc/usulnet-agent/config.yaml:/app/config.yaml:ro \
usulnet/usulnet-agent:latest \
validate-config --config /app/config.yaml
validate-config exits 0 only when a startable config can be
assembled (the required field today is token). Fail the deploy if
validation does not exit 0. See Recommended Deploy
Workflow in the agent guide
for the full procedure.
For detailed agent configuration, see Agent Configuration Guide.
Recon Module (Opt-In)
The v26.5.0 recon and metadata module ships off by default. A fresh install (or an upgrade from v26.4.x) behaves exactly like a Docker-management-only deployment unless an administrator explicitly turns it on. Setting USULNET_RECON_ENABLED=true (or recon.enabled: true in config.yaml) is necessary but not sufficient: every /recon/* and /metadata/* route returns 409 acknowledgement_required until an admin signs the in-app legal notice, and the SpiderFoot + recon-toolkit containers are only pulled and started after that acknowledgement is recorded in the audit log. Before flipping the flag in production, read docs/v26.5/security-review-checklist.md — it cites the file:line of every sandbox control (capability drop, read-only rootfs, seccomp default, egress allow-list, retention worker, append-only audit log) and links the unit test that exercises it.
Capability Requirements (v26.5.1 modules)
v26.5.1 ports eleven modules from v26.2.7 into the AGPL build. The
defaults in docker-compose.yml cover every
module that operates strictly inside the usulnet container. The modules
that reach out to the host (firewall, WireGuard, docker-engine config,
image builder) require extra mounts or capabilities, listed here.
Operators who do not use a given module can ignore its row.
Firewall (/firewall)
The firewall service does not open raw sockets from the usulnet
container; it issues ufw/nft/iptables commands against the host
via the host-management transport. Two prerequisites:
- The host (or remote agent) must have at least one of
ufw,nftables, oriptablesinstalled. The service detects the active backend on first use and surfaces it on the Operations → Firewall page. - The user account the transport runs as needs
NET_ADMINon the host to apply rules. For the bundled installer this means running the agent process underrootor a user with thecap_net_admincapability granted viasetcap cap_net_admin+ep /usr/local/bin/iptables(or the equivalent fornft/ufw). The usulnet container itself does not receive a new capability — the privilege lives on the host.
WireGuard (/wireguard)
WireGuard peer/interface management runs on each agent host via the existing NATS gateway. Per agent:
- The Linux WireGuard kernel module must be loaded
(
modprobe wireguard; verify withlsmod | grep wireguard). Kernel 5.6+ ships the module in-tree; earlier kernels needwireguard-dkmsorwireguard-toolsfrom the distro repos. - The
wgandwg-quickbinaries must be onPATH. usulnet probes both at startup (wireguard.ProbeLocal) and surfaces a yellow banner on the WireGuard list page when either is missing — installation continues but mutating routes return503 service_unavailableuntil the dependency is satisfied. - The agent process needs
NET_ADMINto bring interfaces up, same justification as firewall. Same options apply (root,cap_net_admin). - Setting
USULNET_ENCRYPTION_KEYis required — when the installation data encryption key is unavailable at boot, the service is intentionally not constructed so cleartext WireGuard private keys never land in the database. The web UI then renders a "not configured" page rather than persisting unsealed material.
Docker engine config (/docker-engine)
The Monaco diff editor needs read-write access to the host's
/etc/docker/daemon.json. The default docker-compose.yml does not
mount this path — opt in by adding the following volume to the
usulnet service:
volumes:
- /etc/docker:/etc/docker:rw
The atomic writer creates daemon.json.tmp.<rand> in the same
directory, fsyncs, then rename(2)s onto daemon.json, so a crash
during apply leaves the previous daemon.json intact. Reload uses
SIGHUP to the daemon and polls dockerd's API for up to 60 s; on
timeout or unhealthy response the editor restores the pre-apply
snapshot and re-issues SIGHUP. Snapshot history lives on disk under
/etc/docker/usulnet-snapshots/ (rotated; default keep = 50). When the
mount is missing the entire /docker-engine route family returns
503 service_unavailable with an explicit "host /etc/docker not
mounted" message.
The v26.2.7 nsenter-via-
docker execself-exec path is not ported. v26.5.1 expects an explicit/etc/dockerbind mount.
Image builder (/image-builder)
Local Docker builds shell out to the host Docker socket that is
already mounted into the usulnet container by the default compose file
(/var/run/docker.sock). No extra capability is required. Build
context uploads are capped at image_builder.max_context_bytes
(default 256 MiB; 413 from the API past that). Optional cosign signing
is wired through the existing imagesign service and only fires when
image_sign.enabled=true.
If your install does not mount the Docker socket (rare; the platform
fundamentally needs it for container management), every
/image-builder route returns 503 service_unavailable.
Backup verification (/backup-verify)
Verification jobs launch a sandboxed container via the recon sandbox
launcher (internal/services/recon/sandbox/launcher.go). The sandbox
shares the same posture as the recon engines:
- Read-only rootfs,
CAP_DROP=ALL,no-new-privileges, default seccomp, non-root UID65534:65534. - 512 MiB memory cap, 1 vCPU, 256 PIDs.
- Dedicated egress-controlled bridge network.
- The per-installation encryption key is passed to the verification container via a tmpfs file mount — never an environment variable.
No additional host capability is required; the Docker socket the launcher needs is already mounted for the platform itself.
DNS providers (/dns)
Cloudflare, AWS Route 53, DigitalOcean, and RFC 2136 providers all
operate over outbound HTTPS / UDP from the usulnet container. The only
requirement is egress to the provider's API endpoint (and DNS
resolution for that endpoint). Provider credentials are AES-256-GCM
encrypted at rest with the installation data encryption key (same key
as recon + WireGuard); set USULNET_ENCRYPTION_KEY to a 32-byte
hex/base64 secret to enable the service.
Other v26.5.1 modules
The remaining modules — firewall (above), crontab, rollback, SSL observatory, calendar, marketplace, proxy-extended — operate entirely inside the usulnet container and need no extra mounts or capabilities beyond what the default compose file already grants. They each persist to PostgreSQL (migrations 046, 049, 054, 051, 046, 056, 047 respectively); see CHANGELOG.md for per-module detail.
Upgrading
v26.5.0 → v26.5.1
v26.5.1 is migration-additive only. Migrations 046–056 create new
tables; no destructive ALTER runs against an existing v26.5.0
table, and the v26.5.0 recon_* schema (044/045) is untouched. The
schema check scripts/verify-migrations.sh is part of make quality
and asserts no gap, no duplicate, no orphan up.sql/down.sql.
No application config keys are removed. New keys default to safe values:
image_builder.max_context_bytes→ 256 MiBimage_builder.image_sign.enabled→ falsessl_observatory.per_target_concurrency→ 4crontab.*→ no global toggles; the page is reachable as soon as the user has the newcrontab:viewpermission
A v26.5.0 install can roll forward by pulling the new image and restarting:
docker compose pull
docker compose up -d
To roll back from v26.5.1 to v26.5.0, see the Rollback section in
docs/v26.5/release-notes-v26.5.1.md
— migrations 046–056 drop cleanly in reverse order, and the rolled-back
binary will ignore the now-empty recon_*/v26.5.0 data.
Docker Compose
# Pull new images
docker compose pull
# Restart with new images (migrations run automatically)
docker compose up -d
# Verify health
docker compose ps
curl -sf http://localhost:8080/health
Standalone Binary
# Download new binary
curl -LO https://github.com/fr4nsys/usulnet/releases/latest/download/usulnet-linux-amd64
# Stop the service
sudo systemctl stop usulnet
# Replace binary
sudo mv usulnet-linux-amd64 /usr/local/bin/usulnet
sudo chmod +x /usr/local/bin/usulnet
# Run migrations
usulnet migrate up --config /etc/usulnet/config.yaml
# Start the service
sudo systemctl start usulnet
Uninstalling
Docker Compose
# Stop and remove containers
docker compose down
# Remove all data (WARNING: destructive)
docker compose down -v
Standalone Binary
sudo systemctl stop usulnet
sudo systemctl disable usulnet
sudo rm /etc/systemd/system/usulnet.service
sudo systemctl daemon-reload
sudo rm /usr/local/bin/usulnet
sudo rm -rf /etc/usulnet /var/lib/usulnet
Troubleshooting
Docker Socket Permission Denied
Error: permission denied while trying to connect to the Docker daemon socket
Solution: Ensure the usulnet container has access to the Docker socket. The container runs an entrypoint script that adjusts the Docker socket group. If you still see errors:
# Check Docker socket permissions on the host
ls -la /var/run/docker.sock
# Ensure the docker group exists and the container uses it
docker compose restart usulnet
Database Connection Failed
Error: connection refused (PostgreSQL)
Solutions:
- Verify PostgreSQL is running:
docker compose ps postgres - Check database password matches in
.env - Check logs:
docker compose logs postgres - Verify the database was created:
docker exec -it usulnet-postgres psql -U usulnet -c '\l'
Port Already in Use
Error: bind: address already in use
Solution: Change the port in .env:
USULNET_HTTP_PORT=9090
USULNET_HTTPS_PORT=9443
NATS Connection Issues
Error: nats: no servers available for connection
Solutions:
- Verify NATS is running:
docker compose ps nats - Check NATS health:
curl http://localhost:8222/healthz(if port exposed) - Check logs:
docker compose logs nats - Ensure JetStream is enabled (default in the compose file)
Redis Connection Issues
Error: dial tcp: connection refused (Redis)
Solutions:
- Verify Redis is running:
docker compose ps redis - Check logs:
docker compose logs redis - Verify Redis responds:
docker exec usulnet-redis redis-cli ping
Migrations Failed
Error: migration failed
Solutions:
- Check migration status:
usulnet migrate status(standalone) or check container logs - Verify database connectivity
- Check for advisory lock conflicts if running multiple instances:
docker exec -it usulnet-postgres psql -U usulnet -c "SELECT * FROM pg_locks WHERE locktype = 'advisory';"
Container Logs Show "Unhealthy" Dependencies
If usulnet reports unhealthy dependencies at startup:
# Check all service health
docker compose ps
# View detailed logs
docker compose logs -f usulnet
# Restart problematic service
docker compose restart <service-name>
Memory Issues
If you see OOM kills, adjust resource limits in docker-compose.yml:
deploy:
resources:
limits:
memory: 2G # Increase from default 1G
For more information, see the Architecture Guide and Development Guide.