Skip to main content

Manual Installation

This guide walks you through every step of deploying Hawkra Self-Hosted manually. Use this approach when you need full control over the configuration, are working in a restricted environment, or prefer to understand each component before deploying.

Step 1: Install Docker and Docker Compose

Install Docker Engine 24.0+ and Docker Compose v2+ following the official Docker installation guide.

Verify the installation:

docker --version && docker compose version

Step 2: Create Directory Structure

Create the Hawkra installation directory and subdirectories:

sudo mkdir -p /opt/hawkra/{caddy,certs,license}
cd /opt/hawkra

The final directory structure will look like this:

/opt/hawkra/
.env
docker-compose.selfhosted.yml
Caddyfile
caddy/
docker-entrypoint.sh
certs/ # Empty unless using custom certificates
license/ # Place your license file here

Step 3: Create the Environment File

Generate a secure database password:

POSTGRES_PW=$(openssl rand -hex 24)
echo "Database password: $POSTGRES_PW"

Create the .env file with the two required variables. Replace your-domain.com with your actual domain, hostname, or IP address:

sudo tee /opt/hawkra/.env > /dev/null <<EOF
# =============================================================================
# Hawkra Self-Hosted Configuration
# =============================================================================

# Domain (REQUIRED)
APP_DOMAIN=your-domain.com

# Database (REQUIRED)
POSTGRES_PASSWORD=$POSTGRES_PW
EOF
tip

APP_DOMAIN should be your domain name without https:// (e.g., hawkra.yourcompany.com). If you only intend to access Hawkra by IP address, enter the IP instead (e.g., 192.168.1.100).

Restrict the file permissions so only root can read it:

sudo chmod 600 /opt/hawkra/.env

All other settings (JWT secret, master encryption key, URLs, etc.) are auto-generated on first startup or can be configured through the Admin > Settings dashboard. If you prefer to set them explicitly via environment variables, add any of the optional variables below to your .env file:

Optional environment variables
# -----------------------------------------------------------------------------
# Authentication (auto-generated if absent)
# -----------------------------------------------------------------------------
# JWT_SECRET=
# JWT_EXPIRY_SECONDS=3600
# JWT_REFRESH_EXPIRY_SECONDS=604800

# -----------------------------------------------------------------------------
# Encryption (auto-generated if absent)
# -----------------------------------------------------------------------------
# MASTER_ENCRYPTION_KEY=

# -----------------------------------------------------------------------------
# URLs and Networking (derived from APP_DOMAIN if absent)
# -----------------------------------------------------------------------------
# FRONTEND_URL=https://your-domain.com
# BACKEND_URL=https://your-domain.com
# CORS_ALLOWED_ORIGINS=https://your-domain.com
# COOKIE_DOMAIN=your-domain.com
# COOKIE_SECURE=true

# -----------------------------------------------------------------------------
# TLS
# -----------------------------------------------------------------------------
# LETS_ENCRYPT=true

# -----------------------------------------------------------------------------
# Self-Hosted License
# -----------------------------------------------------------------------------
# SELFHOSTED_ANNUAL_KEY=your-license-key-here

# -----------------------------------------------------------------------------
# Version Pinning
# -----------------------------------------------------------------------------
# VERSION=1.0.0

# -----------------------------------------------------------------------------
# Rate Limiting
# -----------------------------------------------------------------------------
# API_RATE_LIMIT_PER_MINUTE=500

# -----------------------------------------------------------------------------
# Storage
# -----------------------------------------------------------------------------
# STORAGE_BACKEND=local
# STORAGE_MAX_FILE_SIZE_MB=100
# STORAGE_MAX_TOTAL_MB=10000
# STORAGE_S3_BUCKET=your-bucket
# STORAGE_S3_REGION=us-east-1
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=

# -----------------------------------------------------------------------------
# AI Assistant
# -----------------------------------------------------------------------------
# LLM_MODE=cloud
# GEMINI_API_KEY=
# GEMINI_MODEL=gemini-2.0-flash
# LOCAL_LLM_SERVER=http://10.10.10.12:8080

# -----------------------------------------------------------------------------
# OSINT API Keys
# -----------------------------------------------------------------------------
# SHODAN_API_KEY=
# HIBP_API_KEY=
# GEOIP_API_KEY=
# BRAVE_SEARCH_API_KEY=

