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
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
| Service | Image | Purpose |
|---|---|---|
postgres | postgres:16-alpine | Application database with health check |
redis | redis:7-alpine | Caching and job queue with health check |
talon-strike-cache | ghcr.io/reconhawk/talon-strike | Pre-caches the TalonStrike pentest container image (exits immediately) |
backend | ghcr.io/reconhawk/hawkra-backend | API server with nmap capabilities |
frontend | ghcr.io/reconhawk/hawkra-frontend | Web application |
docs | ghcr.io/reconhawk/hawkra-docs | Documentation site |
caddy | caddy:2-alpine | Reverse 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
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:
- Ensure ports 80 and 443 are reachable from the internet
- Add the following to your
.envfile:
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.
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
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-alpineredis:7-alpineghcr.io/reconhawk/talon-strikeghcr.io/reconhawk/hawkra-backendghcr.io/reconhawk/hawkra-frontendghcr.io/reconhawk/hawkra-docscaddy: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.
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).
- If using self-signed certificates, accept the browser security warning.
- Log in with
admin@hawkra.localand the password from Step 14. - You will be redirected to the License Setup page.
- Upload the license file provided with your purchase, or enter your license key.
- 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
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
| Issue | Cause | Solution |
|---|---|---|
| Browser shows "connection refused" | Containers not running | Run docker compose ps to check status. Wait 30-60 seconds after startup. |
| Certificate warning in browser | Self-signed certificates | Expected behavior. Accept the warning to proceed. |
| Caddy fails to start | Missing APP_DOMAIN or non-executable entrypoint | Verify APP_DOMAIN is set in .env. Run chmod +x caddy/docker-entrypoint.sh. |
| 503 on all pages | License setup not complete | Log in as admin and complete the license setup flow. |
| Admin password not in logs | Database volume exists from a prior run | The password is only printed on first boot. Reset with docker compose down -v (destroys all data). |
| Backend cannot connect to database | Wrong POSTGRES_PASSWORD | Verify the password in .env matches. Check: docker compose logs postgres. |
| CORS errors in browser | URL mismatch | Ensure CORS_ALLOWED_ORIGINS exactly matches your browser URL including https://. |
| License upload fails | Permission denied on license directory | Run sudo chmod 755 /opt/hawkra/license and restart. |
| Domain not resolving | Missing /etc/hosts entry | Add the entry on both the server and the client machine. |
| Nmap scans fail | Missing kernel capabilities | Verify 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
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:
- License Setup -- Activate your instance with your license key.
- Configure AI -- Set up a Gemini API key or local LLM through Admin > Settings > AI Configuration.
- Configure SMTP -- Enable email features through Admin > Settings > Email (SMTP).
- Invite Users -- Add team members through Workspace Settings > Members.
- Back Up -- Set up a backup schedule for the database and
backend_configvolume. See the Overview for critical volume details.