Skip to content

Commit 7637701

Browse files
akoclaude
andcommitted
feat: add Podman support as Docker alternative (#34)
Add container runtime abstraction so mxcli works with both Docker and Podman 4.7+. Auto-detects the runtime on PATH, with MXCLI_CONTAINER_CLI env var as explicit override. Adds --container-runtime flag to mxcli init and a Podman-in-Podman devcontainer config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5e6dfca commit 7637701

File tree

12 files changed

+148
-27
lines changed

12 files changed

+148
-27
lines changed

.claude/skills/mendix/docker-workflow.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@ Use this when:
1414

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

20+
### Podman Support
21+
22+
mxcli auto-detects Docker or Podman. To force Podman:
23+
```bash
24+
export MXCLI_CONTAINER_CLI=podman
25+
```
26+
27+
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).
28+
2029
## Architecture
2130

2231
```
2332
Host machine (browser at localhost:8080)
24-
└── Docker (host daemon)
33+
└── Docker or Podman (host daemon)
2534
└── Devcontainer (VS Code)
2635
├── mxcli, JDK 21, project files
27-
└── Docker daemon (docker-in-docker)
28-
└── docker compose stack
36+
└── Docker/Podman daemon (docker-in-docker or podman-in-podman)
37+
└── docker/podman compose stack
2938
├── mendix container (8080, 8090)
3039
│ └── /mendix ← volume mount from .docker/build/
3140
└── postgres container (5432)
@@ -392,7 +401,7 @@ All defaults can be overridden in `.docker/.env`.
392401

393402
| Problem | Solution |
394403
|---------|----------|
395-
| `docker: command not found` | Rebuild devcontainer — docker-in-docker feature needs rebuild to activate |
404+
| `docker: command not found` | Rebuild devcontainer — docker-in-docker feature needs rebuild to activate. Or use Podman: `export MXCLI_CONTAINER_CLI=podman` |
396405
| `mxbuild not found` | Run `mxcli setup mxbuild -p app.mpr` to download from CDN |
397406
| `JDK 21 not found` | Rebuild devcontainer — JDK 21 should be pre-installed |
398407
| Build fails with version error | Requires Mendix >= 11.6.1 for PAD support |
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "Mendix Model SDK Go (Podman)",
3+
"build": {
4+
"dockerfile": "../Dockerfile"
5+
},
6+
"features": {
7+
"ghcr.io/devcontainers/features/podman-in-podman:1": {},
8+
"ghcr.io/devcontainers/features/github-cli:1": {}
9+
},
10+
"forwardPorts": [8080, 8090, 5432],
11+
"portsAttributes": {
12+
"8080-8099": { "onAutoForward": "silent" },
13+
"5432-5499": { "onAutoForward": "silent" }
14+
},
15+
"containerEnv": {
16+
"MXCLI_CONTAINER_CLI": "podman"
17+
},
18+
"customizations": {
19+
"vscode": {
20+
"extensions": [
21+
"golang.go",
22+
"ms-vscode.makefile-tools",
23+
"anthropic.claude-code",
24+
"mike-lischke.vscode-antlr4"
25+
],
26+
"settings": {
27+
"go.toolsManagement.autoUpdate": true,
28+
"go.useLanguageServer": true,
29+
"go.lintTool": "golangci-lint",
30+
"editor.formatOnSave": true,
31+
"[go]": {
32+
"editor.defaultFormatter": "golang.go"
33+
}
34+
}
35+
}
36+
},
37+
"postCreateCommand": "curl -fsSL https://claude.ai/install.sh | bash && go mod download",
38+
"remoteUser": "vscode",
39+
"remoteEnv": {
40+
"CGO_ENABLED": "0",
41+
"LANG": "en_US.UTF-8",
42+
"LC_ALL": "en_US.UTF-8"
43+
}
44+
}

cmd/mxcli/docker.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ Requirements:
3232
- Mendix 11.6.1 or later
3333
- MxBuild (auto-downloaded from CDN if not found)
3434
- JDK 21 (installed in devcontainers created by 'mxcli init')
35-
- Docker with Compose V2
35+
- Docker with Compose V2, or Podman 4.7+ with podman compose
36+
37+
Podman Support:
38+
mxcli auto-detects the container runtime (Docker or Podman).
39+
Override with: export MXCLI_CONTAINER_CLI=podman
3640
3741
Examples:
3842
# One command to setup, build, and start

cmd/mxcli/docker/m2ee.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func callM2EEViaDocker(opts M2EEOptions, dockerDir string, action string, params
163163
)
164164

165165
composePath := filepath.Join(dockerDir, "docker-compose.yml")
166-
cmd := exec.Command("docker", "compose", "-f", composePath, "exec", "-T", "mendix", "sh", "-c", curlCmd)
166+
cmd := exec.Command(ContainerCLI(), "compose", "-f", composePath, "exec", "-T", "mendix", "sh", "-c", curlCmd)
167167

168168
var stdout, stderr bytes.Buffer
169169
cmd.Stdout = &stdout

cmd/mxcli/docker/runtime.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ import (
1515
"time"
1616
)
1717

18+
// ContainerCLI returns the container runtime binary ("docker" or "podman").
19+
// Resolution order:
20+
// 1. MXCLI_CONTAINER_CLI env var (explicit override)
21+
// 2. "docker" if available on PATH
22+
// 3. "podman" if available on PATH
23+
// 4. "docker" as fallback (will fail with a clear error at exec time)
24+
func ContainerCLI() string {
25+
if cli := os.Getenv("MXCLI_CONTAINER_CLI"); cli != "" {
26+
return cli
27+
}
28+
if _, err := exec.LookPath("docker"); err == nil {
29+
return "docker"
30+
}
31+
if _, err := exec.LookPath("podman"); err == nil {
32+
return "podman"
33+
}
34+
return "docker"
35+
}
36+
1837
// RuntimeOptions configures docker runtime commands.
1938
type RuntimeOptions struct {
2039
// ProjectPath is the path to the .mpr file.
@@ -85,7 +104,7 @@ func WaitForReady(opts RuntimeOptions, timeout time.Duration) error {
85104
ctx, cancel := context.WithTimeout(context.Background(), timeout)
86105
defer cancel()
87106

88-
cmd := exec.CommandContext(ctx, "docker", "compose", "logs", "--follow", "--no-log-prefix", "--since", "1s", "mendix")
107+
cmd := exec.CommandContext(ctx, ContainerCLI(), "compose", "logs", "--follow", "--no-log-prefix", "--since", "1s", "mendix")
89108
cmd.Dir = dockerDir
90109

91110
stdout, err := cmd.StdoutPipe()
@@ -205,7 +224,7 @@ func Status(opts RuntimeOptions) error {
205224
}
206225

207226
// Get JSON output from docker compose ps
208-
cmd := exec.Command("docker", "compose", "ps", "--format", "json")
227+
cmd := exec.Command(ContainerCLI(), "compose", "ps", "--format", "json")
209228
cmd.Dir = dockerDir
210229

211230
output, err := cmd.Output()
@@ -261,7 +280,7 @@ func Shell(opts RuntimeOptions, execCmd string) error {
261280
args = []string{"exec", "-it", "mendix", "sh"}
262281
}
263282

264-
cmd := exec.Command("docker", append([]string{"compose"}, args...)...)
283+
cmd := exec.Command(ContainerCLI(), append([]string{"compose"}, args...)...)
265284
cmd.Dir = dockerDir
266285
cmd.Stdin = os.Stdin
267286
cmd.Stdout = opts.Stdout
@@ -278,7 +297,7 @@ func Shell(opts RuntimeOptions, execCmd string) error {
278297

279298
// runCompose executes a docker compose command in the given directory.
280299
func runCompose(dockerDir string, opts RuntimeOptions, args ...string) error {
281-
cmd := exec.Command("docker", append([]string{"compose"}, args...)...)
300+
cmd := exec.Command(ContainerCLI(), append([]string{"compose"}, args...)...)
282301
cmd.Dir = dockerDir
283302
cmd.Stdout = opts.Stdout
284303
if cmd.Stdout == nil {

cmd/mxcli/init.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import (
1717
)
1818

1919
var (
20-
initTools []string
21-
initAllTools bool
22-
initListTools bool
20+
initTools []string
21+
initAllTools bool
22+
initListTools bool
23+
initContainerRuntime string
2324
)
2425

2526
var initCmd = &cobra.Command{
@@ -57,6 +58,10 @@ Supported Tools:
5758
- vibe Mistral Vibe CLI agent with skills
5859
5960
All tools receive universal documentation in AGENTS.md and .ai-context/
61+
62+
Container Runtime:
63+
--container-runtime docker Use Docker-in-Docker (default)
64+
--container-runtime podman Use Podman-in-Podman
6065
`,
6166
Args: cobra.MaximumNArgs(1),
6267
Run: func(cmd *cobra.Command, args []string) {
@@ -445,7 +450,7 @@ All tools receive universal documentation in AGENTS.md and .ai-context/
445450
if err := os.MkdirAll(devcontainerDir, 0755); err != nil {
446451
fmt.Fprintf(os.Stderr, "Error creating .devcontainer directory: %v\n", err)
447452
} else {
448-
dcJSON := generateDevcontainerJSON(projectName, mprFile)
453+
dcJSON := generateDevcontainerJSON(projectName, mprFile, initContainerRuntime)
449454
if err := os.WriteFile(devcontainerJSON, []byte(dcJSON), 0644); err != nil {
450455
fmt.Fprintf(os.Stderr, " Error writing devcontainer.json: %v\n", err)
451456
}
@@ -1252,4 +1257,5 @@ func init() {
12521257
initCmd.Flags().StringSliceVar(&initTools, "tool", []string{}, "AI tool(s) to configure (claude, opencode, cursor, continue, windsurf, aider)")
12531258
initCmd.Flags().BoolVar(&initAllTools, "all-tools", false, "Initialize for all supported AI tools")
12541259
initCmd.Flags().BoolVar(&initListTools, "list-tools", false, "List supported AI tools and exit")
1260+
initCmd.Flags().StringVar(&initContainerRuntime, "container-runtime", "docker", "Container runtime for devcontainer (docker or podman)")
12551261
}

cmd/mxcli/testrunner/runner.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"path/filepath"
1414
"strings"
1515
"time"
16+
17+
"github.com/mendixlabs/mxcli/cmd/mxcli/docker"
1618
)
1719

1820
// RunOptions configures the test runner.
@@ -310,7 +312,7 @@ func captureRuntimeLogs(dockerDir string, timeout time.Duration, w io.Writer, ve
310312
ctx, cancel := context.WithTimeout(context.Background(), timeout)
311313
defer cancel()
312314

313-
cmd := exec.CommandContext(ctx, "docker", "compose", "logs", "--follow", "--no-log-prefix", "--since", "1s", "mendix")
315+
cmd := exec.CommandContext(ctx, docker.ContainerCLI(), "compose", "logs", "--follow", "--no-log-prefix", "--since", "1s", "mendix")
314316
cmd.Dir = dockerDir
315317

316318
stdout, err := cmd.StdoutPipe()
@@ -442,7 +444,7 @@ func findMxcli() (string, error) {
442444

443445
// runCompose executes a docker compose command in the given directory.
444446
func runCompose(dockerDir string, args ...string) error {
445-
cmd := exec.Command("docker", append([]string{"compose"}, args...)...)
447+
cmd := exec.Command(docker.ContainerCLI(), append([]string{"compose"}, args...)...)
446448
cmd.Dir = dockerDir
447449
cmd.Stdout = os.Stdout
448450
cmd.Stderr = os.Stderr

cmd/mxcli/tool_templates.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,22 +270,30 @@ recognize:
270270
`, projectName, mprFile, mprFile, mprFile)
271271
}
272272

273-
func generateDevcontainerJSON(projectName, mprPath string) string {
273+
func generateDevcontainerJSON(projectName, mprPath, containerRuntime string) string {
274+
feature := `"ghcr.io/devcontainers/features/docker-in-docker:2": {}`
275+
containerEnv := `"PLAYWRIGHT_CLI_SESSION": "mendix-app"`
276+
if containerRuntime == "podman" {
277+
feature = `"ghcr.io/devcontainers/features/podman-in-podman:1": {}`
278+
containerEnv = `"PLAYWRIGHT_CLI_SESSION": "mendix-app",
279+
"MXCLI_CONTAINER_CLI": "podman"`
280+
}
281+
274282
return fmt.Sprintf(`{
275283
"name": "%s",
276284
"build": {
277285
"dockerfile": "Dockerfile"
278286
},
279287
"features": {
280-
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
288+
%s
281289
},
282290
"forwardPorts": [8080, 8090, 5432],
283291
"portsAttributes": {
284292
"8080-8099": { "onAutoForward": "silent" },
285293
"5432-5499": { "onAutoForward": "silent" }
286294
},
287295
"containerEnv": {
288-
"PLAYWRIGHT_CLI_SESSION": "mendix-app"
296+
%s
289297
},
290298
"postCreateCommand": "curl -fsSL https://claude.ai/install.sh | bash && if [ -f ./mxcli ] && ! file ./mxcli | grep -q Linux; then echo '⚠ ./mxcli is not a Linux binary. Replace it with the linux-amd64 or linux-arm64 build.'; fi",
291299
"customizations": {
@@ -300,7 +308,7 @@ func generateDevcontainerJSON(projectName, mprPath string) string {
300308
},
301309
"remoteUser": "vscode"
302310
}
303-
`, projectName)
311+
`, projectName, feature, containerEnv)
304312
}
305313

306314
func generateDockerfile(projectName, mprPath string) string {

docs-site/src/tools/devcontainer.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ mxcli init -p app.mpr
5757
# - VS Code MDL extension (auto-installed)
5858
```
5959

60-
## Docker-in-Docker
60+
## Container Runtime (Docker or Podman)
6161

62-
For `mxcli docker build`, `mxcli docker run`, and `mxcli test` (which require Docker), the dev container must have Docker-in-Docker support enabled. This is typically configured in `devcontainer.json`:
62+
For `mxcli docker build`, `mxcli docker run`, and `mxcli test` (which require a container runtime), the dev container must have Docker-in-Docker or Podman-in-Podman support enabled.
63+
64+
### Docker-in-Docker (default)
6365

6466
```json
6567
{
@@ -69,6 +71,23 @@ For `mxcli docker build`, `mxcli docker run`, and `mxcli test` (which require Do
6971
}
7072
```
7173

74+
### Podman-in-Podman
75+
76+
For organizations that cannot use Docker Desktop due to licensing:
77+
78+
```json
79+
{
80+
"features": {
81+
"ghcr.io/devcontainers/features/podman-in-podman:1": {}
82+
},
83+
"containerEnv": {
84+
"MXCLI_CONTAINER_CLI": "podman"
85+
}
86+
}
87+
```
88+
89+
When running `mxcli init`, use `--container-runtime podman` to generate this configuration automatically. Requires Podman 4.7+ (ships `podman compose` with Docker Compose V2 compatibility).
90+
7291
## Typical Dev Container Workflow
7392

7493
1. Open the project in VS Code

docs-site/src/tools/docker-run.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ mxcli docker run -p app.mpr
1717

1818
## Prerequisites
1919

20-
- Docker must be installed and running
20+
- Docker (or Podman 4.7+) must be installed and running
2121
- The project must be buildable (no errors in `mxcli docker check`)
2222
- A PostgreSQL database must be available (Docker can provide one)
2323

0 commit comments

Comments
 (0)