Skip to main content

Self-Hosting

CDT is distributed as a containerized stack. All services start in the correct order via a single docker-compose.yml file — no manual orchestration required.

Deployment Scenarios

Before you begin, decide how CDT will be accessed. The setup steps are the same in both cases, but the environment configuration differs.

Local / Single-User (localhost)

Run CDT on your own machine for personal evaluation or development. All services are only reachable from that machine — no network exposure, no DNS work required. Use http://localhost URLs throughout your .env.

Organization / Multi-User

Deploy CDT on a server so that multiple people can access it through a browser. This requires:

  1. A host machine with a stable IP address — a server on your internal network, or a cloud VM (Azure, AWS, GCP, DigitalOcean, etc.)
  2. Three subdomains pointing at that server — CDT runs three browser-accessible services (the app, the tile server, and file storage), each assigned its own subdomain. All three point to the same server IP; the reverse proxy routes traffic to the right container based on the hostname. See DNS Configuration below.
  3. A reverse proxy — routes incoming HTTPS requests to the right container and handles TLS certificates automatically. See Reverse Proxy below.
  4. Updated environment variables — swap all localhost URLs in your .env for the real subdomains. See the notes in Environment Configuration below.

Prerequisites

These instructions assume you already have a host environment ready to run containers — either a Linux/Windows server you control, or a cloud platform such as Azure, AWS, GCP, or DigitalOcean with a virtual machine provisioned and accessible over SSH. CDT does not prescribe a specific cloud provider; any host that can run Docker Engine 24.0+ (or Podman v5.7.1) and Docker Compose will work.

If you are evaluating providers or sizing a VM, the stack runs comfortably on a 4 vCPU / 8 GB RAM instance with at least 50 GB of persistent disk for typical pilot deployments. Production workloads with large point cloud or BIM datasets require additional storage proportional to your asset volume.


Stack Components

ServicePurpose
postgresPostgreSQL 15 with PostGIS 3.4 — relational and geospatial data
minioObject storage for BIM, point clouds, and GIS files
martinVector tile server consuming PostGIS spatial tables
cdtCore application — API, business logic, and web UI
minio-initOne-time init job: creates the public buckets required for the app to function
migrateOne-time init job: applies pending database migrations on startup

Init Services

Two short-lived services run automatically on every docker compose up and exit when their work is done:

  • minio-init — waits for MinIO to be healthy, then creates the public buckets required for the app to function. Safe to re-run; it skips creation for any bucket that already exists.
  • migrate — waits for PostgreSQL to be ready, then runs prisma migrate deploy to apply any pending schema migrations. This is idempotent and safe across upgrades.

Container Engine Options

Linux headless / server: Install Docker Engine and Compose CLI: docs.docker.com/engine/install

Linux GUI or cross-platform: Install Docker Desktop (includes Engine + Compose): docs.docker.com/desktop/setup/install/linux

Option 2 — Podman

If Docker is not permitted on your infrastructure, Podman is a compatible alternative.

The docker-compose.yml included with CDT has already been adapted for Podman: volume mounts use the :Z SELinux relabeling flag required on Fedora/RHEL systems. You should be able to run podman-compose up -d with the same .env setup described below.

That said, Podman compatibility is best-effort. Networking behavior, depends_on condition handling, and rootless container permissions can differ between Podman versions and Linux distributions. If you encounter issues, consult the Podman Compose documentation for your specific environment.


Environment Configuration

Before starting the stack, create a .env file in the project root. You can use .env.example as a starting point:

cp .env.example .env

Then fill in the values described in the sections below.

Organization deployments: anywhere a variable takes a localhost URL in the examples below, replace it with the corresponding subdomain you configured in DNS Configuration.

Required Variables

These must be set before the stack will start correctly.

Authentication

VariableDescription
AUTH_SECRETRandom secret used to sign session tokens. Generate with: openssl rand -base64 32
AUTH_TRUST_HOSTSet to true when running behind a reverse proxy or on a non-localhost domain
AUTH_URLThe public URL of your CDT deployment — http://localhost:6012 for local use, or https://cdt.yourorg.com for org deployments

Database

VariableDescription
POSTGRES_USERPostgreSQL superuser username
POSTGRES_PASSWORDPostgreSQL superuser password
POSTGRES_DBDatabase name
DATABASE_URLFull Prisma connection string, e.g. postgresql://user:pass@postgres:5432/db

Object Storage (MinIO)

