Skip to content

Manual Installation

Install Mantis components directly on servers without containers.

SoftwareVersionPurpose
RustPinned channel 1.96.1 (rust-toolchain.toml; build images use -rust-1.96.1-bookworm)Building components
Node.js24 (vite 8 needs ^20.19 || >=22.12; CI/images use 24 — Node 20.0–20.18 fail)Building Lens UI
npm10+Node package manager
PostgreSQL16+Database
RabbitMQ3.12+Message queue
Redis7.0+Cache and coordination
ResourceMinimumRecommended
CPU4 cores8+ cores
RAM8 GB16+ GB
Storage50 GB SSD100+ GB SSD
Terminal window
git clone https://github.com/MantisPlatform/mantis.git
cd mantis
Terminal window
# Install Rust if needed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# Build all components in release mode
just build-release
# Or build individually
cd mandible && cargo build --release
cd ../thorax && cargo build --release
cd ../tarsus && cargo build --release

Lens is a Rust binary that embeds its web UI. Build the UI first (Node is a build-time-only dependency), then compile the binary with the UI embedded:

Terminal window
# Build the SPA (produces lens/ui/build)
npm --prefix lens/ui ci
npm --prefix lens/ui run build
# Compile the binary with the UI embedded
cargo build --release -p lens --features lens/embed-ui
Terminal window
# Create directories
sudo mkdir -p /usr/local/bin
sudo mkdir -p /etc/mantis
sudo mkdir -p /var/lib/mantis
sudo mkdir -p /var/log/mantis
# Install binaries
for bin in mandible thorax tarsus mantisctl lens; do
sudo install -m 0755 "target/release/$bin" /usr/local/bin/
done

The release set is five binaries: mandible, thorax, tarsus, mantisctl (admin CLI) and lens (UI).

Terminal window
# Ubuntu/Debian
sudo apt update
sudo apt install postgresql-16 postgresql-contrib
# RHEL/Rocky/Alma
sudo dnf install postgresql16-server postgresql16
sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
Terminal window
# Start service
sudo systemctl enable postgresql
sudo systemctl start postgresql
# Create database and user
sudo -u postgres psql <<EOF
CREATE USER mantis WITH PASSWORD 'your-secure-password';
CREATE DATABASE mantis OWNER mantis;
GRANT ALL PRIVILEGES ON DATABASE mantis TO mantis;
EOF
Terminal window
# Install diesel CLI
cargo install diesel_cli --no-default-features --features postgres
# Run migrations from the instinct directory
cd /path/to/mantis/instinct
export DATABASE_URL="postgres://mantis:your-secure-password@localhost/mantis"
diesel migration run
Terminal window
# Ubuntu/Debian (using official repository)
curl -1sLf 'https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey' | sudo gpg --dearmor -o /usr/share/keyrings/rabbitmq.gpg
echo "deb [signed-by=/usr/share/keyrings/rabbitmq.gpg] https://packagecloud.io/rabbitmq/rabbitmq-server/ubuntu/ jammy main" | sudo tee /etc/apt/sources.list.d/rabbitmq.list
sudo apt update
sudo apt install rabbitmq-server
# RHEL/Rocky/Alma
sudo dnf install rabbitmq-server
Terminal window
# Enable and start
sudo systemctl enable rabbitmq-server
sudo systemctl start rabbitmq-server
# Enable management plugin
sudo rabbitmq-plugins enable rabbitmq_management
# Create user
sudo rabbitmqctl add_user mantis your-secure-password
sudo rabbitmqctl set_permissions -p / mantis ".*" ".*" ".*"
sudo rabbitmqctl set_user_tags mantis administrator

Create /etc/rabbitmq/rabbitmq.conf:

listeners.ssl.default = 5671
ssl_options.cacertfile = /etc/rabbitmq/certs/ca.crt
ssl_options.certfile = /etc/rabbitmq/certs/server.crt
ssl_options.keyfile = /etc/rabbitmq/certs/server.key
ssl_options.verify = verify_peer
ssl_options.fail_if_no_peer_cert = false
Terminal window
# Ubuntu/Debian
sudo apt install redis-server
# RHEL/Rocky/Alma
sudo dnf install redis

Edit /etc/redis/redis.conf:

bind 127.0.0.1
port 6379
requirepass your-secure-password
maxmemory 1gb
maxmemory-policy allkeys-lru
Terminal window
sudo systemctl enable redis
sudo systemctl start redis

Create /etc/mantis/mandible.toml:

[server]
host = "0.0.0.0"
port = 3000
[server.tls]
cert_path = "/etc/mantis/certs/mandible.crt"
key_path = "/etc/mantis/certs/mandible.key"
[database]
url = "postgres://mantis:password@localhost/mantis"
max_connections = 10
[jwt]
algorithm = "HS256"
# secret = "" # Set via MANDIBLE__JWT__SECRET env var (min 32 chars)
# Generate with: openssl rand -base64 32
# For RS256 (asymmetric, optional):
# algorithm = "RS256"
# private_key_path = "/etc/mantis/jwt/private.pem"
# public_key_path = "/etc/mantis/jwt/public.pem"
[grpc]
orchestration_url = "https://localhost:50051"
tls_ca_cert_path = "/etc/mantis/certs/ca.crt"
# Dispatch to Thorax is mTLS with a dedicated client identity whose CN is
# "mandible" -- the shared internal cert is not trusted, so tenant-scoped deploys
# would 403 without these.
tls_cert_path = "/etc/mantis/certs/dispatch-cert.pem"
tls_key_path = "/etc/mantis/certs/dispatch-key.pem"

Create /etc/mantis/thorax.toml:

[tls]
auth_mode = "thumbprint"
cert_path = "/etc/mantis/certs/thorax.crt"
key_path = "/etc/mantis/certs/thorax.key"
ca_cert_path = "/etc/mantis/certs/ca.crt"
[grpc]
address = "0.0.0.0"
port = 50051
# Thorax has NO database — all persistence is owned by Mandible, reached over
# gRPC. Set THORAX__MANDIBLE__ENDPOINT to Mandible's internal gRPC (host:50052).
# The endpoint MUST be https:// (Thorax rejects any other scheme), and the
# connection is mTLS -- all three client-cert paths are required.
[mandible]
endpoint = "https://localhost:50052"
tls_ca_cert_path = "/etc/mantis/certs/ca.crt"
tls_cert_path = "/etc/mantis/certs/thorax.crt"
tls_key_path = "/etc/mantis/certs/thorax.key"
# RabbitMQ is OPTIONAL (off by default -- Thorax polls the database instead).
# Only include this block to use it, and ca_cert_path is mandatory because
# queue TLS is required; Thorax refuses to start without it.
[queue]
enabled = true
host = "localhost"
port = 5671
username = "mantis"
password = "password"
ca_cert_path = "/etc/mantis/certs/rabbitmq-ca.pem"
# Cluster mode is also opt-in; required only for coordinated multi-instance Thorax.
[cluster]
enabled = true
redis_url = "redis://:password@localhost:6379"

Create /etc/mantis/<instance>.toml (Tarsus searches ./tarsus.toml, /etc/mantis/client.toml, ~/.config/mantis/client.toml/etc/mantis/certs/ is not a search path):

name = "server-name"
mode = "listen"
# Tarsus registers with Mandible (internal gRPC port 50052), not Thorax.
# Mandible assigns the appropriate Thorax instance during registration.
[mandible]
endpoint = "mandible.example.com:50052"
[tls]
cert_path = "/etc/mantis/certs/tarsus-cert.pem"
key_path = "/etc/mantis/certs/tarsus-key.pem"
ca_cert_path = "/etc/mantis/certs/ca-cert.pem"
[registration]
heartbeat_interval_secs = 30

Migrations run as a separate one-shot unit so that serving replicas never race to migrate. Create /etc/systemd/system/mandible-migrate.service:

[Unit]
Description=Mantis Mandible DB migration
After=network.target postgresql.service
[Service]
Type=oneshot
User=mantis
Group=mantis
# ProtectHome=yes hides the user's HOME, but libpq probes ~/.postgresql for a
# client cert; point HOME at the data dir so DB connect works.
Environment="HOME=/var/lib/mantis"
Environment="MANDIBLE_CONFIG=/etc/mantis/mandible.toml"
EnvironmentFile=-/etc/mantis/mandible.env
ExecStart=/usr/local/bin/mandible --migrate-only
ProtectHome=yes
ReadWritePaths=/var/lib/mantis

