Developing With Craft CMS 2 in Docker

A quick post to describe my Craft CMS 2 in Docker template. I use Ubuntu as my host operating system. If you are using something other than Linux YMMV and you should consider moving to Linux 😉

TL;DR you can get my template here. It should work out of the box with just a 777 permissions update to src/config.

This is an old project template and as such it has some old things in it, but the general Craft-in-Docker rules still apply now.

The project runs four custom docker containers and a PHPMyAdmin image for database access. I'll run over each in ascending order of complexity.

Before we begin it's important to understand what each of the docker components are used for:

  • Images (built by Dockerfiles) - Reusable, portable blueprints for our application components. Once built, an image will normally contain everything needed in order to run successfully.
  • Docker-compose files - Define the context in which an image runs and also allows files, commands and ENV vars inside the image to be changed during the execution of an image. We can use these tweaks to make real time development easier.

In general we can build our docker images so that they're production ready and then mount in our development files which are stored on our host machines using docker-compose. Once we've made the changes we want to our app's files we can rebuild the images which copies the newly updated files into a new version of the built image.



FROM mariadb:10.3

RUN echo "[mysqld]" >> /etc/mysql/my.cnf

All I've done here is change some configuration settings to allow older versions of Craft 2 to talk to newer versions of MariaDB/MySQL. This probably isn't needed with recent versions of Craft 2.


I've separated the nginx and PHP processes into different containers so that they can be deployed and scaled individually. Nginx is acting as a reverse proxy for our PHP-FPM container and will also be serving all of our static assets so that they don't need to be streamed through the PHP processes.

In order to allow for this we need to make sure nginx has access to the static files on its filesystem. To make that happen we can just copy them into the image when it is built.


FROM nginx:1.13

COPY ./.docker-config/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY ./src/public /var/www/html/public

Notice that we're only copying the public folder into the nginx image. That's because nginx will only be serving static files which are publicly accessible. If you need nginx to have access to files outside of the webroot you'll need to make a few tweaks here to copy them in.

The only other thing we're doing here is copying a default nginx config which is set up to serve static files from the filesystem and forward any requests it can't serve to the PHP container for it to have a try at resolving them.


server {
    listen 80 default_server;
    root /var/www/html/public;
    index index.html index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/error.log error;

    sendfile off;

    client_max_body_size 10m;

    gzip              on;
    gzip_http_version 1.0;
    gzip_proxied      any;
    gzip_min_length   500;
    gzip_disable      "MSIE [1-6]\.";
    gzip_types        text/plain text/xml text/css

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;

    location ~ /\.ht {
        deny all;


This is where most of the work happens. We set up this image by firstly installing all required project dependencies at both the OS and PHP levels. We then download a specific version of the Craft 2 codebase and extract it to the correct location on the filesystem. At this point we have an image with a base Craft install, but we need to add our own files to modify that base. We do this by copying the following folders into the image's filesystem:

  • src/public - All of our publicly accessible files including a copy of index.php
  • src/config - All of our customisable Craft config files
  • src/templates - All of our Twig templates
  • src/plugins - All of the plugins that we want to include in the project

When we copy these folders into the image we're just overwriting the folders that are packaged with Craft's default folder structure which we extracted earlier.

Finally there are also a couple of lines which update PHP's maximum upload size because 2MB is never enough.


FROM php:7.1-fpm

RUN apt-get update && apt-get install -y \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libmcrypt-dev \
        libpng-dev \
        libbz2-dev \

RUN docker-php-ext-install -j$(nproc)  mcrypt pdo_mysql

RUN docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/

RUN docker-php-ext-install \
        bcmath \
        bz2 \
        exif \
        ftp \
        gd \
        gettext \
        mbstring \
        opcache \
        shmop \
        sockets \
        sysvmsg \
        sysvsem \
        sysvshm \
        zip \

RUN apt-get install -y wget unzip

RUN echo "upload_max_filesize = 10M" > /usr/local/etc/php/php.ini
RUN echo "post_max_size = 10M" >> /usr/local/etc/php/php.ini

RUN wget -O
RUN unzip -d /var/www/html
RUN chown -R www-data:www-data /var/www/html

COPY --chown=www-data:www-data ./src/public /var/www/html/public
COPY --chown=www-data:www-data ./src/config /var/www/html/craft/config
COPY --chown=www-data:www-data ./src/templates /var/www/html/craft/templates
COPY --chown=www-data:www-data ./src/plugins /var/www/html/craft/plugins

Notice that we're ensuring the owner of the base Craft install files and all of the files we copy in is set to www-data. This is the user inside the container which the PHP process is executing as. This www-data user only exists inside the container and is completely separate from any www-data user that might exist on your host. Any files or folders which PHP needs to write to must be writable by the www-data user inside the container. This is important and we'll come back to it later.


I've included an old version of my dockerized buildchain which will do the following:

  • Compress images
  • Compile sass
  • Concat and uglify JS files
  • Create svg spritesheets
  • Create png spritesheets
  • Launch a browsersync proxy which support CSS live reload

I won't go into any detail about how all this is set up, feel free to explore. I will however mention that it can run alongside all of the other containers during development, mounts your src folder and recompiles assets whenever they are changed on your host.

I hate installing build tools on my laptop so keeping node, gulp et al. in a docker container - version controlling their installed versions and configuration is a no-brainer. I never run build chains on my host machine any more...

We now have everything we need to build our images and they'll all contain the files they need to do their jobs, but we have no method of editing the files that they contain during runtime which is necessary during development. We solve this by using docker-compose to set up 'volume mounts' - a method of mounting folders which exist on our host into a running container.

If you have a look in docker-compose.yml you'll see that we're mounting our src/public folder into the nginx container. This allows us to update our static assets on our host machine's filesystem and have those changes reflected within our nginx container in real time.

Similarly we're mounting all four of the src folders that we previously copied into the PHP image back into the running container. This allows us to make changes to all of the contents of these folders and for the changes to be reflected in our PHP container in real time.

I mentioned earlier that we need to ensure that files and folders which the PHP process wants to write to need to be writable by the www-data user inside the container. We made sure this was true in the image by changing the owner of the copied files to www-data. However, our mounted folders contain representations of the files as they exist on our host's filesystem.

If I were to use an editor on my host to create a file inside src/public this file would be created with an owner of 'matt' and a group 'matt' which is the user I use to log into Ubuntu. By default the permissions on this file will be 755, writable only by the user 'matt'. Inside our running container this also holds true - the mounted file is only writable by my host's user 'matt', but 'matt' doesn't exist inside the container! Any files which need to be editable by PHP need to have their permissions set to allow www-data to edit it.

The easiest way to achieve this is to just 'chmod -R 777 path' on any mounted files or folders which need to by writable by PHP.

You will need to do this with any asset source folders inside your public directory and your config folder which Craft insists on having write access to.

Also note that we haven't mounted a 'storage' folder into our PHP container. Craft writes A LOT of stuff here and it's unlikely we'll need to put anything custom there so we can just make use of the storage folder which will have been created inside the image when we extracted the Craft base files - we changed the owner of all of these files to www-data remember!


I've left my Gitlab CI build script in the repo which shows how I build images in CI ready for production deployment.

I've also left a cache busting macro in the templates folder. This busts cache by generating a new random version string each time the project passes through CI and the images are rebuilt.

You don't need an .env file when using Docker - you can just set all your environment vars inside docker-compose. Much easier than messing with files inside the container.

Hopefully this quick description serves as a useful starting point for getting started with Craft in Docker. I'll share my Craft 3 dev environment soon as composer gives us some nice new options there.

Happy Crafting.

Read Next

2024 Goals
Write Things