An Annotated Docker Config for Frontend Web Development
Tags: docker, devops, frontend
Published: 2020-04-01 10:34:51

---
title: An Annotated Docker Config for Frontend Web Development
published: true
date: 2020-03-22 16:12:00 UTC
tags: docker,devops,frontend
canonical_url: https://nystudio107.com/blog/an-annotated-docker-config-for-frontend-web-development
---

# An Annotated Docker Config for Frontend Web Development

### A local devel­op­ment envi­ron­ment with Dock­er allows you to shrink-wrap the devops your project needs as con­fig, mak­ing onboard­ing frictionless

Andrew Welch / [nystudio107](https://nystudio107.com)

![An annotated docker config for frontend web development](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/5189/an-annotated-docker-config-for-frontend-web-development.jpg)

[Dock­er](https://www.docker.com/) is a tool for con­tainer­iz­ing your appli­ca­tions, which means that your appli­ca­tion is shrink-wrapped with the envi­ron­ment that it needs to run.

This allows you to define the devops your appli­ca­tion needs in order to run as con­fig, which can then be eas­i­ly repli­cat­ed and reused.

<aside>
                                The prin­ci­pals and approach dis­cussed in this arti­cle are universal.
                            </aside>

While there are many uses for Dock­er, this arti­cle will focus on using Dock­er as a local envi­ron­ment for fron­tend web development.

Although [Craft CMS](https://craftcms.com/) is ref­er­enced in this arti­cle, Dock­er works well for any kind of web devel­op­ment with any kind of CMS or dev stack (Lar­avel, Node­JS, Rails, whatevs).

The Dock­er con­fig used here is used in both the [dev​Mode​.fm GitHub repo](https://github.com/nystudio107/devmode), and in the [nystudio107/​craft](https://github.com/nystudio107/craft) boil­er­plate Com­pos­er project if you want to see some ​“in the wild” examples.

## Why Dock­er?

If you’re doing fron­tend web devel­op­ment, you very like­ly already have some kind of a local devel­op­ment environment. 

<aside>
                                So why should you use Dock­er instead?
                            </aside>

This is a very rea­son­able ques­tion to ask, because any kind of switch of tool­ing requires some upskilling, and some work.

![Docker containers frontend web development](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-frontend-web-development.jpg)

I’ve long been using [Home­stead](https://laravel.com/docs/6.x/homestead)—which is real­ly just a cus­tom [Vagrant](https://www.vagrantup.com/) box with some extras — as my local dev envi­ron­ment as dis­cussed in the [Local Devel­op­ment with Vagrant / Home­stead](https://dev.to/gaijinity/local-development-with-vagrant-homestead-2kn7) article.

I’d cho­sen to use Home­stead because I want­ed a local dev envi­ron­ment that was deter­min­is­tic, dis­pos­able, and sep­a­rat­ed my devel­op­ment envi­ron­ment from my actu­al computer.

Dock­er has all of these advan­tages, but also a much more light­weight approach. Here are the advan­tages of Dock­er for me:

- Each appli­ca­tion has exact­ly the envi­ron­ment it needs to run, includ­ing spe­cif­ic ver­sions of any of the plumb­ing need­ed to get it to work (PHP, MySQL, Post­gres, whatever)
- Onboard­ing oth­ers becomes triv­ial, all they need to do is [install Dock­er](https://docs.docker.com/install/) and type <kbd>docker-compose up</kbd> and away they go
- Your devel­op­ment envi­ron­ment is entire­ly dis­pos­able; if some­thing goes wrong, you just delete it and fire up a new one
- Your local com­put­er is sep­a­rate from your devel­op­ment envi­ron­ment, so switch­ing com­put­ers is triv­ial, and you won’t run into issues where you hose your com­put­er or are stuck with con­flict­ing ver­sions of devops services
- The cost of try­ing dif­fer­ent ver­sions of var­i­ous ser­vices is low; just change a num­ber in a <kbd>.yaml</kbd> file, <kbd>docker-compose up</kbd>, and away you go

There are oth­er advan­tages as well, but these are the more impor­tant ones for me.

Addi­tion­al­ly, con­tainer­iz­ing your appli­ca­tion in local devel­op­ment is a great first step to using a con­tainer­ized deploy­ment process, and run­ning Dock­er in pro­duc­tion as well.

## Under­stand­ing Docker

This arti­cle is not a com­pre­hen­sive tuto­r­i­al on Dock­er, but I will attempt to explain some of the more impor­tant, broad­er concepts.

Dock­er has the notion of con­tain­ers, each of which run one or more ser­vices. You can think of each con­tain­er as a mini vir­tu­al machine (even though tech­ni­cal­ly, they are not).

While you can run mul­ti­ple ser­vices in a sin­gle Dock­er con­tain­er, sep­a­rat­ing each ser­vice out into a sep­a­rate con­tain­er has many advantages.

![Docker containers separate services](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-separate-services.jpg)

If [PHP](https://www.php.net/), [Apache](https://httpd.apache.org/), and [MySQL](https://www.mysql.com/) are all in sep­a­rate con­tain­ers, they won’t affect each oth­er, and also can be more eas­i­ly swapped in and out.

If you decide you want to use [Nginx](https://www.nginx.com/) or [Post­gres](https://www.postgresql.org/) instead, the decou­pling into sep­a­rate con­tain­ers makes it easy!

Dock­er con­tain­ers are built from [Dock­er images](https://stackify.com/docker-image-vs-container-everything-you-need-to-know/), which can be thought of as a recipe for build­ing the con­tain­er, with all of the files and code need­ed to make it happen.

<aside>
                                If a Dock­er image is the recipe, a Dock­er con­tain­er is the fin­ished result­ing meal.
                            </aside>

Dock­er images almost always are lay­ered on top of oth­er exist­ing images that they extend <kbd>FROM</kbd>. For instance, you might have a base image from [Ubun­tu](https://hub.docker.com/_/ubuntu) or [Alpine](https://hub.docker.com/_/alpine) Lin­ux which pro­vide in the nec­es­sary oper­at­ing sys­tem ker­nel lay­er for oth­er process­es like Nginx to run.

![Docker image layers](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x983_crop_center-center_100_line/docker-image-layers.png)

This lay­er­ing works thanks to the [Union file sys­tem](https://medium.com/@nagarwal/docker-containers-filesystem-demystified-b6ed8112a04a), which han­dles com­pos­ing all the lay­ers of the cake togeth­er for you.

We said ear­li­er that Dock­er is more light­weight than run­ning a full Vagrant VM, and it is… but unfor­tu­nate­ly, unless you’re run­ning Lin­ux there still is a vir­tu­al­iza­tion lay­er run­ning, which is [Hyper­K­it](https://docs.docker.com/docker-for-mac/docker-toolbox/) for the Mac, and [Hyper‑V](https://docs.docker.com/docker-for-windows/install/) for Windows.

<aside>
                                Dock­er for Mac <span>&amp;</span> Win­dows still has a vir­tu­al­iza­tion lay­er, it’s just rel­a­tive­ly lightweight.
                            </aside>

For­tu­nate­ly, you don’t need to be con­cerned with any of this, but the per­for­mance impli­ca­tions do inform some of the deci­sions we’ve made in the Dock­er con­fig pre­sent­ed here.

For more infor­ma­tion on Dock­er, for that I’d high­ly rec­om­mend the [Dock­er Mas­tery](https://www.udemy.com/course/docker-mastery/) course (if it’s not on sale now, don’t wor­ry, it will be at some point) and also the fol­low­ing [dev​Mode​.fm](https://devmode.fm/) episodes:

- [Con­tainer­ize your Devel­op­ment with Docker!](https://devmode.fm/episodes/containerize-your-development-with-docker)

…and there are tons of oth­er excel­lent edu­ca­tion­al resources on Dock­er out there such as Matt Gray’s [Craft in Dock­er: Every­thing I’ve Learnt](https://dotall.com/sessions/craft-in-docker-everything-ive-learnt) pre­sen­ta­tion, and his excel­lent [A Craft CMS Devel­op­ment Work­flow With Dock­er](https://mattgrayisok.com/a-craft-cms-development-workflow-with-docker-part-1-local-development)series.

In our arti­cle, we will focus on anno­tat­ing a real-world Dock­er con­fig that’s used in pro­duc­tion. We’ll dis­cuss var­i­ous Dock­er con­cepts as we go, but the pri­ma­ry goal here is doc­u­ment­ing a work­ing config.

<aside>
                                This arti­cle is what I wished exist­ed when I start­ed learn­ing Docker
                            </aside>

I learn best by look­ing at a work­ing exam­ple, and pick­ing it apart. If you do, too, let’s get going!

## My Dock­er Direc­to­ry Structure

This Dock­er set­up uses a direc­to­ry struc­ture that looks like this (don’t wor­ry, it’s not as com­plex as it seems, many of the Dock­er images here are for ref­er­ence only, and are actu­al­ly pre-built):

```

├── cms
│ ├── composer.json
│ ├── config
│ ├── craft
│ ├── craft.bat
│ ├── example.env
│ ├── modules
│ ├── templates
│ └── web
├── docker-compose.yml
├── docker-config
│ ├── mariadb
│ │ └── Dockerfile
│ ├── nginx
│ │ ├── default.conf
│ │ └── Dockerfile
│ ├── php-dev-base
│ │ ├── Dockerfile
│ │ └── zzz-docker.conf
│ ├── php-dev-craft
│ │ └── Dockerfile
│ ├── postgres
│ │ └── Dockerfile
│ ├── redis
│ │ └── Dockerfile
│ ├── webpack-dev-base
│ │ └── Dockerfile
│ └── webpack-dev-craft
│ ├── Dockerfile
│ ├── package.json
│ ├── postcss.config.js
│ ├── tailwind.config.js
│ ├── webpack.common.js
│ ├── webpack.dev.js
│ ├── webpack.prod.js
│ └── webpack.settings.js
├── scripts
│ ├── common
│ ├── docker_pull_db.sh
│ ├── docker_restore_db.sh
│ ├── example.env.sh
│ └── seed_db.sql
└── src
    ├── conf
    ├── css
    ├── img
    ├── js
    ├── php
    ├── templates -> ../cms/templates
    └── vue

```

Here’s an expla­na­tion of what the top-lev­el direc­to­ries are:

- <kbd>cms</kbd> — every­thing need­ed to run Craft CMS. The is the ​“app” of the project
- <kbd>docker-config</kbd> — an indi­vid­ual direc­to­ry for each ser­vice that the Dock­er set­up uses, with a <kbd>Dockerfile</kbd> and oth­er ancil­lary con­fig files therein
- <kbd>scripts</kbd> — helper shell scripts that do things like pull a remote or local data­base into the run­ning Dock­er con­tain­er. These are derived from the [Craft-Scripts](https://github.com/nystudio107/craft-scripts) shell scripts
- <kbd>src</kbd> — the fron­tend JavaScript, CSS, Vue, etc. source code that the project uses

Each ser­vice is ref­er­enced in the <kbd>docker-compose.yaml</kbd> file, and defined in the <kbd>Dockerfile</kbd> that is in the cor­re­spond­ing direc­to­ry in the <kbd>docker-config/</kbd> directory.

It isn’t strict­ly nec­es­sary to have a sep­a­rate <kbd>Dockerfile</kbd> for each ser­vice, if they are just derived from a base image. But I like the con­sis­ten­cy, and ease of future expan­sion should some­thing cus­tom be nec­es­sary down the road.

You’ll also notice that there are <kbd>php-dev-base</kbd> and <kbd>php-dev-craft</kbd> direc­to­ries, as well as <kbd>webpack-dev-base</kbd> and <kbd>webpack-dev-craft</kbd> direc­to­ries, and might be won­der­ing why they aren’t consolidated.

The rea­son is that there’s a whole lot of the base set­up in both that just nev­er changes, so instead of rebuild­ing that each time, we can build it once and pub­lish the images up on Dock​er​Hub​.com as [nys­tu­dio107/php-dev-base](https://hub.docker.com/repository/docker/nystudio107/php-dev-base) and [nys­tu­dio107/web­pack-dev-base](https://hub.docker.com/repository/docker/nystudio107/webpack-dev-base).

Then we can lay­er any­thing spe­cif­ic about our project on top of these base images in the respec­tive <kbd>-craft</kbd> ser­vices. This saves us sig­nif­i­cant build­ing time, while keep­ing flexibility.

## The docker-compose.yaml file

While a <kbd>docker-compose.yaml</kbd> file isn’t required when using Dock­er, from a prac­ti­cal point of view, you’ll almost always use it. The <kbd>docker-compose.yaml</kbd> file allows you to define mul­ti­ple con­tain­ers for run­ning the ser­vices you need, and coor­di­nate start­ing them up and shut­ting them down in unison.

Then all you need to do is run <kbd>docker-compose up</kbd> via the ter­mi­nal in a direc­to­ry that has a <kbd>docker-compose.yaml</kbd> file, and Dock­er will start up all of your con­tain­ers for you!

Here’s an exam­ple of what that might look like, start­ing up your Dock­er containers:

![Docker compose up terminal](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x592_crop_center-center_100_line/docker-compose-up-terminal.png)

Let’s have a look at our <kbd>docker-compose.yaml</kbd> file:

```

version: '3.7'

services:
  # nginx - web server
  nginx:
    build:
      context: .
      dockerfile: ./docker-config/nginx/Dockerfile
    env_file: &env
      - ./cms/.env
    links:
      - php
    ports:
      - "8000:80"
    volumes:
      - cpresources:/var/www/project/cms/web/cpresources
      - ./cms/web:/var/www/project/cms/web:cached
  # php - personal home page
  php:
    build:
      context: .
      dockerfile: ./docker-config/php-dev-craft/Dockerfile
    depends_on:
      - "mariadb"
      - "redis"
    env_file:
      *env
    expose:
      - "9000"
    links:
      - mariadb
      - redis
    volumes:
      - cpresources:/var/www/project/cms/web/cpresources
      - storage:/var/www/project/cms/storage
      - ./cms:/var/www/project/cms:cached
      - ./cms/vendor:/var/www/project/cms/vendor:delegated
      - ./cms/storage/logs:/var/www/project/cms/storage/logs:delegated
  # mariadb - database
  mariadb:
    build:
      context: .
      dockerfile: ./docker-config/mariadb/Dockerfile
    env_file:
      *env
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: project
      MYSQL_USER: project
      MYSQL_PASSWORD: project
    ports:
      - "3306:3306"
    volumes:
      - db-data:/var/lib/mysql
  # redis - key/value database for caching & php sessions
  redis:
    build:
      context: .
      dockerfile: ./docker-config/redis/Dockerfile
    expose:
      - "6379"
  # webpack - frontend build system
  webpack:
    build:
      context: .
      dockerfile: ./docker-config/webpack-dev-craft/Dockerfile
    env_file:
      *env
    ports:
      - "8080:8080"
    volumes:
      - ./docker-config/webpack-dev-craft:/var/www/project/docker-config/webpack-dev-craft:cached
      - ./docker-config/webpack-dev-craft/node_modules:/var/www/project/docker-config/webpack-dev-craft/node_modules:delegated
      - ./cms/web/dist:/var/www/project/cms/web/dist:delegated
      - ./src:/var/www/project/src:cached
      - ./cms/templates:/var/www/project/cms/templates:cached

volumes:
  db-data:
  cpresources:
  storage:

```

This [.yaml file](https://yaml.org/) has 3 top-lev­el keys:

- <kbd>version</kbd> — the ver­sion num­ber of the [Dock­er Com­pose file,](https://docs.docker.com/compose/compose-file/) which cor­re­sponds to dif­fer­ent capa­bil­i­ties offered by dif­fer­ent ver­sions of the [Dock­er Engine](https://docs.docker.com/engine/)
- <kbd>services</kbd> — each ser­vice cor­re­sponds to a sep­a­rate Dock­er con­tain­er that is cre­at­ed using a sep­a­rate Dock­er image
- <kbd>volumes</kbd> — [named vol­umes](https://docs.docker.com/compose/compose-file/#volumes) that are mount­ed and can be shared amongst your Dock­er con­tain­ers (but not your host com­put­er), for stor­ing per­sis­tent data

We’ll detail each ser­vice below, but there are a few inter­est­ing tid­bits to cov­er first.

**BUILD**

When you’re cre­at­ing a Dock­er con­tain­er, you can either base it on an exist­ing <kbd>image</kbd> (either a local image or one pulled down from [Dock​er​Hub​.com](https://DockerHub.com)), or you can build it local­ly via a Dockerfile.

As men­tioned above, I chose the method­ol­o­gy that each ser­vice would be cre­at­ing as a <kbd>build</kbd> from a <kbd>Dockerfile</kbd> (all of which extend <kbd>FROM</kbd> an image up on [Dock​er​Hub​.com](https://DockerHub.com)) to keep things consistent.

This means that some of our <kbd>Dockerfile</kbd>s we use are noth­ing more than a sin­gle line, e.g.: <kbd>FROM mariadb:10.3</kbd>, but this set­up does allow for expansion.

The two keys used for <kbd>build</kbd> are are:

- <kbd>context</kbd> — this spec­i­fies where the work­ing direc­to­ry for the build should be, rel­a­tive to the <kbd>docker-compose.yaml</kbd> file. We have this set to <kbd>.</kbd> (the cur­rent direc­to­ry) for each service
- <kbd>dockerfile</kbd> — this spec­i­fies a path to the <kbd>Dockerfile</kbd> to use to <kbd>build</kbd> the ser­vice Dock­er con­tain­er. Think of the <kbd>Dockerfile</kbd> as a local Dock­er image

So the con­text is always the root direc­to­ry of the project, with the <kbd>Dockerfile</kbd> and any sup­port­ing files for each ser­vice are off in a sep­a­rate direc­to­ry. We do it this way to keep the paths con­sis­tent (always rel­a­tive to the project root) regard­less of the service.

**DEPENDS\_ON**

This just lets you spec­i­fy what oth­er ser­vices this par­tic­u­lar ser­vice depends on; this allows you to ensure that oth­er con­tain­ers are up and run­ning before this con­tain­er starts up.

**ENV\_FILE**

The <kbd>env_file</kbd> set­ting spec­i­fies a path to your <kbd>.env</kbd> file for key/​value pairs that will be inject­ed into a Dock­er container.

[Dock­er does not allow for quotes](https://github.com/docker/compose/issues/3702) in its <kbd>.env</kbd> file, which is con­trary to how <kbd>.env</kbd> files work almost every­where else… so remove any quotes you have in your <kbd>.env</kbd> file.

You’ll notice that for the nginx ser­vice, there’s a strange <kbd>&amp;env</kbd> val­ue in the <kbd>env_file</kbd> set­ting, and for the oth­er ser­vices, the set­ting is <kbd>*env</kbd>. This is tak­ing advan­tage of [YAML alias­es](https://github.com/cyklo/Bukkit-OtherBlocks/wiki/Aliases-(advanced-YAML-usage)), so if we do change the <kbd>.env</kbd> file path, we only have to do it in one place.

Doing it this way also ensures that all of the .env envi­ron­ment vari­ables are avail­able in every con­tain­er. For more on envi­ron­ment vari­ables, check out the [Flat Mul­ti-Envi­ron­ment Con­fig for Craft CMS 3](https://dev.to/gaijinity/flat-multi-environment-config-for-craft-cms-3-53h7-temp-slug-6416116) article.

Because it’s Dock­er that is inject­ing these <kbd>.env</kbd> envi­ron­ment vari­ables, if you change your <kbd>.env</kbd> file, you’ll need to restart your Dock­er containers.

**LINKS**

[Links in Dock­er](https://docs.docker.com/compose/networking/#links) allow you to define extra alias­es by which a ser­vice is reach­able from anoth­er ser­vice. They are not required to enable ser­vices to com­mu­ni­cate, but I like being explic­it about it.

The come into play when one con­tain­er needs to talk to anoth­er. For exam­ple, if you nor­mal­ly would com­mu­ni­cate with your data­base via the <kbd>localhost</kbd> sock­et, instead in our set­up you’d use the sock­et named mariadb.

The key take-away is that when con­tain­ers need to talk to each oth­er, they are doing so over the inter­nal Dock­er net­work, and refer to each oth­er via their <kbd>service</kbd> or <kbd>links</kbd> name.

**PORTS**

This spec­i­fies the port that should be exposed out­side of the con­tain­er, fol­lowed by the port that the con­tain­er uses inter­nal­ly. So for exam­ple, the nginx ser­vice has <kbd>"8000:80"</kbd>, which means the exter­nal­ly acces­si­ble port for the Nginx web­serv­er is <kbd>8000</kbd>, and the inter­nal port the ser­vice runs on is <kbd>80</kbd>.

If this sounds con­fus­ing, under­stand that [Dock­er uses its own inter­nal net­work](https://docs.docker.com/network/) to allow con­tain­ers to talk to each oth­er, as well as the out­side world.

**VOL­UMES**

Dock­er con­tain­ers run in their own lit­tle world, which is great for iso­la­tion pur­pos­es, but at some point you do need to share things from your ​“host” com­put­er with the Dock­er container.

[Dock­er vol­umes](https://docs.docker.com/compose/compose-file/#volumes) allow you to do this. You spec­i­fy either a named vol­ume or a path on your host, fol­lowed by the path where this vol­ume should be [bind mount­ed](https://docs.docker.com/storage/bind-mounts/) in the Dock­er container.

This is where per­for­mance prob­lems can hap­pen with Dock­er on the Mac and Win­dows. So we use some hints to [help with per­for­mance](https://docs.docker.com/docker-for-mac/osxfs-caching/#tuning-with-consistent-cached-and-delegated-configurations):

- <kbd>consistent</kbd> — per­fect con­sis­ten­cy (host and con­tain­er have an iden­ti­cal view of the mount at all times)
- <kbd>cached</kbd> — the host’s view is author­i­ta­tive (per­mit delays before updates on the host appear in the container)
- <kbd>delegated</kbd> — the container’s view is author­i­ta­tive (per­mit delays before updates on the con­tain­er appear in the host)

So for things like <kbd>node_modules/</kbd> and <kbd>vendor/</kbd> we mark them as :del­e­gat­ed because while we want them shared, the con­tain­er is in con­trol of mod­i­fy­ing these volumes.

Some Dock­er setups I’ve seen put these direc­to­ries into a named vol­ume, which means they are vis­i­ble only to the Dock­er containers.

But the prob­lem is we lose out on our edi­tor auto-com­ple­tion, because our edi­tor has noth­ing to index.

<aside>
                                This is a non-nego­tiable for me
                            </aside>

See the [Auto-Com­plete Craft CMS 3 APIs in Twig](https://dev.to/gaijinity/auto-complete-craft-cms-3-apis-in-twig-with-phpstorm-5go9) with Php­Storm arti­cle for details.

## Ser­vice: Nginx

[Nginx](https://www.nginx.com/) is the web serv­er of choice for me, both in local dev and in production. 

```

FROM nginx:1.16

COPY ./docker-config/nginx/default.conf /etc/nginx/conf.d/default.conf

```

We’ve based the con­tain­er on the [nginx image](https://hub.docker.com/_/nginx), tagged at ver­sion <kbd>1.16</kbd>

The only mod­i­fi­ca­tion it makes is <kbd>COPY</kbd>ing our <kbd>default.conf</kbd> file into place:

```

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

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

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

    sendfile off;
    ssi on;

    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
                      text/comma-separated-values
                      text/javascript
                      application/x-javascript
                      application/javascript
                      application/atom+xml;

    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;
        fastcgi_read_timeout 300;
    }

    location ~ /\.ht {
        deny all;
    }
}

```

This is just a sim­ple Nginx con­fig that works well with Craft CMS. You can find more about Nginx con­figs for Craft CMS in the [nginx-craft](https://github.com/nystudio107/nginx-craft) GitHub repo.

## Ser­vice: MariaDB

[Mari­aDB](https://mariadb.org/) is a drop-in replace­ment for MySQL that I tend to use instead of MySQL itself. It was writ­ten by the orig­i­nal author of MySQL, and is bina­ry com­pat­i­ble with MySQL.

```

FROM mariadb:10.3

```

We’ve based the con­tain­er on the [mari­adb image](https://hub.docker.com/_/mariadb), tagged at ver­sion <kbd>10.3</kbd>

There’s no mod­i­fi­ca­tion at all to the source image.

## Ser­vice: Postgres

[Post­gres](https://www.postgresql.org/) is a robust data­base that I am using more and more for Craft CMS projects. It’s not used in the docker-compose.yaml pre­sent­ed here, but I keep the con­fig­u­ra­tion around in case I want to use it.

Post­gres is used in local dev and in pro­duc­tion on the [dev​Mode​.fm GitHub repo](https://github.com/nystudio107/devmode), if you want to see it implemented.

```

FROM postgres:12.2

```

We’ve based the con­tain­er on the [post­gres image](https://hub.docker.com/_/postgres), tagged at ver­sion <kbd>12.2</kbd>

There’s no mod­i­fi­ca­tion at all to the source image.

## Ser­vice: Redis

[Redis](https://redis.io/) is a key/​value pair data­base that I set all of my Craft CMS installs to use both as a [caching method](https://github.com/nystudio107/craft/blob/craft-webpack/cms/config/app.php#L33), and as a [ses­sion han­dler](https://github.com/nystudio107/craft/blob/craft-webpack/cms/config/app.php#L41) for PHP.

```

FROM redis:5.0

```

We’ve based the con­tain­er on the [redis image](https://hub.docker.com/_/redis/), tagged at ver­sion <kbd>5.0</kbd>

There’s no mod­i­fi­ca­tion at all to the source image.

## Ser­vice: PHP

[PHP](https://www.php.net/) is the lan­guage that the Yii2 frame­work and Craft CMS itself is based on, so we need it in order to run our app.

This ser­vice is com­posed of a base image that con­tains all of the pack­ages and PHP exten­sions we’ll always need to use, and then the project-spe­cif­ic image that con­tains what­ev­er addi­tion­al things are need­ed for our project.

```

FROM php:7.3-fpm

# Install packages
RUN apt-get update \
    && \
    # apt Debian packages
    apt-get install -y \
        apt-utils \
        autoconf \
        ca-certificates \
        curl \
        g++ \
        libbz2-dev \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        libpq-dev \
        libssl-dev \
        libicu-dev \
        libmagickwand-dev \
        libzip-dev \
        unzip \
        zip \
    && \
    # pecl PHP extensions
    pecl install \
        imagick-3.4.4 \
        redis \
        xdebug-2.8.1 \
    && \
    # Configure PHP extensions
    docker-php-ext-configure \
        gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
    && \
    # Install PHP extensions
    docker-php-ext-install \
        bcmath \
        bz2 \
        exif \
        ftp \
        gettext \
        gd \
        iconv \
        intl \
        mbstring \
        opcache \
        pdo \
        shmop \
        sockets \
        sysvmsg \
        sysvsem \
        sysvshm \
        zip \
    && \
    # Enable PHP extensions
    docker-php-ext-enable \
        imagick \
        redis \
        xdebug

# Append our php.ini overrides to the end of the file
RUN echo "upload_max_filesize = 10M" > /usr/local/etc/php/php.ini && \
    echo "post_max_size = 10M" >> /usr/local/etc/php/php.ini && \
    echo "max_execution_time = 300" >> /usr/local/etc/php/php.ini && \
    echo "memory_limit = 256M" >> /usr/local/etc/php/php.ini && \
    echo "opcache.revalidate_freq = 0" >> /usr/local/etc/php/php.ini && \
    echo "opcache.validate_timestamps = 1" >> /usr/local/etc/php/php.ini

# Copy the `zzz-docker.conf` file into place for php-fpm
COPY ./zzz-docker.conf /usr/local/etc/php-fpm.d/zzz-docker.conf

```

We’ve based the con­tain­er on the [php image](https://hub.docker.com/_/php), tagged at ver­sion <kbd>7.3</kbd>

We’re then adding a bunch of [Debian](https://www.debian.org/) pack­ages that we want avail­able for our [Ubun­tu](https://ubuntu.com/) oper­at­ing sys­tem base, some debug­ging tools, as well as some PHP exten­sions that [Craft CMS requires](https://docs.craftcms.com/v3/requirements.html#required-php-extensions).

Then we add some defaults to the <kbd>php.ini</kbd>, and copy into place the <kbd>zzz-docker.conf</kbd> file:

```

[www]
pm.max_children = 10
pm.process_idle_timeout = 10s
pm.max_requests = 1000

```

This just sets some defaults for <kbd>php-fpm</kbd> that make sense for local development.

By itself, this image won’t do much for us, and in fact we don’t even spin up this image. But we’ve built this image, and made it avail­able as [nys­tu­dio107/php-dev-base](https://hub.docker.com/repository/docker/nystudio107/php-dev-base) on DockerHub.

Since it’s pre-built, we don’t have to build it every time, and can lay­er on top of this image any­thing project-spe­cif­ic via the <kbd>php-dev-craft</kbd> con­tain­er image:

```

FROM nystudio107/php-dev-base

# Install packages
RUN apt-get update \
    && \
    # apt Debian packages
    apt-get install -y \
        nano \
    && \
    # Install PHP extensions
    docker-php-ext-install \
        pdo_mysql \
    && \
    # Install Composer
    curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer

WORKDIR /var/www/project

# Copy over the directories/files php needs access to
COPY --chown=www-data:www-data ./cms/composer.* /var/www/project/cms/

# Create the storage directory and make it writeable by PHP
RUN mkdir -p /var/www/project/cms/storage && \
    mkdir -p /var/www/project/cms/storage/runtime && \
    chown -R www-data:www-data /var/www/project/cms/storage

# Create the cpresources directory and make it writeable by PHP
RUN mkdir -p /var/www/project/cms/web/cpresources && \
    chown -R www-data:www-data /var/www/project/cms/web/cpresources

WORKDIR /var/www/project/cms

# Do a `composer install` without running any Composer scripts
# - If `composer.lock` is present, it will install what is in the lock file
# - If `composer.lock` is missing, it will update to the latest dependencies
# and create the `composer.lock` file
# Force permissions, update Craft, and start php-fpm
CMD if [! -f "./composer.lock"]; then \
        composer install --no-scripts --optimize-autoloader --no-interaction; \
    fi \
    && \
    if [! -d ./vendor -o ! "$(ls -A ./vendor)"]; then \
        composer install --no-scripts --optimize-autoloader --no-interaction; \
    fi \
    && \
    chown -R www-data:www-data /var/www/project/cms/vendor \
    && \
    chown -R www-data:www-data /var/www/project/cms/storage \
    && \
    composer craft-update \
    && \
    php-fpm

```

This is the image that we actu­al build into a con­tain­er, and use for our project. We install the <kbd>nano</kbd> edi­tor because I find it handy some­times, and we also install <kbd>pdo_mysql</kbd> so that PHP can con­nect to our Mari­aDB database.

We do it this way so that if we want to cre­ate a Craft CMS project that uses Post­gres, we can just swap in the PDO exten­sion need­ed here.

Then we make sure the var­i­ous <kbd>storage/</kbd> and <kbd>cpresources/</kbd> direc­to­ries are in place, with the right own­er­ship so that Craft will run properly.

Then we do a bit of mag­ic to do a <kbd>composer install</kbd>, but only if:

- The <kbd>composer.lock</kbd> file does­n’t exist
- The <kbd>vendor/</kbd> direc­to­ry does­n’t exist, or is empty

We have to do the <kbd>composer install</kbd> as part of the Dock­er image <kbd>CMD</kbd> because the file sys­tem mounts aren’t in place until the <kbd>CMD</kbd> is run.

This allows us to update our [Com­pos­er](https://getcomposer.org/) depen­den­cies just by delet­ing the <kbd>composer.lock</kbd> file or the <kbd>vendor/</kbd> direc­to­ry, and doing <kbd>docker-compose up</kbd>

Sim­ple.

The alter­na­tive is doing a <kbd>docker exec -it craft_php_1 /bin/bash</kbd> to open up a shell in our con­tain­er, and run­ning the com­mand man­u­al­ly. Which is fine, but a lit­tle con­vo­lut­ed for some.

Then we make sure that the own­er­ship on impor­tant direc­to­ries is cor­rect, and we run the <kbd>craft-update</kbd> [Com­pos­er script](https://getcomposer.org/doc/articles/scripts.md):

```

{
  "require": {
    "craftcms/cms": "^3.4.0",
    "vlucas/phpdotenv": "^3.4.0",
    "yiisoft/yii2-redis": "^2.0.6",
    "nystudio107/craft-imageoptimize": "^1.0.0",
    "nystudio107/craft-fastcgicachebust": "^1.0.0",
    "nystudio107/craft-minify": "^1.2.5",
    "nystudio107/craft-typogrify": "^1.1.4",
    "nystudio107/craft-retour": "^3.0.0",
    "nystudio107/craft-seomatic": "^3.2.0",
    "nystudio107/craft-webperf": "^1.0.0",
    "nystudio107/craft-twigpack": "^1.1.0",
    "nystudio107/dotenvy": "^1.1.0"
  },
  "autoload": {
    "psr-4": {
      "modules\\sitemodule\\": "modules/sitemodule/src/"
    }
  },
  "config": {
    "sort-packages": true,
    "optimize-autoloader": true,
    "platform": {
      "php": "7.0"
    }
  },
  "scripts": {
    "craft-update": [
      "@php craft migrate/all",
      "@php craft project-config/sync",
      "@php craft clear-caches/all"
    ],
    "post-root-package-install": [
      "@php -r \"file_exists('.env') || copy('example.env', '.env');\""
    ],
    "post-update-cmd": "@craft-update",
    "post-install-cmd": "@craft-update"
  }
}

```

So the <kbd>craft-update</kbd> script auto­mat­i­cal­ly does the fol­low­ing when our con­tain­er starts up:

- All [migra­tions](https://docs.craftcms.com/v3/extend/migrations.html) are run
- [Project Con­fig](https://docs.craftcms.com/v3/project-config.html) is sync’d
- All caches are cleared

Start­ing from a clean slate like this is so help­ful in terms of avoid­ing sil­ly prob­lems like things being cached, not up to date, etc.

## Ser­vice: webpack

[web­pack](https://webpack.js.org/) is the build tool that we use for build­ing the CSS, JavaScript, and oth­er parts of our application.

The set­up used here is entire­ly based on the [An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment](https://dev.to/gaijinity/an-annotated-webpack-4-config-for-frontend-web-development-1p36) arti­cle, just with some set­tings tweaked.

That means our web­pack build process runs entire­ly inside of a Dock­er con­tain­er, but we still get all of the [Hot Mod­ule Replace­ment](https://webpack.js.org/concepts/hot-module-replacement/) good­ness for local development.

This ser­vice is com­posed of a base image that con­tains <kbd>node</kbd> itself, all of the Debian pack­ages need­ed for head­less Chrome, the [npm pack­ages](https://www.npmjs.com/) we’ll always need to use, and then the project-spe­cif­ic image that con­tains what­ev­er addi­tion­al things are need­ed for our project.

```

FROM node:11

# Install packages for headless chrome
# https://medium.com/@ssmak/how-to-fix-puppetteer-error-while-loading-shared-libraries-libx11-xcb-so-1-c1918b75acc3
RUN apt-get update \
    && \
    # apt Debian packages
    apt-get install -y \
        ca-certificates \
        fonts-liberation \
        gconf-service \
        libgl1-mesa-glx \
        libasound2 \
        libatk1.0-0 \
        libc6 \
        libcairo2 \
        libcups2 \
        libdbus-1-3 \
        libexpat1 \
        libfontconfig1 \
        libgcc1 \
        libgconf-2-4 \
        libgdk-pixbuf2.0-0 \
        libglib2.0-0 \
        libgtk-3-0 \
        libnspr4 \
        libpango-1.0-0 \
        libpangocairo-1.0-0 \
        libstdc++6 \
        libx11-6 \
        libx11-xcb1 \
        libxcb1 \
        libxcomposite1 \
        libxcursor1 \
        libxdamage1 \
        libxext6 \
        libxfixes3 \
        libxi6 \
        libxrandr2 \
        libxrender1 \
        libxss1 \
        libxtst6 \
        libappindicator1 \
        libnss3 \
        lsb-release \
        wget \
        xdg-utils

```

We’ve based the con­tain­er on the [node image](https://hub.docker.com/_/node/), tagged at ver­sion 11

We’re then adding a bunch of [Debian](https://www.debian.org/) pack­ages that we need in order to get [head­less Chrome work­ing](https://medium.com/@ssmak/how-to-fix-puppetteer-error-while-loading-shared-libraries-libx11-xcb-so-1-c1918b75acc3) (need­ed for Crit­i­cal CSS gen­er­a­tion), as well as oth­er libraries for the [Sharp image library](https://sharp.pixelplumbing.com/) to work effectively.

By itself, this image won’t do much for us, and in fact we don’t even spin up this image. But we’ve built this image, and made it avail­able as [nys­tu­dio107/web­pack-dev-base](https://hub.docker.com/repository/docker/nystudio107/webpack-dev-base) on DockerHub.

Since it’s pre-built, we don’t have to build it every time, and can lay­er on top of this image any­thing project-spe­cif­ic via the <kbd>webpack-dev-craft</kbd> con­tain­er image:

```

FROM nystudio107/webpack-dev-base

WORKDIR /var/www/project/docker-config/webpack-dev-craft/

# We'd normally use `npm ci` here, but by using `install`:
# - If `package-lock.json` is present, it will install what is in the lock file
# - If `package-lock.json` is missing, it will update to the latest dependencies
# and create the `package-lock-json` file
# Run our webpack build in debug mode
CMD if [! -f "./package-lock.json"]; then \
        npm install; \
    fi \
    && \
    if [! -d "./node_modules" -o ! "$(ls -A ./node_modules)"]; then \
        npm install; \
    fi \
    && \
    npm run debug

```

Then, just like with the <kbd>php-dev-craft</kbd> image, we do a bit of mag­ic to do a <kbd>npm install</kbd>, but only if:

- The <kbd>package-lock.json</kbd> file does­n’t exist
- The <kbd>node_modules/</kbd> direc­to­ry does­n’t exist, or is empty

We have to do the <kbd>npm install</kbd> as part of the Dock­er image <kbd>CMD</kbd> because the file sys­tem mounts aren’t in place until the <kbd>CMD</kbd> is run.

This allows us to update our [npm](https://www.npmjs.com/) depen­den­cies just by delet­ing the <kbd>package-lock.json</kbd> file or the <kbd>node_modules/</kbd> direc­to­ry, and doing <kbd>docker-compose up</kbd>

The alter­na­tive is doing a <kbd>docker exec -it craft_webpack_1 /bin/bash</kbd> to open up a shell in our con­tain­er, and run­ning the com­mand manually.

## All Aboard!

Hope­ful­ly this anno­tat­ed Dock­er con­fig has been use­ful to you. If you use Craft CMS, you can dive in and start using it your­self; if you use some­thing else entire­ly, the con­cepts here should still be very salient for your project.

![Docker containers local development](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/docker-containers-local-development.jpg)

I think that Dock­er — or some oth­er con­cep­tu­al­ly sim­i­lar con­tainer­iza­tion strat­e­gy — is going to be an impor­tant tech­nol­o­gy going for­ward. So it’s time to jump on board.

As men­tioned ear­li­er, the Dock­er con­fig used here is used in both the [dev​Mode​.fm GitHub repo](https://github.com/nystudio107/devmode), and in the [nystudio107/​craft](https://github.com/nystudio107/craft) boil­er­plate Com­pos­er project if you want to see some ​“in the wild” examples.

Hap­py containerizing!

## Further Reading

_If you want to be notified about new articles, follow [nystudio107](https://twitter.com/nystudio107) on Twitter._

<small>Copyright ©2020 nystudio107. Designed by nystudio107</small>