So far, we've been working locally, and our images essentially exist only on our own computers for our personal use, mainly to avoid the need to install tools on our host machines. However, the real challenge is sharing this new setup with our coworkers who are also working on the same project. This is where Docker Hub comes into play. In simple terms, Docker Hub is like GitHub, but for Docker images. The first step is to create an account.

By the way, before we dive into this new topic, here's a useful command. If you want to start fresh and remove all your images, networks, and containers, you can use the system prune command with the -a flag. If you only want to remove images that are not being used, simply avoid using the -a flag.

$ docker system prune -a

Ok, login into your docker account from the CLI by typing docker login -u, where <username> is your docker hub account username, don’t forget the password

$ docker login -u <username>
Password: 
WARNING! Your password will be stored unencrypted in /home/<username>/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credential-stores

Login Succeeded

After the login we are ready to rock, so let’s create a dummy docker image using Alpine, something simple like

# Fetch a new image from alpine 
FROM alpine

# install bash just for fun
RUN apk add --no-cache bash

# display a mesage
ENTRYPOINT ["echo", "Hola mundo"]

Build our crappy image BUT!!!, this time you have to name your image appending your docker hub username, otherwise it won be possible to upload your image

$ docker build -t <username>/dummy .

Upload our recent image to docker hub using docker push

Now, just for testing purposes lets pull this image in our machine, but first remove the one you have locally to avoid any confusion or conflict

$ docker image rm <username>/dummy
$ docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

Use docker pull command

$ docker run <username>/dummy
Unable to find image '<username>/dummy:latest' locally
latest: Pulling from <username>/dummy
43c4264eed91: Pull complete 
1df2f3cc77e8: Pull complete 
Digest: sha256:783d8414ebec4ea19eb9038401376949f67fe87012bab65b221c4d1ee99201ba
Status: Downloaded newer image for <username>/dummy:latest
Hola mundo

That’s it —end of the story. Well… 99% of the time, this is how you’ll use Docker Hub. But please, don’t, seriously—don’t just do that. It's like those meaningless Git commit messages we all regret later. We need to put some care into every image we upload, and good documentation is essential. Let’s follow best practices and, in the process, learn some new Docker tips along the way.

Improve your image

For this example, let’s go back to our OpenOCD image, but with the following additions and new recommendations:

  1. Always use tags for your base images. In our case, we are using the Linux Alpine version 3.20.3.
  2. Always use fixed versions of the packages you install in your images.
  3. Learn how to use your Linux package manager!

The reason for specifying package versions is to maintain consistency across all build versions of your image. If you always install the latest versions, the image will depend on when it was built, which could lead to different versions being used across your team. This can introduce inconsistency and potential issues, so it's important to lock down package versions to ensure everyone is working with the same setup.

# Fetch a new image from alpine version 3.17
FROM alpine:3.17

# install openocd version 12.0 revision 0
RUN apk add --no-cache openocd=0.12.0-rc2-r0

# display openocd version
ENTRYPOINT [ "openocd", "-v" ]

In the Dockerfile above, we are using Alpine 3.17, which is the version that comes with OpenOCD 12.0 release 0. If you need to install an older version of OpenOCD or other tools, it will depend on the distribution you are using.

💡
As you can see, creating Docker images the right way is not trivial, but it comes with significant benefits, including better control over your tools and environments.

The previous image only displays the OpenOCD version, which is pretty basic, perhaps even a bit useless, but it's sufficient for what we want to achieve: building the image, tagging it with a version, and then pushing it to Docker Hub. The tag should clearly indicate the version of your image, so it's easy to track and manage.

$ docker build -t <username>/openocd:12.0-r0 .
...
$ docker push <username>/openocd:12.0-r0
...

Got o docker hub and write a nice and pretty repository overview, you use Markdown notation, and please make sure to put some love specifying how to use your image, people and yourself form the future will thank you.

*OpenOCD* display version image, if you only run the image it only display the version

# Supported tags and respective Dockerfile links

