Real-time Emergency Services Dispatch System for Fire, EMS, and Public Safety agencies across North America.
iCAD Dispatch v2 ingests real-time radio audio from emergency services, automatically:
- Detects tones (two-tone paging, long-tone, MDC, DTMF)
- Transcribes speech using Whisper AI
- Extracts addresses from transcripts using LLM + geocoding
- Classifies incidents (Fire, Medical, Traffic, Rescue, etc.)
- Sends notifications to Discord, Telegram, Email, Pushover, n8n, Make, and Ntfy
- Displays calls on a live public map with real-time updates
Built for North American emergency services — fire departments, EMS agencies, and dispatch centers.
iCAD Dispatch runs as 3 separate Docker containers. Think of them as 3 separate programs that talk to each other:
What it does: Stores every call, transcript, address, and configuration.
Why you need it: Without this, nothing is saved. The other two containers read from and write to this database.
Exposed port: 5432 (only inside Docker, not to the internet)
Special notes:
- Uses PostgreSQL 16 with PostGIS (for map coordinates)
- Data is stored in a Docker volume called
postgres_data - Never expose port 5432 to the internet — only the other two containers need it
What it does: This is the brain. It processes radio calls, runs AI transcription, extracts addresses, and sends notifications.
Why you need it: This is what you log into. It has:
- Admin dashboard (port
9911) - Call upload API endpoint
- Tone detection, transcription, geocoding
- Notification dispatch (Discord, Telegram, Email, etc.)
Exposed port: 9911
Special notes:
- Must be behind a reverse proxy (nginx/Caddy) with HTTPS in production
- Requires the
postgrescontainer to be healthy before it starts - Needs environment variables from
.env(see below)
What it does: Shows a real-time map of emergency calls that the public can view.
Why you need it: This is what citizens see. It reads call data from the database and pushes updates to browsers in real-time.
Exposed port: 5000
Special notes:
- Completely separate from the main app — it cannot modify anything
- Has read-only access to the database
- Uses Socket.IO (WebSocket) to push new calls to browsers instantly
- Must also be behind a reverse proxy with HTTPS
- Needs the same
PUBLIC_MAP_API_KEYas the main app (this is how they authenticate to each other)
┌─────────────────────────────────────────────────────────────┐
│ INTERNET │
│ │
│ YOU ──► https://dispatch.yourdomain.com (main app: 9911) │
│ PUBLIC ──► https://map.yourdomain.com (public map: 5000)│
└─────────────────────────────────────────────────────────────┘
│
┌───────▼───────┐
│ Reverse Proxy │ ← nginx or Caddy handles HTTPS
│ (port 443) │
└───────┬───────┘
│
┌───────────────┼───────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ icad_dispatch│ │ public_map │ │ postgres │
│ (port 9911) │ │ (port 5000)│ │ (port 5432) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────┴───────────────┘
│
All three containers
share the same Docker network
(they can talk to each other by name)
Key point: The main app (icad_dispatch) pushes new calls to the public map. The public map polls the database as a backup. The database is the single source of truth.
- A Linux server (Ubuntu 22.04+ or Debian 12) with a public IP
- Docker and Docker Compose installed
- A domain name pointing to your server (e.g.,
dispatch.yourdomain.comandmap.yourdomain.com) - Basic command-line knowledge (copy/paste commands)
If you don't have Docker yet, run this:
# Ubuntu / Debian
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in, or run: newgrp dockerVerify Docker is working:
docker --version
docker compose versioncd ~
git clone https://github.com/YOUR_GITHUB_USERNAME/icad_dispatch_v2.git
cd icad_dispatch_v2cp .env.example .env
nano .envYou MUST change these 6 values:
| Variable | What it is | Example |
|---|---|---|
BASE_URL |
Your dashboard URL | https://dispatch.yourdomain.com |
TIMEZONE |
Your timezone | America/New_York |
PG_PASSWORD |
Database password | MyVerySecretPassword123! |
PUBLIC_MAP_API_KEY |
Secret key shared between containers | openssl rand -hex 24 |
MAP_SECRET_KEY |
Secret for the public map | openssl rand -hex 32 |
ROOT_PASSWORD |
Your admin login password | MyAdminPassword456! |
Generate random secrets:
openssl rand -hex 24 # Use this for PUBLIC_MAP_API_KEY
openssl rand -hex 32 # Use this for MAP_SECRET_KEYSpecial note on PUBLIC_MAP_API_KEY:
This is a shared password between the main app and the public map. Both containers must have the exact same value. If they don't match, the public map won't receive new calls.
docker compose -f docker-compose.production.yml up -dThis will:
- Download PostgreSQL 16 + PostGIS
- Build the main app container
- Build the public map container
- Start all three in the background
First run takes 5–10 minutes because it downloads and builds everything.
docker compose -f docker-compose.production.yml psYou should see:
NAME STATUS PORTS
icad_dispatch_v2-postgres-1 Up (healthy) 5432/tcp
icad_dispatch_v2-icad_dispatch-1 Up (healthy) 0.0.0.0:9911->9911/tcp
icad_dispatch_v2-public_map-1 Up 0.0.0.0:5000->5000/tcp
If any container says Exited or Restarting, check the logs:
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 postgresYou cannot use HTTP in production. Browsers block audio and WebSocket over HTTP. You need a reverse proxy.
sudo apt-get install caddy
sudo nano /etc/caddy/CaddyfilePaste this (replace yourdomain.com with your actual domain):
dispatch.yourdomain.com {
reverse_proxy localhost:9911
}
map.yourdomain.com {
reverse_proxy localhost:5000
}
Reload Caddy:
sudo systemctl reload caddyCaddy automatically gets HTTPS certificates from Let's Encrypt. Zero configuration.
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;
}
}
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 nginx- Open
https://dispatch.yourdomain.comin your browser - Login:
- Username:
root - Password: The
ROOT_PASSWORDfrom your.envfile
- Username:
- Immediately change the password via the user menu (top right)
- Add Your Radio System — Configure tone detection and upload settings
- Enable Address Extraction — Set up geocoding for your region
- Set Up Notifiers — Discord, Telegram, Email, etc.
- Test Upload — Send a test call
- Security Hardening — Firewall, fail2ban, etc.
| Feature | Description |
|---|---|
| Tone Detection | Automatic two-tone, long-tone, MDC, and DTMF detection |
| AI Transcription | OpenAI Whisper local or API-based speech-to-text |
| Address Extraction | LLM-powered address parsing with Nominatim + Google Maps geocoding |
| Incident Classification | AI-categorized incident types with confidence scores |
| Multi-Channel Alerts | Discord, Telegram, Email, Pushover, n8n, Make, Ntfy |
| Live Public Map | Real-time WebSocket map with dark mode, filters, and audio playback |
| Map Corrections | Drag-and-drop location correction via dashboard |
| Call History | Searchable database with configurable retention |
| Security | Rate limiting, CSRF protection, path traversal prevention |
- Quick Start — Full 15-minute setup guide
- Docker Installation — Detailed Docker guide with every command explained
- One-Click Installer — Automated install script
- Environment Variables — Every config option explained
- Geocoding Setup — Nominatim + Google Maps configuration
- Notifier Setup — Discord, Telegram, Email, Pushover, n8n, Make, Ntfy
- Public Map — Live map configuration
- Security — Hardening checklist
- API Reference — REST API documentation
- Troubleshooting — Common issues and fixes
| Component | Minimum | Recommended |
|---|---|---|
| OS | Ubuntu 22.04 / Debian 12 | Ubuntu 24.04 LTS |
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8 GB |
| Disk | 20 GB SSD | 50 GB SSD |
| Network | Public IP or reverse proxy | Dedicated server / VPS |
| Docker | 24.0+ | Latest |
MIT License — free for personal and commercial use.
Built with ❤️ for first responders everywhere.