DEV Community

Laurent Tardif
Laurent Tardif

Posted on • Originally published at ouelcum.wordpress.com on

[Devops / Docker] How to write a good dockerfile

Reading or writing a dockerfile, as soon as you get a bit familiar with the syntax is quiet simple. But how many time you spent some time wondering why your image is so big, or why the image you download was so big, how many time lost trying to find the port, the volume defined, have you loose. So, writing a good docker file is not so easy. I’ll try to explain the process that we have defined with Jules

If you want some kind of introduction to docker, have a look to Aurélie's post

First of all, what’s a good docker file ? is it a secure one ? , a readable one ?, one that generate a small docker image ? all that ? that’s a challenge.

In this article, we’ll not focus on security aspect, but we’ll explain how to write a readable docker file, easy to maintain and that will generate a small docker image.

Let’s start with a simple example :

FROM node  
WORKDIR /usr/src/app  
COPY babel.config.js ./  
COPY package.json ./  
COPY yarn.lock ./  
COPY public ./public  
COPY src ./src  
RUN yarn  
RUN yarn lint  
RUN yarn build  
RUN yarn serve

if we docker build this image, we can notice several point :

  1. it may download a new version of the referenced image “node”
  2. it create several layers. A layer is an immutable set of files that can be shared among several docker images for performances and disk usage optimization. If you use several images having the same root image, you will have the files of this images present only once on your disk.

if you rebuild this image, without any modification, you will notice that docker is using a cache mechanism. To give some hints, if nothing change on your disk (for example, you do not change the package.json) it will not redo the step (and if all the previous steps have not changed). In summary, docker will do the minimal stuff needed.

So, let’s take this 2 points in account.

FROM  node: **10.15.0-Alpine**  
WORKDIR /usr/src/app  
COPY  “babel.config.js” “package.json” “yarn.lock” ./  
COPY public ./public  
COPY src ./src  
RUN yarn  
RUN yarn lint  
RUN yarn build  
RUN yarn serve

So, to avoid a random node image being picked, i defined a strict version for it. And to avoid useless layer, i defined a layer with the 3 configurations files.

Now that we have reduced the two first “issues”, let’s take advantage of the cache mechanism.

FROM  node:10.15.0-Alpine  
WORKDIR /usr/src/app  
COPY “babel.config.js” “package.json” “yarn.lock” ./  
RUN yarn  
COPY public ./public  
COPY src ./src  
RUN yarn lint  
RUN yarn build  
RUN yarn serve

So, I move up the “RUN yarn” just after the copy of the configuration file. Like that, as long as the configuration files do not change, my “RUN yarn” will not be executed, and I’ll benefit of the docker cache mechanism.

Now, I’ll not run directly use yarn to run my application. Let’s set up quickly an nginx server in front of it. So, I’ll use the multi stage feature of docker. It’s allow me to reuse the result of a docker image in an another one, define in the same dockerfile. Let’s do it.

FROM  node:10.15.0-Alpine **AS builder**  
WORKDIR /usr/src/app  
COPY “babel.config.js” “package.json” “yarn.lock” ./  
RUN yarn  
COPY public ./public  
COPY src ./src  
RUN yarn lint  
RUN yarn build  
RUN yarn serve 

# Volume inherited from nginx image  
# VOLUME /usr/share/nginx/html  
# VOLUME /etc/nginx   

FROM nginx:1.12.1-alpine  
EXPOSE 80    
HEALTHCHECK –interval=5m –timeout=3s -CMD curl -f http://localhost/ || exit 1   
COPY –from=builder /usr/src/app/dist/ /usr/share/nginx/html/

In order to do it, I’ve defined a logical name to my first image (I named it “builder”, it will allow me to refer to it in the second image).

Then I defined a new image, from nginx, and I copy the website content from the first image. So like that, my nginx image is minimal and contains only mandatory stuff (from a security stuff, the less you have, the better it is).

You can notice I’ve define the expose and healtheck in the top of my docker image definition. Because it a kind of API of my image, i make it quickly visible to the reader. You can access my image on port 80, there’s a check that insure the nginx server respond, the run and the copy element are a kind of implementation, and only few people want to read it.

Of course, we can improve this image, but if you reach this point, it’s a very good starting point for people using your image.

Top comments (0)