Then the serving unit, which depends on it and skips migration itself. Create /etc/systemd/system/mandible.service:

[Unit]
Description=Mantis Mandible API Server
After=network.target postgresql.service mandible-migrate.service
Requires=mandible-migrate.service
[Service]
Type=simple
User=mantis
Group=mantis
Environment="HOME=/var/lib/mantis"
Environment="MANDIBLE_CONFIG=/etc/mantis/mandible.toml"
Environment="MANDIBLE_SKIP_MIGRATE=1"
Environment="RUST_LOG=info"
EnvironmentFile=-/etc/mantis/mandible.env
ExecStart=/usr/local/bin/mandible
Restart=always
RestartSec=5
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/mantis /var/log/mantis
PrivateTmp=yes
[Install]
WantedBy=multi-user.target

Create /etc/systemd/system/thorax.service:

[Unit]
Description=Mantis Thorax Orchestration Server
After=network.target rabbitmq-server.service redis.service
[Service]
Type=simple
User=mantis
Group=mantis
Environment="THORAX_CONFIG=/etc/mantis/thorax.toml"
Environment="RUST_LOG=info"
EnvironmentFile=-/etc/mantis/thorax.env
ExecStart=/usr/local/bin/thorax
Restart=always
RestartSec=5
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/mantis /var/log/mantis
PrivateTmp=yes
[Install]
WantedBy=multi-user.target

Tarsus is a templated unit — one instance per agent, keyed by %i. Create /etc/systemd/system/tarsus@.service:

[Unit]
Description=Mantis Tarsus Agent (%i)
After=network.target
[Service]
Type=simple
User=root
Environment="TARSUS_CONFIG=/etc/mantis/%i.toml"
Environment="RUST_LOG=info"
EnvironmentFile=-/etc/mantis/%i.env
ExecStart=/usr/bin/tarsus
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

Start an agent named web-01 with systemctl start tarsus@web-01, and read its logs with journalctl -u tarsus@web-01journalctl -u tarsus matches nothing.

Terminal window
# Create mantis user
sudo useradd -r -s /sbin/nologin mantis
# Set ownership
sudo chown -R mantis:mantis /etc/mantis
sudo chown -R mantis:mantis /var/lib/mantis
sudo chown -R mantis:mantis /var/log/mantis
# Reload systemd
sudo systemctl daemon-reload
# Enable and start services
sudo systemctl enable mandible thorax
sudo systemctl start mandible thorax
# On target servers
sudo systemctl enable tarsus@<instance>
sudo systemctl start tarsus@<instance>

Lens is a self-contained binary (lens) — no Node.js runtime. The same release tarball that carries mandible/thorax/tarsus/mantisctl also carries lens; it embeds the web UI and reads its Mandible origin at runtime (MANTIS_API_URL), so one binary serves any environment.

Terminal window
# `lens` is already in /usr/local/bin if you installed the release tarball above.
# Otherwise copy it there:
sudo install -m 0755 lens /usr/local/bin/lens
# Runtime config (EnvironmentFile). MANTIS_API_URL is injected into the served
# shell + CSP at runtime; LENS_REQUIRE_TLS=1 fails fast if the certs are missing.
sudo tee /etc/mantis/lens.env >/dev/null <<'EOF'
PORT=8443
HOST=0.0.0.0
MANTIS_API_URL=https://mantis.example.com
TLS_CERT_PATH=/etc/mantis/certs/lens-cert.pem
TLS_KEY_PATH=/etc/mantis/certs/lens-key.pem
LENS_REQUIRE_TLS=1
EOF

Create /etc/systemd/system/lens.service:

[Unit]
Description=Mantis Lens — management UI
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=mantis
Group=mantis
EnvironmentFile=/etc/mantis/lens.env
ExecStart=/usr/local/bin/lens
Restart=always
RestartSec=5
ProtectSystem=strict
ProtectHome=yes
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target

Enable and start:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now lens

Lens and Mandible must share an origin so the HTTP-only refresh-token cookie is visible to both. Front them with a single nginx server that serves Lens and proxies /api to Mandible on the same origin:

