The production deployment packages every GeoSentinel service into a dedicated Docker container coordinated byDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/danizd/GeoSentinel/llms.txt
Use this file to discover all available pages before exploring further.
docker-compose.prod.yml. Unlike the local development setup — which runs PostgreSQL in Docker and everything else directly on the host — production runs all four application services as containers on a shared internal network (geosentinel_net) and connects them to a pre-existing PostgreSQL instance on the external database_network. Inbound HTTPS traffic is handled by Nginx Proxy Manager, which terminates SSL and forwards requests to the frontend container. The frontend’s bundled Nginx configuration then reverse-proxies /v1/ API calls to the backend container internally.
Architecture
The four production containers and their roles:| Service | Internal Port | Container Name | Description |
|---|---|---|---|
backend | 8000 | geosentinel-backend | FastAPI application serving incidents, AOI, corrections, admin, and health endpoints |
ais-relay | 8003 | geosentinel-ais | WebSocket AISStream client with automatic mock fallback for vessel tracking |
military-relay | 8002 | geosentinel-military | FastAPI relay filtering military flights via OpenSky Network; auto-updates hex database at startup |
frontend | 80 (external: 9090) | geosentinel-frontend | Nginx serving the compiled React bundle; proxies /v1/, /docs, and /openapi.json to the backend |
geosentinel_net (a bridge network created by Compose). The backend and military-relay services additionally join database_network (an external network) to reach the PostgreSQL container. Only the frontend container publishes a host port (9090:80); all other inter-service communication is internal.
Prerequisites
Before running the production deployment, ensure the following are in place on your server:- PostgreSQL 16 + PostGIS 3.4 already running as a container named
postgreson the Docker networkdatabase_network. Thedatabase_networkmust be declared as an external network in your PostgreSQL compose stack. - Nginx Proxy Manager managing SSL certificates and routing for your domain (e.g.
geosentinel.movilab.es). - Docker Engine and Docker Compose v2 installed (
docker compose version≥ 2.0). - A populated
.env.productionfile at the project root (loaded by all services viaenv_file: .env.productionin the compose file). See the Environment Variables reference. - Your
VITE_MAPBOX_TOKENavailable as a shell variable at build time — it is baked into the React bundle during the Docker image build.
Deployment Steps
Create the GeoSentinel database (first time only)
Connect to the running PostgreSQL container and create the database. This step only needs to be done once:
The PostgreSQL superuser in the production container is
admin (not postgres as in the dev compose file). Adjust the -U flag if your setup differs.Build and start all services
Pass Compose will:
VITE_MAPBOX_TOKEN as a shell variable so Compose forwards it as a Docker build argument to the frontend image. The --build flag ensures all images are rebuilt from the current source:- Build
docker/Dockerfile.backend(Python 3.12-slim +uv-installed dependencies) - Build
docker/Dockerfile.ais-relay(Python 3.12-slim + FastAPI + websockets) - Build
docker/Dockerfile.military-relay(Python 3.12-slim + FastAPI + the bundleddata/military_hex.txt) - Build
docker/Dockerfile.frontend(Node 20-alpine build stage → nginx:alpine runtime;VITE_MAPBOX_TOKENbaked in at this step)
frontend service has a depends_on: backend: condition: service_healthy guard — it will not start until the backend passes its healthcheck (GET /v1/health).Run database migrations
Apply all Alembic migrations to the production database:This runs inside the backend container where Alembic and the database connection string from
.env.production are already configured. Re-running is safe — Alembic skips already-applied revisions.Seed initial data
Insert the Alternatively, once the stack is reachable via your domain, you can call the seed endpoint directly:
sources_metadata records and three sample incidents (conflict, earthquake, wildfire) needed to validate the frontend is rendering correctly:Verify the deployment
Check that the backend is healthy from inside the container (no external network required):Expected output: Then open
{"status":"healthy"}Verify the frontend Nginx container is serving the React app:https://geosentinel.movilab.es in a browser to confirm end-to-end routing through NPM → frontend nginx → backend.Nginx Proxy Manager Configuration
Create a Proxy Host entry in NPM pointing to the frontend container. Because the frontend container publishes port9090 on the Docker host, NPM connects to the host via the Docker bridge gateway IP:
| Setting | Value |
|---|---|
| Domain Names | geosentinel.movilab.es (or your domain) |
| Scheme | http |
| Forward Hostname / IP | 172.17.0.1 (Docker bridge gateway) |
| Forward Port | 9090 |
| Cache Assets | Off |
| Block Common Exploits | On |
| SSL Certificate | Let’s Encrypt (request via NPM) |
| Force SSL | ✅ Enabled |
| HTTP/2 Support | ✅ Enabled |
172.17.0.1 is the default Docker bridge gateway address. If you have customised Docker’s default bridge network or are using a different bridge, verify the correct address with ip route | grep docker or docker network inspect bridge.Nginx Reverse Proxy
The frontend Docker image copiesdocker/nginx/geosentinel.conf into the Nginx container at /etc/nginx/conf.d/default.conf. This configuration handles all reverse-proxying internally, so no Custom Locations are needed in NPM:
| Location | Proxied To | Purpose |
|---|---|---|
/v1/ | http://backend:8000 | All REST API calls (incidents, AOI, health, seed, military-flights) |
/docs | http://backend:8000 | Swagger UI |
/openapi.json | http://backend:8000 | OpenAPI schema for Swagger |
/ | Static files → index.html | React SPA with client-side routing |
*.js, *.css, images, fonts | Static files | 1-year cache with immutable header |
X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto headers to the backend and allows a generous proxy_read_timeout of 300 seconds for long-running ingestor calls.
