On the previous post we explained the basics about managing existing Docker images. On this post we will describe how to create your own Docker images.
How to create your own Docker images
There are two ways to create a Docker image:
• Via the docker commit command
• Via the docker build command with a Dockerfile
The docker commit method is deprecated, building with Dockerfile is far more flexible and powerful, therefore we will explain how to create images using the build command.
Building images with a Dockerfile
Docker recommendation for creating images is to build images using a definition file called a Dockerfile and the docker build command. The Dockerfile uses a basic DSL (Domain Specific Language) with instructions for building Docker images. The Dockerfile approach is more repeatable, transparent, and idempotent mechanism for creating images.
Once we have a Dockerfile we then use the docker build command to build a new image from the instructions in the Dockerfile.
Creating a Dockerfile
Let’s now create a directory and an initial Dockerfile . We’re going to build a Docker image that contains a simple web server.
$ mkdir static_web
$ cd static_web/
$ touch Dockerfile
We’ve created a directory called static_web to hold our Dockerfile . This directory is our build environment, which is what Docker calls a context or build context. Docker will upload the build context, as well as any files and directories contained in it, to our Docker daemon when the build is run. This provides the Docker daemon with direct access to any code, files or other data you might want to include in the image.
We’ve also created an empty Dockerfile file to get started. Now let’s look at an example of a Dockerfile to create a Docker image that will act as a Web server.
Dockerfile to create a Docker image
# Version: 0.0.1
FROM ubuntu:16.04
RUN apt-get update; apt-get install -y nginx
RUN echo 'Hello World' >/var/www/html/index.html
EXPOSE 80
The Dockerfile contains a series of instructions paired with arguments. Each instruction, for example FROM , should be in upper-case and be followed by an argument: FROM ubuntu:16.04 . Instructions in the Dockerfile are processed from the top down, so you should order them accordingly.
Each instruction adds a new layer to the image and then commits the image.
Docker executing instructions roughly follow a workflow:
- Docker runs a container from the image.
- An instruction executes and makes a change to the container.
- Docker runs the equivalent of docker commit to commit a new layer.
- Docker then runs a new container from this new image.
- The next instruction in the file is executed, and the process repeats until all instructions have been executed.
This means that if your Dockerfile stops for some reason (for example, if an
instruction fails to complete), you will be left with an image you can use. This is highly useful for debugging: you can run a container from this image interactively and then debug why your instruction failed using the last image created.
The first instruction in a Dockerfile must be FROM . The FROM instruction specifies an existing image that the following instructions will operate on; this image is called the base image.
In our sample Dockerfile we’ve specified the ubuntu:16.04 image as our base image. This specification will build an image on top of an Ubuntu 16.04 base operating system. As with running a container, you should always be specific about exactly from which base image you are building.
We’ve followed these instructions with two RUN instructions. The RUN instruction executes commands on the current image. The commands in our example: updating the installed APT repositories and installing the nginx package and then creating the /var/www/html/index.html file containing some example text. As we’ve discovered, each of these instructions will create a new layer and, if successful, will commit that layer and then execute the next instruction.
By default, the RUN instruction executes inside a shell using the command wrapper /bin/sh -c . If you are running the instruction on a platform without a shell or you wish to execute without a shell you can specify the instruction in exec format:
RUN [ "apt-get", " install", "-y", "nginx" ]
We use this format to specify an array containing the command to be executed and then each parameter to pass to the command.
Next, we’ve specified the EXPOSE instruction, which tells Docker that the application in this container will use this specific port on the container. That doesn’t mean you can automatically access whatever service is running on that port (here, port 80 ) on the container. For security reasons, Docker doesn’t open the port automatically, but waits for you to do it when you run the container using the docker run command. We’ll see this shortly when we create a new container from this image.
You can specify multiple EXPOSE instructions to mark multiple ports to be exposed.
Building the image from our Dockerfile
All of the instructions will be executed and committed and a new image returned when we run the docker build command. Let’s try that now:
First, remember to login using your docker hub account, if you don’t have one, create on at docker hub.
docker login
docker build -t="npucheta/static_web" .
You will see a bunch of stuff happening, we have shortened the output and left the most interesting parts below:
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM ubuntu:16.04
---> 9361ce633ff1
Step 2/4 : RUN apt-get update; apt-get install -y nginx
---> Running in 56fabc985e58
Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [109 kB]
Processing triggers for sgml-base (1.26+nmu4ubuntu1) …
Processing triggers for systemd (229-4ubuntu21.16) …
Removing intermediate container 56fabc985e58
---> db91da893446
Step 3/4 : RUN echo 'Hello World' >/var/www/html/index.html
---> Running in 981ec57fa290
Removing intermediate container 981ec57fa290
---> 97adde1ef809
Step 4/4 : EXPOSE 80
---> Running in 38078b43687a
Removing intermediate container 38078b43687a
---> 6a9db4f47ca3
Successfully built 6a9db4f47ca3
Successfully tagged npucheta/static_web:latest
You can see that each instruction in the Dockerfile has been executed with the image ID, 6a9db4f47ca3 , being returned as the final output of the build process. Each step and its associated instruction are run individually, and Docker has committed the result of each operation before outputting that final image ID.
Docker is able to be really clever about building images.
As a result of each step being committed as an image, Docker is able to be really clever about building images. It will treat previous layers as a cache.
This can save you a lot of time when building images if a previous step has not changed. If, however, you did change something, then Docker would restart from the first changed instruction.
Sometimes, though, you want to make sure you don’t use the cache. For example, if you’d cached Step 3 above, apt-get update , then it wouldn’t refresh the APT package cache. You might want it to do this to get a new version of a package. To skip the cache, we can use the –no-cache flag with the docker build command.
Now that we had created our image, lets check if the image is there with the docker images command:
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
npucheta/static_web latest 6a9db4f47ca3 6 minutes ago 199MB
nginx latest 27a188018e18 5 days ago 109MB
ubuntu 16.04 9361ce633ff1 5 weeks ago 118MB
ubuntu latest 94e814e2efa8 5 weeks ago 88.9MB
fedora 21 ba6369d667d1 2 years ago 241MB
You can see at the top of our images list our new image.
If we want to see how our image was created, we can use the docker history command.
docker history npucheta/static_web
IMAGE CREATED CREATED BY SIZE COMMENT
6a9db4f47ca3 7 minutes ago /bin/sh -c #(nop) EXPOSE 80 0B
97adde1ef809 7 minutes ago /bin/sh -c echo 'Hello World' >/var/www/html… 12B
db91da893446 7 minutes ago /bin/sh -c apt-get update; apt-get install -… 81.5MB
9361ce633ff1 5 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 5 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 5 weeks ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
<missing> 5 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B
<missing> 5 weeks ago /bin/sh -c #(nop) ADD file:c02de920036d851cc… 118MB
Launching a container from our new image
Let’s launch a new container using our new image and see if what we’ve built has worked.
# docker run -d -p 80:80 --name static_web npucheta/static_web nginx -g "daemon off;"
2d369e06a58d68855a315da2e1335640b501eaf90f559eedaa66e9075dea8a57
Here I’ve launched a new container called static_web using the docker run command and the name of the image we’ve just created. We’ve specified the -d option, which tells Docker to run detached in the background. This allows us to run long-running processes like the Nginx daemon. We’ve also specified a command for the container to run: nginx -g “daemon off;” . This will launch Nginx in the foreground to run our web server.
We’ve also specified a new flag, -p . The -p flag manages which network ports Docker publishes at runtime.
Lets check now if the container was created and the port 80 open:
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2d369e06a58d npucheta/static_web "nginx -g 'daemon of…" 3 seconds ago Up 2 seconds 0.0.0.0:80->80/tcp static_web
We can now find out the IP of the container and then connect to check our Hello World page is UP:
# docker exec static_web hostname -i
172.17.0.2
# curl 172.17.0.2
Hello World
As the port 80 is bound to the 0.0.0.0 address on the host we could also accomplished the same running:
# curl localhost
Hello World
That is all for now, we have created our own image based on NGINX and run our container successfully. Stay tune and as usual if you have any questions, don’t hesitate to get in touch.