---
name: docker-workflow
description: "Build, run and validate a Mendix app in Docker from inside a devcontainer. Use when asked to run the app in Docker or produce a deployable image. For the faster Docker-free loop, prefer run-local."
---

# Docker Build & Run Skill

This skill guides you through building, running, and testing a Mendix application using Docker inside a devcontainer.

## When to Use This Skill

Use this when:
- The user wants to run their Mendix app locally in Docker
- The user wants to build a deployable container image
- The user asks about testing or validating their app
- The user needs to set up the Docker workflow for the first time

## Prerequisites

The devcontainer created by `mxcli init` includes:
- **JDK 21** (Adoptium temurin-21-jdk) — required by MxBuild
- **Docker-in-Docker** (or **Podman-in-Podman**) — container runtime inside the devcontainer
- **Port forwarding** — ports 8080 (app) and 8090 (admin) auto-forwarded

### Podman Support

mxcli auto-detects Docker or Podman. To force Podman:
```bash
export MXCLI_CONTAINER_CLI=podman
```

When using `mxcli init`, pass `--container-runtime podman` to generate a devcontainer with Podman-in-Podman instead of Docker-in-Docker. Requires Podman 4.7+ (ships `podman compose` natively).

## Architecture

```
host machine (browser at localhost:8080)
  └── Docker or Podman (host daemon)
      └── Devcontainer (VS Code)
          ├── mxcli, JDK 21, project files
          └── Docker/Podman daemon (docker-in-docker or podman-in-podman)
              └── docker/podman compose stack
                  ├── mendix container (8080, 8090)
                  │     └── /mendix ← volume mount from .docker/build/
                  └── postgres container (5432)
```

The Mendix container uses a **volume mount** (not COPY) — `.docker/build/` is mounted directly into the container at `/mendix`. This means after rebuilding the PAD, you only need to restart the container (no Docker image rebuild). The generated Dockerfile is kept in the build output for production image builds.

## Quick Start: One Command

The easiest way to get a Mendix app running in Docker:

```bash
# Setup, build, and start in one command
mxcli docker run -p app.mpr

# with startup confirmation (waits for "runtime successfully started")
mxcli docker run -p app.mpr --wait

# Fresh start (removes database volumes first)
mxcli docker run -p app.mpr --fresh --wait
```

`docker run` handles everything: downloads MxBuild and runtime (if not cached), initializes the Docker stack (if needed), builds the PAD package, starts the containers, and optionally waits for the runtime to report successful startup.

## Creating an Empty Mendix App

**Recommended:** Use `mxcli new` to create a project with all tooling in one step:

```bash
mxcli new MyApp --version 11.8.0
```

This downloads MxBuild, creates a blank project, sets up AI tooling and Dev Container, and installs the correct Linux mxcli binary. Open the resulting folder in VS Code and reopen in Dev Container.

**Manual approach** (if you need more control):

```bash
# Download mxbuild if not already cached
mxcli setup mxbuild --version 11.6.4

# create a blank project
mkdir -p /path/to/my-app
~/.mxcli/mxbuild/{version}/modeler/mx create-project --app-name MyApp --output-dir /path/to/my-app

# Initialize AI tooling
mxcli init /path/to/my-app
```

The `mx create-project` command creates an MPR v2 project with the standard Mendix module structure. You can then use the Docker workflow to build and run it.

**Caveat:** Blank projects have no demo users — login will fail until you configure security via MDL or Studio Pro. See `manage-security` for setting up demo users.

## Step-by-Step Workflow

If you prefer more control, use the individual commands:

### 1. Setup MxBuild and Runtime (first time only)

MxBuild and the Mendix runtime are auto-downloaded when needed, but you can pre-download:

```bash
# Download MxBuild matching the project version
mxcli setup mxbuild -p app.mpr

# Download Mendix runtime matching the project version
mxcli setup mxruntime -p app.mpr

# or specify version explicitly (without a project)
mxcli setup mxbuild --version 11.6.4
mxcli setup mxruntime --version 11.6.4

# or preview what would be downloaded
mxcli setup mxbuild -p app.mpr --dry-run
mxcli setup mxruntime -p app.mpr --dry-run
```

MxBuild is cached at `~/.mxcli/mxbuild/{version}/` and the runtime at `~/.mxcli/runtime/{version}/`. Both are reused across builds.

#### Runtime-to-MxBuild Copying (PAD Build Prerequisite)