server {
listen 443 ssl http2;
server_name mantis.example.com;
ssl_certificate /etc/nginx/certs/mantis.crt;
ssl_certificate_key /etc/nginx/certs/mantis.key;
# Lens — the binary serves the UI + its own CSP/security headers over TLS.
location / {
proxy_pass https://127.0.0.1:8443;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Mandible REST — same origin as Lens (shared refresh cookie).
location /api {
proxy_pass https://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Enable the site:

Terminal window
sudo ln -s /etc/nginx/sites-available/lens /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Prefer mantisctl cert (or just dev-certs), which set the SANs and EKUs rustls requires — hand-rolled openssl below must add subjectAltName and extendedKeyUsage or the certs are rejected with NotValidForName (webpki matches the SAN only, with no CN fallback). Only use raw openssl on air-gapped hosts, with the extensions shown.

Terminal window
# Create CA private key
openssl genrsa -out /etc/mantis/certs/ca.key 4096
# Create CA certificate
openssl req -x509 -new -nodes \
-key /etc/mantis/certs/ca.key \
-sha256 -days 3650 \
-out /etc/mantis/certs/ca.crt \
-subj "/CN=Mantis CA/O=Your Organization"
Terminal window
# SAN + EKU extension file (REQUIRED — rustls matches the SAN, enforces the EKU)
cat > /etc/mantis/certs/server-ext.cnf <<'EOF'
subjectAltName = DNS:mandible, DNS:mandible.example.com
extendedKeyUsage = serverAuth, clientAuth
EOF
# Mandible
openssl req -new -nodes \
-keyout /etc/mantis/certs/mandible.key \
-out /etc/mantis/certs/mandible.csr \
-subj "/CN=mandible.example.com"
openssl x509 -req \
-in /etc/mantis/certs/mandible.csr \
-CA /etc/mantis/certs/ca.crt \
-CAkey /etc/mantis/certs/ca.key \
-CAcreateserial \
-extfile /etc/mantis/certs/server-ext.cnf \
-out /etc/mantis/certs/mandible.crt \
-days 365 -sha256
# Thorax (reuse server-ext.cnf; adjust the SAN to the Thorax hostname)
openssl req -new -nodes \
-keyout /etc/mantis/certs/thorax.key \
-out /etc/mantis/certs/thorax.csr \
-subj "/CN=thorax.example.com"
openssl x509 -req \
-in /etc/mantis/certs/thorax.csr \
-CA /etc/mantis/certs/ca.crt \
-CAkey /etc/mantis/certs/ca.key \
-CAcreateserial \
-extfile /etc/mantis/certs/server-ext.cnf \
-out /etc/mantis/certs/thorax.crt \
-days 365 -sha256
Terminal window
chmod 600 /etc/mantis/certs/*.key
chmod 644 /etc/mantis/certs/*.crt
chown -R mantis:mantis /etc/mantis/certs
Terminal window
# Service status
sudo systemctl status mandible
sudo systemctl status thorax
# Health endpoints
curl -k https://localhost:3000/api/v1/health
# gRPC health
grpcurl -insecure localhost:50051 grpc.health.v1.Health/Check
Terminal window
# Mandible logs
journalctl -u mandible -f
# Thorax logs
journalctl -u thorax -f
# Tarsus logs (on target)
journalctl -u tarsus -f
Terminal window
# Database
psql -h localhost -U mantis -d mantis -c "SELECT 1"
# RabbitMQ
rabbitmqctl status
# Redis
redis-cli -a password ping
Terminal window
# Mandible API
sudo firewall-cmd --permanent --add-port=3000/tcp
# Thorax gRPC
sudo firewall-cmd --permanent --add-port=50051/tcp
# Reload
sudo firewall-cmd --reload
Terminal window
# Mandible API
iptables -A INPUT -p tcp --dport 3000 -j ACCEPT
# Thorax gRPC
iptables -A INPUT -p tcp --dport 50051 -j ACCEPT
  1. Check configuration syntax
  2. Verify file permissions
  3. Check logs: journalctl -u <service> -xe
  1. Verify PostgreSQL is running
  2. Check connection URL
  3. Test manual connection
  1. Verify certificate paths
  2. Check certificate validity
  3. Ensure CA certificate is correct