I needed an excuse to learn how to create my own Docker Image, and fortunately all the stars aligned perfectly :

  1. An Itch that needed Scratching
  2. Time and Resources to do it
  3. An Urgency that forced me to actually see it through !

The Problem

I have a RPM repository of custom packages, which has dependencies on some standard packages. To simplify installation, I want to include just the standard packages (just the ones I need) in my repository as well.

In CentOS 8 however, some of the standard packages have additional "Module Metadata" which requires a "Modular Repository". If you just dump the packages into a folder and index them with "creatrepo .", you'll encounter an error like this when trying to install or update those packages:
No available modular metadata for modular package 'httpd-2.4.37-16.module_el8.1.0+256+ae790463.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'httpd-filesystem-2.4.37-16.module_el8.1.0+256+ae790463.noarch', it cannot be installed on the system
No available modular metadata for modular package 'httpd-tools-2.4.37-16.module_el8.1.0+256+ae790463.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'javapackages-filesystem-5.3.0-1.module_el8.0.0+11+5b8c10bd.noarch', it cannot be installed on the system
No available modular metadata for modular package 'mod_http2-1.11.3-3.module_el8.1.0+213+acce2796.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'nginx-filesystem-1:1.14.1-9.module_el8.0.0+184+e34fea82.noarch', it cannot be installed on the system
No available modular metadata for modular package 'php-7.2.11-2.module_el8.1.0+209+03b9a8ff.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'php-cli-7.2.11-2.module_el8.1.0+209+03b9a8ff.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'php-common-7.2.11-2.module_el8.1.0+209+03b9a8ff.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'php-fpm-7.2.11-2.module_el8.1.0+209+03b9a8ff.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'postgresql-10.6-1.module_el8.0.0+15+f57f353b.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'postgresql-contrib-10.6-1.module_el8.0.0+15+f57f353b.x86_64', it cannot be installed on the system
No available modular metadata for modular package 'postgresql-server-10.6-1.module_el8.0.0+15+f57f353b.x86_64', it cannot be installed on the system
The downloaded packages were saved in cache until the next successful transaction.
You can remove cached packages by executing 'dnf clean packages'.
Error: No available modular metadata for modular package
To create a Modular Repository requires that modifyrepo_c is called. This needs a modules.yaml file, which summaries the module data from the modular RPM packages, and will "inject" the module data into the RPM index file. See this article by Stephen Gallagher. I don't have a modules.yaml file for the standard packages. Fortunately, Stephen has created a utility called repo2module (https://github.com/sgallagher/repo2module) which magically does all the necessary work.

Now, repo2odule was designed to run on Fedora 28+, and has some dependencies that aren't available on Centos 8. Typically this means I would need to need to set up a VM with Fedora 28+, just to index my repository. This would take up precious resources (gigs of RAM), for something I use only occasionally.

The Plan

With Docker however, I can create a relatively lightweight container with Fedora and just the tools I need. My repository is already located on my NAS, and therefore I only need to be able to index it properly each time my build system spits out a new RPM. I will create a Docker Container based on Fedora, with just the packages needed for the job. This should be lightweight enough that it can even run on my NAS.

