Production Deployment¶
Use this guide if you want to run Field-TM on a server.
There are two supported production paths:
Docker Compose (this guide) - for single-server deployments:
- core deployment from
deploy/compose.sub.yaml - optional self-hosted ODK overlay from
deploy/compose.odk.yaml - optional self-hosted Hanko overlay from
deploy/compose.login.yaml
Kubernetes / Helm - for cluster deployments, see
chart/README.md.
The install.sh flow is deprecated and should not be used.
Recommended host¶
- Ubuntu Linux
- a non-root user with
sudo - a DNS record pointing your domain to the server
1. Clone the repo¶
git clone https://github.com/hotosm/field-tm.git
cd field-tm
You do not need to check out a release tag manually.
just start prod will prompt you to select a version interactively.
2. Prepare the machine¶
just prep machine
Run this as a non-root user. It installs and configures the container runtime (runc, containerd, nerdctl) in rootless mode.
3. Configure .env¶
just config setup
This runs an interactive wizard that:
- Generates
.envfrom.env.example - Asks for your domain and Let's Encrypt email
- Lets you choose an auth provider
- Auto-generates secure secrets and database passwords
- Pauses so you can review and edit
.envbefore continuing
For advanced or automated setups you can instead run
just config generate-dotenv and edit .env by hand.
Base settings¶
The setup wizard configures these automatically, but you may want to review:
DEBUG(default:False): Must beFalsein productionLOG_LEVEL(default:INFO): Application log level (DEBUG,INFO,WARNING,ERROR)FTM_DB_USER(default:fieldtm): PostgreSQL userFTM_DB_NAME(default:fieldtm): PostgreSQL database nameFTM_DB_HOST(default:fieldtm-db): Database hostname (use default for the compose stack)FTM_API_DOMAIN(default:api.$FTM_DOMAIN): Subdomain for the JSON API (optional; defaults to theapi.prefix)API_REPLICAS(default:2): Number of backend container replicasEXTRA_CORS_ORIGINS(default: (empty)): Comma-separated additional allowed CORS origins
Authentication options¶
AUTH_PROVIDER controls how users sign in:
hotosm: HOT's hosted login atlogin.hotosm.orgDeploy withjust start prodcustom: Your own Hanko instance Deploy withjust start prodbundled: Self-hosted Hanko via the login overlay Deploy withjust start prod(login overlay is included automatically)
If you use AUTH_PROVIDER=hotosm, set:
OSM_CLIENT_ID: OAuth2 client ID from OpenStreetMapOSM_CLIENT_SECRET: OAuth2 client secretOSM_SECRET_KEY: Secret key for signing OSM tokens
Optional OSM settings:
OSM_URL(default:https://www.openstreetmap.org): OSM server URLOSM_SCOPE(default:["read_prefs","send_messages"]): OAuth2 scopes
If you use AUTH_PROVIDER=custom, also set:
HANKO_API_URL: URL of your Hanko instanceLOGIN_URL: (optional) URL of your login UI, if hosted separately
If you use AUTH_PROVIDER=bundled (self-hosted Hanko), also review:
HANKO_SECRET(default: (dev default)): Secret for Hanko. Generate withopenssl rand -base64 32HANKO_COOKIE_DOMAIN(default:field.localhost): Domain for Hanko cookies. Set this to$FTM_DOMAINHANKO_ALLOWED_ORIGIN(default:http://field.localhost:7050): Allowed origin for Hanko. Set this tohttps://$FTM_DOMAINHANKO_REDIRECT_URL(default:http://field.localhost:7050): Redirect URL after login. Set this tohttps://$FTM_DOMAINGOOGLE_ENABLED(default:false): Enable Google OAuth in HankoGOOGLE_CLIENT_ID(default: (empty)): Google OAuth client IDGOOGLE_CLIENT_SECRET(default: (empty)): Google OAuth client secret
ODK Central configuration¶
Field-TM expects an ODK Central account it can use. For external ODK Central,
this user is not created automatically by Field-TM. You must create it on the
ODK Central side and then provide the credentials in .env.
ODK_CENTRAL_URL(default:http://central:8383): URL of ODK Central (use the default for self-hosted deployments)ODK_CENTRAL_USER(default:admin@hotosm.org): ODK Central admin emailODK_CENTRAL_PASSWD(default: (dev default)): ODK Central admin passwordPYODK_LOG_LEVEL(default:CRITICAL): Log level for the pyodk client library
Use either:
- external ODK Central with
just start prod - self-hosted ODK Central with
just start prod-with-odk
If you self-host ODK Central, Field-TM can run the service as part of the
compose stack, but the application still depends on valid ODK credentials in
.env.
Self-hosted ODK Central also uses:
FTM_ODK_DOMAIN(default:odk.$FTM_DOMAIN): Subdomain for the ODK Central web UICENTRAL_DB_HOST(default:central-db): ODK Central database hostCENTRAL_DB_USER(default:odk): ODK Central database userCENTRAL_DB_PASSWORD(default:odk): ODK Central database passwordCENTRAL_DB_NAME(default:odk): ODK Central database name
QFieldCloud configuration¶
Field-TM expects a QFieldCloud account it can use. For external QFieldCloud,
this user is not created automatically by Field-TM. You must create it in
QFieldCloud first, then provide the credentials in .env.
QFIELDCLOUD_URL(default:http://qfield-app:8000): QFieldCloud server URLQFIELDCLOUD_USER(default:svcftm): QFieldCloud service account usernameQFIELDCLOUD_PASSWORD(default: (dev default)): QFieldCloud service account passwordQFIELDCLOUD_PROJECT_OWNER(default:HOTOSM): Organization owning QFieldCloud projectsQFIELDCLOUD_QGIS_URL(default:http://qfield-qgis:8080): URL of the QGIS wrapper serviceQFIELDCLOUD_TAG(default:26.3): Image tag for the QGIS wrapper containerQGIS_LOG_LEVEL(default:INFO): Log level for the QGIS wrapper
Monitoring options¶
Monitoring is optional. To enable it, set:
| Variable | Description |
|---|---|
MONITORING |
openobserve or sentry |
For OpenObserve, also set:
OPENOBSERVE_USER(default:admin@hotosm.org): OpenObserve admin userOPENOBSERVE_PASSWORD(default: (dev default)): OpenObserve admin passwordOPENOBSERVE_RETENTION_DAYS(default:90): Log retention in daysOTEL_ENDPOINT(default: (empty)): OpenTelemetry collector endpointOTEL_AUTH_TOKEN(default: (empty)): OpenTelemetry auth token
For Sentry, set:
SENTRY_DSN: Sentry DSN for error reporting
Other useful options¶
RAW_DATA_API_URL(default:https://api-prod.raw-data.hotosm.org/v1): Override the default raw-data-api endpointRAW_DATA_API_AUTH_TOKEN(default: (empty)): Token for the raw-data-api, if required
5. Deploy¶
Core Field-TM¶
Use this when ODK Central and QFieldCloud are managed outside this stack.
just start prod
Field-TM with self-hosted ODK Central¶
just start prod-with-odk
Field-TM with self-hosted Hanko login¶
Set AUTH_PROVIDER=bundled in .env (or select it during just config setup),
then run just start prod — the login overlay is included automatically.
All commands will:
- Check for uncommitted changes (and refuse to proceed if dirty)
- Present a numbered list of available release versions
- Check out the selected tag
- Generate
.envif missing - Deploy the compose stack
6. Verify¶
After deployment:
- open
https://<your-domain> - confirm the homepage loads
- if auth is enabled, confirm sign-in works
- inspect running containers with
docker compose ps - inspect logs with
docker compose logs backend
Upgrading¶
To upgrade to a newer release:
cd field-tm
git fetch --tags
just start prod
Select the new version from the menu. Your .env is preserved between
upgrades. If a new release adds env vars, check .env.example for new
entries and add them to your .env.
Compose files¶
deploy/compose.sub.yaml: core Field-TM, PostgreSQL, BunkerWeb, QGIS wrapperdeploy/compose.odk.yaml: adds self-hosted ODK Centraldeploy/compose.login.yaml: adds self-hosted Hanko logindeploy/compose.qfield.yaml: reserved for QFieldCloud-related overlays
Do not run docker compose directly against deploy/compose.sub.yaml without
preprocessing. It uses ${FTM_DOMAIN} in environment key names, so it must go
through envsubst. The just start prod* commands handle this correctly.
Help, Field-TM is broken¶
Production issues usually fall into a few categories. Work through these steps in order.
Check the containers are running¶
docker compose -f deploy/compose.sub.yaml ps
All services should show Up or healthy. If a service is restarting or
exited, check its logs:
docker compose -f deploy/compose.sub.yaml logs <service-name>
Replace <service-name> with backend, proxy, fieldtm-db, migrations,
qfield-qgis, or dns.
Backend won't start¶
- Check
docker compose -f deploy/compose.sub.yaml logs backendfor Python tracebacks. - Common cause: missing or wrong env vars in
.env. Compare your.envagainst.env.examplefor new or renamed variables. - If the
migrationsservice failed, the backend will not start. Checkdocker compose -f deploy/compose.sub.yaml logs migrationsfor SQL errors.
HTTPS / Let's Encrypt issues¶
- BunkerWeb handles TLS automatically. If the site loads on HTTP but not HTTPS,
check the proxy logs:
docker compose -f deploy/compose.sub.yaml logs proxy - Ensure
FTM_DOMAINandCERT_EMAILare set correctly in.env. - Let's Encrypt has rate limits. If you've hit them, wait and retry later.
During testing, uncomment
USE_LETS_ENCRYPT_STAGING: yesindeploy/compose.sub.yaml.
DNS resolution failures inside containers¶
The production stack uses a custom dnsmasq container (dns) because
nerdctl/containerd doesn't resolve container names the same way Docker does.
If services can't reach each other:
- Verify the
dnscontainer is healthy:docker compose -f deploy/compose.sub.yaml logs dns - The dns service must start after all other containers are assigned IPs.
A restart may fix transient issues:
docker compose -f deploy/compose.sub.yaml restart dns
Database issues¶
- Connect directly to check the database is accepting connections:
docker compose -f deploy/compose.sub.yaml exec fieldtm-db pg_isready - If the volume was deleted or corrupted, data will be lost. The database
volume is
field-tm-db-data. Back it up regularly.
ODK Central not reachable (self-hosted)¶
- Check all Central containers are running:
docker compose -f deploy/compose.sub.yaml -f deploy/compose.odk.yaml ps - Verify
ODK_CENTRAL_URL,ODK_CENTRAL_USER, andODK_CENTRAL_PASSWDin.envmatch the Central instance.
Restarting everything cleanly¶
If all else fails, a full recreate (without losing data):
docker compose -f deploy/compose.sub.yaml down
just start prod
This preserves database volumes. To also wipe data and start fresh:
docker compose -f deploy/compose.sub.yaml down -v
just start prod
Getting help¶
- Check the GitHub issues for known problems.
- Open a new issue with your error logs if you're stuck.
- Join the HOT Slack for community support.
Notes¶
- The compose stack exposes ports
80and443. - The backend API is served by the same LiteStar app as the HTMX manager UI.