Skip to main content

Creating and Managing Images

8.1. Now I want to create my own image, can I?​

Of course you can!

And what's more, we'll learn in two simple and intuitive ways.

One of the most interesting things about Docker is the ability to use images created by other people around the world through a registry like Docker Hub. This greatly speeds up your life, especially when you just need to test a particular technology. The POC (Proof of Concept) becomes much more agile, allowing you to test multiple tools in the same time it would take to test just one without Docker.

However, at certain times we need to create our own image from scratch, or modify an image created by third parties and save these changes to a new image.

Now we'll see both cases: how to build a distribution practically from scratch using only instructions through a dockerfile and another performing modifications on an existing image and saving it to a new image.

8.2. Let's start from the beginning then, dockerfile​

We'll build our first image using a dockerfile as our creation roadmap. You'll see how simple it is to create a complete and practical dockerfile. :)

To start, let's create a directory called "/root/Dockerfiles".

# mkdir /root/Dockerfiles

Now we'll start creating our dockerfile, our image creation map. To organize it better, we'll create a directory called "apache", where we'll store this first example:

# cd /root/Dockerfiles/
# mkdir apache

For now, we'll just create a file called "Dockerfile" and add the content as shown in the following example:

# cd apache
# vim Dockerfile
FROM debian

RUN apt-get update && apt-get install -y apache2 && apt-get clean

ENV APACHE_LOCK_DIR="/var/lock"

ENV APACHE_PID_FILE="/var/run/apache2.pid"

ENV APACHE_RUN_USER="www-data"

ENV APACHE_RUN_GROUP="www-data"

ENV APACHE_LOG_DIR="/var/log/apache2"

LABEL description="Webserver"

VOLUME /var/www/html/

EXPOSE 80

Very good! Now that you've added the information as shown in the example, let's understand each section used in this first dockerfile:

  • FROM -- Indicates the image to serve as the base.

  • RUN -- List of commands you want to execute when creating the image.

  • ENV -- Defines environment variables.

  • LABEL -- Adds metadata to the image, such as description, version, etc.

  • VOLUME -- Defines a volume to be mounted in the container.

After creating the file, we'll build (construct our image) as follows:

# docker build .

Remember: you must be in the directory where your dockerfile is located.

All the steps we defined in our dockerfile will be executed, such as installing the requested packages and all other tasks.

Successfully built 53de2cee9e71

Very well! As we can see in the last line of the "docker build" output, the image was successfully created! :D

Let's run "docker image ls" to see if everything is right with our first image!

root@linuxtips:~/Dockerfile/apache# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 53de2cee9e71 2 minutes ago 193.4 MB

Our image was created! However, we have a problem. :/

The image was created and is fully functional, but when we built it, we didn't pass the "-t" parameter, which is responsible for adding a tag ("name:version") to the image.

Let's run the build again, but this time passing the '-t' parameter, as shown in the following example:

# docker build -t linuxtips/apache:1.0 .

Now let's see if the image was really created, adding a name and version to it:

root@linuxtips:~/Dockerfile/apache# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
linuxtips/apache 1.0 53de2cee9e71 5 minutes ago 193.4 MB

Wonderful! It worked as expected!

Let's run a container using our image as the base:

# docker container run -ti linuxtips/apache:1.0

Now we're inside the container. Let's check if Apache2 is running. If it's not yet, we'll start it and verify if port 80 is "LISTEN".

root@70dd36fe2d3b:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 1 21:33 ? 00:00:00 /bin/bash
root 6 1 0 21:33 ? 00:00:00 ps -ef

root@70dd36fe2d3b:/# /etc/init.d/apache2 start
[....] Starting Apache httpd web server: apache2AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.4. Set the 'ServerName' directive globally to suppress this message
. ok

root@70dd36fe2d3b:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:33 ? 00:00:00 /bin/bash
root 30 1 0 21:33 ? 00:00:00 /usr/sbin/apache2 -k start
www-data 33 30 0 21:33 ? 00:00:00 /usr/sbin/apache2 -k start
www-data 34 30 0 21:33 ? 00:00:00 /usr/sbin/apache2 -k start
root 109 1 0 21:33 ? 00:00:00 ps -ef

root@70dd36fe2d3b:/# ss -atn
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 :::80 :::*

root@70dd36fe2d3b:/# ip addr show eth0
50: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.4/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:4/64 scope link
valid_lft forever preferred_lft forever

root@70dd36fe2d3b:/#

In the previous code, it's possible to observe the container IP in the "ip addr" output. Let's test communication with the container from the host.

On the host, type:

# curl <CONTAINER IP>

The "curl" returned the Apache2 welcome page, meaning everything is working very well and Apache2 is responding as expected!

