The devcontainer preset is a VS Code/JetBrains Dev Containers image for developing your Laravel app itself inside a container. That's different from the development/frankenphp-octane presets, which run the app as a service. It's commented out by default, so add it to presets in config/podman.php (or generate it directly) to use it.
php artisan podman:generate devcontainer
This renders stubs/devcontainer/runtimes/ into podman/devcontainer/runtimes/ — a Containerfile, entrypoint.sh, and four devcontainer configs (see below). VS Code/JetBrains look for .devcontainer/devcontainer.json at the project root, not under podman/, so symlink whichever config you want to use:
mkdir -p .devcontainer
ln -sf ../podman/devcontainer/runtimes/devcontainer.json .devcontainer/devcontainer.json
Using a symlink instead of a copy means re-running podman:generate devcontainer keeps it up to date automatically.
There are two independent choices — prebuilt vs. local, and default vs. AI — which give four configs:
| Config | Image source | Use it when |
|---|---|---|
devcontainer.json (default) |
ghcr.io/foxws/laravel-podman-devcontainer:php-8.5, the prebuilt image from this repo's own CI (see CI: Building a Container Image) |
You want to start immediately, with no local build |
devcontainer-local.json |
Builds podman/devcontainer/runtimes/Containerfile (--target=base) |
You ran podman:publish devcontainer and edited the Containerfile (extra PHP_EXTENSIONS, apt packages, etc.) — the prebuilt image won't reflect those changes |
devcontainer-ai.json |
Same as devcontainer.json, but pulls the php-8.5-ai tag |
You want the AI CLIs below, without building locally |
devcontainer-local-ai.json |
Same as devcontainer-local.json, but builds the Containerfile's ai stage (--target=ai) |
You want the AI CLIs and a local build |
Symlink .devcontainer/devcontainer.json to whichever one you need. Switching later is just repointing the symlink.
Debian-based (php:8.5-cli), with:
default-mysql-client, a PostgreSQL client (version-pinned via POSTGRES_VERSION), and sqlite3apcu bcmath exif ffi gd igbinary imagick intl pcntl pdo_mysql pdo_pgsql pdo_sqlite redis sockets zip, plus whatever you pass via the PHP_EXTENSIONS build argNODE_VERSION) with pnpm/yarn via Corepack, and buncpx (Composer's npx equivalent), gh, awsclidevcontainer-ai.json/devcontainer-local-ai.json add Claude Code and OpenAI Codex CLI on top of everything in What's inside, plus whatever npm-installable agent CLI you pass via the AI_NPM_PACKAGES build arg (e.g. @google/gemini-cli). That build arg only matters if you're building locally, since it only takes effect on the ai target. Each CLI has its own build arg — CLAUDE_CLI/CODEX_CLI — which defaults to latest. Set it to false to skip that CLI, or to a version/channel (e.g. stable, 2.1.89) to pin it.
If you want Laravel-specific context (routes, DB schema, config, Tinker) instead of a generic filesystem view, pair either CLI with laravel/boost in your app itself. That's a per-project Composer package (composer require laravel/boost --dev && php artisan boost:install), not something this Containerfile installs.
The ai configs bind-mount ~/.claude, ~/.claude.json, and ~/.codex from the host into the container, read-write (unlike the read-only .ssh mount), so claude/codex stay logged in across container rebuilds instead of asking you to authenticate every time.
~/.claude.json is a file, not a directory. If it doesn't exist yet on your host, create it before first launching the container (touch ~/.claude.json) — otherwise Podman will create an empty directory in its place, and Claude Code won't be able to use it. ~/.claude and ~/.codex don't have this problem, since Podman creates them as directories automatically if they're missing.
If you'd rather not share host credentials with the container at all, drop the .claude/.codex mounts from your copy of the config and set ANTHROPIC_API_KEY/OPENAI_API_KEY in containerEnv instead. This isn't just a simpler login — it's a different product with separate billing. A Claude.ai/ChatGPT consumer subscription can't be used as an API key, so this route needs a pay-per-token account at console.anthropic.com/platform.openai.com, in addition to (or instead of) your regular subscription.
The container starts as root. entrypoint.sh renumbers the docker user to PUID/PGID (from containerEnv, which podman:generate fills in from your actual host UID/GID) before dropping privileges via gosu. Combined with --userns=keep-id:uid=...,gid=... in runArgs, this keeps the container's docker user aligned with your host user's file ownership on the bind-mounted workspace — whether you're running the locally-built image or the prebuilt one from GHCR (which is always built with UID/GID 1000, regardless of your actual host UID).