Create a Docker Image, test, update and version control it, then publish it to Docker hub

This lab will setup a working environment with some website code in a source directory, then we’ll create a Docker container from scratch that runs an Nginx web server hosting the website code. We’ll simulate making a revision to the website code and go through tagging images. Finally, we’ll publish the tagged images to Docker Hub and then download and run them.

Requirements

  • A Linux or Windows workstation where you can work with Docker daemon - Docker Getting Started
  • A free or paid Docker Hub account - Docker Hub
  • Some web code to run, a simpe ‘Hello World’ upwards is good, see link below

Working directory

On your workstation, ensure you have Docker installed, and it’s up to date; this was written using v20.10.23. Set up a working directory to create files and work with Docker data. I use GitHub for these projects as I can save them for later.

My working directory is called lab-web, and it’s a folder from my GitHub repository checked out to my workstation.

Web code

Before creating the Docker image, you need to organize your web code into your working directory. In lab-web create a new sub-directory called src. Put your web code into the src directory.

This src directory will be referenced later in the Docker image as the source for a copy/paste operation that puts the contents into the web server, which will then be published. If you were to run this image with future updates, you’d update the contents of the src folder with your revised web code and build a new Docker Image with a new version tagged.

The filesystem will look like this

.lab-web
  |__src
  |  |__images
  |     |docker.svg
  |  |index.html
  |  |script.js
  |  |style.css
  |Dockerfile

Create the Docker file

In the root of the working directory, create a new file named Dockerfile. It’s essential to capitalize the D and not add an extension.

This file is the instructions Docker will use to create the image. You can copy/paste in the example below. You can make your variation of the file, but if you copy/paste what is below, you’ll create a web server running NGINX with the contents of the src folder as the website and accessible on HTTP. There are many more options to configure if there were to be a production environment, but as a hands-on lab to get the grips with the fundamental concepts of Docker, it’s perfectly viable. You are responsible for your environment, so use it at your own risk.

Example Dockerfile

# Use an official Nginx image as a parent image
FROM nginx:latest

# A LABEL is a key-value pair it defines the maintainer of the image
LABEL maintainer="someone@emaildomain"

# Set the working directory in the container to /usr/share/nginx/html
WORKDIR /usr/share/nginx/html

# Copy the specified directory contents into the container at /usr/share/nginx/html
COPY ./src .

# Make port 80 available to the world outside this container
EXPOSE 80

# Run Nginx when the container launches
CMD ["nginx", "-g", "daemon off;"]

FROM nginx:latest: indicates the base image. In this case, it’s using the latest version of the official Nginx image from Docker Hub.

LABEL maintainer="{someone@emaildomain}": The LABEL instruction adds metadata to an image. A LABEL is a key-value pair. Here, it defines the maintainer of the image, which is an email address in this case.

WORKDIR /usr/share/nginx/html: sets the working directory for any instructions that follow in the Dockerfile. It’s the directory where Nginx expects to find the files to serve on the web.

COPY ./src .: copies files or directories from whichhere is ./src and adds them to the filesystem of the image at the path which here is .. Here, it’s copying everything from the src directory (relative to the Dockerfile on your host machine) to the current directory in the Docker image (which is /usr/share/nginx/html due to the WORKDIR command).

EXPOSE 80: informs Docker that the container listens on the specified network port at runtime. Here, it’s port 80 and we’ll use 8080 as the listener on the Docker host network interface (port mapping 8080 to 80).

CMD ["nginx", "-g", "daemon off;"]: The CMD instruction provides defaults for an executing container. Here, it’s starting the Nginx server. The -g option sets global directives, and daemon off; means run Nginx in the foreground, which is necessary for Docker.

Create the Docker image

Now we have two key data points set, the website code in the src directory and a Dockerfile configured with the parts to host the website on Nginx; we can create the Docker image.

The following command will create a Docker image based on what is defined in the Dockerfile file. In effect, it will create a temporary container, execute the commands, and save the files to a template file we call the Docker image. The -t parameter allows us to tag the image in this example as latest. The trailing period is essential as it tells Docker to use the current directory, so run this command in the root of the working directory lab-web.

docker build -t lab-web:latest .

The command will execute, show some messages, then complete. But where is the Docker image? It’s not in the working directory. The Docker process locates the image file in the local Docker registry. If you run the following command, listing all the Docker images on your local system, you will see a new one called:

$ docker image ls
REPOSITORY             TAG       IMAGE ID       CREATED              SIZE
lab-web                latest    c1f84404810b   About a minute ago   142MB

Test the image

Now we’ll run a rudimentary test by simply opening the website and checking it manually. In a more complex environment you will automate testing and have unit tests and Cyrpress tests and other such tools to do this at scale, but for now this is fine.

This command will now run the image as a container. The -d tells Docker to run the container as a daemon so you get your shell back; otherwise, the shell will be locked while the container runs. The -p defines the port mapping so we can access a port on the Docker host and access the container port, which in this example is 8080 mapping to 80 (remember in the Dockerfile we told Nginx to listen on 80). Lastly, we run the Docker image and tag combination we created earlier.

docker run -d -p 8080:80 lab-web:latest

Now open a broswer and go to http://{dockerhost}:8080 where {dockerhost} is the IP of your workstation. This will open the website.

Alt Text
The example website

Update the image with a change

Remember, a Docker image is immutable. Meaning, once the image is created or running as a container you don’t change it. You create a new version of the image and then deploy it. This is one reason why using tags is so important.

