Skip to main content

Deploying a Siter Environment

This guide walks through deploying a Siter environment using Docker Compose. A standard deployment consists of four services:

  • Siter API — the backend application server
  • Siter UI — the frontend web application
  • PostgreSQL — the primary database (with PostGIS for spatial data)
  • Pufferfish API — the analysis engine for facility evaluation

Optionally, you can also add:

  • MongoDB — enables bug report, test case, and criteria reference features in Pufferfish, as well as MongoDB log shipping
  • Grafana Loki — centralized log aggregation for Pufferfish

Prerequisites

  • Docker and Docker Compose installed on the host machine
  • Access to the Siter container registry (provided by the Siter team)
  • A valid Siter license (provided by the Siter team)

Base Docker Compose Configuration

Below is a base docker-compose.yaml you can use as a starting point. Copy this file and customize the environment variables for your deployment.

networks:
siter:

services:
# ── Siter API (backend) ──────────────────────────────────────────
siter-api:
image: <provided-by-siter-team>/siterapi:latest
ports:
- 5000:5000
networks:
- siter
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://0.0.0.0:5000
- ConnectionStrings__SITER_DB=Host=siter-db;Port=5432;Database=siterdb;Username=siter;Password=CHANGE_ME
- LoginTypes=20
- multiTenant=false
- corsUrls=["https://siter.example.com"]
- AnalysisOptions__AnalysisUrl=http://pufferfish:26667
- AnalysisOptions__DictionaryUrl=http://pufferfish:26667
- Licensing__PublicKey=PROVIDED_BY_SITER_TEAM
- jwt__AuthDomain=siter
- jwt__AuthKey=CHANGE_ME_TO_A_LONG_RANDOM_STRING
- jwt__AuthMinutes=480
- FileStorage__BasePath=/data/storage
volumes:
- siter-storage:/data/storage
depends_on:
siter-db:
condition: service_healthy
pufferfish:
condition: service_started

# ── Siter UI (frontend) ─────────────────────────────────────────
siter-ui:
image: <provided-by-siter-team>/siterui:latest
ports:
- 80:80
networks:
- siter

# ── PostgreSQL + PostGIS (Siter database) ───────────────────────
siter-db:
image: postgis/postgis:18-3.6
networks:
- siter
ports:
- 5432:5432
volumes:
- siter-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=siterdb
- POSTGRES_USER=siter
- POSTGRES_PASSWORD=CHANGE_ME
healthcheck:
test: ["CMD-SHELL", "pg_isready -U siter -d siterdb"]
interval: 5s
timeout: 5s
retries: 5

# ── Pufferfish API (analysis engine) ────────────────────────────
pufferfish:
image: <provided-by-siter-team>/pufferfish:latest
ports:
- 26667:26667
networks:
- siter
environment:
- PINO_LOG_LEVEL=warn

volumes:
siter-data:
siter-storage:
warning

Replace all CHANGE_ME values with secure, unique passwords before deploying. The jwt__AuthKey should be a long, random string used to sign authentication tokens.

Storage and Volumes

Containers are disposable — anything a service writes inside its own filesystem is lost when the container is recreated (every image update recreates containers). All durable state in a Siter deployment therefore lives in mounts, and knowing what lives where is the foundation for backups, upgrades, and host migrations.

The base configuration uses two named volumes:

VolumeMounted atServiceContents
siter-data/var/lib/postgresql/datasiter-dbThe database — projects, features, analysis results, users, licenses. This is the deployment's primary data.
siter-storage/data/storagesiter-apiUploaded files, primarily hosted imagery.

Two optional bind mounts appear elsewhere in this guide, both read-only configuration files: the Active Directory CA certificate and a license file (Licensing__LicenseFilePath).

Named volumes vs. bind mounts

Both make host storage available inside a container; they differ in who manages the location.

  • A named volume (siter-data:/var/lib/postgresql/data) is created and managed by Docker in its own storage area. Docker handles ownership and permissions, the data survives docker compose down and container recreation, and the compose file stays portable because no host paths are hard-coded. Inspect them with docker volume ls and docker volume inspect <name>.
  • A bind mount (./certs/ca-root.crt:/usr/local/share/ca-certificates/ad-ca-root.crt:ro or /mnt/siter/storage:/data/storage) maps an explicit host path into the container. You control exactly where the files live and can read or edit them directly with host tooling, but you also own the consequences: the path must exist, its ownership/permissions must suit the container's user, and the compose file is now tied to that host's layout.

Use a named volume by default for data the services write — the database and the file storage. Use a bind mount for configuration files you maintain on the host (certificates, a license file — mount them :ro), or for written data when you have a specific placement requirement, such as putting imagery storage on a particular disk or network share, or wanting host-level backup tools to see the files directly. Either mount type works for siter-data and siter-storage; the mount type is invisible to Siter itself.

What this means for managing the environment

  • Upgrades are safe by design: pulling new images and running docker compose up -d recreates containers but leaves volumes untouched. Data survives; the containers are rebuilt around it.
  • docker compose down -v deletes named volumes. The -v flag removes the database and all uploaded imagery permanently. Never use it on a production environment. (Bind-mounted paths are never deleted by compose.)
  • Backups: two complementary approaches, protecting against different failures — use both if you can.
    • Block-level snapshots (e.g., EBS snapshots of the disk holding the data) are fast, incremental, and safe to take while the database is running provided the entire data directory lives on the single snapshotted volume — a point-in-time snapshot is crash-consistent, and PostgreSQL recovers from it via WAL replay exactly as from a power loss. Restores are whole-disk and only work with the same PostgreSQL major version.
    • pg_dump is slower but portable: it restores across PostgreSQL versions (this is the upgrade path when the database image moves to a new major), supports selective restore of a single database or table, and reads every row — so silent data corruption fails loudly at backup time instead of at restore time.
    • Whichever you use, back up the siter-storage contents together with the database — a restored database that references imagery files which no longer exist on disk will show those hosted-imagery layers as missing.
  • Moving hosts: bind-mounted paths travel with an ordinary filesystem copy. Named volumes must be exported explicitly (for example, docker run --rm -v siter-data:/from -v $(pwd):/to alpine tar czf /to/siter-data.tar.gz -C /from .) and restored the same way on the new host. If you anticipate frequent migrations, that's a point in favor of bind mounts for the data directories.

Starting the Environment

docker compose up -d

On first startup, Siter will automatically run database migrations to initialize the schema. You can verify the services are running with:

docker compose ps

The API will be available at http://localhost:5000, the UI at http://localhost:80, and the Pufferfish analysis engine at http://localhost:26667.

Air-Gapped Installation

When the deployment host cannot reach the container registry, transfer the images as files with docker save / docker load. The environment runs identically afterward — Docker Compose only pulls images that are missing locally, so once the images are loaded, docker compose up -d never touches the network.

Preparing the transfer (on a machine with registry access)

  1. Pull the exact images the deployment needs — including the PostgreSQL image (the air-gapped host cannot reach Docker Hub either) and MongoDB/Loki if used:

    docker pull <provided-by-siter-team>/siterapi:<version>
    docker pull <provided-by-siter-team>/siterui:<version>
    docker pull <provided-by-siter-team>/pufferfish:<version>
    docker pull postgis/postgis:18-3.6
  2. Export the images into a single archive, then generate a checksum so the transfer can be verified:

    docker save <provided-by-siter-team>/siterapi:<version> \
    <provided-by-siter-team>/siterui:<version> \
    <provided-by-siter-team>/pufferfish:<version> \
    postgis/postgis:18-3.6 | gzip > siter-<version>-images.tar.gz
    sha256sum siter-<version>-images.tar.gz > siter-<version>-images.tar.gz.sha256
  3. Transfer both files to the air-gapped host by whatever channel is available (FTP, physical media).

note

Pin explicit version tags — never :latest. Pinned tags keep the compose file aligned with exactly what was shipped, and prevent Compose from ever attempting a network pull. If the air-gapped host's CPU architecture differs from the machine preparing the transfer, add --platform (e.g., --platform linux/amd64) to each docker pull.

