This article shows how to customize Docker images using a description file named
Dockerfile. You’ll see how to extend existing images, customizing them to your needs, and also how to publish the resulting image to Docker Hub.
In this tutorial you will learn:
- How to customize an image with a Dockerfile.
- How to publish the resulting image in Docker Hub.
Software Requirements and Conventions Used
|Category||Requirements, Conventions or Software Version Used|
|System||Ubuntu 18.04 Bionic Beaver|
|Other||Privileged access to your Linux system as root or via the
# – requires given linux commands to be executed with root privileges either directly as a root user or by use of
$ – requires given linux commands to be executed as a regular non-privileged user
The previous articles presented Docker concepts and some basic Docker commands. In this article, you’ll see how to customize and extend an existing Docker image, describing the modifications in a Dockerfile, and publishing the image to a registry.
In the previous article, you’ve made modifications to a running container and committed the changes to the local image cache. Although it’s a helpful resource for specific situations, it’s recommended that customizations are made in a more documented way, so that the image can be deployed to other hosts. The recommended way is to write a Dockerfile.
The Dockerfile is a YAML file, which is a text file with some syntax: relations are expressed using indentation (spaces) and each line is comprised of key and value pairs.
Let’s start with a simple Dockerfile that installs package
props (contains commands
ps) to a Debian image.
Create a new directory, get into it, and save the file below with the name
Dockerfile (capital D):
FROM debian RUN apt-get update &&\ apt-get -y install procps
This Dockerfile states that the base image is named Debian (
FROM clause). If it doesn’t exist locally, it will be downloaded from the Docker Hub. The
RUN command executes
apt-get twice. Notice the use of a backslash (\) to break a line and the use of
-y to skip the confirmation prompt of
Next step is to build the image with
$ docker build -t mydebian . Sending build context to Docker daemon 2.048kB Step 1/2 : FROM debian ---> be2868bebaba Step 2/2 : RUN apt-get update && apt-get -y install procps ---> Running in 52a16b346afc … Removing intermediate container 52a16b346afc ---> f21a05a59966 Successfully built f21a05a59966 Successfully tagged mydebian:latest
-t mydebian is naming the new image. The dot (.) tells docker to use the current directory to look for a Dockerfile. Notice that new layers are created and removed as the lines of the Dockerfile are interpreted.
There must be a new image in the local cache.
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE mydebian latest f21a05a59966 8 minutes ago 119MB debian latest be2868bebaba 7 weeks ago 101MB
A container from this image can be created.
$ docker run -it --name mydebian_container mydebian root@ef9eb174874a:/# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 02:43 pts/0 00:00:00 bash root 9 1 0 02:43 pts/0 00:00:00 ps -ef
From now on you can create containers running Debian with the
procps package, and the commands
ps will be already installed.
Now let’s create a Dockerfile to have Apache and PHP installed at image build time, to achieve the same objectives of the previous article, when the commands were executed inside the container.
FROM debian RUN apt-get update &&\ apt-get -y install procps libapache2-mod-php CMD service apache2 start
libapache2-mod-php in Line 3 and a
CMD command in Line 4 to start Apache. When the container is started, the
CMD command is executed. There can exist only one
CMD command per Dockerfile. When the
CMD command is specified, it replaces the
CMD command of the image you are extending. If the
CMD command is omitted, the one of the base image will be executed (if any). As you may have guessed, the Dockerfile of the Debian base image has a
CMD command to execute bash. You may check this in the Docker Hub.
$ docker run -d --name mydebian_container2 -d -p 8000:80 -v "$PWD":/var/www/html mydebian ad325685b738464c49bff40b65c6824160105ab5c285282efefbc4ddeec20ba2 roger@slash:~/LinuxConfig/04 Dockerfile$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ad325685b738 mydebian "/bin/sh -c 'service…" 11 seconds ago Up 5 seconds 0.0.0.0:8000->80/tcp mydebian_container2
This time we started the container using the
-d switch because we want it to be detached from the terminal.
Important Dockerfile Commands
The Dockerfile has other commands beyond
ENV is used to set environment variables in the image, like
http_proxy, for example. Many images use environment variables to pass parameters to the new container. For examples, check the images of databases like MySQL and PostgreSQL in docker hub.
COPY copies files and directories from the host to the image at build time. The source path (first argument) is relative to the current directory.
ADD is similar to
COPY, with the difference that, if the source is a compressed tar file, it will be automatically decompressed in the destination directory inside the image. Except for that use, Docker recommends the use of the
COPY command whenever possible.
EXPOSE indicates which ports of the image can be exposed by Docker. During container creation, those ports can be mapped to host ports, if desired.
WORKDIR sets the directory that Docker will use when commands are executed inside the container with
Creating an Image With HTTPS Enabled
Now we’ll extend the official PHP Apache image to activate SSL with an auto-generated certificate to exemplyfy how to use the mentioned commands. In a new directory create the following Dockerfile.
FROM php:7-apache RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/ssl-cert-snakeoil.key -out /etc/ssl/certs/ssl-cert-snakeoil.pem -subj "/C=BR/ST=Rio Grande do Sul/L=Porto Alegre/O=Security/OU=Development/CN=example.com" RUN a2enmod rewrite RUN a2ensite default-ssl RUN a2enmod ssl EXPOSE 443 COPY ./html /var/www/html WORKDIR /var/www/html
In Line 3 we create a certificate. Lines 5 – 7 enable mod_rewrite and SSL. Line 9 exposes port 443 (port 80 is already exposed by the upstream image). Line 11 adds the application directory to the container. Finally, Line 13 sets the working directory as the Apache working directory. All commands executed by
docker exec will use this directory as the base by default.
Now, create a directory named
html and a file named
phpinfo.php with this content.
<?php phpinfo(); ?>
Let’s now build and run the container.
docker build -t app_image . docker run -d --rm -p 80:80 -p 443:443 --name app_container app_image
Now, you can access
phpinfo.php script through both, HTTP and HTTPS.
In HTTPS the browser will complain about the certificate’s security since this is self-signed, but the warning can be ignored.
Publishing Images To The Docker Hub
The images created exist only locally, in Docker’s local cache. You may want to share them with other Docker hosts, or with teammates, or even make them public to the world. In any case, you want to publish your images to a Docker registry. They can be published to a cloud-based registry, like the Docker Hub which, by the way, is the default if you don’t explicitly specify the registry. First create a free Docker ID, then login:
$ docker login Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username: infroger Password: Login Succeeded
Next, tag the image with the repository name (infroger), image name and tag (image version).
$ docker tag app_image infroger/app_image:1 $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE infroger/app_image 1 c093151fc68f 14 hours ago 381MB app3_image latest c093151fc68f 14 hours ago 381MB
Then push the image to the repository.
$ docker push infroger/app_image:1 The push refers to repository [docker.io/infroger/app_image] 27f7f2b01c49: Pushed 81b08cd5fe07: Pushed d1c23d198f84: Pushed e66392ad9b85: Pushed a71f63e3a00f: Pushed 9c58778f21dd: Pushed 973719bed9b7: Pushed 8f5090ef2ac0: Pushed fbdafdbe3319: Pushed a5c4801ecf39: Pushed e9ba112d38b9: Pushed 25ba5230dadf: Pushed f2907ce42b47: Pushed e31bf34cfab9: Pushed 9066d03e98e0: Pushed 96db4ce698ad: Pushed abae6a338e5c: Pushed 4572a80a7a5e: Pushed ef68f6734aa4: Pushed 1: digest: sha256:2e7e53fcdf800ad0c4837cd70014170cc869d36de5c301f2e2ced318803bf963 size: 4279
Now go to Docker Hub and check the image is there:
In Docker Hub with free registration, you can have one private repository, with unlimited public repositories. Otherwise, you may want to run your own Docker registry, which can be done with one command:
docker run -d -p 5000:5000 --restart=always --name registry registry:2
The advantage of having a private registry is privacy. But you have the burden to manage security, high availability, storage requirements, access control, etc.
In this article, we’ve covered how to extend existing images and customize them to your needs using a Dockerfile. We’ve also seen how to publish the images to a Docker registry. You can do a lot so far, but we are just scratching the Docker world. In the next article, we’ll see a very simple form of local container orchestration with Docker Compose.