# -----------------------------------------------------------------------------
# SMTP Configuration
# -----------------------------------------------------------------------------
# SMTP_HOST=smtp.yourprovider.com
# SMTP_PORT=587
# SMTP_ENCRYPTION=starttls
# SMTP_USERNAME=
# SMTP_PASSWORD=
# SMTP_FROM_ADDRESS=hawkra@yourcompany.com

# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
# RUST_LOG=info,hawkra_backend=info

Step 4: Create the Docker Compose File

Create the docker-compose.selfhosted.yml file:

sudo tee /opt/hawkra/docker-compose.selfhosted.yml > /dev/null <<'EOF'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: hawkra
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-hawkra}
POSTGRES_DB: hawkra
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hawkra"]
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5

# Pre-cache the TalonStrike pentest container image on first `docker compose up`.
# Exits immediately — just ensures the image is pulled and available on the host.
talon-strike-cache:
image: ghcr.io/reconhawk/talon-strike:latest
entrypoint: ["true"]
restart: "no"

backend:
image: ghcr.io/reconhawk/hawkra-backend:${VERSION:-latest}
cap_add:
- NET_RAW
- NET_ADMIN
- NET_BIND_SERVICE
env_file: .env
environment:
DATABASE_URL: postgres://hawkra:${POSTGRES_PASSWORD:-hawkra}@postgres:5432/hawkra
REDIS_URL: redis://redis:6379
RUST_LOG: info,hawkra_backend=info
AGENT_BINARY_PATH: /app/agent-binaries
BACKEND_URL: https://${APP_DOMAIN:-localhost}
FRONTEND_URL: https://${APP_DOMAIN:-localhost}
CORS_ALLOWED_ORIGINS: https://${APP_DOMAIN:-localhost}
COOKIE_DOMAIN: ${APP_DOMAIN:-localhost}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
volumes:
- file_storage:/app/data/files
- backend_config:/app/config
- ./license:/app/license
# TalonStrike needs Docker socket to manage Kali containers
- /var/run/docker.sock:/var/run/docker.sock
logging:
options:
max-size: "10m"
max-file: "3"

frontend:
image: ghcr.io/reconhawk/hawkra-frontend:${VERSION:-latest}
environment:
NEXT_PUBLIC_API_URL: https://${APP_DOMAIN:-localhost}
NEXT_PUBLIC_SITE_URL: https://${APP_DOMAIN:-localhost}
NEXT_PUBLIC_EDITION: selfhosted
depends_on:
- backend
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"

docs:
image: ghcr.io/reconhawk/hawkra-docs:${VERSION:-latest}
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"

caddy:
image: caddy:2-alpine
entrypoint: /docker-entrypoint.sh
environment:
APP_DOMAIN: ${APP_DOMAIN:-localhost}
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/docker-entrypoint.sh:/docker-entrypoint.sh:ro
- ./Caddyfile:/etc/caddy/Caddyfile.template:ro
- ./certs:/certs:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- frontend
- backend
- docs
restart: unless-stopped

volumes:
postgres_data:
file_storage:
backend_config:
caddy_data:
caddy_config:
EOF

Service Details

ServiceImagePurpose
postgrespostgres:16-alpineApplication database with health check
redisredis:7-alpineCaching and job queue with health check
talon-strike-cacheghcr.io/reconhawk/talon-strikePre-caches the TalonStrike pentest container image (exits immediately)
backendghcr.io/reconhawk/hawkra-backendAPI server with nmap capabilities
frontendghcr.io/reconhawk/hawkra-frontendWeb application
docsghcr.io/reconhawk/hawkra-docsDocumentation site
caddycaddy:2-alpineReverse proxy with automatic HTTPS

Step 5: Create the Caddyfile

Create the Caddy reverse proxy configuration template:

sudo tee /opt/hawkra/Caddyfile > /dev/null <<'CADDYEOF'
APP_DOMAIN {
# TLS_DIRECTIVE

# Security headers
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
X-XSS-Protection "0"
Cross-Origin-Resource-Policy "same-origin"
Cross-Origin-Opener-Policy "same-origin"
Cross-Origin-Embedder-Policy "require-corp"
-Server
}