VariableDescription
MINIO_ROOT_USERMinIO admin username
MINIO_ROOT_PASSWORDMinIO admin password
S3_ACCESS_KEYAccess key for the app to authenticate with MinIO (set equal to MINIO_ROOT_USER for self-hosted)
S3_ACCESS_SECRETSecret key for the app to authenticate with MinIO (set equal to MINIO_ROOT_PASSWORD for self-hosted)
MINIO_ENDPOINTMinIO host as seen from inside the Docker network — always minio:9000
MINIO_USE_SSLtrue if MinIO is behind TLS, false for local/internal setups
MINIO_REGIONRegion string, e.g. us-east-1 (arbitrary for self-hosted MinIO)
MINIO_URLServer-side URL the app uses to reach MinIO — always http://minio:9000/ (internal Docker network address)
NEXT_PUBLIC_MINIO_BUCKET_URLPublic URL browsers use to load assets — http://localhost:9000 for local use, or https://files.yourorg.com for org deployments
ALLOWED_ORIGINThe CDT app URL, used to configure MinIO's CORS policy so the browser can upload and download files. Set to your AUTH_URL value (e.g. http://localhost:6012 or https://cdt.yourorg.com). Defaults to * if unset, which is fine for local testing but should be locked down for org deployments.

Local deployments on Mac or Windows: NEXT_PUBLIC_MINIO_BUCKET_URL is used by browsers to load files directly from MinIO. On Mac and Windows, Docker Desktop runs containers in a Linux VM, so localhost inside a container refers to that VM — not your machine. If you see file-loading errors in the browser when running locally, set NEXT_PUBLIC_MINIO_BUCKET_URL=http://host.docker.internal:9000 instead of http://localhost:9000. host.docker.internal is a special hostname that Docker Desktop resolves to your actual host machine from inside any container.

reCAPTCHA

CDT uses Google reCAPTCHA v2 on login and registration forms. Register your domain at google.com/recaptcha/admin to obtain keys.

Note for local deployments: reCAPTCHA only validates requests from registered domains. For localhost testing, add localhost as an allowed domain in your reCAPTCHA console settings.

VariableDescription
NEXT_PUBLIC_RECAPTCHA_SITE_KEYPublic site key (sent to the browser)
RECAPTCHA_SECRET_KEYPrivate secret key (server-side verification only — never expose this)

Email / SMTP

CDT sends one-time passcodes for multi-factor authentication via email. You must configure an SMTP relay for logins to work.

VariableDescription
EMAIL_HOSTSMTP server hostname, e.g. smtp.example.com
EMAIL_PORTSMTP port, typically 465 (SSL) or 587 (STARTTLS)
EMAIL_FROMThe sender address that appears on outgoing emails
EMAIL_USERSMTP authentication username
EMAIL_PASSSMTP authentication password

Optional Variables

Google OAuth (account linking and social login)

If you want users to sign in with a Google account, create an OAuth 2.0 client in the Google Cloud Console and add your deployment's callback URL (<AUTH_URL>/api/auth/callback/google) as an authorized redirect URI.