MxBuild 11.6.3+ expects runtime files (`pad/`, `lib/`, `launcher/`, `agents/`) inside its own `runtime/` directory, but `mxcli setup mxbuild` only downloads the build tools (not the full runtime). If the PAD build fails with `StudioPro.conf.hbs does not exist` or `ClassNotFoundException`, copy the runtime directories into mxbuild:

```bash
version=11.6.4  # replace with your version

# after downloading both mxbuild and mxruntime:
cp -r ~/.mxcli/runtime/$version/runtime/pad ~/.mxcli/mxbuild/$version/runtime/pad
cp -r ~/.mxcli/runtime/$version/runtime/lib ~/.mxcli/mxbuild/$version/runtime/lib
cp -r ~/.mxcli/runtime/$version/runtime/launcher ~/.mxcli/mxbuild/$version/runtime/launcher
cp -r ~/.mxcli/runtime/$version/runtime/agents ~/.mxcli/mxbuild/$version/runtime/agents
```

**Important:** The PAD build output may only include partial runtime bundles (5 jars instead of 354). If the runtime fails to start with `ClassNotFoundException: com.mendix.container.support.EventProcessor`, copy the full runtime into the PAD build output:

```bash
rm -rf /path/to/project/.docker/build/lib/runtime
cp -r ~/.mxcli/runtime/$version/runtime /path/to/project/.docker/build/lib/runtime
```

### 2. Initialize Docker stack (first time only)

```bash
# generate docker-compose.yml, .env, Dockerfile in .docker/
mxcli docker init -p app.mpr
```

This creates a `.docker/` directory with Docker Compose configuration for the Mendix app + PostgreSQL.

**Port conflicts:** If default ports (8080/8090/5432) are already in use, check with `ss -tlnp | grep -E '808|809|543'` and use `--port-offset N` to shift all ports:

```bash
# check which ports are occupied
ss -tlnp | grep -E '808[0-9]|809[0-9]|543[0-9]'

# use offset to avoid conflicts (e.g., offset 5 → 8085/8095/5437)
mxcli docker init -p app.mpr --port-offset 5
```

### 3. Check project for errors

```bash
# Quick validation using mx check
mxcli docker check -p app.mpr
```

### 4. Build the Portable App Distribution (PAD) package

```bash
# build (auto-downloads MxBuild if not cached)
mxcli docker build -p app.mpr

# Preview what would happen
mxcli docker build -p app.mpr --dry-run

# Skip pre-build check
mxcli docker build -p app.mpr --skip-check
```

This:
1. Detects the Mendix version (requires >= 11.6.1)
2. Locates or downloads MxBuild and JDK 21
3. Runs MxBuild to produce a PAD package (ZIP)
4. Extracts the PAD ZIP (cleans up old ZIPs after extraction)
5. Generates a Dockerfile if MxBuild didn't produce one (11.6.3+)
6. Downloads and injects the Mendix runtime if not in PAD output
7. Applies version-aware patches (CMD fix, base image, healthcheck, etc.)

### 5. Start the application

```bash
# Start in foreground (see logs directly)
mxcli docker up -p app.mpr

# Start in background
mxcli docker up -p app.mpr --detach

# Start in background and wait for runtime startup confirmation
mxcli docker up -p app.mpr --detach --wait

# Fresh start (removes database volumes)
mxcli docker up -p app.mpr --fresh

# Custom wait timeout (default: 300 seconds)
mxcli docker up -p app.mpr --detach --wait --wait-timeout 600
```

The `--wait` flag (requires `--detach`) tails the container logs and waits until the Mendix runtime reports "Runtime successfully started" or a timeout/failure is detected.

The app is available at:
- **http://localhost:8080** — application
- **http://localhost:8090** — admin console (password in `.docker/.env`)

### 6. Query data with OQL

```bash
# run OQL queries against the live runtime (read-only preview mode)
mxcli oql -p app.mpr "select Name, Email from MyModule.Customer"

# json output for piping
mxcli oql -p app.mpr --json "SELECT count(c.ID) FROM MyModule.Order AS c" | jq '.[0]'

# Test a view entity query before embedding it in MDL
mxcli oql -p app.mpr "select datepart(YEAR, o.Date) as Year, sum(o.Total) as Revenue from Sales.Order as o GROUP by datepart(YEAR, o.Date)"
```

### 7. Monitor and manage

```bash
# view container status
mxcli docker status -p app.mpr

# view logs
mxcli docker logs -p app.mpr
mxcli docker logs -p app.mpr --follow
mxcli docker logs -p app.mpr --tail 50

# open a shell in the container
mxcli docker shell -p app.mpr
mxcli docker shell -p app.mpr --exec "ls -la /mendix"
```

### 8. Stop the application

