Docker Images vs Docker Containers

There are two very common terms you will hear when working with Docker:

Images, and Containers.

The confusing part is that they are very similar concepts, but they are different.

One way of thinking about Docker Images vs Containers is to think about blueprints, and the outcome of constructing something from that blueprint.

This is a typical metaphor given in programming examples, where a class is considered the blueprint (or Docker Image), and the instanceof that class would be the object we work with (or Docker Container).

What Is A Docker Image?

Docker images are the output of building a Dockerfile.

I think of Dockerfiles as being the code that represents the system that we want to build. In some ways they are similar to Ansible playbooks.

We haven't yet seen a Dockerfile, so for reference, here is what one might look like:

FROM nginx:stable

COPY ./conf.d/upstream.conf /etc/nginx/conf.d/

VOLUME /etc/nginx/conf.d/
VOLUME /var/log/nginx/

This is the Dockerfile I used as the basis to create the Docker Image for the nginx service we saw in the docker-compose.yml file used in the first video in this series.

We will cover this process is more depth in a few videos time.

When we run a docker build command against this Dockerfile, a Docker Image will be created. This Docker Image is made up of a series of layers where each layer represents a single instruction from the Dockerfile.

In this case our Docker image would consist of at least 4 layers.

Each layer is one single change in isolation.

You may wonder what is being changed? The answer is simple: the preceding layer.

It's a little like working on a project that uses git. We typically do small chunks of work, staging our changes and then committing those changes to the project's repository. In this way our projects are built up by many isolated changes that each build upon their previous commits, and together form the current state of the project.

Because Docker views each layer as an isolated change we get the benefit of caching. This is particularly useful when building larger Dockerfiles, as making changes to the Dockerfile and then running a docker build means that only the changed layers (and anything after that change) needs to be rebuilt. This can be a particularly helpful time saver on large images such as the one we will build for our base PHP7 image.

The resulting Docker image file in itself does very little. To make it useful we must run the image, which produces a Docker Container.

Going back to our earlier analogy, if our Dockerfile is our service's code, then the Docker Image is the compiled version of our code. It's similar to a Windows .exe file in that it's ready to use, but we haven't started using it just yet.

What Is A Docker Container?

A Docker Container is a Docker Image with one extra layer.

This extra layer is called "the container layer", and it is writable. In other words when we run a Docker Container, we can write new files to the container, modify and delete existing files, and this container remembers these changes.

However - and this is the big gotcha - these changes only affect that Docker Container.

They do not affect the Docker Image from which the container was created. And they do not affect other Docker Containers created from the same Image.

Again if we revert to the class / instanceof analogy then we can do something like:

$one = new Thing();
$two = new Thing();


echo $one->getName(); // outputs: 'bob'

echo $two->getName(); // does not output 'bob'

If you need to keep these changes around (and you often do) then you will need a Docker Volume. We will get to Docker Volumes shortly.

The really nice thing about Docker Containers is that they are very lightweight. Unlike a typical Virtual Machine (e.g. one created using Virtual Box) there is no need for a full operating system, only the essential libraries and settings and nothing more.

What this means is that a Docker Container can start up in just a few seconds, whereas a typical Virtual Machine may take tens of seconds to several minutes to boot.

This speed boost is nice during development, having just one command get your entire dev stack up and running certainly feels productive.

Where this speed boost truly shines in my experience is in the test environment. Particularly within your Continuous Integration pipeline. We will cover this in much greater detail later in this series.

Docker Image Example

We're going to start off by running a docker build command against a brand new Dockerfile of our own creation.

Start by making a new directory and changing directory into that new directory:

mkdir /tmp/docker-nginx-test && cd $_

Then create your Dockerfile (feel free to use a different text editor):

vim Dockerfile

Into the Dockerfile, add the following:

FROM nginx:stable

Save and exit, which on vim means: esc, then :wq.

Now, this Dockerfile isn't super useful. When we do a docker build against this file, Docker is going to take the official nginx image and ... build it :) We might as well have just used the nginx image directly in this case. But worry not, we will expand on this momentarily.

docker build /tmp/docker-nginx-test

If everything goes to plan then you should see something like:

$ docker build /tmp/docker-nginx-test

Sending build context to Docker daemon  2.048kB
Step 1/1 : FROM nginx:stable
 ---> 0346349a1a64
