Skip to content

Instantly share code, notes, and snippets.

@ibnu-ja
Last active January 11, 2026 12:31
Show Gist options
  • Select an option

  • Save ibnu-ja/a76b67f17c3487f13d0baf1185305d21 to your computer and use it in GitHub Desktop.

Select an option

Save ibnu-ja/a76b67f17c3487f13d0baf1185305d21 to your computer and use it in GitHub Desktop.

Creatoing Multi-Container Laravel Inertia.js SSR Image with Podman

This setup is designed for production deployment. For local development, use DevContainers or Laravel Sail.

Images are significantly smaller compared to Laravel Sail, each around 400MB for small to medium applications. Horizontally scalable can be achieved by deploying multiple PHP and SSR containers behind a load balancer. Use Redis for sessions/cache and shared storage (S3, NFS) for multi-instance deployments.

1. Initialize Application

Obviously, we need a Laravel application. If you don't have one, you can install it using Podman:

# Install Laravel Installer
podman run --rm -it -v .:/app:z -v ~/.composer:/tmp:z docker.io/library/composer global require laravel/installer

# Create New Project
podman run --rm -it -v .:/app:z -v ~/.composer:/tmp:z --entrypoint /tmp/vendor/bin/laravel docker.io/library/composer new example-app

cd example-app

2. Create Docker Assets

Note: You must configure the Containerfile builder stages to run any artisan commands required for your build (e.g., wayfinder:generate, ziggy:generate, or nova:install).

File: php.Containerfile

This image runs PHP using FrankenPHP and serves the app with Caddy.

FROM docker.io/library/composer:latest AS composer_builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --ignore-platform-reqs --no-scripts --no-autoloader --prefer-dist
COPY . .
# Add build-time artisan commands here
RUN composer dump-autoload --optimize --classmap-authoritative \
    && php artisan wayfinder:generate --with-form

FROM docker.io/library/node:lts-alpine AS node_builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
COPY --from=composer_builder /app/resources /app/resources
RUN npm run build

FROM docker.io/dunglas/frankenphp:php8.5-alpine

ENV SUPERVISOR_PHP_COMMAND="/usr/local/bin/php -d variables_order=EGPCS /app/artisan octane:frankenphp --host=0.0.0.0 --admin-port=2019 --port=80"

RUN install-php-extensions \
    bcmath pdo_mysql zip intl gd pcntl opcache \
    && apk add --no-cache supervisor \
    && rm -rf /tmp/* /var/cache/apk/*

COPY docker/runtime/start-container /usr/local/bin/start-container
COPY docker/runtime/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/runtime/php.ini /usr/local/etc/php/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container

WORKDIR /app

COPY --from=composer_builder /app/vendor /app/vendor
COPY --from=composer_builder /app/composer.json /app/composer.lock /app/
COPY --from=node_builder /app/public/build /app/public/build

COPY artisan ./
COPY app/ app/
COPY bootstrap/ bootstrap/
COPY config/ config/
COPY database/ database/
COPY routes/ routes/
COPY public/ public/
COPY storage/ storage/
COPY resources/views/ resources/views/
#https://github.com/containers/buildah/issues/5707
COPY lang* ./

EXPOSE 80 2019

ENTRYPOINT ["start-container"]
File: docker/runtime/start-container (Make executable: chmod +x)

Based on Laravel Sail entrypoint

#!/usr/bin/env ash
if [ ! -d /.composer ]; then mkdir /.composer; fi
chmod -R ugo+rw /.composer
if [ $# -gt 0 ]; then exec "$@"; else exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf; fi
File: docker/runtime/supervisord.conf

Based on Laravel Sail supervisord.conf

[supervisord]
nodaemon=true
user=root
logfile=/tmp/supervisord.log
pidfile=/var/run/supervisord.pid

[program:php]
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
user=root
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

You can also add worker process here. Refer to Laravel Queue docs.

File: docker/runtime/php.ini

Copied from Laravel Sail php.ini

[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
pcov.directory = .
File: ssr.Containerfile

This image runs the SSR service using Node.js.

FROM docker.io/library/composer:latest AS composer_builder

WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --ignore-platform-reqs --no-scripts --no-autoloader --prefer-dist
COPY . .
# Add build-time artisan commands here
RUN composer dump-autoload --optimize --classmap-authoritative \
    && php artisan wayfinder:generate --with-form

FROM docker.io/library/node:lts-alpine AS ssr_builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
COPY --from=composer_builder /app/resources /app/resources
RUN npm run build:ssr

FROM docker.io/library/node:lts-alpine

WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=ssr_builder /app/bootstrap/ssr /app/bootstrap/ssr
COPY --from=ssr_builder /app/public /app/public

EXPOSE 13714

CMD ["node", "bootstrap/ssr/ssr.js"]
File: .dockerignore
.git
.env
/vendor
/node_modules
/public/build
/bootstrap/cache/*
.phpunit.result.cache
/storage/*.key
Folder: lang

Somehow buildah requires this folder to exist.

mkdir lang && touch lang/.gitkeep

3. Configure Application

Update: config/inertia.php
'ssr' => [
    'enabled' => (bool) env('INERTIA_SSR_ENABLED', true),
    'url' => env('INERTIA_SSR_URL', 'http://127.0.0.1:13714'),
    'ensure_bundle_exists' => (bool) env('INERTIA_SSR_ENSURE_BUNDLE_EXISTS', true),
],
Update: vite.config.ts

Adjust anything if needed. For example, I comment out the Wayfinder plugin because Node cannot find PHP; run commands separately.

// wayfinder({
//     formVariants: true,
// }),
Update: .env
INERTIA_SSR_URL=http://127.0.0.1:13714
INERTIA_SSR_ENSURE_BUNDLE_EXISTS=false

4. Build & Deploy

Build Images
podman build --file php.Containerfile -t localhost/example-app-php .
podman build --file ssr.Containerfile -t localhost/example-app-ssr .
Run using Pod
# Create Pod
podman pod create --name example-app -p 9999:80

# Start SSR
podman run -d --name node --replace --init --env-file .env --pod example-app localhost/example-app-ssr

# Start PHP
podman run -d --name php --replace --init --env-file .env --pod example-app localhost/example-app-php
Run using container network
podman network create example-app

# Start SSR
podman run -d --name node --replace --init --env-file .env --network example-app localhost/example-app-ssr

# Start PHP (Link to SSR container name)
podman run -d --name php --replace --init --env-file .env \
  -e INERTIA_SSR_URL=http://node:13714 \
  -p 9999:80 --network example-app \
  localhost/example-app-php
Database

For SQLite, create a volume and mount it to /app/database in the PHP container. For a database container, deploy MySQL/PostgreSQL/MariaDB to the same pod or network. Use DB_HOST=127.0.0.1 for pods (shared localhost) or container name for networks.

Storage

If using local storage driver, create a volume and mount it to /app/storage in the PHP container to persist uploaded files and application data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment