MLOps Central Platform starts from a single docker-compose.yml in the source directory. The app image is built from the Dockerfile in the same directory, and Postgres, RustFS, Prometheus, Alertmanager and the package caches come up with it. The examples assume the source is in /opt/geo-mlops. For the full file and a breakdown of its services, ports and volumes, see the docker-compose.yml reference.

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=[email protected]
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. Leave empty on the same 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.

  • 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 (an attack that spoofs an address to make the browser send requests to an internal server) 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. Configure 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 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. The status code is 200 even when degraded, so check that status in the body is ok.

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

API docs (/docs) — available once 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 entries for the shared build images (training runtime, stager, serving builder)—
3Creates the bootstrap administrator account and grants global administrator rightscreated bootstrap admin account [email protected]
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 Compose's restart setting starts it again). 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

Compose reads this value from the calling shell, not from app.env. Pass it again whenever you run up, for example during an upgrade; without it, the cap returns to 16 GB.

Training pods are capped separately from this limit by GEO_MLOPS_TRAINING_RUNTIME_MEMORY_LIMIT (default 64Gi). The pod's /dev/shm also counts toward this cap. If a pod exceeds it, only that training run ends with OOM and shows "Terminated due to out of memory (OOM) (메모리 부족(OOM)으로 종료되었습니다)". If node memory is small, lower it in /etc/geo-mlops/app.env; leave it empty for no cap.

6. Deploy the web UI

The web UI (frontend) is a bundle of static files deployed separately from the app. The API address is included in 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)GEO_MLOPS_CORS_ALLOW_ORIGINS= (empty; the default * shows as a warning in System diagnostics)
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/

Deploy 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. In the same-domain layout, forward the API paths including the trailing slash (/api/). The screen address /api-services also starts with /api, so a plain prefix match sends that screen to the app and a 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. Prometheus (9090), Alertmanager (9093) and the package caches (5000, 3142) are open without authentication. 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-10-02.

© MLOps Central Platform