From 2a916863806f774d8d66df791c656374d70a8732 Mon Sep 17 00:00:00 2001 From: Matt Braun Date: Wed, 21 Oct 2026 15:58:42 -0500 Subject: [PATCH 1/2] scanner: require PHP 8.1 or newer for Laravel apps Laravel apps on PHP 7.4 and 8.0 got a Dockerfile template built on the fideloper/fly-laravel base image. Both PHP versions are end of life and the image's source repository is archived, so fly launch now stops with a clear error for them instead of writing that template. PHP 8.1 and newer keep using the dockerfile-laravel generator, which builds from ubuntu:22.04 and needs no base image. --- scanner/laravel.go | 32 ++---- scanner/laravel_test.go | 50 +++++++++ scanner/templates/laravel/.dockerignore | 28 ----- scanner/templates/laravel/.fly/entrypoint.sh | 15 --- .../templates/laravel/.fly/scripts/.gitkeep | 0 .../templates/laravel/.fly/scripts/caches.sh | 5 - scanner/templates/laravel/Dockerfile | 101 ------------------ 7 files changed, 58 insertions(+), 173 deletions(-) create mode 100644 scanner/laravel_test.go delete mode 100644 scanner/templates/laravel/.dockerignore delete mode 100644 scanner/templates/laravel/.fly/entrypoint.sh delete mode 100644 scanner/templates/laravel/.fly/scripts/.gitkeep delete mode 100644 scanner/templates/laravel/.fly/scripts/caches.sh delete mode 100644 scanner/templates/laravel/Dockerfile diff --git a/scanner/laravel.go b/scanner/laravel.go index 600c9ae3be..55a84dbe3d 100644 --- a/scanner/laravel.go +++ b/scanner/laravel.go @@ -12,7 +12,6 @@ import ( "os/exec" "path/filepath" "regexp" - "strconv" "strings" "github.com/blang/semver" @@ -54,7 +53,7 @@ func configureLaravel(sourceDir string, config *ScannerConfig) (*SourceInfo, err ConsoleCommand: "php /var/www/html/artisan tinker", } - // Min PHP version to use generator + // Oldest PHP the dockerfile-laravel generator supports minVersion, err := semver.Make("8.1.0") if err != nil { panic(err) @@ -63,11 +62,14 @@ func configureLaravel(sourceDir string, config *ScannerConfig) (*SourceInfo, err // The detected PHP version phpVersion, err := extractPhpVersion() if err != nil || phpVersion == "" { - // Fallback to 8.0, which has - // the broadest compatibility phpVersion = "8.1" } + phpNVersion, err := semver.Make(phpVersion + ".0") + if err != nil || phpNVersion.LT(minVersion) { + return nil, fmt.Errorf("Laravel apps on Fly.io need PHP 8.1 or newer; detected PHP %s. Update the php requirement in composer.json and run fly launch again", phpVersion) + } + s.Runtime = plan.RuntimeStruct{Language: "php", Version: phpVersion} s.BuildArgs = map[string]string{ @@ -75,14 +77,8 @@ func configureLaravel(sourceDir string, config *ScannerConfig) (*SourceInfo, err "NODE_VERSION": "18", } - // Use default scanner templates if < min version(8.1.0) - phpNVersion, err := semver.Make(phpVersion + ".0") - if err != nil || phpNVersion.LT(minVersion) { - s.Files = templates("templates/laravel") - } else { - // Else use dockerfile-laravel generator - s.Callback = LaravelCallback - } + // The Dockerfile comes from the dockerfile-laravel generator + s.Callback = LaravelCallback // Extract DB, Redis config from dotenv db, redis, skipDB := extractConnections(".env") @@ -248,18 +244,6 @@ func extractPhpVersion() (string, error) { } if len(match) > 1 { - // If the PHP version is below 7.4, we won't have a - // container for it, so we'll use PHP 7.4 - if match[1][0:1] == "7" { - vers, err := strconv.ParseFloat(match[1], 32) - if err != nil { - return "7.4", nil - } - if vers < 7.4 { - return "7.4", nil - } - } - return match[1], nil } diff --git a/scanner/laravel_test.go b/scanner/laravel_test.go new file mode 100644 index 0000000000..a2ee9004cc --- /dev/null +++ b/scanner/laravel_test.go @@ -0,0 +1,50 @@ +package scanner + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func writeLaravelApp(t *testing.T, phpRequirement string) string { + t.Helper() + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "artisan"), []byte("#!/usr/bin/env php\n"), 0o755)) + composer := `{"require": {"php": "` + phpRequirement + `"}}` + require.NoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composer), 0o644)) + return dir +} + +func TestConfigureLaravelRejectsPhpBelow81(t *testing.T) { + for _, requirement := range []string{"^7.4", "^8.0", "~8.0.2"} { + t.Run(requirement, func(t *testing.T) { + dir := writeLaravelApp(t, requirement) + t.Chdir(dir) + + srcInfo, err := configureLaravel(dir, &ScannerConfig{}) + + require.Error(t, err) + assert.Nil(t, srcInfo) + assert.Contains(t, err.Error(), "PHP 8.1") + }) + } +} + +func TestConfigureLaravelUsesGeneratorFrom81(t *testing.T) { + for _, requirement := range []string{"^8.1", "^8.2", "^8.3"} { + t.Run(requirement, func(t *testing.T) { + dir := writeLaravelApp(t, requirement) + t.Chdir(dir) + + srcInfo, err := configureLaravel(dir, &ScannerConfig{}) + + require.NoError(t, err) + require.NotNil(t, srcInfo) + assert.Empty(t, srcInfo.Files, "no Dockerfile template should be written; the generator produces it") + assert.NotNil(t, srcInfo.Callback) + }) + } +} diff --git a/scanner/templates/laravel/.dockerignore b/scanner/templates/laravel/.dockerignore deleted file mode 100644 index 7db5212b21..0000000000 --- a/scanner/templates/laravel/.dockerignore +++ /dev/null @@ -1,28 +0,0 @@ -# excludes from the docker image/build - -# 1. Ignore Laravel-specific files we don't need -bootstrap/cache/* -storage/framework/cache/* -storage/framework/sessions/* -storage/framework/views/* -storage/logs/* -*.env* -.rr.yml -rr -frankenphp -vendor - -# 2. Ignore common files/directories we don't need -fly.toml -.vscode -.idea -**/*node_modules -**.git -**.gitignore -**.gitattributes -**.sass-cache -**/*~ -**/*.log -**/.DS_Store -**/Thumbs.db -public/hot diff --git a/scanner/templates/laravel/.fly/entrypoint.sh b/scanner/templates/laravel/.fly/entrypoint.sh deleted file mode 100644 index b9ddfa4a83..0000000000 --- a/scanner/templates/laravel/.fly/entrypoint.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env sh - -# Run user scripts, if they exist -for f in /var/www/html/.fly/scripts/*.sh; do - # Bail out this loop if any script exits with non-zero status code - bash "$f" || break -done -chown -R www-data:www-data /var/www/html - -if [ $# -gt 0 ]; then - # If we passed a command, run it as root - exec "$@" -else - exec supervisord -c /etc/supervisor/supervisord.conf -fi diff --git a/scanner/templates/laravel/.fly/scripts/.gitkeep b/scanner/templates/laravel/.fly/scripts/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/scanner/templates/laravel/.fly/scripts/caches.sh b/scanner/templates/laravel/.fly/scripts/caches.sh deleted file mode 100644 index 7ab868a320..0000000000 --- a/scanner/templates/laravel/.fly/scripts/caches.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -/usr/bin/php /var/www/html/artisan config:cache --no-ansi -q -/usr/bin/php /var/www/html/artisan route:cache --no-ansi -q -/usr/bin/php /var/www/html/artisan view:cache --no-ansi -q diff --git a/scanner/templates/laravel/Dockerfile b/scanner/templates/laravel/Dockerfile deleted file mode 100644 index a9f6ac5e0e..0000000000 --- a/scanner/templates/laravel/Dockerfile +++ /dev/null @@ -1,101 +0,0 @@ -# syntax = docker/dockerfile:experimental - -# Default to PHP 8.2, but we attempt to match -# the PHP version from the user (wherever `flyctl launch` is run) -# Valid version values are PHP 7.4+ -ARG PHP_VERSION=8.2 -ARG NODE_VERSION=18 -FROM fideloper/fly-laravel:${PHP_VERSION} as base - -# PHP_VERSION needs to be repeated here -# See https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact -ARG PHP_VERSION - -LABEL fly_launch_runtime="laravel" - -# copy application code, skipping files based on .dockerignore -COPY . /var/www/html - -RUN composer install --optimize-autoloader --no-dev \ - && mkdir -p storage/logs \ - && php artisan optimize:clear \ - && chown -R www-data:www-data /var/www/html \ - && echo "MAILTO=\"\"\n* * * * * www-data /usr/bin/php /var/www/html/artisan schedule:run" > /etc/cron.d/laravel \ - && cp .fly/entrypoint.sh /entrypoint \ - && chmod +x /entrypoint - -# Laravel 11 made changes to how trusting all proxies works, see https://laravel.com/docs/11.x/requests#trusting-all-proxies and https://laravel.com/docs/10.x/requests#trusting-all-proxies -RUN if php artisan --version | grep -q "Laravel Framework 1[1-9]"; then \ - sed -i='' '/->withMiddleware(function (Middleware \$middleware) {/a\ - \$middleware->trustProxies(at: "*");\ -' bootstrap/app.php; \ - else \ - sed -i 's/protected \$proxies/protected \$proxies = "*"/g' app/Http/Middleware/TrustProxies.php; \ -fi - -# If we're using Octane... -RUN if grep -Fq "laravel/octane" /var/www/html/composer.json; then \ - rm -rf /etc/supervisor/conf.d/fpm.conf; \ - if grep -Fq "spiral/roadrunner" /var/www/html/composer.json; then \ - mv /etc/supervisor/octane-rr.conf /etc/supervisor/conf.d/octane-rr.conf; \ - if [ -f ./vendor/bin/rr ]; then ./vendor/bin/rr get-binary; fi; \ - rm -f .rr.yaml; \ - else \ - mv .fly/octane-swoole /etc/services.d/octane; \ - mv /etc/supervisor/octane-swoole.conf /etc/supervisor/conf.d/octane-swoole.conf; \ - fi; \ - rm /etc/nginx/sites-enabled/default; \ - ln -sf /etc/nginx/sites-available/default-octane /etc/nginx/sites-enabled/default; \ - fi - -# Multi-stage build: Build static assets -# This allows us to not include Node within the final container -FROM node:${NODE_VERSION} as node_modules_go_brrr - -RUN mkdir /app - -RUN mkdir -p /app -WORKDIR /app -COPY . . -COPY --from=base /var/www/html/vendor /app/vendor - -# Use yarn or npm depending on what type of -# lock file we might find. Defaults to -# NPM if no lock file is found. -# Note: We run "production" for Mix and "build" for Vite -RUN if [ -f "vite.config.js" ]; then \ - ASSET_CMD="build"; \ - else \ - ASSET_CMD="production"; \ - fi; \ - if [ -f "yarn.lock" ]; then \ - yarn install --frozen-lockfile; \ - yarn $ASSET_CMD; \ - elif [ -f "pnpm-lock.yaml" ]; then \ - corepack enable && corepack prepare pnpm@latest-8 --activate; \ - pnpm install --frozen-lockfile; \ - pnpm run $ASSET_CMD; \ - elif [ -f "package-lock.json" ]; then \ - npm ci --no-audit; \ - npm run $ASSET_CMD; \ - else \ - npm install; \ - npm run $ASSET_CMD; \ - fi; - -# From our base container created above, we -# create our final image, adding in static -# assets that we generated above -FROM base - -# Packages like Laravel Nova may have added assets to the public directory -# or maybe some custom assets were added manually! Either way, we merge -# in the assets we generated above rather than overwrite them -COPY --from=node_modules_go_brrr /app/public /var/www/html/public-npm -RUN rsync -ar /var/www/html/public-npm/ /var/www/html/public/ \ - && rm -rf /var/www/html/public-npm \ - && chown -R www-data:www-data /var/www/html/public - -EXPOSE 8080 - -ENTRYPOINT ["/entrypoint"] From dd86cd39c5d3e0f5f86fa928c17d4a9811d771c3 Mon Sep 17 00:00:00 2001 From: Matt Braun Date: Wed, 21 Oct 2026 16:49:31 -0500 Subject: [PATCH 2/2] scanner: blank line before return in the Laravel test helper (nlreturn) --- scanner/laravel_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/scanner/laravel_test.go b/scanner/laravel_test.go index a2ee9004cc..e5379a0383 100644 --- a/scanner/laravel_test.go +++ b/scanner/laravel_test.go @@ -15,6 +15,7 @@ func writeLaravelApp(t *testing.T, phpRequirement string) string { require.NoError(t, os.WriteFile(filepath.Join(dir, "artisan"), []byte("#!/usr/bin/env php\n"), 0o755)) composer := `{"require": {"php": "` + phpRequirement + `"}}` require.NoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte(composer), 0o644)) + return dir }