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
-
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} - 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 -
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 (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: 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 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:
- 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
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