Creating a Container Image and How to Push it to a Private Container Registry

This article is related to my post on Installing Kubernetes with Microk9s on a Ubuntu 22.04 LTS server, where we discussed Kubernetes and walked through installing it on a Ubuntu Server. If you haven't seen the previous article we recommend reading it because we installed a few plugins at the end to help us manage a cluster and make the creation of Pods much easier. A Pod is basically a set of containers and the containers are created from a container image. In this article we are going to create a custom container image using Docker and Docker Compose and push the new container image to the Private Registry created in Kubernetes.



There are a few requirements you need to be able to follow this post, the first is a container registry image. You can use a public registry like DockerHub or a private registry like we created in the previous article with the Kubernetes cluster.

You also will need a PC or Virtual Machine with Linux or Windows Docker, Docker Compose and Git installed. We're not going to review the process to install these packages and you best option is to review Dockers great documentation that can be found here.

Lastly it isn't required that you use an IDE like Jetbrains IntelliJ but it will certainly be helpful. This article will not mention the use of an IDE and we will use a text editor to create the docker compose file used to create the image


What is Docker?

Docker is an Open Source software that helps build, run, test and deploy containers. Containers are different from Virtual Machines in the sense that containers run on top of the Host operating system and isolate applications insides these units.

We won't go in depth about the technology but will learn how to deploy a base image, modify and customize it, then push that image to a Private or Public Registry that allows us to use us that image again without having to repeat the installation process for customization.

In Docker if we wanted to create an environment with multiple containers each container needed to be created individually. Docker-compose was created to relieve this hassle.


What is Docker Compose?

Docker compose is a tool that was developed to define and share multi-container applications. Docker compose uses a declarative language inside a YAML file to create and tear down applications with multiple containers


Verify that Docker and Docker Compose are installed on your system

Before proceeding we need to check if Docker and Docker Compose are installed on your local workstation by issuing the following commands:


docker –version
docker-compose –version


If you receive an error error please follow the Docker documentation and install docker and docker-compose on your system.  For reference in this tutorial I am using the following versions:


Docker version 23.0.1, build a5ee5b1
docker-compose version 1.24.0, build 0aa59064


Directory Structure of the project

For this project I am using the following directory structure


<Project Root Directory>
|-- docker
|---- ubuntu
|------ dockerfile
|-- docker-compose.yml


“docker-compose.yaml” is the main file where all the services, volumes, etc. are declared.

Inside the “docker” directory we find the “ubuntu” directory that will be the name of one of the services declared in the “docker-compose.yml” file and inside that directory a single file called “dockerfile” with the rest of the declarations to customize the service “ubuntu”


“docker-compose.yml” File anatomy

The Compose file is a YAML file (More info here) defining the following:

  • version (DEPRECATED): This property  is defined by the specification for backward compatibility but is only informative
  • services (REQUIRED): In the file you must declare “services” root element as a map whose keys are string representations of services names and whose values are service definitions
  • networks: Is the definition of the layer that allows services to communicate with each other
  • volumes: This section allows the configuration of named volumes that can be reused across multiple services
  • configs: This section allows services to adapt their behavior without the need to rebuild a Docker Image
  • secrets: This is a section containing sensitive data used to configure the containers

To learn more about how the docker-compose file is created, please view “The Compose Specification” here.


“dockerfile” File Anatomy

With the docker file you can build images automatically by reading the instructions inside the file and is the file used by docker-compose to customize the base or parent image using the following format:

  • FROM: You must start the dockerfile with this instruction, This instruction specifies the parent image to use
  • RUN: This instruction is used to execute any command on top of the current image and commit the results
  • CMD: This is the command the container executes by default when you launch the built image, you can only have one CMD instruction, if you have more than one the last CMD instruction will take effect

There are many other instructions that we are not goin to discuss in this article but you can review them here.


How to choose the Parent or Base Image?

Any custom container is based on a Parent or Base Image, this image can be obtained from different sources, but the best source by far is . This site is a Docker public registry with many images from different sources:

  • Docker Official Images: These are images curated by Docker
  • Verified Publisher: These are high quality images from publishers that were verified by Docker
  • Sponsored OSS: These are images published and maintained by open-source projects that are sponsored by Docker
  • Public Images: If a image doesn’t have a label like the ones specified above they are public images from Docker Hub users

From the website just search for “ubuntu” and you will see results with the keyword ubuntu in its description or name like the image below.

docker-hub-search Creating a Container Image and How to Push it to a Private Container Registry

