Skip to main content

Self-hosted installation

Resgrid Core is open source and can run on your own hardware. The resgrid-setup repository has four ready-made, tested deployments; pick the one that matches where Resgrid will run:

DeploymentBranchUse it forRuns onNetwork
Single server (this page)masterA department or agency serverOne 64-bit Linux server with DockerLAN or internet
Windows laptoplaptopOne person on one computer: a command post laptop, a disconnected incident, trainingWindows 10/11 + Docker DesktopFully offline after setup; local browser only
Kubernetes / k3smultiProduction across several servers, scaling, maintenance without downtimeA Kubernetes or k3s clusterLAN or internet
Incident Command Kit (RICK)rickA portable, battery-powered kit for a base camp or command vehicle4 × Raspberry Pi 5 + WiFi hotspot routerFully offline; responders' phones join the hotspot

All four run the same Resgrid services and use the same configuration settings; they differ in how the services are spread out and reached. Whichever you choose, also read Certificates and Connecting the mobile apps.

Warranty and operational responsibility

The self-hosted version is provided without warranty or a guarantee of suitability. The supplied deployments are a starting point, not a substitute for capacity planning, security review, monitoring, tested backups and a recovery plan. Never run a real system on the template credentials.

What runs​

Single-server stack: Caddy routes the three Resgrid hostnames to the web app, API and events hub, which use PostgreSQL, Redis, RabbitMQ and RustFS
ServicePurposePort on the server
caddyHTTPS for the three Resgrid hostnames, with automatic certificates80, 443
webThe Resgrid web application5151
apiREST API used by the web app and the mobile apps; also the login (OpenID Connect) server5152
eventsReal-time updates (SignalR)5153
workerBackground jobs, notifications and the database schema migrations—
ttsText-to-speech for dispatch audio5154
mcpModel Context Protocol server for AI assistants5155 (published on every interface; keep it off untrusted networks with a firewall)
dbPostgreSQL 18: resgrid, resgridoidc, resgridworkers, resgriddoc127.0.0.1:5432
redisCache, real-time backplane, web session keys—
rabbitmqMessage bus127.0.0.1:5160 (management UI)
rustfsS3-compatible object storage (TTS audio, imports)—

Optional: offline map tiles (maps profile), hardware GPS trackers (tracking), inbound email dispatch (relay). MongoDB and Microsoft SQL Server are no longer used; every database is PostgreSQL.

db and rabbitmq are bound to 127.0.0.1 by Compose. The others publish their port on every interface of the server, so on an internet-facing host let only 80 and 443 in from the internet (plus 25 if you run the relay), and keep the rest for your LAN or VPN: 5155 for MCP, and 5004, 5023 and 5027 for GPS trackers when those optional services are on.

Requirements​

  • A 64-bit Linux server. Ubuntu Server 24.04 LTS or Debian 12/13 is a good baseline.
  • Docker Engine with the Compose v2 plugin, plus openssl and git (or curl).
  • At least 4 CPU cores, 8 GB RAM (16 GB recommended) and 40 GB of disk. The stack idles at about 1 GB of RAM; size production systems from measured load and retention.
  • Three hostnames that resolve to the server: web, API and events, for example dispatch.example.com, dispatchapi.example.com, dispatchevents.example.com.
  • TCP 80 and 443 to the server.
  • Internet access during installation (about 6 GB of images).
  • An SMTP server or Postmark account if Resgrid must send email.

The infrastructure images are Docker Hardened Images from dhi.io. If a pull reports an authorization error run docker login dhi.io; if Docker Hub reports 429 Too Many Requests, run docker login (anonymous pulls are rate limited).

Step 1: Prepare the server​

Install Docker and check it works:

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER # log out and back in
docker info
docker compose version
openssl version

