Optimize Laravel Dockerfile

Considering you made this Dockerfile, I’ll tag you @fideloper-fly :grimacing:

Have you ever tried optimizing (the build speed) the Dockerfile? A small change in a PHP file causes a rebuild that takes about 1:34 minutes (on my local machine). It would be great if we could deploy faster :slight_smile:

Most of the time is spent in these steps:

=> [base 3/4] RUN composer install --optimize-autoloader --no-dev && mkdir -p storage/logs && php artisan optim 43.0s

=> [node_modules_go_brrr 7/7] RUN if [ -f "vite.config.js" ]; then ASSET_CMD="build"; else ASSET_CM 42.9s

Both take around 40 seconds.

I tried to do something like this:

COPY composer.json composer.lock /var/www/html/
RUN composer install --no-dev --no-scripts --no-autoloader

COPY . /var/www/html

RUN composer dump-autoload --optimize \
    && mkdir -p storage/logs \
    && ...

I noticed this really doesn’t make a difference, because apparently composer dump-autoload --optimize is really where the time is spent:

=> [base 3/6] RUN composer install --no-dev --no-scripts --no-autoloader 9.4s => [base 4/6] RUN composer dump-autoload --optimize 33.2s

Maybe there’s nothing to be gained there.

I did have some success optimizing the step where assets are built by doing something like this:

COPY package.json package-lock.json /app/

RUN npm install

COPY . .

RUN npm run build

This seems to work and speed things up a decent bit. Any drawbacks to this? I think you can even do COPY resources /app/ here :thinking:

Thanks!

I asked GPT-4 but it’s not giving a great answer. This doesn’t work because the autoloader can’t be generated when the source files aren’t present.

Perhaps run the autoloader as a separate step? See Faster Docker builds with composer install ⚡ | by Iacovos Constantinou | Medium

Yea I tried doing this, you can see the results in my original post. The slow step is actually the “dump autoload” step, surprisingly enough.

Oops. Sorry, I missed that. Perhaps I can redeem myself with a better suggestion. :slight_smile:

It looks like composer supports a cache, which normally would be empty on each build, but by adding one line to the beginning of a RUN statement, a directory can be saved between builds. See Optimizing your build · Fly Docs for a Rails example.

Here it looks like the line would be something like:

RUN --mount=type=cache,id=composer-cache,sharing=locked,target=/root/.composer \

I’m a bit out of my expertise (it has been many years since I’ve programmed in PHP), but optimizing Dockerfiles is a interest of mine.

1 Like

Hi!

Are you testing this against a specific (perhaps large?) project? I’m curious how that timing stacks up vs smaller projects.

A fresh install of Laravel is really quick for me, but I’ve seen autoloader take a bunch more time on older (well, larger) projects - which makes sense when there are more classes/libaries to autoload.

But I’m curious if there are other contexts/ways to reduce that size :thinking:


In terms of your change - what if you didn’t use the --no-autoloader flag when you ran composer install ? autoload-dumping would then be cached even if you just had a small code change. (Am I missing something there?)

# If no changes in composer, then these lines are cached
COPY composer.json composer.lock /var/www/html/
RUN composer install --no-dev --optimize-autoloader

# If there's a change to the code, we continue on here
COPY . /var/www/html

# Remove dump autoload here
RUN mkdir -p storage/logs \
    && ...

I don’t think changed code in autoloaded files would cause that to break, but I could be wrong!

Are you testing this against a specific (perhaps large?) project? I’m curious how that timing stacks up vs smaller projects.

Not a fresh project, but it’s very small. 4 models, ~10 controller classes.

RUN composer install --no-dev --optimize-autoloader

This doesn’t work because of the post-autoload-dump scripts generally defined in composer.json. Let’s see what happens when I pass --no-scripts

Ok this seems to work.

After making these changes I get build times (on a Fly builder) of 9.1 seconds when there’s only a change to a PHP file.

On my old Dockerfile I get build times of 23.8 seconds.

I’m going to try this now on a larger project.

P.S. the build times mentioned in my original post were from building locally. Docker is still not that great on M1 mac’s I guess :grimacing:

Yea this is pretty good. It takes it down by about 50% when only php files change.

This could still be improved maybe because npm run build still runs on every build. I didn’t manage to fix that. It’s a bit more complicated than I thought. I thought I could just COPY the resources/js and resources/css and the package files, but Vite seems to need more files than just those.

This is what I’m using now:

# syntax = docker/dockerfile:experimental

# Default to PHP 8.1, 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.1
ARG NODE_VERSION=14
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 composer.json composer.lock /var/www/html/
RUN composer install --no-dev --optimize-autoloader --no-scripts

# copy application code, skipping files based on .dockerignore
COPY . /var/www/html

RUN php artisan package:discover \
    && mkdir -p storage/logs \
    && php artisan package:discover \
    && php artisan optimize:clear \
    && chown -R www-data:www-data /var/www/html \
    && sed -i 's/protected \$proxies/protected \$proxies = "*"/g' app/Http/Middleware/TrustProxies.php \
    && 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

# 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 /var/www/html/.fly/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 package.json package-lock.json /app/
RUN npm install

COPY . .

RUN npm run build

# 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
COPY --from=node_modules_go_brrr /app/public/build/assets/widget-loader.*.js /var/www/html/public/js/widget-loader.js
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"]

Cool!

I (and perhaps @rubys ) and test it out on our end too to see what we can do. It would be nice if Vite didn’t need to get run on every build but I’m not totally sure on the feasibility of that.

It sort of depends on if your convention is to commit built static assets or not :thinking:

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.