```bash
# Stop containers
mxcli docker down -p app.mpr

# Stop and remove database volumes
mxcli docker down -p app.mpr --volumes
```

## Common Workflow: Edit, Rebuild, Test

After making MDL changes:

```bash
# 1. apply MDL changes
mxcli exec changes.mdl -p app.mpr

# 2. Rebuild and restart (one command)
mxcli docker run -p app.mpr --fresh --wait
```

Or step by step:

```bash
# 1. apply MDL changes
mxcli exec changes.mdl -p app.mpr

# 2. Validate
mxcli docker check -p app.mpr

# 3. Rebuild
mxcli docker build -p app.mpr

# 4. restart with fresh database and wait for startup
mxcli docker up -p app.mpr --fresh --detach --wait

# 5. check it's running
mxcli docker status -p app.mpr
```

## Hot Reload

The Mendix runtime supports hot reloading via the M2EE admin API. Because mxcli's Docker setup uses a **bind mount** (`.docker/build/` → `/mendix/`), rebuilt PAD output is immediately visible to the running runtime — no Docker image rebuild or container restart needed.

### How It Works

1. `mxcli docker build` compiles the project into `.docker/build/` (the bind-mounted directory)
2. `mxcli docker reload` rebuilds the PAD (same as `docker build`) **then** calls the M2EE `reload_model` action on port 8090
3. The runtime re-reads the model from disk in ~100ms
4. Connected browsers auto-refresh via the `/mxdevtools/` WebSocket

**Note:** By default, `docker reload` includes a full build step (~55s). Use `--model-only` to skip the build when you've already run `docker build` separately:

**Typical hot reload cycle (with separate build):**
```
mxcli exec script.mdl -p app.mpr                   # ~1s   — update model
mxcli docker build -p app.mpr                       # ~55s  — compile PAD
mxcli docker reload -p app.mpr --model-only         # ~100ms — reload only
# Total: ~56s (vs ~75s with full container restart)
```

**One-step reload (build + reload combined):**
```
mxcli exec script.mdl -p app.mpr                    # ~1s   — update model
mxcli docker reload -p app.mpr                      # ~56s  — build + reload
```

### Model Reload

For logic changes (microflows, nanoflows, pages, security):

```bash
# 1. apply changes
mxcli exec changes.mdl -p app.mpr

# 2. Rebuild PAD (skip pre-check for speed)
mxcli docker build -p app.mpr --skip-check

# 3. Hot reload the runtime (--model-only skips redundant rebuild)
mxcli docker reload -p app.mpr --model-only
```

Or combine build + reload in one command (no `--model-only`):

```bash
# 1. apply changes
mxcli exec changes.mdl -p app.mpr

# 2. build and reload in one step
mxcli docker reload -p app.mpr --skip-check
```

The `--direct` flag connects to the admin API via HTTP instead of routing through `docker compose exec` (faster, requires the `admin.addresses = ["*"]` build patch which is applied automatically):

```bash
mxcli docker reload -p app.mpr --direct
```

### CSS-Only Reload

For theme/styling changes, SCSS must first be compiled by MxBuild into the PAD output. The `--css` flag skips the model reload step — it only pushes already-compiled CSS to browsers via WebSocket.

```bash
# Correct workflow for SCSS/theme changes:
mxcli docker build -p app.mpr            # compile SCSS into PAD (~55s)
mxcli docker reload -p app.mpr --css     # push compiled CSS to browsers (~instant)
```

> **Note:** `--css` does NOT compile SCSS. If you skip the build step, the browser will not reflect your SCSS changes. The "instant" refers to the browser update via WebSocket, not end-to-end time.

This calls the M2EE `update_styling` action, which pushes CSS changes to all connected browsers via the `/mxdevtools/` WebSocket. Browsers update their stylesheets without a full page reload.

### The mxdevtools WebSocket

The Mendix runtime exposes a WebSocket at `ws://localhost:8080/mxdevtools/` (on the app port). This is a **server-push-only** channel — the runtime pushes instructions to connected browsers:

| Instruction | Effect |
|-------------|--------|
| `set_deployment_id` | If the ID changes, triggers full browser reload |
| `reload` | Forces full browser reload |
| `update_styling` | Hot-reloads CSS without page reload |

When `reload_model` is called, a new `set_deployment_id` is pushed to browsers, triggering automatic refresh. This is why you don't need to manually refresh the browser after `docker reload`.

### When to Use `reload` vs `run`

| Scenario | Command | Why |
|----------|---------|-----|
| Microflow/nanoflow logic | `docker reload` | No schema change, keeps data |
| Page layout or widget changes | `docker reload` | Runtime reloads pages from model |
| CSS/theme changes only | `docker reload --css` | Instant, no MxBuild needed |
| Security rule changes | `docker reload` | Runtime reloads security config |
| New entity or attribute (additive) | `docker reload` | Runtime applies DDL on reload |
| Destructive schema change (drop column, type change) | `docker up --fresh` | Runtime can't apply destructive DDL |
| First-time setup | `docker run` | Need containers + database |
| Database corruption or reset | `docker run --fresh` | Recreates volumes |

### When Reload is NOT Sufficient

`reload_model` cannot handle destructive database schema changes. Signs you need a full restart with `--fresh`:

- **Removed an entity** — the runtime won't drop the table
- **Removed an attribute** — the runtime won't drop the column
- **Changed an attribute type** (e.g., String → Integer) — the runtime can't ALTER COLUMN type
- **Runtime logs show DDL errors** — look for "Could not execute DDL" or schema mismatch messages

In these cases:

```bash
mxcli docker up -p app.mpr --fresh --detach --wait
```

## Environment Variables

The `docker-compose.yml` generated by `mxcli docker init` sets these environment variables for the Mendix container:

| Variable | Default | Description |
|----------|---------|-------------|
| `ADMIN_ADMINPASSWORD` | `AdminPassword1!` | Admin console password (read by runtime) |
| `RUNTIME_DEBUGGER_PASSWORD` | `AdminPassword1!` | Debugger password (required in non-Development mode) |
| `RUNTIME_PARAMS_DATABASETYPE` | `POSTGRESQL` | Database type |
| `RUNTIME_PARAMS_DATABASEHOST` | `db:5432` | Hostname and port of the `db` service |
| `RUNTIME_PARAMS_DATABASENAME` | `mendix` | Database name |
| `RUNTIME_PARAMS_DATABASEUSERNAME` | `mendix` | Database user |
| `RUNTIME_PARAMS_DATABASEPASSWORD` | `mendix` | Database password |
| `MX_LOG_LEVEL` | `info` | Log level |

All defaults can be overridden in `.docker/.env`.

**Note**: The runtime reads `ADMIN_ADMINPASSWORD`, not `M2EE_ADMIN_PASS`. The `.env` file defines `M2EE_ADMIN_PASS` as the single source, and `docker-compose.yml` maps it to both `ADMIN_ADMINPASSWORD` and `RUNTIME_DEBUGGER_PASSWORD`.

## Troubleshooting

| Problem | Solution |
|---------|----------|
| `docker: command not found` | Rebuild devcontainer — docker-in-docker feature needs rebuild to activate. Or use Podman: `export MXCLI_CONTAINER_CLI=podman` |
| `mxbuild not found` | Run `mxcli setup mxbuild -p app.mpr` to download from CDN |
| `JDK 21 not found` | Rebuild devcontainer — JDK 21 should be pre-installed |
| Build fails with version error | Requires Mendix >= 11.6.1 for PAD support |
| No Dockerfile in PAD output | Normal for MxBuild 11.6.3+ — `mxcli docker build` auto-generates one |
| Runtime not found / runtimelauncher.jar missing | Run `mxcli setup mxruntime -p app.mpr` or let `docker build` auto-download |
| `StudioPro.conf.hbs does not exist` | Runtime not linked into mxbuild — see "Runtime-to-MxBuild Copying" above |
| `ClassNotFoundException: EventProcessor` | PAD has partial runtime bundles — copy full runtime into `.docker/build/lib/runtime/` (see above) |
| Port already allocated | Check ports with `ss -tlnp \| grep 808` and use `docker init --port-offset N --force` |
| `' etc/default' is not a file` | Dockerfile CMD passes config arg — `docker build` patches this automatically |
| `DatabasePassword has no value` | Ensure `RUNTIME_PARAMS_DATABASE*` env vars are in docker-compose.yml — re-run `mxcli docker init --force` |
| `password should not be empty (debugger)` | Add `RUNTIME_DEBUGGER_PASSWORD` — re-run `mxcli docker init --force` |
| `security level should be set to CHECKEVERYTHING` | App in Production mode without security — set to Development mode or configure security |
| Port 8080 not accessible | Check `forwardPorts` in devcontainer.json includes 8080 |
| Database errors on startup | Try `mxcli docker up -p app.mpr --fresh` to reset volumes |
| OQL: "Action not found: preview_execute_oql" | Runtime needs `-Dmendix.live-preview=enabled` JVM flag — re-run `mxcli docker init --force` to get the updated docker-compose.yml |
