Manual Deployment Guide
This guide covers manual Docker deployment for users who prefer not to use the installation wizard, or need custom configurations for air-gapped or specialized environments.
Recommended: For most deployments, use the installation wizard which automates these steps. This manual guide is for advanced configurations or when the wizard isn't suitable.
Technology Stack
DFIRe is built on proven, enterprise-grade technologies:
| Component | Technology |
|---|---|
| Backend | Python / Django REST Framework |
| Frontend | React with TypeScript |
| Database | PostgreSQL 16+ |
| Cache / Message Broker | Redis 7 |
| Web Server | Daphne (ASGI) + Gunicorn (WSGI) with nginx reverse proxy |
| Background Tasks | Django-Q2 |
| Real-time | WebSockets via Django Channels |
Architecture Overview
DFIRe runs as a set of Docker containers:
| Container | Image | Purpose |
|---|---|---|
| backend | dfireadmin/dfire-backend | Daphne ASGI server (port 8000) for API and WebSockets, Gunicorn WSGI server (port 8001) for file downloads |
| qcluster | dfireadmin/dfire-backend | Django-Q2 background task worker |
| slack-socket | dfireadmin/dfire-backend | Slack Socket Mode listener for real-time Slack integration |
| frontend | dfireadmin/dfire-frontend | React application with nginx reverse proxy |
| redis | redis:7-alpine | Cache and message broker |
| db (optional) | postgres:16-alpine | Internal PostgreSQL (testing only) |
External database recommended: For production, use an external PostgreSQL database (self-hosted or managed DBaaS). The internal containerized database is intended for testing only and is more difficult to maintain and back up.
Prerequisites
- Docker Engine 24.0+ with Compose plugin
- openssl for generating security keys
- PostgreSQL 16+ database (external, recommended)
- 4 GB RAM minimum (8 GB recommended)
- 20 GB disk space for application
Manual Deployment Steps
-
Create the installation directory
sudo mkdir -p /opt/dfire cd /opt/dfire -
Create the Docker Compose file
Create
docker-compose.ymlwith the following content:services: redis: image: redis:7-alpine container_name: dfire_redis command: redis-server --appendonly yes ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}} volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped networks: - dfire_internal backend: image: ${DFIRE_BACKEND_IMAGE:-dfireadmin/dfire-backend:latest} container_name: dfire_backend volumes: - media_data:/app/media - static_data:/app/staticfiles environment: - DEBUG=false - SECRET_KEY=${SECRET_KEY} - DATABASE_URL=${DATABASE_URL} - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_PASSWORD=${REDIS_PASSWORD:-} - ALLOWED_HOSTS=${ALLOWED_HOSTS} - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS} - CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS:-} - TRUST_PROXY_HEADERS=${TRUST_PROXY_HEADERS:-false} - DFIRE_ENVIRONMENT=production - CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY} - AUTH_COOKIE_SECURE=${AUTH_COOKIE_SECURE:-True} # Gunicorn workers for download server (WSGI on port 8001) - GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} - DJANGO_SUPERUSER_EMAIL=${DJANGO_SUPERUSER_EMAIL:-} - DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD:-} - DJANGO_SUPERUSER_USERNAME=${DJANGO_SUPERUSER_USERNAME:-} depends_on: redis: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/health/"] interval: 30s timeout: 10s retries: 3 start_period: 60s restart: unless-stopped networks: - dfire_internal - dfire_external qcluster: image: ${DFIRE_BACKEND_IMAGE:-dfireadmin/dfire-backend:latest} container_name: dfire_qcluster command: python manage.py qcluster volumes: - media_data:/app/media environment: - DEBUG=false - SECRET_KEY=${SECRET_KEY} - DATABASE_URL=${DATABASE_URL} - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_PASSWORD=${REDIS_PASSWORD:-} - DFIRE_ENVIRONMENT=production - CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY} depends_on: backend: condition: service_healthy restart: unless-stopped networks: - dfire_internal - dfire_external slack-socket: image: ${DFIRE_BACKEND_IMAGE:-dfireadmin/dfire-backend:latest} container_name: dfire_slack_socket command: python manage.py run_slack_socket volumes: - media_data:/app/media environment: - DEBUG=false - SECRET_KEY=${SECRET_KEY} - DATABASE_URL=${DATABASE_URL} - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_PASSWORD=${REDIS_PASSWORD:-} - DFIRE_ENVIRONMENT=production - CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY} depends_on: backend: condition: service_healthy restart: unless-stopped networks: - dfire_internal - dfire_external frontend: image: ${DFIRE_FRONTEND_IMAGE:-dfireadmin/dfire-frontend:latest} container_name: dfire_frontend ports: - "${FRONTEND_BIND:-0.0.0.0:8080}:80" volumes: - static_data:/app/staticfiles:ro depends_on: - backend healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] interval: 30s timeout: 3s retries: 3 restart: unless-stopped networks: - dfire_internal - dfire_external volumes: redis_data: media_data: static_data: networks: dfire_internal: driver: bridge internal: true dfire_external: driver: bridge -
Generate security keys
Generate the required cryptographic keys:
# Generate Django SECRET_KEY (50 characters) openssl rand -base64 50 | tr -d '\n/+=' | head -c 50 # Generate CREDENTIAL_ENCRYPTION_KEY (Fernet key) openssl rand 32 | base64 | tr '+/' '-_' # Generate REDIS_PASSWORD openssl rand -base64 32 | tr -d '/+=' | head -c 32 -
Create the environment file
Create
.envwith your configuration:# Database (external PostgreSQL - recommended) DATABASE_URL=postgres://username:password@hostname:5432/dfire # Redis REDIS_PASSWORD=your-generated-redis-password # Security Keys (DO NOT CHANGE AFTER DEPLOYMENT) SECRET_KEY=your-generated-secret-key CREDENTIAL_ENCRYPTION_KEY=your-generated-fernet-key # Domain Configuration ALLOWED_HOSTS=dfire.example.com,localhost CORS_ALLOWED_ORIGINS=https://dfire.example.com CSRF_TRUSTED_ORIGINS=https://dfire.example.com # Security Settings AUTH_COOKIE_SECURE=True TRUST_PROXY_HEADERS=false # Docker Images DFIRE_BACKEND_IMAGE=dfireadmin/dfire-backend:latest DFIRE_FRONTEND_IMAGE=dfireadmin/dfire-frontend:latest # Initial Admin User (created on first startup) DJANGO_SUPERUSER_EMAIL=admin@example.com DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_PASSWORD=your-secure-passwordSet secure file permissions:
chmod 600 .env -
Pull and start the containers
docker compose pull docker compose up -d -
Verify the deployment
# Check container status docker compose ps # View logs docker compose logs -f # Test the health endpoint curl http://localhost:8080/api/health/
Environment Variables Reference
Required Variables
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string: postgres://user:pass@host:5432/dbname |
SECRET_KEY |
Django secret key for session signing and CSRF tokens (minimum 32 characters, must differ from CREDENTIAL_ENCRYPTION_KEY) |
CREDENTIAL_ENCRYPTION_KEY |
Fernet key for encrypting stored credentials (webhook secrets, license key, etc.). Cannot be changed after deployment. |
ALLOWED_HOSTS |
Comma-separated list of valid hostnames (e.g., dfire.example.com,localhost) |
CORS_ALLOWED_ORIGINS |
Full URL for CORS (e.g., https://dfire.example.com) |
CSRF_TRUSTED_ORIGINS |
Full URL for CSRF trusted origins (usually same as CORS_ALLOWED_ORIGINS) |
Security Variables
| Variable | Default | Description |
|---|---|---|
AUTH_COOKIE_SECURE |
True | Set to False only for local HTTP testing. Requires HTTPS when True. |
TRUST_PROXY_HEADERS |
false | Set to true when behind a reverse proxy that sets X-Forwarded-Proto. |
REDIS_PASSWORD |
(none) | Password for Redis authentication. |
CSRF_TRUSTED_ORIGINS |
(none) | Also set in Required Variables. Usually same as CORS_ALLOWED_ORIGINS. |
Optional Variables
| Variable | Description |
|---|---|
DJANGO_SUPERUSER_EMAIL |
Admin user email (created on first startup) |
DJANGO_SUPERUSER_USERNAME |
Admin username |
DJANGO_SUPERUSER_PASSWORD |
Admin password (minimum 12 characters) |
GUNICORN_WORKERS |
Number of Gunicorn WSGI workers for the file download server (default: 2). |
FRONTEND_BIND |
Port binding for frontend (default: 0.0.0.0:8080). Use 127.0.0.1:8080 when using a local reverse proxy. |
Critical: The CREDENTIAL_ENCRYPTION_KEY encrypts stored credentials such as webhook secrets and license keys. Back up this key immediately after deployment. If lost, these credentials cannot be recovered and will need to be reconfigured.
HTTPS Configuration
For production deployments, HTTPS is required. You can either:
Option 1: External Reverse Proxy
Use your existing reverse proxy (nginx, Traefik, Caddy, HAProxy, or cloud load balancer) to handle HTTPS termination:
# In .env
AUTH_COOKIE_SECURE=True
TRUST_PROXY_HEADERS=true
FRONTEND_BIND=0.0.0.0:8080
Your reverse proxy must:
- Forward requests to
http://dfire-host:8080 - Set the
X-Forwarded-Proto: httpsheader - Set the
X-Forwarded-Forheader with the client IP
Option 2: nginx with Let's Encrypt
Install nginx and certbot on the host, then configure as a reverse proxy:
# Install packages
sudo apt install nginx certbot python3-certbot-nginx
# Obtain certificate
sudo certbot certonly --standalone -d dfire.example.com
# Configure nginx (see example below)
sudo nano /etc/nginx/sites-available/dfire
Example nginx configuration:
server {
listen 80;
server_name dfire.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name dfire.example.com;
ssl_certificate /etc/letsencrypt/live/dfire.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dfire.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 4096M;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
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 https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
}
location /ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
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 https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
}
Timeouts: The high timeout values (3600s for HTTP, 7 days for WebSocket) are required to prevent connection drops during large file uploads/downloads and to maintain persistent WebSocket connections. The client_max_body_size must be set to at least 4096M to support evidence file uploads up to 4 GB.
TAXII and MISP Feed Paths
If you use the IOC sharing features (TAXII 2.1 server or MISP feed), ensure your reverse proxy forwards these paths to the backend:
/taxii2/- TAXII 2.1 API endpoints (discovery, collections, objects)/misp-feed/- MISP-compatible JSON feed
The default nginx configuration above already handles this since all paths are proxied through the frontend container, which routes /api/, /taxii2/, and /misp-feed/ paths to the backend automatically.
When using a local reverse proxy, bind the frontend to localhost only:
# In .env
FRONTEND_BIND=127.0.0.1:8080
TRUST_PROXY_HEADERS=true
Licensing
DFIRe requires a license for continued operation after the 90-day trial period.
Verifying License Server Connectivity
Before deployment, verify that the server can reach the license server:
curl https://license.dfire.fi/api/v1/
Expected response:
{"detail":"Authentication credentials were not provided."}
This confirms the licensing server is reachable. The actual license validation happens automatically when DFIRe starts.
Offline Licensing
For air-gapped environments without internet access, offline licenses are available:
- Purchase a license from the pricing page
- Contact contact@dfire.fi to convert it to an offline license
- Offline license conversion has no additional cost
Air-Gapped Deployment
DFIRe can operate in isolated networks without internet connectivity. The system is safe to disconnect from the internet after installation.
-
Pull images on a connected system
# Pull all DFIRe images docker pull dfireadmin/dfire-backend:latest docker pull dfireadmin/dfire-frontend:latest docker pull redis:7-alpine docker pull postgres:16-alpine # Only if using internal database # Save images to tar files docker save dfireadmin/dfire-backend:latest -o dfire-backend.tar docker save dfireadmin/dfire-frontend:latest -o dfire-frontend.tar docker save redis:7-alpine -o redis.tar docker save postgres:16-alpine -o postgres.tar -
Transfer files to the air-gapped system
Copy the tar files, docker-compose.yml, and .env to the isolated system via approved media.
-
Load images on the air-gapped system
docker load -i dfire-backend.tar docker load -i dfire-frontend.tar docker load -i redis.tar docker load -i postgres.tar # Only if using internal database -
Obtain an offline license
Before disconnecting, contact contact@dfire.fi to convert your license to an offline license. There is no additional cost for offline licensing.
-
Start services
docker compose up -d
Slack integration is not available in air-gapped environments since it requires an outbound connection to Slack's API. The slack-socket container is included in the Docker Compose file but will simply restart periodically if Slack is not configured. This is harmless and does not affect other services.
Updates: For air-gapped systems, updates must be applied by repeating the image transfer process. Plan for periodic update cycles as part of your maintenance schedule.
Using an Internal Database (Testing Only)
For testing or evaluation, you can run PostgreSQL inside Docker. Add this service to your docker-compose.yml:
services:
db:
image: postgres:16-alpine
container_name: dfire_db
environment:
POSTGRES_DB: dfire
POSTGRES_USER: dfire
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dfire -d dfire"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- dfire_internal
volumes:
postgres_data:
Update the backend and qcluster services to depend on the database:
backend:
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
- DATABASE_URL=postgres://dfire:${POSTGRES_PASSWORD}@db:5432/dfire
Add POSTGRES_PASSWORD to your .env file:
# Generate a password
openssl rand -base64 32 | tr -d '/+=' | head -c 32
Warning: The internal database is for testing only. Data is stored in Docker volumes and can be lost if volumes are removed. For production, always use an external database.
Updating DFIRe
-
Back up your data
Back up your database and the
.envfile before updating. See Backup & Recovery. -
Pull new images
docker compose pull -
Restart services
Database migrations run automatically on startup:
docker compose up -d -
Verify the update
docker compose logs -f backend
Useful Commands
Container Management
# View status
docker compose ps
# View logs (all containers)
docker compose logs -f
# View logs (specific container)
docker compose logs -f backend
# Restart all services
docker compose restart
# Stop all services
docker compose down
# Stop and remove volumes (DELETES DATA)
docker compose down -v
Admin Tasks
# Create admin user manually (if not using env vars)
docker compose exec backend python manage.py createsuperuser
# Open Django shell
docker compose exec backend python manage.py shell
# Run database migrations manually
docker compose exec backend python manage.py migrate