Note for org deployments: Google OAuth does not accept localhost as a redirect URI. You must register your real domain (e.g. https://cdt.yourorg.com/api/auth/callback/google) in the Google Cloud Console.

Leave these unset to disable Google login entirely.

VariableDescription
AUTH_GOOGLE_IDGoogle OAuth client ID
AUTH_GOOGLE_SECRETGoogle OAuth client secret

The map's search bar resolves addresses and place names through a geocoding provider. CDT selects one from the environment, so a self-hosted deployment can run entirely key-free if preferred. Providers are tried in priority order:

PriorityWhenProvider
1NEXT_PUBLIC_GEOCODE_EARTH_API_KEY is setGeocode Earth — hosted Pelias, highest-quality results
2NEXT_PUBLIC_GEOCODER_URL is setYour own Pelias instance — no key, identical response format
3neither is setFree public OpenStreetMap services — Photon for autocomplete, Nominatim for reverse geocoding
VariableDescription
NEXT_PUBLIC_GEOCODE_EARTH_API_KEYGeocode Earth API key. Takes priority when set.
NEXT_PUBLIC_GEOCODER_URLBase URL of a self-hosted Pelias-compatible endpoint, e.g. https://pelias.example.com. No key required.
NEXT_PUBLIC_PHOTON_URLOverrides the Photon autocomplete endpoint. Defaults to the public https://photon.komoot.io.
NEXT_PUBLIC_NOMINATIM_URLOverrides the Nominatim reverse-geocoding endpoint. Defaults to the public https://nominatim.openstreetmap.org.

Production note: the public Photon and Nominatim instances are community-run and rate-limited — fine for evaluation and small pilots, but heavier deployments should run their own Pelias (single provider, best quality) or self-host Photon/Nominatim and point the URLs above at them.


Starting the Full Stack

From the directory containing docker-compose.public.yml and your .env file:

docker compose -f docker-compose.public.yml up -d

This pulls the pre-built CDT images from the GitHub Container Registry and starts all services. No source code or build tooling is required.


Instance Initialization

Once the stack is running, open your CDT URL in a browser (http://localhost:6012 for local deployments, or your configured domain for org deployments). If no organization has been set up yet, you will be redirected to the setup page at /setup.

Fill in the initialization form:

FieldDescription
Org name (slug)A short URL-safe identifier for your organization, e.g. my-org
Org titleThe display name for your organization
Org descriptionA short description shown in the UI
LanguagesComma-separated language codes your org will use, e.g. En, Fr
Admin nameFull name for the initial admin account
Admin emailEmail address for the initial admin account
Admin passwordPassword for the initial admin account (minimum 8 characters)

After submitting, you will be redirected to the sign-in page for your organization. The setup page is only accessible when no organization exists — once initialized, it redirects to sign-in automatically.

Change the admin password immediately after your first login if you shared the credentials with anyone else during setup.


Port Map

ServiceHost PortContainer Port
PostgreSQL54335432
MinIO API90009000
MinIO Console90019001
Martin (tiles)60803000
CDT Application60123000

For local deployments, these ports are accessed directly (e.g. http://localhost:6012). For org deployments behind a reverse proxy, these ports stay internal to the host — only ports 80 and 443 are exposed to the network, and the proxy routes traffic to the right container.


DNS Configuration

This section applies to organization deployments only. For local use, skip to Starting the Full Stack.

CDT has three browser-accessible services, each assigned its own subdomain. All three subdomains point to the same server IP — it is one machine running all three containers. The reverse proxy (configured in the next section) reads the hostname on each incoming request and routes it to the correct container.

Create three DNS A records at your registrar or DNS provider (Cloudflare, Route 53, etc.):

RecordTypeValue
cdt.yourorg.comA<your server's public IP>
tiles.yourorg.comA<your server's public IP>
files.yourorg.comA<your server's public IP>

Replace yourorg.com with your actual domain. The subdomain names (cdt, tiles, files) are conventions — use whatever names make sense to you, as long as they match the URLs you set in your .env.

Internal/VPN-only deployments: if CDT should only be reachable within your organization's network, use your internal DNS server to create the same records pointing to the server's private IP instead. Users on the LAN or VPN will resolve the names without any public DNS entry.


Reverse Proxy

This section applies to organization deployments only. For local use, skip to Starting the Full Stack.

A reverse proxy sits in front of the Docker stack and handles two things: routing incoming requests to the right container based on the hostname, and terminating HTTPS/TLS so that all traffic is encrypted in transit.

Caddy is the recommended option — it obtains and renews TLS certificates from Let's Encrypt automatically with no extra configuration. Install Caddy on the host machine, then create a Caddyfile in any convenient location:

cdt.yourorg.com {
reverse_proxy localhost:6012
}

tiles.yourorg.com {
reverse_proxy localhost:6080
}

files.yourorg.com {
reverse_proxy localhost:9000
}

Start Caddy (or configure it as a systemd service so it starts on boot):

caddy start --config /path/to/Caddyfile

Caddy will automatically issue TLS certificates for all three domains on first request. No additional TLS configuration is needed.

Firewall

On the host machine, ports 80 (HTTP, used by Caddy for certificate issuance and redirect) and 443 (HTTPS) should be open to the network. The application ports (6012, 6080, 9000, 9001, 5433) should be firewalled from external access — all traffic reaches them through Caddy on the same machine.


Stopping the Stack

To stop all running containers without losing any data:

docker compose -f docker-compose.public.yml down

This removes the containers but preserves the named volumes (postgres_data, minio_data), so your database and uploaded files survive the restart.

Removing All Data

Warning: This is irreversible. All database records, uploaded files, and MinIO objects will be permanently deleted.

To stop the stack and delete all volumes:

docker compose -f docker-compose.public.yml down -v

Use this only when you want a completely clean slate — for example, resetting a test environment or decommissioning the deployment entirely.


Upgrading

To update CDT to a newer version, pull the latest images and restart the stack:

docker compose -f docker-compose.public.yml pull
docker compose -f docker-compose.public.yml up -d

The migrate service runs automatically on startup and applies any new database migrations before the application comes back up. No manual migration step is needed.


Service List

All services are documented on a single Services page. Jump directly to a section: