Build Docker images for production
Many people have reached out to me with questions about how I would use the images from this setup in production. After the third question, I thought it would be the best to write a small article about building docker images for production.
The Dockerfile
As a convention, we put the Dockerfile which will be used in production, in the root of the project.
Project structure
We have a conventional project structure (read more) which looks like this:
1- build2 - # files needed for production build3- web4 - # the actual Laravel application5- Dockerfile
In my opinion, it is a good pattern to nest the actual application in a sub folder of the GIT repository because there are many files which are used for infrastructure related stuff, and the application should not be aware of these files.
Production images != development images
The main difference between the production and the development image is that the production image contains the actual application code. The development image is a image where we will mount the application code into. Another difference, at least at my company, is that the development image is a more generic image. It will be used for any PHP project in development and has optional xdebug, preconfigured msmtp, many enabled php extensions and installed composer for example.
The actual Dockerfile
We are leveraging Docker multi stage builds to get our dependencies installed and built.
PHP dependencies
1FROM composer:latest as step12COPY ./web ./app/web3WORKDIR /app/web4RUN composer install —-no-dev —-no-scripts —-optimize-autoloader5 6# ...
In step1
, we will use the composer:latest
image. First, we have to copy our actual application into the container. Next, we set the current WORKDIR
to
/app/web
. Now we can run composer install with a few options.
Frontend dependencies
1# ...2 3FROM node:lts as step24COPY --from=step1 /app/web /app/web5WORKDIR /app/web6RUN npm install && npm run prod && rm -rf node_modules7 8# ...
For step2
, we will use the node:lts
image to install and build our frontend dependencies. The next step is to copy the files form the first stage to the second stage. Unfortunately, we have to set the WORKDIR
in every stage. To install and build the frontend dependencies and to delete the not used node_modules
, we execute npm install && npm run prod && rm -rf node_modules
.
Now we have installed and built all our dependencies.
Webserver configuration
As last stage we use the official php:7.4-apache
image.
1FROM php:7.4-apache2COPY --from=step2 /app /app3COPY build/vhost.conf /etc/apache2/sites-available/000-default.conf4 5# ...
We copy the files with all dependencies installed and built from step2
to its final position. Next, we copy a vhost.conf
file from our build
folder into the containers /etc/apache2/sites-available/
folder with the name
000-default.conf
. This file is the configuration file which is loaded by apache by default. It looks like this:
1<VirtualHost *:80> 2 DocumentRoot /app/web/public 3 4 <Directory "/app/web/public"> 5 AllowOverride all 6 Require all granted 7 </Directory> 8 9 ErrorLog ${APACHE_LOG_DIR}/error.log10 CustomLog ${APACHE_LOG_DIR}/access.log combined11</VirtualHost>
Nothing special is going on here. We just need this file to point the web server to the correct document root /app/web/public
.
PHP extensions
We need to install a few PHP extensions for the runtime.
1# ...2 3# Install needed php extenstions4RUN docker-php-ext-install -j “$(nproc)” \5 bcmath \6 opcache \7 pdo_mysql8 9# ...
Final configuration
As a final configuration, we change the owner of /app
to the web server user www-data
and activate the apache module rewrite
which is needed to enable .htaccess
configuration.
1RUN chown -R www-data:www-data /app \2 && a2enmod rewrite3 4WORKDIR /app/web
Only for convenience do we set the WORKDIR
to the directory of our application. When we execute a command via Docker in the container we do not have to set the WORKDIR
explicitly.
The full Dockerfile
The result at the end should look like this:
1FROM composer:latest as step1 2COPY ./web /app/web 3WORKDIR /app/web 4RUN composer install --quiet --optimize-autoloader —-no-dev 5 6FROM node:lts as step2 7COPY --from=step1 /app /app 8WORKDIR /app/web 9RUN npm install --no-optional && npm run prod && rm -rf node_modules10 11FROM php:7.4-apache12COPY --from=step2 /app /app13COPY build/vhost.conf /etc/apache2/sites-available/000-default.conf14 15# Install needed php extenstions16RUN docker-php-ext-install -j "$(nproc)" \17 bcmath \18 opcache \19 pdo_mysql20 21RUN chown -R www-data:www-data /app \22 && a2enmod rewrite23 24WORKDIR /app/web
To build and start the image, we need to execute the following commands:
1docker build -t docker-example .2docker run -p 8000:80 -e APP_KEY=base64:w0T2so9vxBfHWm5q0jQuJHhtQwHnWGdqRsXf2S7KtcE= docker-example:latest
We build the image and add a tag with -t
. In our case, docker-example
.
To execute the image, we use the docker run
command. With -p 8000:80
, we map the containers port 80
to the port 8000
on our host system. As you can see, we add an APP_KEY
as environment variable. Without this, we would get a 500
server error.
Now we can open the browser and point it to localhost:8000
. Boom, we see the Laravel welcome page.
Thanks for reading my article. If you have questions just drop me a line here.