Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .claude/skills/container-build/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: container-build
description: Set up and use a headless, free, Docker-compatible container engine (Colima on macOS/Linux, Podman on Windows) instead of Docker Desktop, then build and run the Mendix runtime crates on it. Use when a machine needs a working `docker` engine, when `docker info` fails or keychain/credsStore errors block image pulls, when building the crate images (incl. linux/amd64 on Apple Silicon), or when running a crate's docker-compose smoke test.
---

# Container Build — headless engine for the Mendix runtime crates

These crates call only the plain `docker` CLI (`docker build` / `docker run` / `docker compose`), so
they run on **any** Docker-API-compatible engine — no Docker Desktop required.

## The engine, per OS

| OS | Engine | Bring-up |
|----|--------|----------|
| macOS / Linux | **Colima** (Lima-based headless docker) | `scripts/devops/bootstrap-container-engine.sh` → `colima start` |
| Windows | **Podman** (WSL2-backed, rootless; `alias docker=podman`) | `scripts/devops/bootstrap-container-engine.sh` (Git-Bash) |

## One-command bring-up (idempotent)

```bash
./scripts/devops/bootstrap-container-engine.sh
```
Detects OS; installs + starts the engine; removes Docker Desktop's `credsStore`; verifies
`docker info` (+ buildx, compose). Safe to re-run.

## Build + smoke-test a crate

```bash
cd crates/mendix-11
docker build --platform linux/amd64 --build-arg MENDIX_VERSION=11.6.4 \
-t ontologylabs/mendix-runtime:11.6.4 . # runtime pulled from cdn.mendix.com at build
./tests/smoke-test.sh /path/to/unzipped/mda # brings up postgres + runtime, polls :8080
```

## Gotchas

- **Docker Desktop `credsStore` breaks headless pulls.** A leftover `"credsStore": "desktop"` (or
`"osxkeychain"` on a headless/SSH box) in `~/.docker/config.json` makes `docker run` pulls fail with
*"credentials ... keychain cannot be accessed"*. The bootstrap removes the `desktop` helper; anonymous
public pulls then need no creds. **buildx/buildkit pulls bypass credsStore**, so `docker build` can
succeed while `docker run` fails — that's the tell.
- **`--platform linux/amd64` on Apple Silicon** runs under QEMU emulation (correct, slower). For fast
native amd64 builds, run on an x86_64 host or use a remote `docker buildx` builder pointed at one.
- **Colima restart drops a named buildkit builder** — recreate it with `docker buildx create` if a
`buildx build --builder <name>` suddenly fails.
- **Autostart vs a manually-started Colima.** `brew services start colima` can't take ownership while a
hand-started `colima` holds the VM (launchd error 5). The engine is still usable; for boot autostart,
one-time: `colima stop && brew services start colima`.
- **Windows/Podman.** Use `alias docker=podman` and `podman compose ...`; validate a crate's smoke test
on Podman before relying on it (compose + `docker.sock` paths have edge cases).
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ standard support but its final LTS patch (`8.18.35.97`) is still CDN-hosted;
older MX8 patches that aren't are covered by
[`crates/mendix-8/PORTAL-DOWNLOAD.md`](crates/mendix-8/PORTAL-DOWNLOAD.md).

## Container engine (no Docker Desktop needed)