The build system needs some way to invoke the the indexer. I will install openssh inside the container, so that I can just add some ssh instructions into my build system that will trigger the re-indexing. I also have a few perl scripts that trim and prune the RPM repositories, which can be triggered in the same way (Note: Docker does have a remote invocation facility, but I'll explore this at a later time).

Installing Docker on my Desktop PC

My "production" Docker service runs on my NAS, but for development, I decided to setup Docker on my Desktop PC, running Ubuntu 18.04LTS. Not only is this faster, but at least if I really screw things up, it will not affect anything critical.

To install Docker on Ubuntu 18.04LTS, I executed the following:
sudo apt install docker.io
sudo usermod -aG docker ${USER}
This installs the RPM package, and sets up the current user as a member of the Docker group, which allows me to control Docker without having to type sudo each time. The permission change requires logging out and logging in again before it takes effect.

To confirm Docker is working, I queried it:
docker info
Debug Mode: false

Containers: 1
Running: 1
Paused: 0
Stopped: 0
Images: 16
Server Version: 19.03.6
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version:
runc version:
init version:
Security Options:
Profile: default
Kernel Version: 4.15.0-91-generic
Operating System: Ubuntu 18.04.4 LTS
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 61.04GiB
Name: pc-shahada
Docker Root Dir: /var/lib/docker
Debug Mode: false
Registry: https://index.docker.io/v1/
Experimental: false
Insecure Registries:
Live Restore Enabled: false

WARNING: No swap limit support

Creating My First Docker Image

Now on to creating my first Docker Image!

  • I started by creating a project folder (e.g. in ~/docker/{project}). The stuff that goes in here can be thought of as the "source code" for the Docker Image.
  • I created the file ~/docker/{project}/Dockerfile
# This bases my new image off an existing Fedora OS image, from the Docker Hub. We want version 31.
# It is also possible to use a version of "latest", to get the latest and greatest version.
FROM fedora:31

# A temporary container will be created from the image above. The rest of the Dockerfile contains commands
# that "modify" that image to get it to become what we want. The RUN command will execute the given command
# inside the temporary container. Here I am using the Fedora "dnf" command to
# install additional packages into the container/image.
RUN dnf -y install createrepo_c openssh-server supervisor python3-libmodulemd \
python3-libdnf python3-createrepo_c perl perl-RPM2

# The COPY command copies files/folders from my local machine into the temporary docker container. Here
# I am copying the source code from the repo2module project into the temporary container, and then using
# the RUN command to install the software.
COPY repo2module /usr/local/src/repo2module
RUN cd /usr/local/src/repo2module; python3 setup.py install

# This creates public-key ssh for the "root" user and authorizes my personal ssh key to login as the root
# user without any password.
RUN mkdir /root/.ssh; chown root. /root/.ssh; chmod 700 /root/.ssh
COPY authorized_keys.root /root/.ssh/authorized_keys

# I've also added another user "bm" that can ssh in without any passwords.
RUN adduser bm
RUN mkdir /home/bm/.ssh; chown bm. /home/bm/.ssh; chmod 700 /home/bm/.ssh
COPY authorized_keys.bm /home/bm/.ssh/authorized_keys

# This bit sets open openssh server. It generates the server keys, and disallows password authentication,
# which means the only way in via the network is to use public-keys, which I've only created for "root"
# and "bm".
RUN ssh-keygen -A
RUN sed -i 's/^PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config

# This tells openssh that whenever user "bm" logs in, to execute my own script, "bmShell" instead of bash.
# In this script, I restrict the commands that "bm" can execute to just whatever I specify. This is important
# for me as my build system is used by a few other people.
RUN echo -e "Match User bm\n\tForceCommand /usr/local/bin/bmShell" >> /etc/ssh/sshd_config

# Docker containers are essentially a file system that can run isolated processes. For a container to be
# long running, it needs a process that persists. There is no concept of "booting" or running "init" in a
# typical Docker container, so the processes we want to run must be specified individually. In this case,
# I only need openssh to be long running, but if there are multiple, then supervisord (http://supervisord.org/)
# is a useful lightweightlauncher. Here I copy its config file into the image.
COPY supervisord.conf /etc/supervisord.conf

# Importing other scripts I need. These are invoked from my build system via bmShell.
COPY bin/* /usr/local/bin/

# This tells docker that I plan to expose TCP port 22 to the outside world.

# This tells docker to execute this command by default when the container is started. It launches
# supervisord, which doesn't exist, resulting in the container remaining started (until stopped).
CMD ["/usr/bin/supervisord"]

  • Next, I got Docker to build an image from my Dockerfile. This essentially "executes" the commands in the Dockerfile, which results in the fedora image being downloaded and customised with my modifications.
docker build -t myfc_image .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM fedora:31
31: Pulling from library/fedora
5c1b9e8d7bf7: Pull complete
Digest: sha256:c97879f8bebe49744307ea5c77ffc76c7cc97f3ddec72fb9a394bd4e4519b388
Status: Downloaded newer image for fedora:31
---> 536f3995adeb
Step 2/2 : RUN yum -y install createrepo_c
---> Running in 7cc5c268c32d
Fedora Modular 31 - x86_64 5.4 MB/s | 5.2 MB 00:00
Fedora Modular 31 - x86_64 - Updates 1.5 MB/s | 4.0 MB 00:02
Fedora 31 - x86_64 - Updates 7.2 MB/s | 22 MB 00:03
Fedora 31 - x86_64 20 MB/s | 71 MB 00:03
Last metadata expiration check: 0:00:01 ago on Thu Mar 19 16:06:52 2020.
Dependencies resolved.
Package Architecture Version Repository Size
createrepo_c x86_64 0.15.5-1.fc31 updates 75 k
Installing dependencies:
createrepo_c-libs x86_64 0.15.5-1.fc31 updates 105 k
drpm x86_64 0.4.1-1.fc31 fedora 68 k
libmodulemd x86_64 2.9.1-1.fc31 updates 200 k

Transaction Summary
Install 4 Packages

Total download size: 448 k
Installed size: 1.1 M
Downloading Packages:
(1/4): createrepo_c-0.15.5-1.fc31.x86_64.rpm 160 kB/s | 75 kB 00:00
(2/4): drpm-0.4.1-1.fc31.x86_64.rpm 2.5 MB/s | 68 kB 00:00
(3/4): createrepo_c-libs-0.15.5-1.fc31.x86_64.r 210 kB/s | 105 kB 00:00
(4/4): libmodulemd-2.9.1-1.fc31.x86_64.rpm 340 kB/s | 200 kB 00:00
Total 227 kB/s | 448 kB 00:01
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Installing : libmodulemd-2.9.1-1.fc31.x86_64 1/4
Installing : drpm-0.4.1-1.fc31.x86_64 2/4
Installing : createrepo_c-libs-0.15.5-1.fc31.x86_64 3/4
Installing : createrepo_c-0.15.5-1.fc31.x86_64 4/4
Running scriptlet: createrepo_c-0.15.5-1.fc31.x86_64 4/4
Verifying : createrepo_c-0.15.5-1.fc31.x86_64 1/4
Verifying : createrepo_c-libs-0.15.5-1.fc31.x86_64 2/4
Verifying : libmodulemd-2.9.1-1.fc31.x86_64 3/4
Verifying : drpm-0.4.1-1.fc31.x86_64 4/4

createrepo_c-0.15.5-1.fc31.x86_64 createrepo_c-libs-0.15.5-1.fc31.x86_64
drpm-0.4.1-1.fc31.x86_64 libmodulemd-2.9.1-1.fc31.x86_64

Removing intermediate container 7cc5c268c32d
---> d848d99074c4
Successfully built d848d99074c4
Successfully tagged myfc_image:latest
  • The final result is a new image, saved under the tag myfc_image.
docker image ls
myfc_image latest d848d99074c4 About a minute ago 428MB
fedora 31 536f3995adeb 3 weeks ago 193MB
  • The image above is now ready to be used to create a container. The following command creates a container named myfc from the image myfc_image and runs the default CMD inside it. It should be only run once, unless you want to create multiple containers from the same image! The -d tells it to run the CMD in a detached state, so the docker process for the container essentially runs in the background and the prompt returns to me immediately. The -p maps port 2022 on my host to port 22 in the docker container (which is where ssh listens), and the -v maps my repository folder on the NAS to /repo inside the container.
docker run --name myfc -d -p 2022:22 -v /mnt/nas1/Web/my.centos8.repository:/repo myfc_image
  • From the host, we can see that the container still has a running process:
docker container ls -a
a17b4c623a09 myfc_image "/usr/bin/supervisord" 2 minutes ago Up 2 minutes>22/tcp myfc
  • we can execute other stuff within the same container as well, alongside the existing CMD. This launches a shell inside the container, allowing us to test and check on things from within the container itself.
docker exec -it myfc bash
[root@a17b4c623a09 /]#
  • The following will stop the docker container (essentially killing the CMD it is executing and any other exec'd processes):
docker stop myfc
  • The existing container can be restarted (without creating a new container) using:
docker stop myfc

And that is my first docker container created!

To deploy it on my NAS, I just build the image and start the container on the NAS just as I did on my desktop PC (although since both my PC and NAS are x86 architectures, I should be able to just transfer the image across?).

I can now execute commands within the container from my build scripts running elsewhere using:
ssh -p 2022 bm@docker-host-machine "cmd to pass to bmShell"
Interestingly, the docker image only takes up 17MB of memory when idle (basically just supervisord + openssh processes) on my host.
docker stats myfc
a17b4c623a09 myfc 0.02% 17.65MiB / 61.04GiB 0.03% 6.12kB / 0B 0B / 0B 2
Unlike a VM which takes up whatever memory you allocate to it permanently, a docker container's processes shares the memory of the host. The process actually runs under the hosts's kernel, and the docker container just fools it into thinking it has it's own private view of it's resources. The resources it uses are taken up only when it needs them, and returned to the host when it's done. For most of what I have been using VMs for, which is to provide services, I can see that probably Docker is the future for them. VMs are only needed when you really need a "full machine" and there are very few reasons for needing that.