Buuuuuuuttt, it's not ideal that I have to enter the container to start my Apache process. Every container should run its process in the foreground and this process should start automatically and not with someone accessing the container and starting the service. We saw that before only as a first example, now let's improve it and leave it as it should be! :D

The first thing is to update our dockerfile as follows:

# vim Dockerfile
FROM debian

RUN apt-get update && apt-get install -y apache2 && apt-get clean

ENV APACHE_LOCK_DIR="/var/lock"

ENV APACHE_PID_FILE="/var/run/apache2/apache2.pid"

ENV APACHE_RUN_USER="www-data"

ENV APACHE_RUN_DIR="/var/run/apache2"

ENV APACHE_RUN_GROUP="www-data"

ENV APACHE_LOG_DIR="/var/log/apache2"

LABEL description="Webserver"

VOLUME /var/www/html/

EXPOSE 80

ENTRYPOINT ["/usr/sbin/apachectl"]

CMD ["-D", "FOREGROUND"]

Notice that we now added two more options: ENTRYPOINT and CMD!

Curious about what they do? Then let's learn much more possible options that can be added to a dockerfile!

8.3. Let's learn a bit more about dockerfile​

Now let's learn a bit more about the options we can use when creating a dockerfile:

  • ADD -- Copies new files, directories, TAR files, or remote files and adds them to the container's filesystem.

  • CMD -- Executes a command. Unlike RUN, which executes the command when "building" the image, CMD will do so only when the container is started.

  • LABEL -- Adds metadata to the image, such as version, description, and manufacturer.

  • COPY -- Copies new files and directories and adds them to the container's filesystem.

  • ENTRYPOINT -- Allows you to configure a container to run an executable. When this executable finishes, the container will also finish.

  • ENV -- Provides environment variables to the container.

  • EXPOSE -- Indicates which port the container will be listening on.

  • FROM -- Indicates which image will be used as the base. It must be the first line of the dockerfile.

  • MAINTAINER -- Author of the image.

  • RUN -- Executes any command in a new layer on top of the image and "commits" the changes. These changes can be used in the next instructions of your dockerfile.

  • USER -- Determines which user will be used in the image. By default it's root.

  • VOLUME -- Allows the creation of a mount point in the container.

  • WORKDIR -- Responsible for changing from the "/" (root) directory to the one specified in it.

A very important detail to mention is that when working with ENTRYPOINT and CMD within the same dockerfile, CMD only accepts ENTRYPOINT parameters, as shown in our previous dockerfile example:

ENTRYPOINT ["/usr/sbin/apachectl"]

CMD ["-D", "FOREGROUND"]

Where:

  • "/usr/sbin/apachectl" -- This is the command.

  • "-D", "FOREGROUND" -- This is the argument, the parameter.

In the shell, for example, the execution would look like this:

# /usr/sbin/apachectl -D FOREGROUND

In other words, you're starting Apache by passing the instruction for it to be started in the foreground, as it should be. :D

For more details on how to create images, see this presentation created by Jeferson: https://www.slideshare.net/jfnredes/images-deep-dive.

8.4. Multi-stage​

An important and recent feature added to dockerfile aims to make life easier for those who intend to create container images effectively. This feature is multi-stage!

Multi-stage is nothing more than the ability to create a kind of pipeline in our dockerfile, even allowing two FROM entries.

This feature is widely used when we want, for example, to compile our application in a container and run it, but we don't want to have that huge amount of packages installed in our containers that are always needed when compiling code in some language, like C, Java, or Golang.

Let's look at an example so we can better understand how this works!

For this, I prepared a super advanced app written in Golang for our test:

# vim goapp.go
package main

import "fmt"

func main() {

fmt.Println("GIROPOPS STRIGUS GIRUS - LINUXTIPS")

}

Did you think it would be something advanced? Impossible, we made it ourselves. :D

Well, now let's create a dockerfile to build our image and thus run our app.

# vim Dockerfile
FROM golang

WORKDIR /app

ADD . /app

RUN go build -o goapp

ENTRYPOINT ./goapp

Done! Now let's perform the build.

# docker build -t goapp:1.0 .

Listing our image:

# docker image ls | grep goapp
goapp 1.0 50451808b384 11 seconds ago 781MB

Now let's run it and see our fantastic app in action:

# docker container run -ti goapp:1.0
GIROPOPS STRIGUS GIRUS -- LINUXTIPS

Done! Our app and our image are working! Success!

However, we can improve many things if we start using our powerful feature, multi-stage!

Let's redo our dockerfile using multi-stage, understand how it works and the difference between the two images.

Let's update our dockerfile like this:

# vim Dockerfile
FROM golang AS buildando

ADD . /src

WORKDIR /src

RUN go build -o goapp

FROM alpine:3.1

WORKDIR /app

COPY --from=buildando /src/goapp /app