# WebSocket upgrade — bypass encode gzip, direct proxy
@websocket {
path /api/*
header Connection *Upgrade*
header Upgrade websocket
}
handle @websocket {
reverse_proxy backend:3001
}

# Agent poll/connect — small payloads only (8KB)
@agent_small {
path /api/agent/connect
path /api/agent/poll
}
handle @agent_small {
encode gzip
request_body {
max_size 8KB
}
reverse_proxy backend:3001
}

# Agent task results — medium payloads (10MB)
@agent_tasks {
path /api/agent/tasks/*
}
handle @agent_tasks {
encode gzip
request_body {
max_size 10MB
}
reverse_proxy backend:3001
}

# All other API routes (100MB for file uploads)
handle /api/* {
encode gzip
request_body {
max_size 1000MB
}
reverse_proxy backend:3001
}

# Frontend
handle {
encode gzip
reverse_proxy frontend:3000
}
}

docs.APP_DOMAIN {
# TLS_DIRECTIVE

encode gzip

header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
X-Robots-Tag "index, follow"
-Server
}

reverse_proxy docs:80
}
CADDYEOF
How the Caddyfile Template Works

The Caddyfile is a template. At startup, the Caddy entrypoint script replaces APP_DOMAIN with your configured domain and # TLS_DIRECTIVE with the appropriate TLS configuration based on your setup (self-signed, custom certificates, or Let's Encrypt). When using a domain name, the template includes two server blocks: one for the main application and one for the docs. subdomain. When using an IP address, the docs block is automatically removed. You do not need to edit the Caddyfile directly.

Step 6: Create the Caddy Entrypoint Script

Create the entrypoint script that configures TLS at startup:

sudo tee /opt/hawkra/caddy/docker-entrypoint.sh > /dev/null <<'ENTRYEOF'
#!/bin/sh
set -e

CADDYFILE_TEMPLATE="/etc/caddy/Caddyfile.template"
CADDYFILE="/etc/caddy/Caddyfile"
CERT_DIR="/certs"

# Validate required domain environment variable
if [ -z "$APP_DOMAIN" ]; then
echo "ERROR: APP_DOMAIN environment variable is required"
echo "Set it in your .env file (e.g., APP_DOMAIN=hawkra.local)"
exit 1
fi

# Determine TLS mode
if [ "$LETS_ENCRYPT" = "true" ]; then
echo "Let's Encrypt mode — Caddy will auto-provision certificates"
TLS_LINE=""
elif [ -f "$CERT_DIR/cert.pem" ] && [ -f "$CERT_DIR/key.pem" ]; then
echo "Custom certificates found in $CERT_DIR — using provided certs"
TLS_LINE="tls /certs/cert.pem /certs/key.pem"
else
echo "No custom certificates found — using auto-generated self-signed certs"
TLS_LINE="tls internal"
fi

# Substitute domain placeholder and TLS directive
sed -e "s|APP_DOMAIN|$APP_DOMAIN|g" \
-e "s|# TLS_DIRECTIVE|$TLS_LINE|g" \
"$CADDYFILE_TEMPLATE" > "$CADDYFILE"

# If APP_DOMAIN is an IP address, remove the docs server block
# (docs.<IP> is not a valid hostname and breaks TLS)
case "$APP_DOMAIN" in
*[!0-9.]*)
# Contains non-numeric/dot characters — treat as a domain name, keep docs block
;;
*)
# Numeric and dots only — treat as an IPv4 address
# Remove docs block (docs.<IP> is not a valid hostname)
sed -i '/^docs\./,/^}/d' "$CADDYFILE"
# Add default_sni so Caddy serves the cert when clients connect
# without SNI (browsers/curl don't send SNI for IP addresses)
sed -i "1i\\{\n default_sni $APP_DOMAIN\n}" "$CADDYFILE"
echo "IP address detected — docs block removed, default_sni set"
;;
esac

echo "Caddy configured for domain: $APP_DOMAIN"

exec caddy run --config "$CADDYFILE" --adapter caddyfile
ENTRYEOF

Step 7: Configure TLS Certificates (Optional)

Choose one of the following TLS options. If you skip this step, Caddy generates self-signed certificates automatically.

Option A: Self-Signed Certificates (Default)

No action required. Caddy generates self-signed certificates at startup. Browsers will display a certificate warning that you can accept to proceed.

Option B: Custom Certificates

If you have certificates from a corporate CA or commercial provider, place them in the certs/ directory:

sudo cp /path/to/your/cert.pem /opt/hawkra/certs/cert.pem
sudo cp /path/to/your/key.pem /opt/hawkra/certs/key.pem
sudo chmod 644 /opt/hawkra/certs/cert.pem
sudo chmod 600 /opt/hawkra/certs/key.pem

The entrypoint script detects these files automatically at startup.

Option C: Let's Encrypt

For public-facing servers with a DNS record pointing to the server:

  1. Ensure ports 80 and 443 are reachable from the internet
  2. Add the following to your .env file:
echo "LETS_ENCRYPT=true" | sudo tee -a /opt/hawkra/.env

Caddy handles certificate provisioning and renewal automatically.

Step 8: Configure DNS or Hostname

If your APP_DOMAIN is a real domain with a DNS record pointing to the server, skip this step.

For hostnames without DNS records, add entries to /etc/hosts on the server and on every client machine that will access Hawkra:

sudo nano /etc/hosts

Add lines mapping the server IP to your hostname and the docs subdomain:

192.168.1.100   hawkra.yourcompany.local
192.168.1.100 docs.hawkra.yourcompany.local

Replace 192.168.1.100 with the server's actual IP address and hawkra.yourcompany.local with the value you set for APP_DOMAIN in your .env file. The docs. subdomain is required for the documentation site.

warning

This must be done before starting the containers. Caddy generates TLS certificates at startup using the configured domain.

Step 9: Set File Permissions

Ensure the license directory is writable and the Caddy entrypoint is executable:

sudo chmod 755 /opt/hawkra/license
sudo chmod +x /opt/hawkra/caddy/docker-entrypoint.sh

If you pre-placed a license file:

sudo chmod 644 /opt/hawkra/license/license.key

Step 10: Open Firewall Ports

Hawkra requires ports 80, 443, and the TalonStrike listener range (4444–4453) to be open for inbound TCP traffic.

ufw (Ubuntu/Debian):

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 4444:4453/tcp

iptables:

sudo iptables -I INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -I INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -I INPUT -p tcp --dport 4444:4453 -j ACCEPT
tip

iptables rules are lost on reboot. Persist them with sudo iptables-save > /etc/iptables/rules.v4 (Debian/Ubuntu) or sudo iptables-save > /etc/sysconfig/iptables (Fedora).

Step 11: Pull Container Images

cd /opt/hawkra
sudo docker compose -f docker-compose.selfhosted.yml pull

This downloads the following images:

  • postgres:16-alpine
  • redis:7-alpine
  • ghcr.io/reconhawk/talon-strike
  • ghcr.io/reconhawk/hawkra-backend
  • ghcr.io/reconhawk/hawkra-frontend
  • ghcr.io/reconhawk/hawkra-docs
  • caddy:2-alpine

Step 12: Start Services

Start all services in detached mode:

cd /opt/hawkra
sudo docker compose -f docker-compose.selfhosted.yml up -d

Step 13: Verify Startup

Check that all services are running:

sudo docker compose -f docker-compose.selfhosted.yml ps

All services should show a status of Up or Up (healthy).

Step 14: Get the Initial Admin Password

On first boot, the backend generates a default admin account with a random password. Retrieve it from the logs:

sudo docker compose -f docker-compose.selfhosted.yml logs backend | grep -i "password"

The default admin account email is admin@hawkra.local.

danger

The admin password is printed to the logs only on first boot. If the database volume already exists from a previous start, the password was logged during that initial run. Save it immediately.

Step 15: Access the Platform

Open your browser and navigate to https://your-domain.com (replacing with your actual APP_DOMAIN value).

  1. If using self-signed certificates, accept the browser security warning.
  2. Log in with admin@hawkra.local and the password from Step 14.
  3. You will be redirected to the License Setup page.
  4. Upload the license file provided with your purchase, or enter your license key.
  5. Click Complete Setup.

The platform is now live. Change the default admin password immediately under Account Settings.

Step 16: Install WiFi Driver (Optional)

This step is only required if you plan to use the WiFi Strike feature. WiFi Strike requires a USB WiFi adapter that supports monitor mode connected to the host machine.

Alfa AWUS036ACH / AWUS036ACM (Realtek)

If you are using an Alfa AWUS036ACH or AWUS036ACM, install the lwfinger/rtw88 driver on the host — follow the build and installation instructions in that repository's README.

Other Adapters -- Install Non-Free Firmware

Many wireless chipsets require non-free firmware packages that are not included in default Linux installations. Enable the non-free repositories and install wireless firmware for your distribution:

Debian:

# Enable non-free and non-free-firmware components
sudo sed -i 's/^\(deb.*main\)$/\1 contrib non-free non-free-firmware/' /etc/apt/sources.list

# For Debian 12+ with deb822 format (.sources files), also run:
sudo sed -i '/^Components:/ s/$/ contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources

# Update and install wireless firmware and tools
sudo apt-get update
sudo apt-get install -y firmware-linux-nonfree firmware-misc-nonfree \
firmware-realtek firmware-atheros firmware-iwlwifi firmware-ralink \
wireless-tools wpasupplicant aircrack-ng iw

Fedora:

# Enable RPM Fusion free and nonfree repositories
sudo dnf install -y \
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm

# Install wireless firmware and tools
sudo dnf install -y iwlax2xx-firmware linux-firmware \
wireless-tools wpa_supplicant aircrack-ng iw
tip

After installing firmware packages, reboot the host or reload the kernel module for your adapter (sudo modprobe -r <module> && sudo modprobe <module>) to pick up the new firmware.

Without a working driver, no wireless interface will be available within WiFi Strike and the pre-flight check will fail.

Troubleshooting

View Logs

# All services
sudo docker compose -f docker-compose.selfhosted.yml logs -f

# Specific service
sudo docker compose -f docker-compose.selfhosted.yml logs -f backend
sudo docker compose -f docker-compose.selfhosted.yml logs -f frontend
sudo docker compose -f docker-compose.selfhosted.yml logs -f docs
sudo docker compose -f docker-compose.selfhosted.yml logs -f caddy
sudo docker compose -f docker-compose.selfhosted.yml logs -f postgres

Common Issues

IssueCauseSolution
Browser shows "connection refused"Containers not runningRun docker compose ps to check status. Wait 30-60 seconds after startup.
Certificate warning in browserSelf-signed certificatesExpected behavior. Accept the warning to proceed.
Caddy fails to startMissing APP_DOMAIN or non-executable entrypointVerify APP_DOMAIN is set in .env. Run chmod +x caddy/docker-entrypoint.sh.
503 on all pagesLicense setup not completeLog in as admin and complete the license setup flow.
Admin password not in logsDatabase volume exists from a prior runThe password is only printed on first boot. Reset with docker compose down -v (destroys all data).
Backend cannot connect to databaseWrong POSTGRES_PASSWORDVerify the password in .env matches. Check: docker compose logs postgres.
CORS errors in browserURL mismatchEnsure CORS_ALLOWED_ORIGINS exactly matches your browser URL including https://.
License upload failsPermission denied on license directoryRun sudo chmod 755 /opt/hawkra/license and restart.
Domain not resolvingMissing /etc/hosts entryAdd the entry on both the server and the client machine.
Nmap scans failMissing kernel capabilitiesVerify the backend service has cap_add: [NET_RAW, NET_ADMIN, NET_BIND_SERVICE].

Restart All Services

cd /opt/hawkra
sudo docker compose -f docker-compose.selfhosted.yml down
sudo docker compose -f docker-compose.selfhosted.yml up -d

Full Reset (Destroys All Data)

cd /opt/hawkra
sudo docker compose -f docker-compose.selfhosted.yml down -v
sudo docker compose -f docker-compose.selfhosted.yml up -d
danger

The -v flag removes all Docker volumes including the database, encryption keys, and uploaded files. All data will be permanently and irreversibly lost. Only use this as a last resort.

Next Steps

After completing the installation:

  1. License Setup -- Activate your instance with your license key.
  2. Configure AI -- Set up a Gemini API key or local LLM through Admin > Settings > AI Configuration.
  3. Configure SMTP -- Enable email features through Admin > Settings > Email (SMTP).
  4. Invite Users -- Add team members through Workspace Settings > Members.
  5. Back Up -- Set up a backup schedule for the database and backend_config volume. See the Overview for critical volume details.