Foxws
Laravel Podman

Customizing

Behavior is driven by config/podman.php (php artisan vendor:publish --tag="podman-config") and by the preset template files on disk.

Config keys

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.

Custom presets

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.

  • Tweak an existing service — publish it first (php artisan podman:publish frankenphp-octane), then edit containers/stubs/frankenphp-octane/quadlets/pgsql.quadlets.
  • Add a service to a preset — create 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.
  • Customize build files — the same rule applies to runtimes/ (Containerfile, entrypoint.sh, php ini, Caddy templates).
  • Add a new preset — create 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

Custom substitutions

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.

Available services

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.

Swapping a service

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 appmailpit/horizon/reverb/schedule
BindsTo= Like Requires=, but also stops this unit when the target stops horizon/reverb/schedule/inertia-ssrapp
PartOf= A stop/restart of the target propagates here, one-directional typesense/mailpitapp

Increasing a service's memory limit

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

Multi-application hosts

Pass --application= to lpod install (requires Podman 6+) so each app gets its own install subdirectory.

Reverse proxying sibling services without the proxy preset

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