- [`12.0-r0`](https://hub.docker.com/layers/diegomodular/openocd/12.0-r0/images/sha256-61729ac9b7eeb6337cb2f0652966372d819142d0b218132ea2bba6474c93273a?context=repo)

# How to use this image

Just run like any other image and prepare yourself to watch displyed OpenOCD version

```c
docker run <username>/openocd:0.12.0-r0
```

This is written in you Docker Hub page form your repo, with he following result. If you want some inspiration, this is the overview description for the alpine image

An image that just outputs the version isn't particularly useful, so let’s add more functionality. OpenOCD is a debug server, and as such, it has several ports for accepting connections from different services, such as:

  • 3333 for GDB
  • 4444 for Telnet
  • 5555 for TCL

In our image, we need to expose these ports so they can be accessed when the container is running.

# Fetch a new image from alpine version 3.17
FROM alpine:3.17

# install openocd version 12.0 revision 0
RUN apk add --no-cache openocd=0.12.0-rc2-r0

# Expose openocd ports to accept connection from GDB 3333,
# Telnet 4444 and Tcl 5555
EXPOSE 3333
EXPOSE 4444
EXPOSE 5555

# display openocd version
ENTRYPOINT [ "openocd", "-v" ]

If we build and run the image in de-attached mode using flag -dand then we with docker ps we can see, the ports we should publish with flag -p

$ docker build -t <username>/openocd:12.0-r0 .
$ docker run --rm -d <username>/openocd:12.0-r0
$ docker ps
CONTAINER ID   IMAGE                        COMMAND     CREATED          STATUS          PORTS                          NAMES
22218661136f   <username>/openocd:12.0-r0   "/bin/sh"   12 seconds ago   Up 12 seconds   3333/tcp, 4444/tcp, 5555/tcp   relaxed_faraday

Time to make our image useful, instead of only display version we are going to try to connect to a default board using CMD to pass arguments to openocd

# display openocd version
ENTRYPOINT [ "openocd" ]
CMD [ "-f", "board/st_nucleo_g0.cfg", "-c", "bindto localhost" ]

But something much better is to make these parameters free for a user to configure, we can use ENV to define a default board and a default ip address

# Fetch a new image from alpine version 3.17.3
FROM alpine:3.17.3

# install openocd version 12.0 revision 0
RUN apk add openocd=0.12.0_rc2-r0

# expose openocd ports to accept connection from GDB 3333,
# Telnet 4444 and Tcl 5555
EXPOSE 3333
EXPOSE 4444
EXPOSE 5555

# Paramter to select board and IP address 
ENV IP_ADDR=localhost
ENV BOARD=board/st_nucleo_g0.cfg

# open a openocd connection
CMD [ "sh", "-c", "openocd -f ${BOARD} -c \"bindto ${IP_ADDR}\"" ]

You might be wondering why I changed the last two lines of the Dockerfile. The reason is that you cannot use ENV variables directly in an ENTRYPOINT or CMD string. We need to make some adjustments to handle this properly. For more details on the pros and cons of this, check out this https://www.baeldung.com/ops/docker-entrypoint-environment-variables

Additionally, be sure to read more about ARG and ENV variable definitions in Docker here: https://phoenixnap.com/kb/docker-environment-variables

So now after the proper build we can run the container, in the following way it will open a connection to a Nucleo-G0B1RE board in localhost

$ docker run -it --rm --device=/dev/bus/usb:/dev/bus/usb -p 3333:3333 test 

But if you want to change the board and the IP address to connect you can override these parameters with flag -e

$ docker run -it --rm --device=/dev/bus/usb:/dev/bus/usb -p 3333:3333 -e BOARD=board/st_nucleo_f4.cfg -e IP_ADDR=172.17.0.3 test 

It also work in docker compose through directive environment

services:
  open_server:      # container name been used by the image to connect to openocd
    image: open
    ports:          # port mapping
      - 3333:3333
    devices:        # device mapping to usb ports
      - /dev/bus/usb:/dev/bus/usb
    environment:    # set the Enviroment variables
      - BOARD=board/st_nucleo_f4.cfg
      - IP_ADDR=172.17.0.3

Getting back to our image we can push the new version of our image as a minor version like openocd:12.0.r1, because why not, after pushing the new version take a look at your docker hub repo and of course update the overview properly

$ docker build -t <username>/openocd:12.0-r1 .
$ docker push <username>/openocd:12.0-r1

A new major version

If you notice, Docker Hub handles repositories sort of the way GitHub does, allowing us to manage versions. This is important because, in the Docker world, everything should be implemented with specific versions to ensure consistency.

To version our OpenOCD image, it's as simple as assigning a new tag (as we did previously). For example, let’s say we want to migrate to OpenOCD version 12, revision 4. This would include using a new version of our base image, Alpine.

# Fetch a new image from alpine version 3.20
FROM alpine:3.20.3

# Document the purpose of the image and any additional details
LABEL maintainer="John Doe <john@example.com>"
LABEL description="Docker image for running OpenOCD for GDB, Telnet and Tcl connections"

# install openocd version 12.0 revision 4
RUN apk add openocd=0.12.0-r4

# expose openocd ports to accept connection from GDB 3333,
# Telnet 4444 and Tcl 5555
EXPOSE 3333
EXPOSE 4444
EXPOSE 5555

# Parameters to select board and IP address 
ENV IP_ADDR=localhost
ENV BOARD=board/st_nucleo_g0.cfg

# open a openocd connection
CMD [ "sh", "-c", "openocd -f ${BOARD} -c \"bindto ${IP_ADDR}\"" ]

If you notice we add some metadata to enrich our image description

# Document the purpose of the image and any additional details
LABEL maintainer="John Doe <john@example.com>"
LABEL description="Docker image for running OpenOCD for GDB, Telnet and Tcl connections"

Build and push

$ docker build -t <username>/openocd:12.0-r4 .
$ docker push <username>/openocd:12.0-r4

We are not going to see this new information on docker hub, but you can make a docker inspect image to read this and some other information from the image

$ dcoker image inspect <username>/openocd:12.0-r4
... 
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "IP_ADDR=localhost",
                "BOARD=board/st_nucleo_g0.cfg"
            ],
            "Cmd": [
                "sh",
                "-c",
                "openocd -f ${BOARD} -c \"bindto ${IP_ADDR}\""
            ],
            "ArgsEscaped": true,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {
                "description": "Docker image for running OpenOCD for GDB, Telnet and Tcl connections",
                "maintainer": "John Doe <john@example.com>"
            }
