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) 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 Django REST API with Daphne ASGI server
qcluster dfireadmin/dfire-backend Django-Q2 background task worker
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

  1. Create the installation directory
    sudo mkdir -p /opt/dfire
    cd /opt/dfire
  2. Create the Docker Compose file

    Create docker-compose.yml with 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}
          - 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
    
      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
  3. 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
  4. Create the environment file

    Create .env with 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-password

    Set secure file permissions:

    chmod 600 .env
  5. Pull and start the containers
    docker compose pull
    docker compose up -d
  6. 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 (50+ characters)
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)

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) Full URL for CSRF trusted origins (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)
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: https header
  • Set the X-Forwarded-For header 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 100M;

    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";
    }
}

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 30-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:

  1. Purchase a license from the pricing page
  2. Contact contact@dfire.fi to convert it to an offline license
  3. 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.

  1. 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
  2. Transfer files to the air-gapped system

    Copy the tar files, docker-compose.yml, and .env to the isolated system via approved media.

  3. 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
  4. 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.

  5. Start services
    docker compose up -d

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

  1. Back up your data

    Back up your database and the .env file before updating. See Backup & Recovery.

  2. Pull new images
    docker compose pull
  3. Restart services

    Database migrations run automatically on startup:

    docker compose up -d
  4. 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