Foxws
Laravel Podman

Devcontainer

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.

Setup

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.

Four configs, two choices

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.

What's inside

Debian-based (php:8.5-cli), with:

  • default-mysql-client, a PostgreSQL client (version-pinned via POSTGRES_VERSION), and sqlite3
  • PHP extensions: apcu 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 arg
  • Node.js (version-pinned via NODE_VERSION) with pnpm/yarn via Corepack, and bun
  • cpx (Composer's npx equivalent), gh, awscli

AI variant

devcontainer-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.

Laravel-specific context

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.

Login persistence

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.

Using API keys instead

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.

UID/GID handling

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).