Click the Ubuntu  “Docker Official Image” and you will see a section called “Supported tags and respective Dockerfile links” these tags are needed to specify a specific version of the image. If you want to specify the ubuntu 22.04 then the image name that has the format <image name>:tag will be ubuntu:22.04 or ubuntu:jammy or ubuntu:latest, as in the next image

docker-hub-ubuntu-image Creating a Container Image and How to Push it to a Private Container Registry


Install the Github Project

You can download the project created in this article using the following link:

On the machine where Docker, Docker Compose and Git are installed you can issue the following command to clone the project:

git clone

The project will be downloaded into a directory called “custom-ubuntu-docker” where you will find the docker-compose file and the dockerfile for the ubuntu service.


Create the container and test

Once the git repository has been downloaded you can run the project using the following command:

cd custom-ubuntu-docker
docker-compose up –d

You will see that docker will download the parent image first and then will start running the commands in the dockerfile, updating and upgrading the system, installing openssh, then creating the user and starting the service. If everything went well you will see a screen like the one below.

docker-compuse-launch Creating a Container Image and How to Push it to a Private Container Registry

Now verify everything was created by checking the following:

  • Verify that the image ubuntu:22.04 was downloaded with the command


docker image ls ubuntu:22.04

docker-compose-image Creating a Container Image and How to Push it to a Private Container Registry

  • Verify that the container was created and it is running with the command
docker container ls --filter "name=ubuntu"

docker-compose-container Creating a Container Image and How to Push it to a Private Container Registry

You can see in the last image the container was created 6 minutes ago and the status is up, also notice that the host is listening on port 2222 and redirects traffic to the container port 22 for ssh, meaning that if you want to ssh to the container you can use the following command:

ssh ubuntu@ -p 2222

After issuing the above command accept the key fingerprint of the host and you will be prompted to enter your password (Remember the password is “password”). You should be connected to the container and see prompt shell “ubuntu@<random hexadecimal string>”. Disconnect from the container with the command “exit”


Build the custom image from the container

Once the container is created our next step is to create the image. This is done by committing any changes in the container to a new image with the command:

docker commit ubuntu custom-ubuntu

Here “ubuntu” is the name of the container that we created and “custom-ubuntu” is going to be the name of the new image. When you execute the command a cryptographic hash is issued that indicates the image was created. You can verify the new image by issuing the command:

docker image ls custom-ubuntu

You will see the new Image Id, tags and when oy was created.


Push the newly created image to the registry

We will use a private registry that was created when microk8s was installed in the following article: Installing Kubernetes with Microk9s on a Ubuntu 22.04 LTS server

This registry is not using SSL to secure the connection between client and registry so it's considered insecure by Docker and we need to declare it as insecure in order to use it. To do that we need to create/modify the file “/etc/docker/daemon.json” if your docker installation is in Linux restart the docker daemon. You can create or modify the json file using any text editor like vim, nano and the content of the file should be:


Host is the IP address or hostname of the microk8s host server and the port is 32000 (note that you will need elevated privileges to create or modify the file). Once the file is created or modified restart the service, in Ubuntu you will need to issue the following command:

sudo systemctl restart docker

Now we need to tag the image and push it to the registry with the following commands:

docker image tag custom-ubuntu:latest <k8s ip or hostname>:32000/custom-ubuntu:latest
docker image push <k8s ip or hostname>:32000/custom-ubuntu:latest


You will see that the image has been pushed and a cryptographic hash is presented indicating that the push was successfull. You can verify the image was pushed, remove the images locally and pull it from the registry with the following commands:


docker image rm <k8s ip or hostname>:32000/custom-ubuntu:latest
docker image rm custom-ubuntu
docker image pull <k8s ip or hostname>:32000/custom-ubuntu:latest


You see that the image is downloaded from the registry. If you want to use this image in any dockerfile you can reference the image as “<k8s ip or hostname>:32000/custom-ubuntu:latest”.  An example is to modify the file ./docker/ubuntu/dockerfile  with the following:

FROM <k8s ip or hostname>:32000/custom-ubuntu:latest
CMD ["/usr/sbin/sshd","-D"]


Notice that we removed the installation of openssh and the creation of the user because it was already committed to the new image, at this point we only need to tell docker that the default command for the container “/usr/sbin/sshd –D” to keep it open and build the docker compose file again with the command:

docker-compose up –-build –d


Final Note

After the creation of the new container with the new image from the private registry, you can test the connection by sshing to the local machine to the port 2222.

Finally you can check the images in the microk8s private reigstery by connecting to the following url: http://< microk8s ip or hostname>:32000/v2/_catalog

Bokeh Solutions

Focused Results!

Empowering Businesses with Comprehensive Security Solutions!

Contact Us