Devcontainer
The devcontainer preset is a Dev Containers image for VS Code and JetBrains. You write code inside the container. The development and production presets are different: they run your app as a service.
It's commented out by default. Add it to presets in config/podman.php, or generate it directly.
Setup
php artisan podman:generate devcontainer
This writes a Containerfile, entrypoint.sh and four devcontainer configs to podman/devcontainer/runtimes/. Your editor looks for .devcontainer/devcontainer.json, so copy the config you want there:
mkdir -p .devcontainer
cp podman/devcontainer/runtimes/devcontainer.json .devcontainer/devcontainer.json
After running podman:generate devcontainer again, copy it again and rebuild the container in your editor.
The config holds your UID and GID, so add /.devcontainer to .gitignore instead of committing it.
Choosing a config
Pick prebuilt or local, and with or without AI tools:
| Config | Image | Use it when |
|---|---|---|
devcontainer.json (default) |
Prebuilt ghcr.io/foxws/laravel-podman-devcontainer:php-8.5 (see CI) |
You want to start right away |
devcontainer-local.json |
Builds the Containerfile locally (--target=base) |
You published the preset and changed the Containerfile (extra PHP_EXTENSIONS, apt packages, ...) |
devcontainer-ai.json |
Prebuilt php-8.5-ai image |
You want the AI tools without building |
devcontainer-local-ai.json |
Builds the ai stage locally (--target=ai) |
You want the AI tools and a local build |
To switch, copy another config over .devcontainer/devcontainer.json.
Mounting host folders
Only ~/.ssh is mounted by default, read-only, so Git can use your SSH keys. The configs list other host folders under mounts as comments. Uncomment the ones you use:
| Folder | For | Configs |
|---|---|---|
~/.config/gh |
GitHub CLI (gh) login |
All |
~/.aws |
AWS CLI credentials | All |
~/.config/cloud |
Laravel Cloud CLI login | All |
~/workspace |
Other projects, such as packages you develop alongside the app, at /workspace |
All |
~/.claude, ~/.claude.json |
Claude Code login | ai |
~/.codex |
OpenAI Codex CLI login | ai |
They're commented out because Podman won't start the container when a mounted folder doesn't exist on your host. Before uncommenting one, check that it exists.
devcontainer.json allows comments, so you can leave the others in place.
On a desktop with a keyring, such as GNOME, gh auth login stores the token in the keyring instead of in ~/.config/gh. The mount then has no token to share. Run gh auth login --insecure-storage on your host to store it in ~/.config/gh/hosts.yml instead.
SELinux
The workspace mount has no Z flag. --security-opt=label=disable in runArgs already lets the devcontainer read and write your project without relabeling it. Z would give the project folder a label private to the devcontainer, and the development services that mount the same folder (with z) would then get "permission denied" until they're restarted.
If you added ,Z to workspaceMount in your own config, remove it and rebuild the container.
Podman and lpod
The devcontainer has no Podman, lpod or systemd. Run those in a terminal on your host, from the project folder:
lpod install development/app.quadlets --replace
lpod my-app restart
Everything else runs in the devcontainer directly: php artisan, composer, pnpm and tests. It joins the app's systemd-{application} network (created by initializeCommand), so the database, cache and other services in .env are reachable by their container names.
php artisan podman:generate also works in the devcontainer. The configs set PODMAN_WORKING_PATH to ${localWorkspaceFolder}, so the rendered files mount your host's project folder, not /app.
The devcontainer doesn't run Podman in Podman or mount the host's Podman socket, on purpose:
- A nested Podman has its own containers and no access to your host's systemd, so it can't manage the app's Quadlet services.
- The host socket would let anything in the container, including an AI agent, control every container and volume of your user, including database volumes.
With Laravel Boost, the package's podman-lpod skill tells AI agents in the devcontainer to give you the host commands instead of running them.
What's inside
Debian (php:8.5-cli) with:
default-mysql-client, a PostgreSQL client (POSTGRES_VERSION) andsqlite3- PHP extensions:
apcu bcmath exif ffi gd igbinary imagick intl pcntl pdo_mysql pdo_pgsql pdo_sqlite redis sockets zip, plus any in thePHP_EXTENSIONSbuild arg - Node.js (
NODE_VERSION) withpnpm,yarnandbun cpx(likenpx, for Composer packages),gh,awscliandshellcheck
AI variant
The ai configs add these on top of What's inside. Each has a build arg that defaults to latest. Set it to false to skip the tool, or to a version to pin it:
| Tool | Build arg | Pin example |
|---|---|---|
| Claude Code | CLAUDE_CLI |
stable, 2.1.89 |
| OpenAI Codex CLI | CODEX_CLI |
any npm version |
Laravel LSP (laravel-lsp) |
AI_LARAVEL_LSP |
0.0.32 |
To add other npm-based agent CLIs, list them in AI_NPM_PACKAGES, e.g. @google/gemini-cli. Build args only apply when you build locally.
Laravel Boost
For Laravel-specific context (routes, database schema, config, Tinker), add laravel/boost to your app:
composer require laravel/boost --dev
php artisan boost:install
Laravel agent skills
Claude Code uses laravel-lsp through the laravel-lsp plugin from laravel/agent-skills. To enable it, and the laravel plugin with its agents and skills, add this to .claude/settings.json in your project (next to artisan, not in ~/.claude):
{
"extraKnownMarketplaces": {
"laravel": {
"source": { "source": "github", "repo": "laravel/agent-skills" }
}
},
"enabledPlugins": {
"laravel@laravel": true,
"laravel-lsp@laravel": true
}
}
- Commit it, and everyone on the project is asked to install the plugins when they trust the project.
- To keep it to yourself, use
.claude/settings.local.jsoninstead. - Installed plugins are stored in
~/.claude/plugins, which the host and container share. laravel-cloud@laravelandlaravel-nightwatch@laravelare also available.
Login persistence
To stay logged in after rebuilding the container, uncomment the ~/.claude and ~/.claude.json mounts, or ~/.codex, in your config. See Mounting host folders.
Create ~/.claude.json before the first start if it doesn't exist:
touch ~/.claude.json
Otherwise Podman creates a directory with that name, and Claude Code can't use it.
Using API keys instead
To keep your host logins out of the container, leave the .claude/.codex mounts commented out and set ANTHROPIC_API_KEY or OPENAI_API_KEY in containerEnv.
API keys are billed separately, per token. A Claude.ai or ChatGPT subscription doesn't include one, so you need an account at console.anthropic.com or platform.openai.com.
File ownership (UID/GID)
Files you create in the container should be owned by your host user. This works as follows:
podman:generatewrites your host UID/GID intoPUID/PGIDincontainerEnv.- The container starts as root, and
entrypoint.shchanges thedockeruser to that UID/GID. - It then switches to
dockerwithgosu.
Together with --userns=keep-id in runArgs, this works for local builds and for the prebuilt image, which is always built with UID/GID 1000.
Links
- CI: Building a Container Image
- Customizing
- Flatpak-packaged editors, if your editor runs as a Flatpak
- Introduction
