Behavior is driven by config/podman.php (php artisan vendor:publish --tag="podman-config") and by the preset template files on disk.
| Key | Env variable | Default | Purpose |
|---|---|---|---|
enabled |
PODMAN_ENABLED |
true |
Master switch for podman:generate/podman:setup/podman:publish/podman:s3-setup |
quadlet_prefix |
PODMAN_QUADLET_PREFIX |
APP_NAME (falls back to laravel) |
Namespaces installed services, e.g. laravel-pgsql |
proxy_prefix |
PODMAN_PROXY_PREFIX |
proxy |
Namespace used for the proxy service/network |
stubs_path |
PODMAN_STUBS_PATH |
containers/stubs |
Lookup root for custom presets |
working_path |
PODMAN_WORKING_PATH |
Laravel's base_path() |
Host path baked into {{workingPath}}/{{runtimePath}}. Override per run with --working-path= on podman:generate |
config_path |
PODMAN_CONFIG_PATH |
working_path |
Host path baked into {{configPath}}, for a service's config living outside the project |
quadlet_uid/quadlet_gid |
PODMAN_QUADLET_UID/_GID |
Current user's UID/GID | Baked into generated Quadlet files |
publish_path |
PODMAN_PUBLISH_PATH |
podman |
Where podman:generate writes rendered presets. This is a build artifact, so don't commit it |
selinux_volume_mapping |
PODMAN_SELINUX_VOLUME_MAPPING |
true |
Keeps Z/z/U volume flags; disable on non-SELinux hosts |
presets |
PODMAN_DEFAULT_PRESETS |
see config/podman.php |
Presets podman:setup publishes/generates by default |
s3_buckets |
PODMAN_S3_BUCKETS |
see config/podman.php |
Buckets podman:s3-setup creates — see S3 Buckets |
s3_cors_buckets |
PODMAN_S3_CORS_BUCKETS |
see config/podman.php |
Which of s3_buckets get the CORS policy |
substitutions |
(none) | [] |
Extra {{placeholder}} => value pairs merged into every template — see Custom substitutions |
presets/s3_buckets/s3_cors_buckets each accept a comma-separated string or a plain PHP array.
A preset is a folder with a quadlets/ directory (*.quadlets files) and a runtimes/ directory (container build files). stubs_path is the lookup root: stubs_path/{preset} is used if it exists, otherwise the package falls back to its own bundled preset. A custom preset fully replaces the bundled one — it isn't merged file by file.
php artisan podman:publish frankenphp-octane), then edit containers/stubs/frankenphp-octane/quadlets/pgsql.quadlets.containers/stubs/frankenphp-octane/quadlets/my-service.quadlets (same # FileName=... + --- format), then run php artisan podman:generate frankenphp-octane and lpod install frankenphp-octane/my-service.quadlets.runtimes/ (Containerfile, entrypoint.sh, php ini, Caddy templates).containers/stubs/my-preset/quadlets/ and .../runtimes/ directly.Placeholders are kept intact by podman:publish and filled in by podman:generate:
| Placeholder | Value |
|---|---|
{{application}} |
Kebab-cased quadlet_prefix |
{{proxy}} |
Kebab-cased proxy_prefix |
{{appEnv}} |
app.env config value |
{{appName}} |
app.name config value |
{{appUrl}} |
app.url config value |
{{appHost}} |
Host portion of app.url |
{{appUid}}/{{appGid}} |
Resolved quadlet_uid/quadlet_gid |
{{workingPath}} |
Resolved working_path |
{{configPath}} |
Resolved config_path (defaults to working_path) |
{{runtimePath}} |
Preset's generated runtimes/ folder, e.g. podman/frankenphp-octane/runtimes |
{{configPath}} defaults to working_path, so nothing changes unless you set it. It's handy for keeping a service's live config outside the project, e.g. in your own preset's proxy.quadlets:
Volume={{configPath}}/{{proxy}}:/etc/caddy:rw,z,U
Merge your own {{placeholder}} => value pairs into every template via substitutions. Values are plain PHP, so env(...) works just like anywhere else in the config:
'substitutions' => [
'{{apiEndpoint}}' => env('API_ENDPOINT'),
],
Environment=API_ENDPOINT={{apiEndpoint}}
This can also override a built-in placeholder of the same name (e.g. {{appHost}}) — substitutions always wins.
Each preset (except devcontainer/s3) bundles app plus the services below. Only one service per category runs at a time — they're alternatives, not additions.
| Category | Services (default first) |
|---|---|
| Database | pgsql, mariadb, mysql, mongodb |
| Cache/queue | valkey, redis, memcached |
| Search | typesense, meilisearch |
| Object storage | rustfs |
| Mail catcher | mailpit |
frankenphp-octane also bundles horizon, reverb, schedule, and inertia-ssr. These are always on, not alternatives.
pgsql/valkey are wired directly into app.quadlets' [Unit] section, not auto-detected:
php artisan podman:publish frankenphp-octane # or development
Edit containers/stubs/frankenphp-octane/quadlets/app.quadlets:
Requires={{application}}-mysql.container {{application}}-redis.container
After={{application}}-mysql.container {{application}}-redis.container
Then regenerate and reinstall both:
php artisan podman:generate frankenphp-octane
lpod install frankenphp-octane/mysql.quadlets --replace
lpod install frankenphp-octane/app.quadlets --replace
Also update .env (DB_CONNECTION, DB_HOST, etc.) — Podman wires the containers together, but Laravel still needs to know which one to talk to.
[Unit] directives| Directive | Meaning | Used for |
|---|---|---|
Requires= |
Hard dependency — if the target fails, this unit stops too | app → its database + cache |
After= |
Ordering only, doesn't propagate failures | Paired with Requires=/Wants= |
Wants= |
Soft dependency — tries to start the target, but doesn't fail if it can't | app → mailpit/horizon/reverb/schedule |
BindsTo= |
Like Requires=, but also stops this unit when the target stops |
horizon/reverb/schedule/inertia-ssr → app |
PartOf= |
A stop/restart of the target propagates here, one-directional | typesense/mailpit → app |
php artisan podman:publish frankenphp-octane
Add Memory= under [Container] in containers/stubs/frankenphp-octane/quadlets/pgsql.quadlets, then:
php artisan podman:generate frankenphp-octane
lpod install frankenphp-octane/pgsql.quadlets --replace
Pass --application= to lpod install (requires Podman 6+) so each app gets its own install subdirectory.
proxy presetfrankenphp-octane runs Octane's FrankenPHP server, which embeds its own Caddy — separate from the bundled proxy preset's Caddy container (see Proxy). If you're not running that preset — say, you use an external load balancer, Laravel Cloud, or any host that only exposes the app container — the embedded Caddy can still reverse proxy sibling services (Reverb, a mail catcher, S3-compatible storage) directly, via Octane's CADDY_EXTRA_CONFIG environment variable.
Foxws\Podman\Support\PodmanCaddySites builds that value from plain env vars, for use in config/octane.php:
use Foxws\Podman\Support\PodmanCaddySites;
'caddy' => [
'env' => [
// Port must match the "--port" passed to "octane:frankenphp" in APP_COMMAND.
'CADDY_EXTRA_CONFIG' => PodmanCaddySites::render([
PodmanCaddySites::hostFromUrl((string) env('AWS_URL')) => PodmanCaddySites::hostPortFromUrl((string) env('AWS_ENDPOINT')),
(string) env('VITE_REVERB_HOST', env('REVERB_HOST')) => PodmanCaddySites::hostPort(env('REVERB_HOST'), env('REVERB_PORT', 6001)),
(string) env('MAILPIT_UI_HOST') => PodmanCaddySites::hostPort(env('MAIL_HOST'), 8025),
], (int) env('OCTANE_PORT', 8000)),
],
],
Say your .env sets AWS_URL=https://s3.laravel.test, VITE_REVERB_HOST=ws.laravel.test, and MAILPIT_UI_HOST=mail.laravel.test — the same s3./ws./mail. subdomain convention the bundled sites/laravel.Caddyfile uses for the proxy preset (see Proxy). That resolves to:
| Public hostname | Upstream | Env vars used |
|---|---|---|
s3.laravel.test |
e.g. minio:9000 |
AWS_URL, AWS_ENDPOINT |
ws.laravel.test |
e.g. reverb:6001 |
VITE_REVERB_HOST (falls back to REVERB_HOST), REVERB_HOST, REVERB_PORT |
mail.laravel.test |
e.g. mailpit:8025 |
MAILPIT_UI_HOST, MAIL_HOST |
Add or drop rows to match whichever sibling services your own app actually proxies — none of this is fixed by the package.
This has to read raw env() rather than config(), since config files can't safely depend on each other's load order. An empty hostname or upstream (an unset env var) is skipped, so any service you haven't configured is simply left out.
render() pins each block to http:// by default. A bare hostname would make Caddy attempt automatic HTTPS (binding :443), which crashes the server once CAP_NET_BIND_SERVICE is stripped from the FrankenPHP binary and it runs as a non-root user, as the frankenphp-octane image does. Pass a third $scheme argument only if your embedded Caddy is allowed to bind privileged ports itself.