| layout | default |
|---|---|
| title | Docker Installation |
| parent | Installation |
| nav_order | 1 |
{: .no_toc }
The recommended way to run iCAD Dispatch v2. {: .fs-6 .fw-300 }
{: .no_toc .text-delta }
- TOC {:toc}
Think of a Docker container as a boxed-up program that includes everything it needs to run — code, libraries, settings, and dependencies. You don't install Python, PostgreSQL, or anything else manually. Docker handles it all.
iCAD Dispatch uses 3 containers that work together:
What it does: Stores every call, transcript, address, and configuration setting.
Why you need it: Without the database, nothing is saved. The other two containers read from and write to this database.
Image: postgis/postgis:16-3.4 (PostgreSQL 16 with PostGIS extension for map coordinates)
Port: 5432 (inside Docker only — never exposed to the internet)
Data persistence: Uses a Docker volume called postgres_data. Even if you delete the container, the data survives.
Special notes:
- Must start first and be healthy before the other containers start
- Only the other two containers need to talk to it
- Never expose port 5432 to the internet — it's for internal use only
What it does: This is the brain of the operation. It:
- Accepts uploaded radio audio via the
/api/call-uploadendpoint - Detects paging tones
- Transcribes speech using AI
- Extracts addresses from transcripts
- Classifies incident types
- Sends notifications to Discord, Telegram, Email, etc.
- Serves the admin dashboard
Why you need it: This is what you log into to manage everything.
Image: Built from the main Dockerfile in the repo
Port: 9911
Special notes:
- Must be behind a reverse proxy (nginx/Caddy) with HTTPS in production
- Reads environment variables from
.env - Requires
postgresto be healthy before it starts - Needs
PUBLIC_MAP_API_KEYto authenticate with the public map
What it does: Shows a real-time map of emergency calls that anyone can view.
Why you need it: This is what citizens and news outlets see. It displays calls on a map in real-time.
Image: Built from public_map/Dockerfile
Port: 5000
Special notes:
- Completely separate from the main app — it cannot modify anything
- Has read-only database access
- Uses Socket.IO (WebSocket) to push new calls to browsers instantly
- Needs the same
PUBLIC_MAP_API_KEYas the main app - Must also be behind a reverse proxy with HTTPS
Internet
│
▼
┌─────────────────────────────────────────┐
│ Reverse Proxy (Caddy/nginx) │
│ Handles HTTPS and forwards requests │
└─────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ icad_dispatch │ │ public_map │
│ (port 9911) │ │ (port 5000) │
└──────┬───────┘ └──────┬───────┘
│ │
└──────────┬──────────┘
│
┌────────▼────────┐
│ postgres │
│ (port 5432) │
└───────────────────┘
Important: All three containers share the same Docker network. They can talk to each other by name:
icad_dispatchconnects topostgresby hostnamepostgrespublic_mapconnects topostgresby hostnamepostgresicad_dispatchpushes calls topublic_mapvia HTTP tohttp://public_map:5000/api/push-call
Before starting, you need:
- A Linux server (Ubuntu 22.04+ or Debian 12) with a public IP
- Docker 24.0+ and Docker Compose 2.0+
- Git
- A domain name (or subdomain) pointing to your server
- 4 GB RAM, 20 GB disk minimum
- Root or sudo access
docker --version
docker compose versionIf not installed:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back incd ~
git clone https://github.com/YOUR_GITHUB_USERNAME/icad_dispatch_v2.git
cd icad_dispatch_v2What this does: Downloads all the code, configs, and documentation.
cp .env.example .env
nano .envWhat this does: Copies the template to .env and opens it for editing.
Required changes:
| Variable | What it is | Example |
|---|---|---|
BASE_URL |
Your dashboard URL | https://dispatch.yourdomain.com |
TIMEZONE |
Your IANA timezone | America/New_York |
PG_PASSWORD |
Strong PostgreSQL password | your-unique-password-here |
PUBLIC_MAP_API_KEY |
Long random string (shared secret) | Generate with openssl rand -hex 24 |
MAP_SECRET_KEY |
Different long random string | Generate with openssl rand -hex 32 |
ROOT_PASSWORD |
Admin login password | change-me-immediately |
Generate strong secrets:
openssl rand -hex 24 # PUBLIC_MAP_API_KEY
openssl rand -hex 32 # MAP_SECRET_KEYSave in nano:
- Press
Ctrl + O - Press
Enter - Press
Ctrl + X
For full details on all variables, see Environment Variables.
mkdir -p var log audioWhat this does: Creates folders for:
var/— Runtime data (sessions, secret keys)log/— Application logsaudio/— Uploaded radio audio files
These folders are mounted into the containers so data persists even if containers are deleted.
docker compose -f docker-compose.production.yml buildWhat this does:
- Downloads PostgreSQL 16 + PostGIS
- Builds the main app image from
Dockerfile - Builds the public map image from
public_map/Dockerfile
How long it takes: 5–10 minutes on first run (depends on internet speed).
What you'll see:
- Lots of download progress bars
- Python package installation
- "Successfully built icad_dispatch_v2:local"
- "Successfully built icad_public_map:local"
docker compose -f docker-compose.production.yml up -dWhat this does:
- Starts all three containers in the background (
-d= detached) postgresstarts first (it has a health check)icad_dispatchwaits forpostgresto be healthypublic_mapwaits forpostgresto be healthy
docker compose -f docker-compose.production.yml psExpected output:
NAME STATUS PORTS
icad_dispatch_v2-postgres-1 Up 10s (healthy) 0.0.0.0:5432->5432/tcp
icad_dispatch_v2-icad_dispatch-1 Up 10s (healthy) 0.0.0.0:9911->9911/tcp
icad_dispatch_v2-public_map-1 Up 10s 0.0.0.0:5000->5000/tcp
What each column means:
NAME— The container name (auto-generated by Docker Compose)STATUS—Upmeans running,(healthy)means the health check passedPORTS— Which ports are exposed to the host
If any container is not healthy, check its logs:
# Main application logs
docker compose -f docker-compose.production.yml logs -f icad_dispatch
# Public map logs
docker compose -f docker-compose.production.yml logs -f public_map
# Database logs
docker compose -f docker-compose.production.yml logs -f postgresPress Ctrl + C to stop watching logs.
You must use HTTPS in production. Browsers block audio and WebSocket connections over HTTP.
A reverse proxy handles HTTPS for you. It sits in front of your containers and forwards requests.
Caddy automatically gets and renews HTTPS certificates from Let's Encrypt.
sudo apt-get install caddy
sudo nano /etc/caddy/CaddyfilePaste this (replace yourdomain.com):
dispatch.yourdomain.com {
reverse_proxy localhost:9911
}
map.yourdomain.com {
reverse_proxy localhost:5000
}
Save and reload:
sudo systemctl reload caddyWhat this does:
dispatch.yourdomain.com→ forwards to main app on port 9911map.yourdomain.com→ forwards to public map on port 5000- Automatically obtains HTTPS certificates
- Auto-renews certificates before they expire
sudo apt-get install nginx
sudo nano /etc/nginx/sites-available/icadPaste this:
server {
listen 80;
server_name dispatch.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name dispatch.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:9911;
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 $scheme;
}
}
server {
listen 80;
server_name map.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name map.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Enable:
sudo ln -s /etc/nginx/sites-available/icad /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxIf you don't have SSL certificates yet, use Let's Encrypt with Certbot:
sudo apt-get install certbot python3-certbot-nginx
sudo certbot --nginx -d dispatch.yourdomain.com -d map.yourdomain.comCaddy handles this automatically. No extra steps needed.
sudo apt-get install certbot python3-certbot-nginx
sudo certbot --nginx -d dispatch.yourdomain.com -d map.yourdomain.comCertbot will:
- Verify you own the domains
- Obtain certificates from Let's Encrypt
- Configure nginx to use them
- Set up auto-renewal
To update to the latest version:
cd /path/to/icad_dispatch_v2
git pull origin main
docker compose -f docker-compose.production.yml build
docker compose -f docker-compose.production.yml up -dWhat this does:
- Downloads the latest code from GitHub
- Rebuilds the containers with new code
- Restarts everything
Database migrations run automatically on startup. You don't need to do anything.
# View all running containers
docker compose -f docker-compose.production.yml ps
# View logs (follow mode)
docker compose -f docker-compose.production.yml logs -f icad_dispatch
docker compose -f docker-compose.production.yml logs -f public_map
docker compose -f docker-compose.production.yml logs -f postgres
# Restart a specific service
docker compose -f docker-compose.production.yml restart icad_dispatch
docker compose -f docker-compose.production.yml restart public_map
# Stop everything (keeps data)
docker compose -f docker-compose.production.yml down
# Stop and remove volumes (WARNING: deletes database)
docker compose -f docker-compose.production.yml down -v
# Enter database shell
docker exec -it icad_dispatch_v2-postgres-1 psql -U icad -d icad_dispatch
# Backup database
docker exec icad_dispatch_v2-postgres-1 pg_dump -U icad icad_dispatch > backup.sql
# Restore database
cat backup.sql | docker exec -i icad_dispatch_v2-postgres-1 psql -U icad -d icad_dispatchDocker images take up disk space. Clean up old images:
docker system prune -aWarning: This deletes all unused images, containers, and volumes.
Something else is using port 9911:
sudo lsof -i :9911
# Or:
sudo ss -tlnp | grep 9911Fix: Either kill the other process or change the port in docker-compose.production.yml:
ports:
- "9912:9911" # Change 9911 to 9912 on the host sideThen update your reverse proxy to forward to localhost:9912 instead.
The main app starts before the database is ready:
# Check database status
docker compose -f docker-compose.production.yml logs postgres
# If postgres is still starting, wait 30 seconds then restart the main app:
docker compose -f docker-compose.production.yml restart icad_dispatchAlso verify PG_PASSWORD in .env matches the actual database password.
- Check that
PUBLIC_MAP_API_KEYis identical in.envfor both containers - Check the main app logs for push errors:
docker compose -f docker-compose.production.yml logs -f icad_dispatch
- Verify the public map can reach the main app:
docker exec icad_dispatch_v2-public_map-1 curl http://icad_dispatch:9911/health
- Check the container is running:
docker compose -f docker-compose.production.yml ps
- Check logs for errors:
docker compose -f docker-compose.production.yml logs icad_dispatch
- Verify your reverse proxy is configured correctly
- Make sure your domain DNS points to the server IP
- Check firewall rules:
sudo ufw status
For alternative install methods, see Native Installation or One-Click Installer.