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.

  1. 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
  2. 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
  3. Run sudoedit /etc/geo-mlops/app.env and 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_TENANTS and RBAC_BOOTSTRAP_ROLE_PASSWORD are 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 the Host header 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 include localhost:*, 127.0.0.1:* and 10.*, 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.

  1. Check that the port in url is the app port 10000. If the distributed file says :8000, change it to :10000.
  2. Uncomment the http_config.headers block and put in the same value as in app.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

  1. 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
  2. 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
  3. 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/health checks only that the process is alive (it does not touch dependent services). The Compose health check uses it.
  • /api/v1/readyz actually connects to Postgres, the object store, Prometheus and Alertmanager. If any of them is down, the whole result is degraded. Use this one for load balancer readiness checks.

Once the server is up, the API docs are at http://<server>:10000/docs.

API docs (/docs) — available as soon as the server is up

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.

OrderWhat it doesWhat to look for in the log
1DB migration alembic upgrade head (if it fails, the server does not start)Running upgrade …
2Creates the bootstrap administrator account and grants global administrator rightscreated bootstrap admin account admin@example.com
3Creates the slots for the shared build images (training runtime, stager, serving builder)
4Prepares 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.

LayoutBuild valueApp setting
Same domain — a web server serves the static files and forwards /api, /auth, /users, /mlflow and /v2 to 10000VITE_API_BASE_URL= (empty)No CORS setting needed
Different domain — for example, UI at https://mlops.example.com, API at https://api.mlops.example.comVITE_API_BASE_URL=https://api.mlops.example.comGEO_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

Written for the platform as of 2026-09-21.

© Geo-MLOps