Blog  /  Engineering

Running any Mendix™ app in Docker (7–11) without bloating your disk

If you run Mendix™ apps in Docker, you have probably met this: the official cf-mendix-buildpack image bakes the application model into the image. That is exactly what you want for an immutable production artifact — and a chore in an iterative dev/test loop.

We open-sourced the recipes we use internally to avoid that chore: github.com/ontologylabs/mendix-runtime-crates (Apache-2.0). They run any Mendix app, versions 7 through 11, without baking the model into the image — and they ship no Mendix binaries.

The problem: a new image (and a dangling one) every reload

Because the model is baked in, every model change means a fresh docker build, and the previous image becomes a dangling layer — roughly ~785 MB each with the buildpack pattern. Reload a few dozen times across a working day and you have quietly burned tens of GB. You end up running docker image prune as a reflex.

The inversion: bake the runtime once, bind-mount the model

The fix is to separate what changes from what does not:

cf-buildpack image vs. runtime crate
cf-buildpack imageRuntime crate
Mendix™ runtime + JREbakedbaked (once per version)
Application modelbakedbind-mounted
Reloaddocker build --no-cachedocker compose restart
Disk per reload~785 MB dangling0
Images per versionmany (one per commit)1

Same Mendix version → same image. Ten apps on 11.6.4 → one image, ten bind-mounts.

No binaries: pull the runtime from the CDN at build time

The recipes ship no Mendix runtime files. The Dockerfile curls the runtime from the Mendix CDN at build time, on your machine:

ARG MENDIX_VERSION=11.6.4
ARG RUNTIME_CDN_BASE=https://cdn.mendix.com/runtime
RUN curl -fsSL "${RUNTIME_CDN_BASE}/mendix-${MENDIX_VERSION}.tar.gz" ...

So the scaffolding stays clean Apache-2.0 (no redistribution of Mendix IP), and the runtime you bake is the one you are licensed for. The repo even has a CI guard that fails if anyone commits a *.tar.gz / *.mda / *.jar or an oversized file — a runtime binary literally cannot be merged.

Try it

Build a crate image, then bring it up with Postgres next to it. Step 1 is a one-time docker build that pulls the runtime from the CDN (a couple of minutes); after that, reloading a model change is just docker compose restart.

1. Build the runtime image (once per version)

# clone, then build the Mendix 11 crate
git clone https://github.com/ontologylabs/mendix-runtime-crates.git
cd mendix-runtime-crates/crates/mendix-11
docker build --platform linux/amd64 \
  --build-arg MENDIX_VERSION=11.6.4 \
  -t ontologylabs/mendix-runtime:11.6.4 .

2. Drop in your unzipped MDA and bring it up

# place an unzipped MDA where the runtime can read it
unzip /path/to/your.mda -d ./mda/

# docker-compose.yml — runtime + Postgres on one network
services:
  postgres:
    image: postgres:14-alpine
    environment:
      POSTGRES_USER: mendix
      POSTGRES_PASSWORD: mendix
      POSTGRES_DB: mendix
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mendix"]
      interval: 5s
      timeout: 3s
      retries: 10

  mendix-runtime:
    image: ontologylabs/mendix-runtime:11.6.4
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "8080:8080"
      - "8090:8090"
    environment:
      DATABASE_ENDPOINT: "postgres://mendix:mendix@postgres:5432/mendix"
      ADMIN_PASSWORD: "a-strong-password"
      DEVELOPMENT_MODE: "true"
    volumes:
      - ./mda:/opt/mendix/app
      - mendix-data:/opt/mendix/data

volumes:
  mendix-data:
docker compose up
# then: curl -fsS http://localhost:8080/

The contract. The runtime reads DATABASE_ENDPOINT (parsed for host, port, user, password and database), requires a strong ADMIN_PASSWORD, and bind-mounts the unzipped MDA at /opt/mendix/app. DEVELOPMENT_MODE=true relaxes the MX 11 project-security gate for local runs. To reload a model change: replace ./mda and run docker compose restart mendix-runtime — no rebuild, no dangling layer.

Which versions

Crates exist for Mendix 7, 9, 10, and 11 (the runtimelauncher.jar + m2ee boot path is broadly the same across them; the per-version differences — mostly how model constants are supplied — are handled in each crate’s start.sh). Each crate’s versions.yaml lists the verified patch versions. Need one we do not have? Mendix 8 is the current gap — open a “Request a Mendix version” issue, or PR it (build + smoke-test + a versions.yaml row).

Why we built it

We are building mxto — the AI delivery engine for the Mendix™ platform, which reads, authors and ships Mendix apps headless. Clean, reproducible local runtimes are table stakes for that, so these recipes already existed; open-sourcing them was easy. If they save you some disk, a star helps other Mendix developers find them.