Install with Docker Compose
Create the configuration file (/etc/geo-mlops/app.env), start the stack with docker compose, and verify it with the health checks
Geo-MLOps starts from a single docker-compose.yml in the source directory. The app image is built on the spot from the Dockerfile in the same directory, and Postgres, RustFS, Prometheus, Alertmanager and the package caches come up with it. This page assumes the source is in /opt/geo-mlops.
1. Create the configuration file
The app's secret settings live in /etc/geo-mlops/app.env, outside the source directory. The file survives a fresh checkout of the source or a CI cleanup of the checkout. The format is one GEO_MLOPS_KEY=value per line.
-
Create the directory and the file with narrow permissions.
sudo install -d -m 0750 /etc/geo-mlops sudo install -m 0600 /dev/null /etc/geo-mlops/app.env -
Generate three different random values for the signing key, the pepper and the webhook token.
openssl rand -hex 32 # for GEO_MLOPS_AUTH_SECRET openssl rand -hex 32 # for GEO_MLOPS_EDGE_TOKEN_PEPPER openssl rand -hex 32 # for GEO_MLOPS_ALERTMANAGER_WEBHOOK_TOKEN -
Run
sudoedit /etc/geo-mlops/app.envand enter the content below. Replace each<your-secret>with a value you just generated, and change the addresses to your real domain.
# --- Signing keys (change right after installation) --------------------
GEO_MLOPS_AUTH_SECRET=<your-secret>
GEO_MLOPS_EDGE_TOKEN_PEPPER=<your-secret>
# --- First global administrator ----------------------------------------
GEO_MLOPS_RBAC_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
GEO_MLOPS_RBAC_BOOTSTRAP_ADMIN_PASSWORD=<your-secret>
# --- Turn off the development seed (must be empty in production) -------
GEO_MLOPS_RBAC_BOOTSTRAP_TENANTS=
GEO_MLOPS_RBAC_BOOTSTRAP_ROLE_PASSWORD=
# --- Alertmanager webhook shared secret ---------------------------------
GEO_MLOPS_ALERTMANAGER_WEBHOOK_TOKEN=<your-secret>
# --- Access addresses ---------------------------------------------------
# true if users connect over HTTPS (default). false if you only use http:// on an internal network.
GEO_MLOPS_AUTH_COOKIE_SECURE=true
# The web UI's address, when the web UI calls the API from a different domain
GEO_MLOPS_CORS_ALLOW_ORIGINS=https://mlops.example.com
# Host headers MLflow accepts (see below)
GEO_MLOPS_MLFLOW_ALLOWED_HOSTS=api.mlops.example.com,localhost:*,127.0.0.1:*,10.*
Required security settings explains each line and what is at risk if you leave it unchanged. Only the essentials are noted here.
- Why
RBAC_BOOTSTRAP_TENANTSandRBAC_BOOTSTRAP_ROLE_PASSWORDare left empty — their defaults are for development. If you do not empty them, the first start creates a development tenant and per-role demo accounts (with default passwords). Seeding runs on every start, so even if you delete that tenant later, it comes back on the next restart. Empty them before the first start. You create tenants in the UI after installation. MLFLOW_ALLOWED_HOSTS— the embedded MLflow checks theHostheader to block DNS rebinding and returns 403 for unknown hosts. If you leave it empty, only localhost and private IPs (10.*, 192.168.* and so on) pass. If you set a value, it replaces this default list entirely, so along with your domain you must also includelocalhost:*,127.0.0.1:*and10.*, which build pods use. If you do not use MLflow through a domain, you can drop this line.
2. Match the Alertmanager webhook
monitoring/alertmanager.yml holds the address to which Alertmanager sends alerts back to the app. If GEO_MLOPS_ALERTMANAGER_WEBHOOK_TOKEN is empty, the app rejects the webhook with 503; if the value is wrong, it returns 401. Fix two places.
- Check that the port in
urlis the app port 10000. If the distributed file says:8000, change it to:10000. - Uncomment the
http_config.headersblock and put in the same value as inapp.env.
receivers:
- name: geo-mlops-webhook
webhook_configs:
- url: http://host.docker.internal:10000/api/v1/integrations/alertmanager/webhook
send_resolved: true
http_config:
headers:
X-Webhook-Token: <your-secret> # same value as GEO_MLOPS_ALERTMANAGER_WEBHOOK_TOKEN
3. Start the stack
-
Build the image and start all services. The first build downloads packages from the internet, so it takes a few minutes.
docker compose -f /opt/geo-mlops/docker-compose.yml up -d --build --remove-orphans -
Wait until the app container is
healthy(a little over 30 seconds on the first start).docker inspect --format='{{.State.Health.Status}}' geo-mlops-app docker compose -f /opt/geo-mlops/docker-compose.yml ps -
Call the two health checks. Both are open without authentication.
curl -s http://localhost:10000/api/v1/health # {"status":"ok","version":"…"} curl -s http://localhost:10000/api/v1/readyz # {"status":"ok","database":"up","object-store":"up","prometheus":"up","alertmanager":"up"}
/api/v1/healthchecks only that the process is alive (it does not touch dependent services). The Compose health check uses it./api/v1/readyzactually connects to Postgres, the object store, Prometheus and Alertmanager. If any of them isdown, the whole result isdegraded. Use this one for load balancer readiness checks.
Once the server is up, the API docs are at http://<server>:10000/docs.
4. What happens automatically on the first start
You do not need to run any command. The app container's start-uvicorn.sh and the app's startup do the following.
| Order | What it does | What to look for in the log |
|---|---|---|
| 1 | DB migration alembic upgrade head (if it fails, the server does not start) | Running upgrade … |
| 2 | Creates the bootstrap administrator account and grants global administrator rights | created bootstrap admin account admin@example.com |
| 3 | Creates the slots for the shared build images (training runtime, stager, serving builder) | — |
| 4 | Prepares each tenant's MLflow workspace and cleans up interrupted work | — |
docker logs geo-mlops-app 2>&1 | grep -E "upgrade|bootstrap|seeded"
The bootstrap administrator is created only if the account does not exist. The password of an existing account is not changed. If seeding fails, the server still starts and the failure is logged as startup seeding failed.
5. Memory limit of the app container
The app container is capped at 16 GB by default (if it goes over, the kernel kills only the app and restart brings it back). On machines where the GPU shares system memory, this leaves room for the training pods. To change it, pass a shell variable when you call Compose.
GEO_APP_MEM_LIMIT=24g docker compose -f /opt/geo-mlops/docker-compose.yml up -d
6. Deploy the web UI
The web UI (frontend) is a bundle of static files deployed separately from the app. The API address is baked into the bundle at build time (VITE_API_BASE_URL). Choose one of two layouts.
| Layout | Build value | App setting |
|---|---|---|
Same domain — a web server serves the static files and forwards /api, /auth, /users, /mlflow and /v2 to 10000 | VITE_API_BASE_URL= (empty) | No CORS setting needed |
Different domain — for example, UI at https://mlops.example.com, API at https://api.mlops.example.com | VITE_API_BASE_URL=https://api.mlops.example.com | GEO_MLOPS_CORS_ALLOW_ORIGINS=https://mlops.example.com |
# in the frontend source directory
npm ci
VITE_API_BASE_URL=https://api.mlops.example.com npm run build # output: dist/
Upload dist/ to static hosting (a web server, a CDN and so on). Screen addresses (/training, /system-health …) are all routed in the browser, so configure the host to return index.html for unknown paths (SPA fallback); otherwise a page refresh gives a 404.
7. Default accounts of the supporting services
The Postgres (mlflow/mlflow) and RustFS (rustfsadmin/rustfsadmin) accounts are fixed in the Compose file, and their ports (5433, 9000, 9001) are open on every address of the server. Block these ports from outside the server with a firewall. To change the accounts, change the Compose file, the matching settings in app.env (GEO_MLOPS_DATABASE_URL, GEO_MLOPS_MLFLOW_BACKEND_URI, GEO_MLOPS_MLFLOW_S3_ACCESS_KEY_ID, GEO_MLOPS_MLFLOW_S3_SECRET_ACCESS_KEY) and the backup settings together.
Common commands
C="docker compose -f /opt/geo-mlops/docker-compose.yml"
$C ps # status
$C logs -f --tail=100 app # app logs
$C up -d app # after editing app.env — recreate only the app so it rereads the settings
$C down # stop (data is kept)
Next: Prepare k3s and GPUs