Loading on the air-gapped host

  1. Verify the archive arrived intact, then load it:

    sha256sum -c siter-<version>-images.tar.gz.sha256
    gunzip -c siter-<version>-images.tar.gz | docker load
  2. Confirm the images are present with docker image ls. They keep their original registry-qualified names and tags, so the standard compose file works unchanged — just make sure its image: references match the loaded tags exactly.

  3. Start the environment normally with docker compose up -d. Do not run docker compose pull (or up with --pull always) — there is no registry to pull from.

Upgrades follow the same loop: receive and verify a new archive, docker load it, update the version tags in the compose file, and docker compose up -d. Volumes are untouched (see Storage and Volumes), so data carries forward. Reclaim space from superseded images periodically with docker image prune -a.

Air-gap configuration notes

  • Map background: the default OpenStreetMap basemap loads tiles from the internet and will be blank offline. Use hosted imagery (GeoTIFFs stored on the siter-storage volume) or an internal tile/WMS server via custom imagery instead — see Custom and hosted imagery.
  • Authentication: cloud sign-in providers are unreachable, so use on-premises Active Directory (LoginTypes=32) — the Login Types table lists this as the air-gapped configuration.
  • Licensing: supply the license as a file or environment value rather than relying on any external service — see Licensing.

Environment Variables Reference

All configuration in Siter follows the ASP.NET Core configuration model. Environment variables override values from config files and use double underscores (__) as separators for nested settings (e.g., AnalysisOptions__AnalysisUrl).

Core Application

VariableRequiredDefaultDescription
ASPNETCORE_ENVIRONMENTNoProductionThe application environment name.
ASPNETCORE_URLSNoThe URL(s) for Kestrel to listen on (e.g., http://0.0.0.0:5000).

Database Connection

The database connection can be configured as a full connection string or as individual components. If both are provided, the full connection string takes precedence.

Option A: Full connection string

VariableRequiredDefaultDescription
ConnectionStrings__SITER_DBYes*Full PostgreSQL connection string (e.g., Host=db;Port=5432;Database=siterdb;Username=siter;Password=pass).

Option B: Individual components

VariableRequiredDefaultDescription
SITER_DB_SERVERYes*Database server hostname or IP address.
SITER_DB_USERYes*Database username.
SITER_DB_PWYes*Database password.
SITER_DB_DATAYes*Database name.

* One of Option A or Option B is required.

Database Behavior

VariableRequiredDefaultDescription
SITER_NO_MIGRATIONSNofalseSet to true to disable automatic database migrations on startup. Useful for environments where migrations are applied separately.

Authentication

VariableRequiredDefaultDescription
LoginTypesNo0 (None)Bitmask controlling which authentication methods are enabled. See Login Types below.
jwt__AuthDomainYesThe issuer and audience string used when generating and validating JWT tokens.
jwt__AuthKeyYesThe secret key used to sign JWT tokens. Must be a long, random string.
jwt__AuthMinutesNo480How long (in minutes) JWT tokens remain valid before expiring.

Login Types

LoginTypes is a bitmask (flags enum). Combine values by adding them together.

ValueTypeDescription
1LoginReserved.
2WindowsWindows/Kerberos authentication (NTLM/Negotiate).
4GoogleGoogle OAuth sign-in.
8NoAuthAuto-login without credentials. Development/testing only.
16MicrosoftMicrosoft Entra ID (Azure AD) sign-in.
32Active DirectoryOn-premises Active Directory (LDAP) sign-in.

Common configurations:

ValueMethodsUse Case
12Google + NoAuthDevelopment with quick login
20Google + MicrosoftProduction (typical)
4Google onlyProduction (Google only)
16Microsoft onlyProduction (Microsoft only)
48Microsoft + Active DirectoryProduction (hybrid cloud + on-prem)
32Active Directory onlyAir-gapped / on-premises only

Microsoft Entra ID (Azure AD) SSO

These are required when Microsoft login is enabled (LoginTypes includes 16). See the SSO Setup Guide for detailed instructions on configuring the Azure App Registration.

VariableRequiredDefaultDescription
AzureAd__ClientIdConditionalThe Application (client) ID from your Azure App Registration.
AzureAd__RedirectUriConditionalThe redirect URI configured in your App Registration (must match exactly).
AzureAd__InstanceNohttps://login.microsoftonline.com/The Azure AD authority URL. Change for government clouds (e.g., https://login.microsoftonline.us/).
AzureAd__TenantIdNocommonThe Azure AD tenant ID. Use common for multi-tenant, or a specific tenant GUID to restrict access.

Active Directory (LDAP)

These settings provide default values for Active Directory authentication when no SSO configuration exists for a user's email domain. In most deployments, AD settings are managed per-organization through the Admin > SSO Integrations UI instead.

VariableRequiredDefaultDescription
ActiveDirectory__ServerNoDefault AD domain controller hostname or IP. Only used as fallback when no SSO config matches the user's email domain.
ActiveDirectory__PortNo636LDAP port. Use 636 for LDAPS (recommended) or 389 for plain LDAP.
ActiveDirectory__UseSslNotrueWhether to use TLS for the LDAP connection.
ActiveDirectory__DefaultUpnSuffixNoOptional UPN suffix override. When set, replaces the email domain in the UPN sent to AD (e.g., if users sign in with user@company.com but the AD UPN is user@corp.local).
ActiveDirectory__SkipCertificateValidationNofalseDevelopment only. Set to true to disable TLS certificate validation for LDAP connections. In production, the API container must trust the AD domain controller's CA certificate — see TLS Certificate Setup below.

Active Directory TLS Certificate Setup

AD domain controllers use certificates issued by the organization's internal Certificate Authority (typically AD Certificate Services). The Siter API container must trust this CA for LDAPS connections to succeed.

To configure certificate trust, volume-mount the CA root certificate into the API container and run update-ca-certificates before the application starts:

siter-api:
volumes:
- ./certs/ca-root.crt:/usr/local/share/ca-certificates/ad-ca-root.crt:ro
entrypoint: ["/bin/sh", "-c", "update-ca-certificates && dotnet Siter.Api.dll"]
warning

Do not set ActiveDirectory__SkipCertificateValidation=true in production. This disables certificate validation for all LDAP connections from the API and exposes the connection to man-in-the-middle attacks.

Licensing

Applying and renewing the license itself is covered in the Licensing guide.

VariableRequiredDefaultDescription
Licensing__PublicKeyYesThe public key used to validate your Siter license (provided by the Siter team along with the license itself).
multiTenantNofalseSet to true for multi-tenant deployments where each customer/organization has its own license. Set to false for single-tenant deployments with one system-wide license.
SITER_SYSTEM_LICENSENoSingle-tenant only: the system license text, applied to the Default customer at startup. Alternatives: Licensing__SystemLicenseString (config value) or Licensing__LicenseFilePath (path to a mounted license file). See Licensing.

Analysis Engine (Pufferfish)

The analysis engine (Pufferfish) provides automated facility evaluation capabilities. When running in the same Docker Compose stack, point these at the internal service name (e.g., http://pufferfish:26667).

VariableRequiredDefaultDescription
AnalysisOptions__AnalysisUrlYesURL of the Pufferfish analysis engine (e.g., http://pufferfish:26667).
AnalysisOptions__DictionaryUrlYesURL of the criteria dictionary service. Typically the same as the analysis URL.
AnalysisOptions__UseWktNofalseSet to true to transmit geometry data as WKT instead of WKB format.

File Storage (Hosted Imagery)

Self-hosted imagery (hosted GeoTIFF basemaps) is stored on the API container's disk. The API needs a persistent volume at the storage path — without one, every uploaded image is lost when the container is recreated (existing hosted-imagery layers will show as missing on disk).

Mount a named volume and point FileStorage__BasePath at it, as shown in the base compose file:

siter-api:
environment:
- FileStorage__BasePath=/data/storage
volumes:
- siter-storage:/data/storage
VariableRequiredDefaultDescription
FileStorage__BasePathNostorageDirectory where the API stores uploaded files, including hosted imagery. The default is relative to the application directory inside the container — set an absolute path and back it with a persistent volume in production.
Imagery__MaxFileSizeBytesNo314572800 (300 MB)Maximum size of a single hosted-imagery upload.
Imagery__MaxCustomerStorageBytesNo10737418240 (10 GB)Cumulative hosted-imagery storage cap per customer. Leave unset to keep the default; set empty to disable the cap.
Imagery__GdalEnabledNotrueWhether the API looks for GDAL to auto-convert uploaded GeoTIFFs to Cloud-Optimized GeoTIFF (COG). When GDAL is not available, the server rejects non-COG uploads at upload time with a clear message.
Imagery__GdalPathNogdal_translatePath to the gdal_translate executable. The default resolves on the container's PATH.
Imagery__MaxConcurrentConversionsNo1How many COG conversions may run at once.
Imagery__ConversionTimeoutSecondsNo600Hard timeout for a single COG conversion.

CORS

VariableRequiredDefaultDescription
corsUrlsNoJSON array of allowed CORS origins. Must include the URL where your UI is hosted (e.g., ["https://siter.example.com"]). Required when the UI and API are served from different origins.

Seed Administrator Account

On first startup, you can automatically create a system administrator account.

VariableRequiredDefaultDescription
SITER_SANoCreates a seed SA account. Format: LOGIN::LOGIN_TYPE::DISPLAY_NAME. The LOGIN_TYPE is the numeric value from the Login Types table (e.g., user@example.com::16::Jane Doe for a Microsoft account).

UI and Hosting

These are typically only needed in standalone or self-hosted deployments where the API serves the UI directly.

VariableRequiredDefaultDescription
useDefaultFilesNofalseSet to true to have the API serve static UI files from wwwroot.
wwwRootNoCustom path to the static content folder (when useDefaultFiles is true).
baseHrefNo/Base href for the Angular application.
useResponseCompressionNotrueEnables Brotli/Gzip response compression.

Telemetry and Debugging

VariableRequiredDefaultDescription
EnableTelemetryNotrueEnables internal process statistics collection.
echoRequestsNofalseLogs all incoming HTTP requests to the console. Useful for debugging.
SiterEnableDbContextSensitiveLoggingNofalseEnables Entity Framework sensitive data logging. Do not enable in production as it may log passwords and other sensitive values.

PostgreSQL Container

These variables configure the PostgreSQL container itself (not the Siter application).

VariableRequiredDefaultDescription
POSTGRES_DBYesName of the database to create.
POSTGRES_USERYesDatabase superuser name.
POSTGRES_PASSWORDYesDatabase superuser password.

Pufferfish Container

These variables configure the Pufferfish analysis engine service.

VariableRequiredDefaultDescription
PINO_LOG_LEVELNowarnLogging verbosity. One of debug, info, warn, error.
DB_IPNoHostname of a MongoDB instance. Enables bug report, test case, and criteria reference routes, as well as MongoDB log shipping.
DB_PORTNo27017Port of the MongoDB instance. Only used when DB_IP is set.
LOKI_IPNoHostname of a Grafana Loki instance for centralized log shipping.
LOKI_PORTNoPort of the Loki instance. Required when LOKI_IP is set.
VERSIONNodevelopmentApplication version string (informational).

Configuration Precedence

Siter loads configuration in the following order, where later sources override earlier ones:

  1. appsettings.json (base defaults, built into the image)
  2. appsettings.{ASPNETCORE_ENVIRONMENT}.json (environment-specific overrides)
  3. Environment variables (the primary way to configure deployments)
  4. Command-line arguments

For most deployments, environment variables in the Docker Compose file are the recommended configuration method.

Verifying Your Deployment

After starting the services, verify the deployment:

  1. API health: Navigate to http://localhost:5000/swagger to confirm the API is running.
  2. Database connectivity: Check the API container logs for the DB: log line confirming the connection string.
  3. Analysis engine: Check the Siter API logs for the line listing supported criteria types — this confirms the API can reach Pufferfish. If the engine is unreachable, you'll see a warning — the application will still start but analysis features will be unavailable until the engine is accessible. You can also verify Pufferfish directly at http://localhost:26667.
  4. Authentication: Navigate to the UI and confirm the expected login options appear.
  5. Hosted imagery (if used): Check GET /api/system/info for the server's imagery capability status (whether GDAL conversion is available), and confirm the storage volume is mounted by uploading a test GeoTIFF and recreating the API container — the imagery should survive the restart.