Intro to Dockerfiles
Dockerfiles play an essential role in Docker. Dockerfiles is a formatted text file which essentially serves as an instruction manual for what containers should do and ultimately assembles a Docker image.
You use Dockerfiles to contain the commands the container should execute when it is built. To get started with Dockerfiles, we need to know some basic syntax and instructions. Dockerfiles are formatted in the following way:
INSTRUCTION argument
First, let’s cover some essential instructions:
Instruction | Description | Example |
---|---|---|
FROM | This instruction sets a build stage for the container as well as setting the base image (operating system). All Dockerfiles must start with this. | FROM ubuntu |
RUN | This instruction will execute commands in the container within a new layer. | RUN whoami |
COPY | This instruction copies files from the local system to the working directory in the container (the syntax is similar to the | COPY /home/cmnatic/myfolder/app/ |
WORKDIR | This instruction sets the working directory of the container. (similar to using | WORKDIR / (sets to the root of the filesystem in the container) |
CMD | This instruction determines what command is run when the container starts (you would use this to start a service or application). | CMD /bin/sh -c script.sh |
EXPOSE | This instruction is used to tell the person who runs the container what port they should publish when running the container. | EXPOSE 80 (tells the person running the container to publish to port 80 i.e. docker run -p 80:80) |
Now that we understand the core instructions that make up a Dockerfile, let’s see a working example of a Dockerfile. But first, I’ll explain what I want the container to do:
Use the “Ubuntu” (version 22.04) operating system as the base.
Set the working directory to be the root of the container.
Create the text file “helloworld.txt”.
Remember, the commands that you can run via the RUN
instruction will depend on the operating system you use in the FROM
instruction. (In this example, I have chosen Ubuntu. It’s important to remember that the operating systems used in containers are usually very minimal. I.e., don’t expect a command to be there from the start (even commands like curl, ping, etc., may need to be installed.)
Building Your First Container
Once we have a Dockerfile, we can create an image using the docker build
command. This command requires a few pieces of information:
Whether you want to name the image yourself (we will use the
-t
(tag) argument).The name that you are going to give the image.
The location of the Dockerfile you wish to build with.
I’ll provide the scenario and then explain the relevant command. Let’s say we want to build an image - let’s fill in the two required pieces of information listed above:
We are going to name it ourselves, so we are going to use the
-t
argument.We want to name the image.
The Dockerfile is located in our current working directory (
.
).
The Dockerfile we are going to build is the following:
The command would look like so: docker build -t helloworld .
(we are using the dot to tell Docker to look in our working directory). If we have filled out the command right, we will see Docker starting to build the image:
Great! That looks like a success. Let’s use docker image ls
to now see if this image has been built:
Note: Whatever base operating system you list in the FROM
instruction in the Dockerfile will also be downloaded. This is why we can see two images:
helloworld (our image).
ubuntu (the base operating system used in our image).
You will now be able to use this image in a container. Refer to the “Running Your First Container” task to remind you how to start a container.
Levelling up Our Dockerfile
Let’s level up our Dockerfile. So far, our container will only create a file - that’s not very useful! In the following Dockerfile, I am going to:
Use Ubuntu 22.04 as the base operating system for the container.
Install the “apache2” web server.
Add some networking. As this is a web server, we will need to be able to connect to the container over the network somehow. I will achieve this by using the
EXPOSE
instruction and telling the container to expose port 80.Tell the container to start the “apache2” service at startup. Containers do not have service managers like
systemd
(this is by design - it is bad practice to run multiple applications in the same container. For example, this container is for the apache2 web server - and the apache2 web server only).
For reference, the command to build this would be docker build -t webserver .
(assuming the Dockerfile is in the same directory as where you run the command from). Once starting the container with the appropriate options (docker run -d --name webserver -p 80:80 webserver
), we can navigate to the IP address of our local machine in our browser!
Optimising Our Dockerfile
There’s certainly an art to Docker - and it doesn’t stop with Dockerfiles! Firstly, we need to ask ourselves why is it essential to optimise our Dockerfile? Bloated Dockerfiles are hard to read and maintain and often use a lot of unnecessary storage! For example, you can reduce the size of a docker image (and reduce build time!) using a few ways:
Only installing the essential packages. What’s nice about containers is that they’re practically empty from the get-go - we have complete freedom to decide what we want.
Removing cached files (such as APT cache or documentation installed with tools). The code within a container will only be executed once (on build!), so we don’t need to store anything for later use.
Using minimal base operating systems in our
FROM
instruction. Even though operating systems for containers such as Ubuntu are already pretty slim, consider using an even more stripped-down version (i.e.ubuntu:22.04-minimal
). Or, for example, using Alpine (which can be as small as 5.59MB!).Minimising the number of layers - I’ll explain this further below.
Each instruction (I.E. FROM
, RUN
, etc.) is run in its own layer. Layers increase build time! The objective is to have as few layers as possible. For example, try chaining commands from RUN
together like so:
Before
After
Note here how there are now only two build steps (this will be two layers, making the build much quicker). This is just a tiny example of a Dockerfile, so the build time will not be so drastic, but in much larger Dockerfiles - reducing the number of layers will have a fantastic performance increase during the build.
Last updated