Laravel Podman

Ci Build

CI: Building a Container Image

This is an example GitHub Actions workflow that renders a preset's Containerfile via podman:generate and builds/pushes a multi-arch image with buildah. Copy it into your own app's .github/workflows/ and adjust the preset name/paths for your setup.

The devcontainer preset is the exception: it ships no template placeholders and copies no app source, so it builds as-is straight from this repo, with no rendering step needed. This repo's own .github/workflows/build-devcontainer.yml does exactly that, and publishes the result to ghcr.io/foxws/laravel-podman-devcontainer for devcontainer.json to pull directly (see Devcontainer). Every build is tagged by PHP version and variant (php-8.5, php-8.5-ai, ...) — the workflow's variant matrix axis maps to the Containerfile's base/ai stages via --target. The PHP version marked default: true additionally gets the floating main/commit-sha/latest tags on its default variant. To build/tag additional versions (e.g. 8.4, 8.6), add another entry to the php: matrix array.

Prerequisites

Requirement Why
A committed .env (or one written in CI, e.g. from an .env.ci template), with APP_KEY generated before podman:generate runs The preset's Containerfile/templates may read app config at render time
The preset you're building must ship a runtimes/Containerfile The bundled frankenphp-octane does; custom presets need their own — see Customizing

Example: .github/workflows/build.yml

name: Build

on:
    push:
        branches:
            - main
    pull_request:
    release:
        types: [published]
    workflow_dispatch:

concurrency:
    group: docker-${{ github.workflow }}-${{ github.ref }}
    cancel-in-progress: true

env:
    REGISTRY: ghcr.io
    IMAGE_NAME: ${{ github.event.repository.name }}
    IMAGE: ghcr.io/${{ github.repository }}
    CACHE_IMAGE: ghcr.io/${{ github.repository }}-cache

permissions:
    contents: read

jobs:
    build:
        name: Build (${{ matrix.platform }})
        runs-on: ${{ matrix.runner }}
        timeout-minutes: 30
        permissions:
            contents: read
            packages: write

        strategy:
            fail-fast: false
            matrix:
                include:
                    - platform: linux/amd64
                      runner: ubuntu-latest
                    - platform: linux/arm64
                      runner: ubuntu-24.04-arm

        steps:
            - name: Checkout code
              uses: actions/checkout@v7

            - name: Setup PHP
              uses: shivammathur/setup-php@v2
              with:
                  php-version: 8.5
                  coverage: none

            - name: Install Composer dependencies
              run: composer install --prefer-dist --no-interaction --no-progress

            - name: Render Podman quadlets
              run: |
                  cp .env.ci .env
                  php artisan key:generate
                  php artisan podman:generate frankenphp-octane

            - name: Set platform pair
              id: platform
              env:
                  PLATFORM: ${{ matrix.platform }}
              run: echo "pair=${PLATFORM//\//-}" >> "$GITHUB_OUTPUT"

            - name: Login to GitHub Container Registry
              run: echo "${{ secrets.GITHUB_TOKEN }}" | podman login ghcr.io -u ${{ github.actor }} --password-stdin

            - name: Determine build args
              id: buildargs
              run: |
                  {
                    echo "args<<EOF"
                    echo "--target=production"
                    echo "--cache-from=${CACHE_IMAGE}"
                    if [ "${{ github.event_name }}" != "pull_request" ]; then
                      echo "--cache-to=${CACHE_IMAGE}"
                    fi
                    echo "EOF"
                  } >> "$GITHUB_OUTPUT"

            - name: Build image
              id: build
              uses: redhat-actions/buildah-build@v3
              with:
                  image: ${{ env.IMAGE_NAME }}
                  tags: ci-${{ steps.platform.outputs.pair }}-${{ github.sha }}
                  containerfiles: podman/frankenphp-octane/runtimes/Containerfile
                  context: .
                  platform: ${{ matrix.platform }}
                  layers: true
                  squash: false
                  extra-args: ${{ steps.buildargs.outputs.args }}

            - name: Push image by tag
              if: ${{ github.event_name != 'pull_request' }}
              id: push
              uses: redhat-actions/push-to-registry@v3
              with:
                  image: ${{ steps.build.outputs.image }}
                  tags: ${{ steps.build.outputs.tags }}
                  registry: ${{ env.REGISTRY }}/${{ github.repository_owner }}
                  username: ${{ github.actor }}
                  password: ${{ secrets.GITHUB_TOKEN }}
                  digestfile: /tmp/digest.txt

            - name: Upload digest
              if: ${{ github.event_name != 'pull_request' }}
              uses: actions/upload-artifact@v7
              with:
                  name: digests-${{ steps.platform.outputs.pair }}
                  path: /tmp/digest.txt
                  if-no-files-found: error
                  retention-days: 1

    merge:
        name: Merge & push manifest
        needs: build
        if: ${{ github.event_name != 'pull_request' }}
        runs-on: ubuntu-latest
        permissions:
            contents: read
            packages: write

        steps:
            - name: Download digests
              uses: actions/download-artifact@v8
              with:
                  path: /tmp/digests
                  pattern: digests-*

            - name: Login to GitHub Container Registry
              run: echo "${{ secrets.GITHUB_TOKEN }}" | podman login ghcr.io -u ${{ github.actor }} --password-stdin

            - name: Generate image tags
              id: meta
              uses: docker/metadata-action@v6
              with:
                  images: ${{ env.IMAGE }}
                  flavor: |
                      latest=false
                  tags: |
                      type=ref,event=branch
                      type=sha,format=long,prefix=commit-
                      type=semver,pattern={{version}}
                      type=semver,pattern={{major}}.{{minor}}
                      type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}

            - name: Create and push manifest list
              run: |
                  podman manifest create manifest-list

                  for digest_file in /tmp/digests/*/digest.txt; do
                    digest=$(tr -d '[:space:]' < "$digest_file")
                    podman manifest add manifest-list "docker://${IMAGE}@${digest}"
                  done

                  while IFS= read -r tag; do
                    [ -z "$tag" ] && continue
                    podman manifest push --all manifest-list "docker://${tag}"
                  done <<< "${{ steps.meta.outputs.tags }}"

Adapting it

Change What to do
Different preset Swap frankenphp-octane in both the podman:generate call and the containerfiles: path for your own (see Customizing)
Single-arch only Drop the matrix and the merge job; push straight from build instead of uploading digests
Registry other than GHCR Swap the login step and the REGISTRY/IMAGE env vars — buildah/podman work with any OCI registry