Successfully built 0346349a1a64

The docker build command at the bare minimum needs one argument: the path.

In this case our path is the full path to our directory: /tmp/docker-nginx-test

However, as we learned in the previous video, we can refer to the current directory with a single period: .

Therefore it is far more common to see this command written as:

docker build .

Unusual yes, but not altogether odd when you know the convention.

One of the most confusing things I found when first working with Docker is that finding the contents of the underlying Docker image is often not easy.

To illustrate this point let's take a look at our Dockerfile once more:

FROM nginx:stable

All Dockerfiles must start with a FROM instruction.

This FROM line sets the base image from which all other instructions / lines will be applied.

This base image can be any valid image, including ones we create. We will explore this concept later when we start to build our own nginx and php images.

You can read more about FROM here.

However, what is this image? How was it built? What commands make up the nginx images layers?

The way to find this information is to look on Docker Hub. The Docker Hub entry for nginx:stable at the time of recording is 1.12.1, also tagged stable, and has this Dockerfile.

This is good if you are the trusting sort. But how do you validate this?

By looking at the image's history:

$ docker image history 0346349a1a64
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
0346349a1a64        5 months ago        /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem...   0B                  
<missing>           5 months ago        /bin/sh -c #(nop)  EXPOSE 443/tcp 80/tcp        0B                  
<missing>           5 months ago        /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   22B                 
<missing>           5 months ago        /bin/sh -c apt-key adv --keyserver hkp://p...   58.2MB              
<missing>           5 months ago        /bin/sh -c #(nop)  ENV NGINX_VERSION=1.10....   0B                  
<missing>           5 months ago        /bin/sh -c #(nop)  MAINTAINER NGINX Docker...   0B                  
<missing>           5 months ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           5 months ago        /bin/sh -c #(nop) ADD file:4eedf861fb567ff...   123MB

Why the command:

docker image history 0346349a1a64

Well, docker image history is the base command. You can run docker image to see a bunch of other choices:

$ docker image

Usage:  docker image COMMAND

Manage images

      --help   Print usage

  build       Build an image from a Dockerfile
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Display detailed information on one or more images
  load        Load an image from a tar archive or STDIN
  ls          List images
  prune       Remove unused images
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rm          Remove one or more images
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE

Run 'docker image COMMAND --help' for more information on a command.

But why the long hash: 0346349a1a64?

Because that was the output of our earlier run of docker build .:

$ docker build .

Sending build context to Docker daemon  2.048kB
Step 1/1 : FROM nginx:stable
 ---> 0346349a1a64

# this bit here
Successfully built 0346349a1a64

0346349a1a64 is the layer hash.

From running the docker image history 0346349a1a64 command we can see all the <missing> entries relate to the build that happened for our base image: nginx:stable. We can compare this output to that found in nginx:stable's Dockerfile.

They seem to match :)

It's useful to look at the base image to figure out what it's doing for you. In this case the nginx:stable image has a particularly interesting line:


Here the container's port 80 is being exposed. This is great as http communicates over port 80 by default. However, this does not immediately allow public access to port 80 by default. To do this you must explicitly publish port 80, as we will see shortly. This is confusing and unintuitive.

What if we wanted https?

We could augment our own Dockerfile:

FROM nginx:stable


And by building our image again we could ensure port 443 / https would be listening:

$ docker build .

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx:stable
stable: Pulling from library/nginx
94ed0c431eb5: Pull complete 
b616fca08db5: Pull complete 
a5481c3400ae: Pull complete 
Digest: sha256:ff5f07fdf2dd003adb3589c8f9e2c84d54599bd13a307f385e23d3f02297c887
Status: Downloaded newer image for nginx:stable
 ---> 7f1c878a4833
Step 2/2 : EXPOSE 443
 ---> Running in a90e34fa844d
 ---> dfe0dd23e4af
Removing intermediate container a90e34fa844d
Successfully built dfe0dd23e4af

To confirm, we can view the docker history / docker image history output:

$ docker history dfe0dd23e4af

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
dfe0dd23e4af        19 seconds ago      /bin/sh -c #(nop)  EXPOSE 443/tcp               0B                  
7f1c878a4833        4 weeks ago         /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem...   0B                  
<missing>           4 weeks ago         /bin/sh -c #(nop)  STOPSIGNAL [SIGTERM]         0B                  
<missing>           4 weeks ago         /bin/sh -c #(nop)  EXPOSE 80/tcp                0B                  
<missing>           4 weeks ago         /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   22B                 
<missing>           4 weeks ago         /bin/sh -c apt-get update  && apt-get inst...   52.2MB              
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV NJS_VERSION=1.12.1....   0B                  
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV NGINX_VERSION=1.12....   0B                  
<missing>           4 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker...   0B                  
<missing>           4 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B                  
<missing>           4 weeks ago         /bin/sh -c #(nop) ADD file:fa8dd9a679f473a...   55.3MB  

The truly peculiar thing there is that the base image does appear to EXPOSE 443 already, but it's not there in the Dockerfile. If you know why this is, please do let me know as I struggled to find an answer on this.

That said, I personally prefer to explicitly specify the open ports in my own Dockerfile, so for me I would do:

FROM nginx:stable


Either way this leaves us with two images on our system:

  • The nginx:stable image
  • Our customised variant

We can see this Docker images list:

$ docker images

REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
<none>              <none>              dfe0dd23e4af        About a minute ago   107MB
nginx               stable              7f1c878a4833        4 weeks ago          107MB

There's just no way you could get a working version of nginx on a Virtual Machine in 107mb.

As ever, there are multiple commands that do the same thing. Another command to achieve a list of Docker images would be:

docker image ls

Note here image, not images. Nothing quite like a bit of confusion :/

Running a Docker Container from a Docker Image

Now that we have built ourselves a Docker Image, let's fire up our first Docker Container from that image:

docker run --rm dfe0dd23e4af

Change the value of dfe0dd23e4af to whatever your IMAGE ID value is from the docker images output.

Notice that in doing this your terminal window has been taken over by Docker.

To exit, press ctrl+c.

By using the flag --rm we ensure that the running container is deleted / removed when the running container process is stopped. This is useful as Docker has a real tenancy to leave its leftovers all over your disk. It's easy to forget this fact and find your system out of disk space.

By itself, docker run --rm dfe0dd23e4af is fairly useless.

To make things more interesting it would be helpful to make port 80 publicly accessible:

docker run --rm -p 8080:80 dfe0dd23e4af

Here I am passing in the -p 8080:80 flag to map port 8080 on my local machine to port 80 in the running Docker container.

This is particularly useful if you have multiple projects running on your machine at any one time. I typically use 801, 802, 803, etc for my projects. Again, more on this as we progress through the course.

With the container running, we should now be able to hit our nginx webserver. Run the command again and browse to:, and in your terminal you should see something like the following:

$ docker run --rm -p 8080:80 dfe0dd23e4af - - [26/Aug/2017:11:03:22 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36" "-"

Each time you refresh the page in your browser a new log entry line should appear. Fairly neat.

Ok, time to wrap up our Docker container example:

Remember, to exit, press ctrl+c.

As we ran the command with --rm our container should have been removed. Confirm this with:

docker ps -a

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

You should see no entries.

We still have the image left over. Let's delete it:

docker rmi dfe0dd23e4af

Deleted: sha256:dfe0dd23e4afe3d8873a16ccc7f1206e4e02e23452cf861492bb066e92e71aed

You may also want to clear up the nginx base image:

$ docker images

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               stable              7f1c878a4833        4 weeks ago         107MB

$ docker rmi nginx:stable

Untagged: nginx:stable
Untagged: nginx@sha256:ff5f07fdf2dd003adb3589c8f9e2c84d54599bd13a307f385e23d3f02297c887
Deleted: sha256:7f1c878a4833621e106cedffbe3b9d88e4a7ee2673577b92f74417c72a818c28
Deleted: sha256:0627a452e15be7d0ceeabe719083487fa5eeca6544776d39f05feffdf44955e0
Deleted: sha256:938f33ad92af39e79310318b22e7b6e0dd30a80c672db37c178732c8503e1937
Deleted: sha256:eb78099fbf7fdc70c65f286f4edc6659fcda510b3d1cfe1caa6452cc671427bf

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

Ok, that wraps up our quick tour of Docker Images and Docker Containers.

These are two of the essential building blocks for working with Docker.

The next big one is Volumes. Let's get to it.

Code For This Course

Get the code for this course.