Images are blueprints for containers. An image itself is often defined by no more than a single file, usually some form of a Dockerfile. This file gives the image instructions on what it needs to be run. Let’s put together our own image.
On your host machine create a Dockerfile in ~/projects/docksal-training-docker
$ cd ~
$ mkdir projects/docksal-training-docker
$ cd docksal-training-docker
$ touch Dockerfile
Open up that new file in your favorite text editor and add the following:
FROM ubuntu
RUN echo "Hello, world!"
This file is using another image ubuntu
and building off of that to create its own image. The only thing this image will do is create a container using Docksal’s CLI image and then echo the words Hello, world!
in the build output. Nothing too fancy, but there is a lot going on here.
Let’s build this simple image and see what happens:
$ docker build --tag "image-example:1.0.0" "$(pwd)"
Sending build context to Docker daemon 50.44MB
Step 1/2 : FROM ubuntu
---> bb4c72e4b656
Step 2/2 : RUN echo "Hello, world!"
---> Using cache
---> d2c00859cae2
Successfully built d2c00859cae2
Successfully tagged image-example:1.0.0
NOTE: In the previous command the "$(pwd)"
is a bash scripting method that takes the output of the pwd
command and returns it as a string to print. This creates a reference to the current folder.
Doesn’t seem like a lot, but let’s break down the command and output.
$ docker build --tag "image-example:1.0.0" "$(pwd)"
This tells Docker we’re building an image with a tag of image-example:1.0.0
and that we want to use the Dockerfile in the current folder. Notice that we don’t need to include Dockerfile
in the command.
Sending build context to Docker daemon 50.44MB
Remember how we talked about the Docker client primarily communicating with the Docker daemon? That is what this line is representing.
Step 1/2 : FROM ubuntu
---> bb4c72e4b656
Step 2/2 : RUN echo "Hello, world!"
---> d2c00859cae2
Each step in the Dockerfile creates a new layer of the image with a snapshot of each instruction saved. If for some reason the build doesn’t work, then you can debug using the hash generated by each step. The FROM
is pulling the parent image, in this case ubuntu
, into the current image. While RUN
is running a command on that image and saving the output in the next layer.
Successfully built d2c00859cae2
Successfully tagged image-example:1.0.0
This output gives us a unique identifier of our image and a human-readable name so that we can work with it a bit easier. Now this image is built and ready to be used to create a container.
If this were your first time building this image, you would see a lot of status indicators that look like this:
177e7ef0df69: Already exists
9bf89f2eda24: Already exists
350207dcf1b7: Already exists
a8a33d96b4e7: Already exists
82350ee8f11f: Pulling fs layer
2d9047762251: Pulling fs layer
196d943fac59: Pulling fs layer
ff00d78cbcf3: Pulling fs layer
8b971b61b7b6: Pulling fs layer
337d6d904976: Downloading [===========> ] 7.646MB/12.49MB
20c027cb1a77: Waiting
ba27c2e2de1c: Waiting
What this is doing is following the instructions provided by the Dockerfile to build an image. From our two-line Dockerfile, we’re pulling instructions in from another Dockerfile and from another until we reach the base file, most likely ending up at scratch, a base image provided on Docker Hub created with the sole purpose of having a starting point for completely custom images.
Docker is smart enough to use caching to prevent having to re-download images every time a container is spun up, which is one of the reasons it’s so quick to load.
So now we have an image, but it’s not doing us a lot of good. So, what’s next?
You can view the completed code for this section at https://github.com/JDDoesDev/docksal-training-docker/tree/images