Getting started
Docker Compose deployment
Deploy DFIRe from the release-pinned Docker Compose bundle, without the installer.
The installer downloads this same bundle and generates the secrets for you. It never rewrites the Compose files. Deploy by hand when you want to control the configuration yourself, or when you work in an air-gapped network.
Requirements
- Docker Engine 24.0 or later, with the Compose plugin 2.24.4 or later.
curl,tar,openssl, and eithersha256sumorshasum.- PostgreSQL 16 or later. Use a server you manage.
- 4 GB of RAM, 8 GB recommended.
- 20 GB of disk for the application, plus whatever your evidence needs.
Use an external database in production. The bundle can run PostgreSQL in a container, but that database lives in a Docker volume with no backup or availability plan of its own. Treat it as an evaluation convenience.
What runs
The bundle starts five services, or six with the bundled database. Three of them run the same backend image with different commands.
| Container | Image | Purpose |
|---|---|---|
| backend | dfireadmin/dfire-backend | The API and WebSockets on port 8000, and attachment transfers on port 8001 |
| qcluster | dfireadmin/dfire-backend | Background tasks: IOC enrichment, webhook deliveries, backups, scheduled work |
| slack-socket | dfireadmin/dfire-backend | The Slack listener, idle unless you connect Slack |
| frontend | dfireadmin/dfire-frontend | The web interface, and the nginx that routes every path to the backend |
| redis | redis:8-alpine | Cache and message broker |
| db (optional) | postgres:16-alpine | The bundled evaluation database |
Only the frontend publishes a port. Everything else talks over an internal Docker network.
Deploy from a release bundle
DFIRe publishes every release as an immutable set of Compose files at a versioned dfire.fi URL. The bundle pins each container image to the digest tested for that release.
-
Create the installation directory
sudo install -d -o "$(id -un)" -g "$(id -gn)" /opt/dfire cd /opt/dfire -
Download and verify the release
curl -fsSLO https://dfire.fi/release/1.8.0/dfire-1.8.0.tar.gz curl -fsSLO https://dfire.fi/release/1.8.0/SHA256SUMS grep ' dfire-1.8.0.tar.gz$' SHA256SUMS | sha256sum -c - tar -xzf dfire-1.8.0.tar.gzThe same directory, dfire.fi/release/1.8.0/, also holds the loose Compose files, the environment example, the README, the checksums and the supply-chain evidence.
-
Configure the environment
cp .env-example .env chmod 600 .env $EDITOR .envThe file documents every value. The section below covers the settings that decide the shape of the deployment.
-
Validate, pull and start
docker compose config docker compose pull docker compose up -d --wait --wait-timeout 600Compose reads the base file and the database overlay from
COMPOSE_FILEin.env. -
Check both routes
curl -f http://127.0.0.1:8080/health curl -f http://127.0.0.1:8080/api/health/The first checks the frontend. The second checks that the backend reached the database.
Keep the project identity stable. Use the same COMPOSE_PROJECT_NAME and the same installation directory on every update. Compose then recreates only the changed services and keeps the project's named volumes.
A deployment built from these published files can later switch to the installer. Run the current install.sh from the installation directory. It checks the release files, the environment topology, the running Compose project, the version and the volumes before it takes over. A customized Compose deployment is never overwritten, and stays manual.
The environment file
The bundle's .env-example is the reference for that release. For every value it states whether it is required and which containers read it. It also marks the secrets, and the values that must never change.
Five settings decide the shape of the deployment.
COMPOSE_FILE=compose.yaml:compose.external-db.yamlselects your own PostgreSQL. Use this in production.COMPOSE_FILE=compose.yaml:compose.internal-db.yamlselects the bundled evaluation database instead.COMPOSE_PATH_SEPARATOR=:must be present. Compose separates the files inCOMPOSE_FILEwith;on Windows and:elsewhere. Without this line, Compose on Windows reads the whole value as one filename and reports a missing file.COMPOSE_PROJECT_NAMEmust stay unchanged across upgrades, so Compose keeps using the same named volumes.FRONTEND_BINDsets the published port. Use127.0.0.1:8080:80for a reverse proxy on the same host, or0.0.0.0:8080:80for a proxy elsewhere.
Do not add image overrides for the backend or the frontend. The bundle already pins every image to the digest tested for the release.
Back up SECRET_KEY and CREDENTIAL_ENCRYPTION_KEY. Neither may change after installation. A new credential encryption key makes stored secrets such as webhook credentials and license keys unreadable, and makes existing backups unrestorable.
Other container platforms
Not a supported deployment model. DFIRe tests and supports one thing: Docker Compose on a Linux host. It does run on managed container platforms such as Amazon ECS, and customers run it there today. DFIRe still does not test those platforms and cannot support a deployment on one. Treat the notes below as a starting point you own, not as a procedure.
The bundle's .env-example describes the environment the backend, qcluster and Slack listener each need, and marks the few values Compose supplies for them. On another platform you take on the networking, secret injection, persistent storage, service ordering, migrations and reverse proxy yourself.
Two details catch people out, because Compose handles both silently.
The backend and the worker share a filesystem
A chunked upload does not stream straight to its final storage. The backend encrypts each chunk as it arrives and spools it to a file. The background worker later reads that file and moves it to the storage target. Both containers need the same directory, on storage that lasts the life of the upload.
Compose gives them one volume, so the question never comes up. Separate tasks with their own ephemeral disks break every upload, because the worker cannot find what the backend wrote. DFIRe always uses temp_uploads inside the media volume. No setting changes that path.
The backend listens on two ports
Port 8000 serves the API and WebSockets. Port 8001 serves attachment transfers, which are long-running and do not belong on the same server. The frontend container's nginx splits the traffic between them. Anything you put in front of the backend has to do the same, or attachment downloads and direct uploads reach the wrong port.
HTTPS
Serve DFIRe over HTTPS in production. Terminate TLS on your own reverse proxy or load balancer. The bundle does not install or change any proxy or certificate service on the host.
Point an existing proxy at DFIRe
Set these three values in .env:
AUTH_COOKIE_SECURE=True
TRUST_PROXY_HEADERS=true
FRONTEND_BIND=0.0.0.0:8080:80
Use FRONTEND_BIND=127.0.0.1:8080:80 instead when the proxy runs on the same host.
Your proxy must forward to port 8080 on the DFIRe host, set X-Forwarded-Proto: https, and support both WebSocket upgrades and long-running upload and download requests.
Original client addresses do not survive this topology. The frontend nginx replaces X-Real-IP with the address of the machine that connected to it, which is your proxy. Audit entries and rate limits therefore record the proxy rather than the end user. Do not build access controls or investigations on client-IP attribution here.
Run nginx on the host
Install nginx and certbot, get a certificate, then use the configuration below.
sudo apt install nginx certbot python3-certbot-nginx
sudo certbot certonly --standalone -d dfire.example.com
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-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-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;
}
}
Keep the timeouts long. Evidence uploads run for a long time, and WebSocket connections stay open. A short timeout drops both mid-transfer. Set client_max_body_size to at least 4096M, which is what the frontend container allows.
One proxy rule covers every path. The frontend container routes /api/, /ws/, /taxii2/, /misp-feed/, /mcp/, /oidc/ and /admin/ to the backend on its own. TAXII and the MISP feed need no separate proxy configuration.
Bundled database for evaluation
Select the internal overlay in .env, then start the stack as usual.
COMPOSE_PATH_SEPARATOR=:
COMPOSE_FILE=compose.yaml:compose.internal-db.yaml
POSTGRES_DB=dfire
POSTGRES_USER=dfire
POSTGRES_PASSWORD=replace-with-a-strong-password
Evaluation only. This database stores everything in a Docker named volume. Production needs an external PostgreSQL service with its own backup and recovery plan.
Licensing
A new installation runs for 90 days without a license. After that it needs one.
Check that the host can reach the license server before you deploy:
curl https://license.dfire.fi/api/v1/
A reply of {"detail":"Authentication credentials were not provided."} means the server is reachable. DFIRe validates the license itself once it starts.
Setting up a new installation also reads which version of the End User License Agreement is published, so the host needs to reach dfire.fi once:
curl -I https://dfire.fi/eula
The setup screen then shows that version and links to it, and you accept it there. See accepting the agreement offline if the host has no internet access at all.
Air-gapped installations use an offline license instead. Buy a license from the pricing page, then write to [email protected] to convert it. Conversion is free.
Air-gapped deployment
Prepare the bundle and its images on a connected host, then move both through your approved media process. The connected Docker daemon must run the same operating system and CPU architecture as the isolated target. The pull fetches only the platform that daemon selects, and the save exports only what it finds locally.
-
Prepare and pull on a connected system
tar -xzf dfire-1.8.0.tar.gz cp .env-example .env $EDITOR .env docker compose config docker compose pull -
Export every image the bundle selects
docker image save -o dfire-images-1.8.0.tar $(docker compose config --images) -
Transfer and load
Move the verified release archive,
SHA256SUMS, the configured.envand the image archive. Protect.envas a secret.docker image load -i dfire-images-1.8.0.tar tar -xzf dfire-1.8.0.tar.gz chmod 600 .env docker compose up --pull never -d --wait --wait-timeout 600--pull nevermakes a missing or corrupted image fail locally instead of reaching for the registry. Rehearse the whole save, load and start sequence on an equivalent disconnected host before you depend on it. -
Install an offline license
Arrange it with [email protected] before you disconnect the host.
-
Accept the End User License Agreement
The setup screen reads the agreement's version from dfire.fi/eula and an isolated host cannot reach it, so accept the agreement from the command line instead. Read it first, on a machine that has internet access.
docker compose exec backend python manage.py accept_eulaThis prints the version, its publication date and where to read it, and changes nothing. When you are ready:
docker compose exec -it backend python manage.py accept_eula --acceptThe command shows the version again and asks you to confirm it, because it reads the version fresh each time. A host that cannot reach the site gets the agreement the release shipped with instead. The command says so, and warns that a newer version may be published. Answer
yand DFIRe records it against the installation, and the setup screen moves on. Acceptance is recorded for theadminaccount; name a different superuser with--user.
Updates repeat this process with the next release bundle. Slack integration needs an outbound connection and does not work in an isolated network.
Updating
Back up the database, the file storage and .env first. Then run the current installer from the installation directory. To update by hand instead, replace the bundle and run the same three commands you used to deploy:
docker compose config
docker compose pull
docker compose up -d --wait --wait-timeout 600
Do not run docker compose down as part of a routine update. See Updating DFIRe for adoption of an existing layout, manual bundle replacement and recovery.
Admin tasks
Run these from the installation directory.
# Create an administrator account
docker compose exec backend python manage.py createsuperuser
# Open a Django shell
docker compose exec backend python manage.py shell
# Apply database migrations by hand
docker compose exec backend python manage.py migrate
Migrations run automatically when the backend container starts, so the last command is only for recovery.