Perhaps we want to change the website code. The random number should be placed beneath the Random number title like the hostname and current time. First, you make the change to the code in the src folder in your working directory and test it locally. Once happy, you then need to go through a process to ensure you maintain transparency about which image is what version.

First, you test the new image in Docker. So create a new Docker image with the updated web site code tagged as staging. Notice the additon of --no-cache. Docker uses caching during the build process, and if there are no changes to a specific build step or layer, Docker will use the cached results. In the case of changing a value in a HTML file, which is typically part of the build context, Docker will not be aware of the specific change in the file unless you explicitly instruct it to rebuild that layer.

To ensure that Docker picks up the updated HTML file and rebuilds the relevant layer, you can use the --no-cache flag with the docker build command. This option disables caching and forces Docker to rebuild all the layers from scratch, including the one containing the updated HTML file.

docker build --no-cache -t lab-web:staging .

You now have two images in your local registry.

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
lab-web      staging   e362543d6ad3   2 minutes ago    142MB
lab-web      latest    7b34175cd9d1   57 minutes ago   142MB

Start the new image as a container and test it works okay. You can change the Docker host port so it does not clash with the latest container you may already be running. This example changes the port to 8081.

docker run -d -p 8081:80 lab-web:staging

Now open a broswer and go to http://{dockerhost}:8081 where {dockerhost} is the IP of your workstation. This will open the revised staging website.

Alt Text
The revised example website

Now we have two Docker images but the one tagged as latest is out of date and you want to update your environment to the new revised web code. The standard process for promoting a Docker image from a staging version to the latest version typically involves re-tagging the current latest image with a version number and then re-tagging the staging image as the new latest. Here’s a step-by-step process:

List your Docker images to identify the current latest and stage versions:

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
lab-web      staging   e362543d6ad3   2 minutes ago    142MB
lab-web      latest    7b34175cd9d1   57 minutes ago   142MB

Determine the IMAGE ID of the current latest image and tag it with a version number:

$ docker tag {IMAGE ID} lab-web:{VER NUMBER}

Verify that the re-tagging was successful by listing your Docker images again:

docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED             SIZE
lab-web      staging   e362543d6ad3   8 minutes ago       142MB
lab-web      0.1.0     7b34175cd9d1   About an hour ago   142MB
lab-web      latest    7b34175cd9d1   About an hour ago   142MB

Determine the IMAGE ID of the current staging image and tag it with as latest:

$ docker tag {IMAGE ID} lab-web:latest

Verify that the re-tagging was successful by listing your Docker images again:

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED             SIZE
lab-web      latest    e362543d6ad3   10 minutes ago      142MB
lab-web      staging   e362543d6ad3   10 minutes ago      142MB
lab-web      0.1.0     7b34175cd9d1   About an hour ago   142MB

After completing these steps, you will have a duplicate of staging tagged as the latest, and the previous latest version will be re-tagged with a version number. The staging image will still exist and you would follow the same process to create a new staging image when required, then promote that new staging image to become the new latest, and so on.

Publish the Docker image to a registry (Docker Hub)

At this stage, we have some Docker images in a local Docker registry that are tested and working. But as it’s bad practice to version control built images in GitHub and our workstation is not durable, we need somewhere safe to keep our images plus, nobody else can use our excellent image from their Docker environment as they’d first need the Dockerfile, and then would have to build their version or better still they can pull our pre-built image from a Docker registry they do have access to.

In real life, you typically develop your Docker image locally, then publish the generated image to a registry for your enterprise teams to pull the image onto the enterprise Docker environment to run. You would then store your Dockerfile and application code in a VCS such as GitHub. There is of course, a lot more to it, but the point here is to publish a Docker image to a 3rd party registry and then run it as a container using the Docker pull command.

Docker Hub is the default choice but you can use private registries as well. The example below uses Docker Hub for simplicity.

First login to Docker Hub from the Terminal on your workstation by running the following command and completing the workflow. When completed your Docker environment will have access to Docker Hub as whomever you logged in as which grants you permissions to publish images there.

docker login

The image must have a tag that works in a public registry like Docker Hub. The image of lab-web:latest won’t work as it would try to put the image into a repository called lab-web. So you need to prefix your image with the target repository which matches your account profile. My account profile is grinntec:

docker image tag lab-web:latest grinntec/lab-web:latest

Now check the images in your local repository and you have a new one named after the tag you just created.

docker image ls
REPOSITORY         TAG       IMAGE ID       CREATED             SIZE
grinntec/lab-web   latest    e362543d6ad3   32 minutes ago      142MB
lab-web            latest    e362543d6ad3   32 minutes ago      142MB
lab-web            staging   e362543d6ad3   32 minutes ago      142MB
lab-web            0.1.0     7b34175cd9d1   About an hour ago   142MB

Now push the newly tagged image to the Docker Hub repository

docker image push grinntec/lab-web:latest

You can repeat these same steps for the images with staging and a version number so you end up with a current latest plus the staging and previous image you can roll back to as needed with a version number. This can easily be checked on the Docker Hub website as per below.

Alt Text
The three images as viewed on Docker Hub

Pull the image and run it

Now your images are published to a registry that is accesible to others (Docker Hub). If someone wants to run the latest version of your excellent website they can simply run the command below from their Docker environment which will assume you mean Docker Hub as the source, then check the local registry for the same image and if not present download a copy, then exexcute it. All that from one command which can easily be put into an IaC or Ansible process to setup your application environment!

docker run -d -p 8080:80 grinntec/lab-web:latest
Last modified July 21, 2024: update (e2ae86c)