...

One last thing. We prepared an image to run an application with an specific purpose, like connect to a hardware and keep listening to a port. This is not an image to serve as base for other images, since we are setting an entry point

Last but not least, remember to update your Docker Hub repository overview to include information about the new version, without removing details about the previous version.

One of the key benefits of using Docker is that we have versioned environments that are already tested and working. This is especially useful if, for any reason, you need to revert to a past version—perhaps to update or maintain previous projects.

Docker Registry

But what is a docker registry?, well this is basically the service where you store your docker images, you see in a team of developer you are going to distribute the images so they can build their containers with all the tools to start working, but in theory they a re not going to modify this image, usually there is going to be one person in charge of doing this if necessary.

Not only docker hub is the only registry service you can use, there is

Introduction to GitHub Packages - GitHub Docs
GitHub Packages is a software package hosting service that allows you to host your software packages privately or publicly and use packages as dependencies in your projects.
ECR Public Gallery
Amazon ECR Public Gallery is a website that allows anyone to browse and search for public container images, view developer-provided details, and see pull commands
Package registry | GitLab
GitLab product documentation.

Just to name some of them, There is also some self hosted apps like And also your own computer,

Gitea 1.17.0 is released | Gitea Blog
We are proud to present the release of Gitea version 1.17.0, a relatively big release with a lot of new and exciting features and plenty breaking changes.
Harbor
Our mission is to be the trusted cloud native repository for Kubernetes

I choose not going into details about this topic because is not exclusive on embedded, and there already some other resource where to get more information