ENTRYPOINT ./goapp

Notice that now we have two FROM entries, which wasn't possible before multi-stage. But why is this?

What's happening is that now we have the dockerfile divided into two sections. Each FROM entry defines the beginning of a block, a stage.

So, in our first block we have:

  • FROM golang AS buildando -- We're using the Golang image to create the container image, and here we're nicknaming this block as "buildando".

  • ADD . /src -- Adding our app code inside the container in the "/src" directory.

  • WORKDIR /src -- Defining that the working directory is "/src", meaning when the container starts, we'll be in this directory.

  • RUN go build -o goapp -- We'll run the build of our Golang app.

In the second block we have the following:

  • FROM alpine:3.1 -- Starting the second block and using the Alpine image to create the container image.

  • WORKDIR /app -- Defining that the working directory is "/app", meaning when the container starts, we'll be in this directory.

  • COPY --from=buildando /src/goapp /app -- Here's the magic: we'll copy from the block called "buildando" a file inside "/src/goapp" to the "/app" directory of the container we're working on in this block, meaning we copied the binary that was compiled in the previous block and brought it to this one.

  • ENTRYPOINT ./goapp -- Here we'll run our sensational app. :)

Now that we understand all the lines of our new dockerfile, let's perform the build.

# docker build -t goapp_multistage:1.0 .

Let's run our image to see if everything is working:

# docker container run -ti goapp_multistage:1.0
GIROPOPS STRIGUS GIRUS - LINUXTIPS

Is there a size difference between them? Let's check:

# docker image ls | grep goapp
goapp_multistage 1.0 dfe57485b7f0 22 seconds ago 7.07MB
goapp 1.0 50451808b384 15 minutes ago 781MB

The size difference is huge, because in our first image we need to have a bunch of packages for the build of the Golang app to occur. In our second image, we also used the Golang image and all its packages to build our app, but we discarded the first image and only copied the binary to the second block, where we're using the Alpine image, which is super lean.

In other words, we used the first block to compile our app and the second block only to run it. Simple as that, simple as flying! :D

8.5. Let's customize a base image now?​

Now let's create a new image, but without using the dockerfile. We'll run a container with a base image, make whatever modifications we want, and then save that container as a new image!

Simple, fast and easy!

Well, first we need to create a container. This time we'll use a Debian container, just for variety. :D

root@linuxtips:~# docker container run -ti debian:8 /bin/bash
root@0b7e6f606aae:/#

Now let's make the changes we want. We'll do the same thing we did when we built our first image with the dockerfile, that is, install Apache2. :D

root@0b7e6f606aae:/# apt-get update && apt-get install -y apache2 && apt-get clean

Now that we've installed Apache2, let's exit the container so we can commit our image based on this running container.

Remember that to exit the container and leave it still running, you need to press Ctrl + p + q. ;)

# docker container ls
# docker commit -m "my container" CONTAINERID
# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> fd131aedd43a 4 seconds ago 193.4 MB

Notice that our image has "<none>" in its name and "TAG". To adjust and give a name and version to our image, we'll use the "docker tag" command, as shown below:

# docker tag IMAGEID linuxtips/apache_2:1.0
REPOSITORY TAG IMAGE ID CREATED SIZE
linuxtips/apache_2 1.0 fd131aedd43a 2 minutes ago 193.4 MB

Now yes!!! We have our image created with name and version specified.

Let's start a container using the image we just created:

# docker container run -ti linuxtips/apache_2:1.0 /bin/bash

Let's start Apache2 and test the container communication:

root@57094ec894ce:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:48 ? 00:00:00 /bin/bash
root 6 1 0 21:48 ? 00:00:00 ps -ef

root@57094ec894ce:/# /etc/init.d/apache2 start
[....] Starting web server: apache2AH00558: apache2: Could notreliably determine the server's fully qualified domain name, using 172.17.0.6. Set the 'ServerName' directive globally to suppress this message
. ok

root@70dd36fe2d3b:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:43 ? 00:00:00 /bin/bash
root 30 1 0 21:44 ? 00:00:00 /usr/sbin/apache2 -k start
www-data 33 30 0 21:44 ? 00:00:00 /usr/sbin/apache2 -k start
www-data 34 30 0 21:44 ? 00:00:00 /usr/sbin/apache2 -k start
root 111 1 0 21:44 ? 00:00:00 ps -ef

root@70dd36fe2d3b:/# ss -atn
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 :::80 :::*

root@57094ec894ce:/# ip addr show eth0
54: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:06 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.6/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:6/64 scope link
valid_lft forever preferred_lft forever

Great! Now we have Apache2 running. Let's exit the container and test communication with Apache2 from the host:

# curl <Container IP>

It will return the Apache2 welcome page! Everything working as expected!