Create the DNS records for your three hostnames (A records to the server's address). For a LAN-only server without DNS you can use the defaults rg.mylocal, rgapi.mylocal, rgevents.mylocal and add them to each client's hosts file later.

Step 2: Run the installer​

curl -fsSL https://raw.githubusercontent.com/Resgrid/resgrid-setup/master/setup.sh | bash

The installer downloads the stack to ~/resgrid and asks four questions:

==> Configuration (press Enter to accept the defaults).
Web app hostname [rg.mylocal]: dispatch.example.com
API hostname [rgapi.mylocal]: dispatchapi.example.com
Events hostname [rgevents.mylocal]: dispatchevents.example.com
Let's Encrypt email ('internal' = local CA, for LAN/offline) [internal]: admin@example.com
  • Give an email address when the server is reachable from the internet with public DNS: Caddy gets and renews Let's Encrypt certificates for all three hostnames.
  • Answer internal for LAN-only or offline servers: Resgrid's own certificate authority issues the certificates, and you install its certificate on each client once (Certificates).

It then generates every password, passphrase and encryption key into .env, creates the OpenID Connect certificates and the local certificate authority (certs/), pulls the images, starts the stack and waits until Resgrid answers:

==> Generating passwords, keys and certificates...
==> Creating the Resgrid local certificate authority (10 year validity)...
==> Configuration written to /home/admin/resgrid/.env (it holds all your secrets: back it up and keep it private).
==> Pulling container images (the first run downloads several GB)...
==> Starting Resgrid...
==> Waiting for Resgrid to answer (the first start creates the database, this can take 5-10 minutes)...
==> Resgrid is up.
==> Waiting for the database migration to finish...

Last, it walks you through the optional features: modules such as Records, Unified search and Chat that ship switched off. One question per feature turns on every flag, setting and extra service it needs (Optional features and upgrades):

==> Optional features
Resgrid ships some modules switched off. Answer y to turn a feature on for
every department on this server, n to turn it off; Enter takes the answer
in brackets. Run this again any time with: ./setup.sh features

[1/10] Records (RMS) (now: off)
Incident and field records (replaces Logs), records in the Responder, Unit and Dispatch apps,
prevention (occupancy, inspections, hydrants, permits, community risk reduction),
investigations, quality review and records analytics.
After switching it on, a department administrator activates Records in the department settings.
Turn on Records (RMS)? (y/n) [n]: y
...
==> Records (RMS): on
  Open:   https://dispatch.example.com
then choose "Sign Up" to create the first account and department.

Unattended installs can answer every question with a variable:

curl -fsSL https://raw.githubusercontent.com/Resgrid/resgrid-setup/master/setup.sh \
| RESGRID_WEB_HOST=dispatch.example.com \
RESGRID_API_HOST=dispatchapi.example.com \
RESGRID_EVENTS_HOST=dispatchevents.example.com \
RESGRID_TLS=admin@example.com \
RESGRID_FEATURES=records,search \
bash

The install one-liner pipes the script straight into Bash and leaves no setup.sh behind, so use that same form for the first unattended install. The variables go on the bash side of the pipe: assignments apply only to the command they precede, so putting them in front of curl sets them for curl and the installer never sees them. Once the stack is in ~/resgrid, later runs use cd ~/resgrid && RESGRID_FEATURES=... bash setup.sh.

Other variables: RESGRID_INSTALL_DIR, RESGRID_HTTP_PORT / RESGRID_HTTPS_PORT (if 80/443 are taken; Let's Encrypt then can't be used), RESGRID_TLS_CERT_FILE + RESGRID_TLS_KEY_FILE (your own certificate), RESGRID_FEATURES (optional features, comma separated, or all / none), RESGRID_NONINTERACTIVE=1 (never ask), RESGRID_NO_START=1 (configure only).

caution

.env holds every secret, including the encryption key for stored data and the token signing certificates. Back it up somewhere safe and private. Losing it makes encrypted data and existing logins unrecoverable.

Step 3: Create your department​

  1. Open https://<web hostname>. The login page has a Create an account button.

    Resgrid login page with the Create an account button

  2. Fill in your name, email, the department name and type, a user name and a password, then choose Register.

    Sign up form filled in for the first department

  3. You are signed in to your new department as its administrator.

    Resgrid dashboard after signing up

There is no default administrator account; Sign Up creates the department and its first administrator. Self-hosted sign-up is enabled by RESGRID__SystemBehaviorConfig__AllowSignupWithoutBilling=true in .env. Once your department exists, set it to false and run docker compose up -d in ~/resgrid, so nobody else can create departments on your server. Add people with Personnel > Add Person or invitations.

Continue with the department setup guide: stations, units, roles, statuses, and then connect the mobile apps.

Step 4: Check that everything works​

Create a test call: Calls > New Call, enter a name, nature and location, then Create Call. It appears under Active Calls and on the map, and every open browser and app updates in real time.

The Calls page showing a newly created call

From the server:

cd ~/resgrid
docker compose ps # every service "running"; db, redis, rabbitmq "healthy"
docker compose logs -f worker # migrations and background jobs
curl -k https://<api hostname>/health

Configuration​

Everything is in ~/resgrid/.env. Resgrid settings use the pattern RESGRID__{ConfigClass}__{FieldName} and map to the static classes in Core/Resgrid.Config; see the Docker reference. After editing, run docker compose up -d and Compose recreates the containers whose settings changed.

The settings you are most likely to change:

SettingPurpose
RESGRID__OutboundEmailServerConfig__Host, Port, UserName, Password, EnableSsl, FromMailSMTP for invitations, password resets and notifications (no mail server is included)
RESGRID__MappingConfig__LeafletTileUrlMap tiles for the web map. The OpenStreetMap default is for testing only; use a tile provider or offline maps
RESGRID__MappingConfig__GoogleMapsApiKey, What3WordsApiKeyOptional geocoding
RESGRID__MappingConfig__ResponderAppMapboxKey, UnitAppMapBoxKeyMap tiles in the mobile apps (Mapbox)
RESGRID__WebConfig__RecaptchaPublicKey, RecaptchaPrivateKeyreCAPTCHA on Sign Up and Forgot Password for internet-facing servers
RESGRID__SystemBehaviorConfig__AllowSignupWithoutBillingfalse after your department exists

Optional services​

Offline maps (maps profile): import an OpenStreetMap extract for your area once, while online, and the web map no longer needs a tile provider:

cd ~/resgrid
./maps/import.sh https://download.geofabrik.de/north-america/us/nevada-latest.osm.pbf

Pick the smallest Geofabrik extract that covers your area (a US state takes 10–60 minutes). The import adds maps to COMPOSE_PROFILES in .env, so the tile server starts with the stack from then on.

Optional services run while their Compose profile is listed in COMPOSE_PROFILES in .env (comma separated); docker compose up -d reads that setting itself.

Virus scanning (clamav profile): turned on with the Attachment virus scanning optional feature.

GPS trackers (tracking profile): set RESGRID__UnitTrackingConfig__Enabled, NativeGatewayEnabled and at least one of EnableQueclink, EnableGt06, EnableTeltonika to true, add tracking to COMPOSE_PROFILES, then docker compose up -d. Trackers connect to TCP/UDP 5004 (Queclink), 5023 (GT06) and 5027 (Teltonika).

Inbound email dispatch (relay profile): set RESGRID__RELAY__Resgrid__DepartmentId and the dispatch domains (RESGRID__RELAY__Smtp__DepartmentAddressDomains__0, and GroupAddressDomains__0 for group dispatch), point the domains' MX records at the server, add relay to COMPOSE_PROFILES, then docker compose up -d. The relay turns mail addressed to {dispatchcode}@<your dispatch domain> into a Resgrid call; it is not an open mail relay, but it accepts any sender and offers no TLS or SMTP authentication, so the recipient-domain list is its only application-level gate. Restrict port 25 to the senders you expect with a firewall, or put a filtering MTA that terminates TLS in front of it, before exposing it to the internet.

MCP for AI assistants listens on port 5155. Users log in inside the protocol, but the HTTP endpoint itself is unauthenticated, and Compose publishes 5155 on every interface of the server. Block the port from untrusted networks with a host firewall, or reach it over a VPN only.

Operations​

cd ~/resgrid
docker compose ps # status
docker compose logs -f api # logs of one service
docker compose restart web # restart one service
docker compose down # stop everything; data stays in Docker volumes
docker compose up -d # start

Optional features​

./setup.sh features

turns optional modules such as Records, Unified search, Chat, Run cards and virus scanning on or off for every department on the server. See Optional features and upgrades.

Upgrades​

cd ~/resgrid
./setup.sh upgrade

or the install one-liner again. Re-running setup is the upgrade and is always safe: it gets the newest setup files, backs up .env, adds the settings introduced since you installed (new secrets get random values) and repairs missing or broken ones, pulls the new images, starts new services and restarts; the worker migrates the database. It never replaces a value you set or deletes data, and asks only about optional features that are new. Details: Upgrade and repair. Back up first: rolling back after a migration needs the matching database backup.

Backups​

Back up the database, .env, certs/ and the RustFS object storage:

mkdir -p backups
day=$(date +%F)
(
set -e
docker compose exec -T db sh -c 'pg_dumpall -U "$POSTGRES_USER" --clean --if-exists' > backups/resgrid-$day.sql
gzip -f backups/resgrid-$day.sql
cp .env backups/env-$day
tar -czf backups/certs-$day.tar.gz certs
docker compose stop rustfs
docker compose cp rustfs:/data backups/rustfs-$day
)
rc=$?
docker compose start rustfs
[ "$rc" -eq 0 ] || echo "BACKUP INCOMPLETE: see the output above; do not rely on $day"

The subshell and set -e stop at the first failure instead of carrying on to a command that succeeds and leaves a partial backup looking usable; docker compose start rustfs runs outside it so object storage comes back even when the copy fails. Compressing only after pg_dumpall succeeds matters for the same reason: pg_dumpall | gzip returns gzip's status, so a failed dump otherwise leaves a .sql.gz that looks valid and is empty or truncated. Before you rely on a backup, check that all four artifacts for that date exist and are a plausible size.

RustFS holds the TTS audio and import files that database rows point at, so skipping it leaves records referencing objects that no longer exist. The Unified search index is not in the list: the worker rebuilds it.

Restore onto an install with the same .env and certs/:

day=2026-10-27                     # the date of the backup you are restoring
docker compose stop web api events worker tts mcp
gunzip -c backups/resgrid-$day.sql.gz | docker compose exec -T db sh -c 'psql -q -U "$POSTGRES_USER" -d postgres'
docker compose stop rustfs
docker compose cp backups/rustfs-$day/. rustfs:/data
docker compose up -d

Messages that a role or database "already exists" during a restore are expected. Restore the database and the RustFS data from the same day: a database restored on its own points at objects the matching RustFS backup holds.

Upgrading an install created before October 2026​

Run the one-liner or ./setup.sh upgrade as for any upgrade. Setup recognizes the old layout: it keeps the .env that older versions tracked in git across the git pull, renames the NGINX_* settings, switches the services to their new names and ports, and copies the data of installs up to April 2025 (docker-data/sql, PostgreSQL 17) into the new PostgreSQL 18 database, once and only while the new database is empty. The old folder isn't changed and a dump is kept in backups/. See Upgrading an install created before October 2026.

Troubleshooting​

SymptomCause and fix
Certificate warning in the browserinternal certificates: install certs/ca/resgrid-root-ca.crt on the device (Certificates). Accepting the warning for the web hostname alone is not enough, because the web app also calls the API and events hostnames.
Login works but chat, maps or live updates don'tThe API or events hostname doesn't resolve on that client or isn't trusted. Open https://<api hostname>/health and https://<events hostname>/health/GetCurrent from the same device.
Containers restart during the first minutesNormal while PostgreSQL, RabbitMQ and the migrations come up; follow docker compose logs -f worker.
port is already allocated for 80 or 443Another web server (IIS, Apache, nginx) holds the port. Stop it or set RESGRID_HTTP_PORT / RESGRID_HTTPS_PORT.
password authentication failed for user "resgrid"The Docker volumes are from an earlier install with a different .env. Restore that .env, or delete the old data with docker compose down -v (destroys it).
Sign Up sends you back to the login pageRESGRID__SystemBehaviorConfig__AllowSignupWithoutBilling isn't true, or RESGRID__ApiConfig__BackendInternalApikey is empty (run ./setup.sh upgrade).
web, api or events exit mentioning BypassSslChecksIt must be false; the containers trust the local CA instead. ./setup.sh upgrade fixes it.
Worker logs transient_nonexcl_queues is deprecatedRabbitMQ 4 is missing rabbitmq/rabbitmq.conf; use the repository's docker-compose.yml.
Worker logs ConstraintExists is not supported by the connectionless processorRESGRID__DataConfig__CoreConnectionString is empty; set it to the same value as ConnectionString.