These recipes use only the plain `docker` CLI, so **any** Docker-API-compatible engine works — you do
not need Docker Desktop. If `docker info` already succeeds, skip ahead to the [Quick start](#quick-start).
Otherwise an OS-aware, idempotent bootstrap is included:

```bash
./scripts/devops/bootstrap-container-engine.sh
```

It sets up a headless, free engine — **macOS / Linux → [Colima](https://github.com/abiosoft/colima)**,
**Windows → [Podman](https://podman.io)** — installs and starts it, removes Docker Desktop's `credsStore`
(which otherwise breaks headless image pulls), and verifies `docker info`, `buildx`, and `compose`. See
[`.claude/skills/container-build/`](.claude/skills/container-build/SKILL.md) for engine gotchas.

## Quick start

```bash
Expand Down
229 changes: 229 additions & 0 deletions scripts/devops/bootstrap-container-engine.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
#!/usr/bin/env bash
# bootstrap-container-engine.sh — OS-aware, idempotent setup of a headless,
# free, Docker-API-compatible container engine. Replaces Docker Desktop.
#
# macOS / Linux -> Colima (Lima-based headless docker)
# Windows -> Podman (WSL2-backed, rootless; `docker` shim)
#
# The baseline is an ABSTRACTION — "a headless, free, Docker-compatible engine".
# These crates call only the plain `docker` CLI, so once this script reports
# `docker info` green, `docker build` / `docker compose` and the crate smoke
# tests run unchanged on any of the engines above.
#
# Idempotent: safe to re-run. Verifies `docker info` (+ buildx, compose) at the
# end and exits non-zero if the engine is not actually usable. Vendor this file
# into your own repo and extend it locally as needed.
set -euo pipefail

# ---------------------------------------------------------------------------
# output helpers
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_ERR=$'\033[31m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'
else
C_OK=''; C_WARN=''; C_ERR=''; C_DIM=''; C_RST=''
fi
log() { printf '%s[engine]%s %s\n' "$C_DIM" "$C_RST" "$*"; }
ok() { printf '%s[ ok ]%s %s\n' "$C_OK" "$C_RST" "$*"; }
warn() { printf '%s[warn ]%s %s\n' "$C_WARN" "$C_RST" "$*" >&2; }
die() { printf '%s[fail ]%s %s\n' "$C_ERR" "$C_RST" "$*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }

# ---------------------------------------------------------------------------
# OS detection
# ---------------------------------------------------------------------------
detect_os() {
local s
s="$(uname -s 2>/dev/null || echo unknown)"
case "$s" in
Darwin) echo macos ;;
Linux)
# WSL presents as Linux but the *host* is Windows; the in-WSL engine is
# still a Linux engine, so treat WSL as Linux here. The Windows branch is
# for native Windows shells (Git-Bash / MSYS) driving Podman.
echo linux ;;
MINGW*|MSYS*|CYGWIN*) echo windows ;;
*)
[ "${OS:-}" = "Windows_NT" ] && { echo windows; return; }
echo "unknown:$s" ;;
esac
}

# ---------------------------------------------------------------------------
# shared: strip Docker Desktop's credsStore (breaks headless engines —
# the `docker-credential-desktop` helper is absent without Desktop).
# ---------------------------------------------------------------------------
strip_creds_store() {
local cfg="${HOME}/.docker/config.json"
[ -f "$cfg" ] || return 0
grep -q '"credsStore"[[:space:]]*:[[:space:]]*"desktop"' "$cfg" 2>/dev/null || return 0
log "removing Docker Desktop credsStore from ${cfg} (breaks headless auth)"
if have python3; then
python3 - "$cfg" <<'PY'
import json, sys
p = sys.argv[1]
with open(p) as f:
cfg = json.load(f)
if cfg.get("credsStore") == "desktop":
cfg.pop("credsStore", None)
with open(p, "w") as f:
json.dump(cfg, f, indent=2)
print(" credsStore removed")
PY
else
warn "python3 absent; edit ${cfg} by hand and delete the \"credsStore\": \"desktop\" line"
fi
}

# ---------------------------------------------------------------------------
# macOS — Colima
# ---------------------------------------------------------------------------
setup_macos() {
have brew || die "Homebrew required on macOS. Install: https://brew.sh then re-run."

local pkg
for pkg in colima docker docker-buildx docker-compose; do
if brew list --formula "$pkg" >/dev/null 2>&1; then
ok "$pkg already installed"
else
log "brew install $pkg"
brew install "$pkg"
fi
done

# buildx/compose are CLI plugins; link them into the docker cli-plugins dir so
# `docker buildx` / `docker compose` resolve without Docker Desktop.
local plugdir="${HOME}/.docker/cli-plugins"
mkdir -p "$plugdir"
local bx cp brewpfx
brewpfx="$(brew --prefix)"
bx="${brewpfx}/opt/docker-buildx/bin/docker-buildx"
cp="${brewpfx}/opt/docker-compose/bin/docker-compose"
[ -x "$bx" ] && ln -sf "$bx" "${plugdir}/docker-buildx"
[ -x "$cp" ] && ln -sf "$cp" "${plugdir}/docker-compose"

strip_creds_store

# Autostart check. brew colorizes 'started' under a TTY, which would defeat a
# plain whitespace anchor — strip ANSI, then match the status field exactly.
colima_service_started() {
local esc; esc=$(printf '\033')
brew services list 2>/dev/null \
| sed "s/${esc}\[[0-9;]*m//g" \
| awk '$1=="colima" && $2=="started"{f=1} END{exit !f}'
}

if colima status >/dev/null 2>&1; then
ok "Colima already running"
if colima_service_started; then
ok "Colima enrolled in brew services (autostart)"
else
# Up via a manual `colima start` — launchd can't take ownership of a held
# VM (bootstrap error 5). Don't stop a running engine; just inform.
warn "autostart not configured: a manually-started Colima holds the VM."
warn " one-time fix (engine restarts): colima stop && brew services start colima"
fi
else
# Down — start THROUGH brew services so the same step also enrols autostart
# (no manual/launchd ownership conflict). Fall back to a direct start.
log "starting Colima via brew services (starts + enrols autostart)"
if brew services start colima >/dev/null 2>&1 && colima_service_started; then
ok "Colima started and enrolled in brew services (autostart)"
else
log "brew services start unavailable — starting Colima directly"
colima start
warn "autostart not configured. For boot autostart: colima stop && brew services start colima"
fi
fi

docker context use colima >/dev/null 2>&1 || true
}

# ---------------------------------------------------------------------------
# Linux — prefer an already-working native dockerd; else Colima via brew.
# (Native Docker Engine is the sane headless engine on Linux; Colima adds a VM
# layer that is only worth it for parity with macOS. We don't force a VM here.)
# ---------------------------------------------------------------------------
setup_linux() {
strip_creds_store
if docker info >/dev/null 2>&1; then
ok "native docker engine already usable on Linux"
return 0
fi
if have colima; then
colima status >/dev/null 2>&1 || { log "starting Colima"; colima start; }
return 0
fi
warn "no usable docker engine found. Install one of:"
warn " - Docker Engine (CE): https://docs.docker.com/engine/install/ (recommended on Linux)"
warn " - Colima: brew install colima docker (if you want macOS parity)"
die "re-run this script once a Linux docker engine is installed."
}

# ---------------------------------------------------------------------------
# Windows — Podman (WSL2-backed, rootless). Run from Git-Bash / MSYS.
# Cannot be fully validated from a non-Windows host; this guides + best-effort
# automates via winget when present.
# ---------------------------------------------------------------------------
setup_windows() {
log "Windows engine = Podman (WSL2-backed, rootless)."
if have podman; then
ok "podman present"
elif have winget; then
log "winget install RedHat.Podman"
winget install -e --id RedHat.Podman || warn "winget install failed; install Podman Desktop manually: https://podman.io"
else
warn "Podman not found and winget unavailable."
warn "Install Podman Desktop (https://podman.io) or run in PowerShell: winget install RedHat.Podman"
die "re-run this script after Podman is installed."
fi

# Initialise + start a Podman machine (the WSL2 backend) if not yet running.
if podman machine list --format '{{.Running}}' 2>/dev/null | grep -qi true; then
ok "podman machine running"
else
podman machine inspect >/dev/null 2>&1 || { log "podman machine init"; podman machine init || true; }
log "podman machine start"
podman machine start || warn "could not start podman machine; start it from Podman Desktop"
fi

cat <<'EOF'
[engine] To make `docker ...` commands work against Podman, add to your shell profile:
alias docker=podman
and (PowerShell, for tools that read the var):
podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}'
$env:DOCKER_HOST = "npipe:////./pipe/docker_engine" # or the Podman socket
Compose: `podman compose ...` (podman-compose) is the drop-in for `docker compose`.
EOF
}

# ---------------------------------------------------------------------------
# verification gate — the engine must actually be usable
# ---------------------------------------------------------------------------
verify() {
have docker || { warn "docker CLI not on PATH (Windows/Podman: use \`podman\` or set the alias)"; return 0; }
log "verifying engine (docker info)..."
if ! docker info >/dev/null 2>&1; then
die "docker info failed — the engine is installed but not reachable. Check the engine is started."
fi
local summary
summary="$(docker info --format '{{.ServerVersion}} / {{.OperatingSystem}} / {{.Architecture}}' 2>/dev/null || echo '?')"
ok "engine reachable: ${summary}"
if docker buildx version >/dev/null 2>&1; then ok "buildx: $(docker buildx version 2>/dev/null | head -1)"; else warn "docker buildx missing (multi-arch builds unavailable)"; fi
if docker compose version >/dev/null 2>&1; then ok "compose: $(docker compose version 2>/dev/null | head -1)"; else warn "docker compose missing (compose smoke tests unavailable)"; fi
}

# ---------------------------------------------------------------------------
main() {
local os; os="$(detect_os)"
log "detected OS: ${os}"
case "$os" in
macos) setup_macos ;;
linux) setup_linux ;;
windows) setup_windows ;;
*) die "unsupported/unknown OS: ${os}. Supported: macOS, Linux, Windows." ;;
esac
verify
ok "container engine baseline ready."
}
main "$@"
Loading