Labs: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14.
Please, see latest news in issue #112 (from April 15).
The main topic for this lab is the use of containers: of lightweight virtual machines that are very useful for testing and development.
They will be used in the flagship of this lab: setting up a continuous integration in GitLab so that you can keep your software in a healthy state with as little effort as possible.
Preflight checklist
- You know that software is installed via package managers on Linux.
Containers
Containers are another approach for isolation. We have already seen project sandboxing and many of you have tried running a virtualized Linux installation.
Containers are somewhere in between.
They offer an isolated environment that generally behaves as a fully virtualized
machine.
From the implementation point of view, they are closer to virtual environment
as processes inside a container are visible from the host system.
We can imagine them as if we gave the container one directory
(containing all the usual subdirectories such as /dev
, /proc
or /home
)
to run in without an option to escape.
Because of the above, containers can run only the applications written for the same operating system (unlike a full-fledged virtual machine).
Because of their separation from the host system, containers are extremely useful in many scenarios. Note that using a fully virtualized machine (e.g., VirtualBox or QEMU) is an option too, but containers are light-weight and thus have a smaller overhead (e.g., faster start-up time).
The separation from the host system is very high: without extra configuration, the container cannot access host’s file systems and cannot listen on host’s ports for incoming connections. But it can initiate outgoing connections (e.g., to fetch packages that are to be installed). A container can be also limited in the amount of RAM it can use. By default, container processes are scheduled as normal processes (e.g., they have the same priority) but it is also possible to limit their CPU usage (e.g., throttle them as low-priority jobs).
A typical example is the need to run an isolated server that you need for development. You can imagine a database server or a web server here. You can certainly install such server system-wide (recall lab 08) but it does not provide the isolation and the easiness of removal. Recall how it works with virtual environments: removing a single directory cleans up the whole environment.
Similarly, removing a container is a simple and fast operation and you can start with a fresh one in matter of seconds.
Using a container also has the advantage that you can specify how exactly the container
shall look like: what processes it spawns, on which ports it listens etc.
Such specification can be easily codified (like with requirements.txt
) and thus
easily reproduced on a different machine.
Container images are also often used when you need to ship a complex application which requires several services to execute correctly. Instead of providing a detailed manual or a VirtualBox image, you provide a ready-to-be-run container. The user then launches the whole container and internally, the container takes care of the rest, exposing the final service. For example, the whole GitLab server can be downloaded and hosted as a container.
Docker and Podman
In this lab, we will show the basics of Linux containers based on
Docker
and Podman.
Both implementations are virtually the same.
Their main commands (docker
and podman
) support exactly the same arguments
and have the same semantics in most cases.
The main difference is that Docker is a bit older (though still actively developed) and was intended for system-wide containers (e.g., when you wanted to run a self-hosted instance of GitLab). Podman is a bit younger and it uses newer features of the Linux kernel which allow it to execute containers without superuser privileges (that is actually still quite a new feature of Linux). Also, Podman integrates better with the rest of the system.
In this sense, Podman is the perfect choice for a developer. You need a database server? Use Podman to get the right container and start it. Your database is clean and ready to be used. Without a need for superuser – root – privileges (this is often called rootless mode).
On the other hand, if you run an older version of Linux or the container requires some Docker-specific features, Docker might be a better choice.
Terminology …
There are two main concepts related to this lab. An image and a container. They are somewhat similar to a class and an object (instance), or an executable and a running process.
The image is like a hard disk for the isolated environment. It contains all the necessary files, including executables as well as data files.
To run it, we create a container. The container starts with the same state as the image, but it contains the running processes that might be modifying its state. Unless explicitly stated otherwise, the changes done by the container are not propagated to the image: instead, the container starts with a copy of the image (files) and modifies the copy.
Processes inside the container are isolated from the outside (the host) and the container does not see processes of the host.
On the other hand, processes in the container are visible in the host system. Root directory of the container corresponds to a subdirectory of the host. User IDs in the container are translated to a range of user IDs of the host. The same applies to group IDs.
Docker/Podman containers usually run processes inside the container
with privileges of container’s root
user, which looks as a normal user
(usually with a very high UID) in the host system.
Distributions and Alpine
The images can be built on the top of different distributions. For this reason, containers are an easy way to test your program in multiple distributions without having to setup triple-(or higher-) boot or having to manage multiple virtual machines.
You will notice that many containers are built on the top of a distribution called Alpine Linux. That is a minimalistic distribution designed for size and simplicity – its has about 6MB and the distribution does not use any complex configuration.
Alpine uses Apk (Alpine package manager) for its own packages. For example, the following command installs curl (which is not installed by default):
apk add curl
Setting up Docker/Podman
Install Docker or Podman.
To determine which one, the following command would help you.
grep cgroup /proc/filesystems
If you can see only the following line, then your kernel has not loaded cgroups v2 that are required for Podman.
nodev cgroup
However, if you can see the following, you have cgroups v2 enabled and you should use Podman.
nodev cgroup
nodev cgroup2
Then proceed with the installation.
Note that new versions of Fedora already switched to cgroup v2 and Podman
is the only option to use.
Hence, install with sudo dnf install podman
.
All the following examples in this lab will use podman
.
If your distribution does not support Podman, replace with sudo docker
.
Podman: setup of /etc/subuid
and /etc/subgid
As we explained above, Podman needs a range of free user and group IDs on the host to map the container’s UIDs and GIDs to.
The superuser can assign blocks of UIDs/GIDs to regular users, which can be used
for this purpose. These are called sub-UIDs/sub-GIDs and their assignment is recorded
in /etc/subuid
and /etc/subgid
.
First of all, please check if your /etc/subuid
already contains something like
intro:100000:65536
. If it does, you already have everything set up and you can skip
the rest of this section.
Otherwise, make sure that the files exist and create new assignments in them using usermod
:
sudo touch /etc/subuid /etc/subgid
sudo usermod --add-subuids 100000-165536 --add-subgids 100000-165536 YOUR_LOGIN
System (packages) upgrade may break Podman for various reasons.
If this happens to you, you may try to run podman system migrate
which is able
to fix most of the errors related to transition to a newer version.
Docker: starting the service
For Docker, you need to ensure that docker
is up and running.
Typically, the following commands would be sufficient:
sudo package-manager-of-your-distribution install docker
sudo systemctl enable docker
sudo systemctl start docker
Basic health check
Execute podman info
to get basic information about your system.
You will see something like this:
host:
arch: amd64
...
cgroupManager: systemd
cgroupVersion: v2
conmon:
...
...
idMappings:
gidmap:
- container_id: 0
host_id: 1000
size: 1
- container_id: 1
host_id: 100000
size: 65536
uidmap:
- container_id: 0
host_id: 1000
size: 1
- container_id: 1
host_id: 100000
size: 65536
...
os: linux
...
store:
graphRoot: $HOME/.local/share/containers/storage
...
runRoot: /run/user/1000/containers
volumePath: $HOME/.local/share/containers/storage/volumes
version:
APIVersion: 3.0.0
...
When debugging issues with Podman, always paste this information (unedited) into
the Issue description (obviously, as a text inside ```
, not as a screenshot!).
To check that you can execute containers, try the following command:
podman run --rm docker.io/library/alpine:latest cat /etc/os-release
If you see something like the following, you have everything set up. Otherwise feel free to open an Issue on the Forum and we will try to help you (do not forget to state which distribution you are using).
Trying to pull docker.io/library/alpine:latest...
Getting image source signatures
Copying blob 4abcf2066143 done |
Copying config 05455a0888 done |
Writing manifest to image destination
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.19.1
PRETTY_NAME="Alpine Linux v3.19"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
The first half of output is related to the download of the image. Only the second half of the output corresponds to the output of the command. Feel free to run the above command one more time (since the image is already downloaded) to get the following:
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.19.1
PRETTY_NAME="Alpine Linux v3.19"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
Podman is partially available in IMPAKT labs and the installation (albeit with some limitations) should be good enough for our purposes.
But it is much more comfortable to use your own machine.
Prepare for the labs
Before starting further experiments with Podman, ensure you have up-to-date copy of the examples repository.
We will be using the subdirectory 13/
.
Running the first container
The first execution will be a bit more complex to give you a taste of what is possible. We will explain the details in the following sections.
The following assumes you are inside the directory 13
in the
examples repository.
It will launch an Nginx web server.
podman run --rm --publish 8080:80/tcp -v ./web:/usr/share/nginx/html:ro docker.io/library/nginx:1.27
You will see similar output to the following.
Trying to pull docker.io/library/nginx:1.27...
Getting image source signatures
Copying blob 10fe6d2248e3 done |
Copying blob 3dce86e3b082 done |
Copying blob 75b642592991 done |
Copying blob 3b6e18ae4ce6 done |
Copying blob 8a628cdd7ccc done |
Copying blob 553c8756fd66 done |
Copying blob e81a6b82cf64 done |
Copying config 4cad75abc8 done |
Writing manifest to image destination
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2025/04/09 11:26:03 [notice] 1#1: using the "epoll" event method
2025/04/09 11:26:03 [notice] 1#1: nginx/1.27.4
2025/04/09 11:26:03 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
2025/04/09 11:26:03 [notice] 1#1: OS: Linux 6.12.1-arch1-1
2025/04/09 11:26:03 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 524288:524288
2025/04/09 11:26:03 [notice] 1#1: start worker processes
2025/04/09 11:26:03 [notice] 1#1: start worker process 24
2025/04/09 11:26:03 [notice] 1#1: start worker process 25
2025/04/09 11:26:03 [notice] 1#1: start worker process 26
2025/04/09 11:26:03 [notice] 1#1: start worker process 27
Open http://localhost:8080/ in your browser. You should see a NSWI177 Test Page in the browser.
If you see 403 Forbidden instead, append ,Z
to the -v
.
Thus, the command would contain -v ./web:/usr/share/nginx/html:ro,Z
.
This is needed (and generally a good practice) when you are running on a machine
with SELinux enabled in enforcing mode (default installation of Fedora but
not on the USB disks from us).
Terminate the execution by killing Podman with Ctrl-C
.
Note that the running Nginx webserver was printing its log – i.e., the list of accessed pages – to stdout.
Now open the page web/index.html
in your browser.
Again, you shall see a NSWI177 Test Page, but the URL would point to your local
filesystem (i.e., file:///home/.../examples/13/web/index.html
).
The above example illustrated three important features that are available with containers:
- The web server in the container does not need any configuration or system-wide installation.
- The container can listen on ports of the host system and forward network communication inside the container.
- The container can access host’s files and use them.
All very good features for development, testing as well as distribution of your software.
Pulling and inspecting the images
The first thing that needs to be done when starting a container is to get
its image.
While Podman is able to pull the image as a part of the run
subcommand,
it is sometimes useful to fetch it as a separate step.
The command podman images
prints a list of images that are present on your system.
The output may look like this.
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/library/alpine latest 9ed4aefc74f6 2 weeks ago 7.34 MB
docker.io/library/nginx 1.20.0 7ab27dbbfbdf 6 days ago 137 MB
docker.io/library/fedora 34 8d788d646766 2 weeks ago 187 MB
...
The repository refers to the on-line repository we fetched the image from. The tag is basically a version string. The image id is a unique identification of the image, it is generally derived from a cryptographic hash of the image contents. The remaining columns are self-descriptive.
When you execute podman pull IMAGE:TAG
, Podman will fetch the image without starting
any container. If you use latest
as a tag, the latest available version will be fetched.
Pull docker.io/library/python:3-alpine
and check that it has appeared in podman images
afterwards.
Shorter image names
If you paste the following content into /etc/containers/registries.conf.d/unqualified.conf
,
you will not need to type docker.io/
in front of every image name.
It is called an unqualified search and it is tried first for every image name.
unqualified-search-registries = ["docker.io"]
Companies can have their own repositories and you may set up multiple repositories here if you wish to try more of them when fully-qualified name is not provided.
Image repository
If you wonder where the images are coming from, have a look at https://hub.docker.com/. Anyone can upload their images there for others to use.
Similarly to Python package index, you may find malicious
images here.
At least, the containers are running isolated, so the chances of misbehaviour
are limited a little bit (compared to pip install
that you execute in the context
of a normal user).
Images from the library
group are official images endorsed by Docker itself
and hence are relatively trustworthy.
Running containers
After the image is pulled, we can create a container from it.
We will start with an Alpine image because it is very small and thus very fast.
podman run --interactive --tty alpine:latest /bin/sh
If all went fine, you should see an interactive prompt / #
and
inspecting /etc/os-release
should show you the following
text (version numbers may differ):
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.19.1
PRETTY_NAME="Alpine Linux v3.19"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
The run
subcommand starts a container from a specified image.
With --interactive
and --tty
(that are often combined into
single -it
) we specify that we want to attach a terminal to
the container as we would use it interactively.
The last part of the command is the program to run.
Inside the container, we can execute any commands we wish. We are securely contained and the changes will not affect the host system.
Install curl
and check that you have functional network
access.
Solution.
Open a second terminal so that we can inspect how the container looks from the outside.
Inside the container, execute sleep 111
and in the other
terminal (that is running in the host) execute ps -ef --forest
.
You shall see lines like the following:
student 1477313 1 0 16:29 ? 00:00:00 /usr/bin/conmon ...
student 1477316 1477313 0 16:29 pts/0 00:00:00 \_ /bin/sh
student 1477370 1477316 0 16:33 pts/0 00:00:00 \_ sleep 111
This confirms that the processes inside a container are visible from the outside.
Run ps -ef
inside a container (or look into /proc
there).
What do you see? Is there something surprising?
Solution.
Execute also podman ps
.
That prints list of running containers.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
643b5e7cea06 docker.io/library/alpine:latest /bin/sh 4 minutes ago Up 4 minutes ago practical_bohr
Container ID is again a unique identification, the other columns are self-descriptive. Note that since we have not specified a name, Podman assigned a random one.
If you terminate the session inside the container (exit
or Ctrl-D
),
you will return to the host terminal.
Execute podman ps
again.
It is empty: the container is not running.
If you add --all
, you will see that the STATUS
has changed.
Exited (130) 1 second ago
Note that if we would execute podman run ...
again, we would start
a new container.
Try it now.
We will describe the container life cycle later on, if you wish to remove
the container now, execute podman rm NAME
.
As NAME
use the randomly assigned one or the CONTAINER ID
.
Single shot runs
You can pass any command to podman run
to be executed.
If you know that you would be removing the container immediately afterwards,
you can add --rm
to tell Podman to remove it automatically once it finishes execution.
podman run --rm alpine:latest cat /etc/os-release
If you want to pass a more complicated command, it is better via sh -c
.
Change the above command to first cd
to etc
and then call cat os-release
.
Why the following does not work podman run --rm alpine:latest cd /etc && cat os-release
?
Solution.
Managing container life cycle
The containers are actually rather similar to services that we have talked about in Lab 09.
Starting a container
After we have terminated the interactive session, the container exited.
We can call podman start CONTAINER
to start it again.
Each container has a so-called entry point that is executed when the container is started. For a service-style container (e.g., with a web server), the service would be started again.
For our Alpine example, the entry point is /bin/sh
(shell), so
nothing interesting will happen.
Check that the container is running with podman ps
.
Attaching to a running container
When the container is running, we can attach to it.
podman attach
basically connects the stdout of the entrypoint
to your terminal.
With our Alpine container, we can run command again inside
the container.
We can also call podman exec -it CONTAINER CMD
that connects
to the running container in a new terminal (like a new tab).
For us, running the following would work (replace with your container name).
podman exec -it practical_bohr /bin/sh
Run again ps -ef
inside the container.
Which processes do you see?
Solution.
Terminating the exec
-ed shell returns us back to the host.
Terminating the attach
-ed shell terminates the whole container.
Containers in background (with names)
For service-style containers (e.g. nginx
that provides the webserver), we
often want to run them in daemon mode – in background.
That is possible with a --detach
option to the run
command.
We will also add a name webserver
to it so we can easily refer it.
podman run --detach --name webserver --publish 8080:80/tcp -v ./web:/usr/share/nginx/html:ro nginx:1.20.0
We will explain the -v
and --publish
later on.
This command starts the container and terminates. The webserver is running in the background. Check that you can again access http://localhost:8080/ in your browser.
You can stop such container with podman stop webserver
.
Kind of similar to systemctl stop ...
.
Not a coincidence.
Check that after stopping the webserver, http://localhost:8080/ no longer works.
Starting the container again is possible with podman start webserver
.
start
and stop
and stdout
Note that both start
and stop
print the name of the container that
was started (stopped) on stdout.
That is useful when executed in scripts, for interactive use we can
simply ignore the output.
Clean-up actions
When we are done with a container, we can remove it
(but first, we need to stop
it).
Executing the following command would remove webserver
container completely.
podman rm webserver
You can also remove pull
-ed images using rmi
subcommand.
For example, to remove the nginx:1.20.0
, you can execute the following command.
podman rmi nginx:1.20.0
Note that Podman will refuse to remove an image if it is used by an existing container. Recall that the images are stacked and hence Podman cannot remove the underlying layers.
Limiting the isolation
By default, container is an isolated world.
If you want to access it from the outside, you have to exec
into it
(for terminal-style work) or publish its services to the outside.
Port forwarding (a.k.a. port publishing)
For server-style containers (e.g. Nginx one we used above), that means
exposing some of ports to the host computer.
That is done with the --publish
argument where you specify which
port on the host (e.g., 8080
) shall be forwarded into the container:
to which port and which protocol (e.g., 80
and tcp
).
Therefore, the argument --publish 8080:80/tcp
means that we expect
that the container itself offers a service on its port 80
and we want to
make this (container’s) port available as 8080
.
This is very similar to SSH port forwarding we introduced in last labs.
We can start the nginx
container without --publish
, but it does not
make much sense. Why?
Solution.
Volume mounts
Another option how to break the container isolation is to bind a certain
directory into the container.
There are several options how to do that, we will show the
--volume
(or -v
) parameter.
It takes (again colon-separated) three arguments: source directory on the host, mapping inside the container and options.
Our example ./web:/usr/share/nginx/html:ro
thus specified that local
(host) directory web
shall be visible under /usr/share/nginx/html
inside the container in read-only mode.
It is very similar to normal mounts you already know.
If you specify rw
instead of ro
, you can modify the host files inside the
container.
Volume mounting is useful for any service-style container. A typical example is a database server. You start the container and you give it a mounted volume. To this volume (directory), it will store the actual database (the data files). Thus, when the container terminates, your data are actually persistent as they were stored outside of the container.
This has a huge advantage for testing service updates. You stop the container, make a backup of the data directory and start a new container (with a newer version) on the top of the same data directory. If everything works fine, you are good to go. Otherwise, you can stop the new container, restore from the backup and return to the old version.
Very simple and effective.
Check your understanding
Exercise
Why we have all the systemctl
, dnf
, podman
, pip
, …
At this moment there might be certain confusion why there are so many concepts around that are basically dealing with the same issues.
- We have package managers to install software (
dnf install
). But some software we can install also through language-specific managers (pip install
). - Web server can be started via
systemctl start
or via creating a container. - We have virtual environments for software development but we have also containers and full-fledged virtual machines.
- …
The truth is that some concepts and tools are consequences of historical development while others tackle some of the issues from different angles.
Feel free to return to this text at some later stage, e.g., after digesting the topic of containers a bit.
GitLab CI
If you have never heard the term continuous integration (CI), then it is the following in a nutshell.
About continuous integration
To ensure that the software you build is in healthy state, you should run tests on it often and fix broken things as soon as possible (because the cost of bug fixes rises dramatically with each day they are undiscovered).
The answer to this is that the developer shall run tests on each commit. Since that is difficult to enforce, it is better to do this automatically. CI in its simplest form refers to state where automated tests (e.g., BATS-based or Python-based) are executed automatically after each push, e.g. after pushing changes to any branch to GitLab.
But CI can do much more: if the tests are passing, the pipeline of jobs can package the software and publish it as an artifact (e.g., as an installer). Or it can trigger a job to deploy to a production environment and make it available to the customers. And so on.
In this course we will stay with the narrowest approach and focus how to run automated tests (and we will still proudly call it CI).
Setting up CI on GitLab
The important thing to know is that GitLab CI can run on top of
Podman containers.
Hence, to setup a GitLab pipeline, you choose a Podman image
and the commands which should be executed inside the container.
GitLab will run
the container for you and run your commands in it.
Depending on the outcome of the whole script (i.e., its exit code), it will mark the pipeline as passing or failing.
In this course we will focus on the simplest configuration where we want to execute tests after each commit. GitLab can be configured for more complex tasks where software can be even deployed to a virtual cloud machine but that is unfortunately out of scope.
If you are interested in this topic, GitLab has an extensive documentation. The documentation is often densely packed with a lot of information, but it is a great source of knowledge not only about GitLab, but about many software engineering principles in general.
.gitlab-ci.yml
The configuration of the GitLab CI is stored inside file .gitlab-ci.yml
that has to be stored in the root directory of the project.
We expect you have your own fork of the
web repository
and that you have extended the original Makefile
(recall lab 10 about make
).
If you do not have your own fork, create it now. And merge with our branch
lab/13
to have an up-to-date version.
We will now setup a CI job that only builds the web. It will be the most basic CI one can imagine. But at least it will ensure that the web is always in a buildable state.
However, to speed things up, we will remove the generation of PDF from
our Makefile
as OpenOffice installation requires downloading of 400MB which
is quite a lot to be done for each commit.
Place the following into .gitlab-ci.yml
in the root of your project.
image: fedora:41
build:
script:
- dnf install -y make pandoc python3
- make
It specifies a pipeline job build (you will see this name
in the web UI) that is executed using
fedora image
and it executes two commands.
The first one installs a dependency and the second one runs make
.
Add the .gitlab-ci.yml
to your Git repository (i.e. your fork),
commit and push it.
If you open the project page in GitLab, you should see the pipeline icon next to it and it should eventually turn green.
The log of the job would probably look like this.
Running with gitlab-runner 17.6.1 (6826a62f)
on gitlab.mff docker Mtt-jvRo, system ID: s_7f0691b32461
Preparing the "docker" executor 00:03
Using Docker executor with image fedora:41 ...
Pulling docker image fedora:41 ...
Using docker image sha256:9645f4e2280d9175edc72cd6195576bf9fa396cbde7d1c120756768a5e7399e0 for fedora:41 with digest fedora@sha256:f84a7b765ce09163d11de44452a4b56c1b2f5571b6f640b3b973c6afc4e63212 ...
Preparing environment 00:01
Running on runner-mtt-jvro-project-19856-concurrent-0 via gitlab-runner...
Getting source from Git repository 00:00
Fetching changes with git depth set to 20...
Reinitialized existing Git repository in /builds/teaching/nswi177/infra/experiments/web/.git/
Checking out accfb9f7 as detached HEAD (ref is master)...
Removing out/group-a.html
Removing out/group-b.html
Removing out/index.html
Removing out/main.css
Removing out/news.html
Removing out/rules.html
Removing out/score.html
Skipping Git submodules setup
Executing "step_script" stage of the job script 00:12
Using docker image sha256:9645f4e2280d9175edc72cd6195576bf9fa396cbde7d1c120756768a5e7399e0 for fedora:41 with digest fedora@sha256:f84a7b765ce09163d11de44452a4b56c1b2f5571b6f640b3b973c6afc4e63212 ...
$ dnf install -y make pandoc python3
Updating and loading repositories:
Fedora 41 openh264 (From Cisco) - x86_ 100% | 12.9 KiB/s | 6.0 KiB | 00m00s
Fedora 41 - x86_64 100% | 25.2 MiB/s | 35.4 MiB | 00m01s
Fedora 41 - x86_64 - Updates 100% | 17.2 MiB/s | 12.2 MiB | 00m01s
Repositories loaded.
Package Arch Version Repository Size
Installing:
make x86_64 1:4.4.1-8.fc41 fedora 1.8 MiB
pandoc x86_64 3.1.11.1-32.fc41 fedora 185.0 MiB
python3 x86_64 3.13.2-1.fc41 updates 31.8 KiB
Installing dependencies:
expat x86_64 2.7.1-1.fc41 updates 298.3 KiB
libb2 x86_64 0.98.1-12.fc41 fedora 42.2 KiB
mpdecimal x86_64 2.5.1-16.fc41 fedora 204.9 KiB
pandoc-common noarch 3.1.11.1-31.fc41 fedora 1.9 MiB
python-pip-wheel noarch 24.2-1.fc41 fedora 1.2 MiB
python3-libs x86_64 3.13.2-1.fc41 updates 40.4 MiB
Installing weak dependencies:
python-unversioned-command noarch 3.13.2-1.fc41 updates 23.0 B
Transaction Summary:
Installing: 10 packages
Total size of inbound packages is 38 MiB. Need to download 38 MiB.
After this operation, 231 MiB extra will be used (install 231 MiB, remove 0 B).
[ 1/10] pandoc-common-0:3.1.11.1-31.fc4 100% | 16.4 MiB/s | 537.1 KiB | 00m00s
[ 2/10] make-1:4.4.1-8.fc41.x86_64 100% | 15.9 MiB/s | 586.1 KiB | 00m00s
[ 3/10] python3-0:3.13.2-1.fc41.x86_64 100% | 4.0 MiB/s | 28.5 KiB | 00m00s
[ 4/10] libb2-0:0.98.1-12.fc41.x86_64 100% | 4.2 MiB/s | 25.7 KiB | 00m00s
[ 5/10] mpdecimal-0:2.5.1-16.fc41.x86_6 100% | 10.9 MiB/s | 89.0 KiB | 00m00s
[ 6/10] python-pip-wheel-0:24.2-1.fc41. 100% | 54.6 MiB/s | 1.2 MiB | 00m00s
[ 7/10] expat-0:2.7.1-1.fc41.x86_64 100% | 16.2 MiB/s | 116.0 KiB | 00m00s
[ 8/10] python-unversioned-command-0:3. 100% | 1.9 MiB/s | 11.6 KiB | 00m00s
[ 9/10] python3-libs-0:3.13.2-1.fc41.x8 100% | 103.6 MiB/s | 9.1 MiB | 00m00s
[10/10] pandoc-0:3.1.11.1-32.fc41.x86_6 100% | 132.0 MiB/s | 26.0 MiB | 00m00s
--------------------------------------------------------------------------------
[10/10] Total 100% | 120.8 MiB/s | 37.7 MiB | 00m00s
Running transaction
[ 1/12] Verify package files 100% | 104.0 B/s | 10.0 B | 00m00s
[ 2/12] Prepare transaction 100% | 357.0 B/s | 10.0 B | 00m00s
[ 3/12] Installing expat-0:2.7.1-1.fc41 100% | 29.3 MiB/s | 300.4 KiB | 00m00s
[ 4/12] Installing python-pip-wheel-0:2 100% | 177.4 MiB/s | 1.2 MiB | 00m00s
[ 5/12] Installing mpdecimal-0:2.5.1-16 100% | 40.2 MiB/s | 206.0 KiB | 00m00s
[ 6/12] Installing libb2-0:0.98.1-12.fc 100% | 5.3 MiB/s | 43.3 KiB | 00m00s
[ 7/12] Installing python3-libs-0:3.13. 100% | 147.2 MiB/s | 40.8 MiB | 00m00s
[ 8/12] Installing python3-0:3.13.2-1.f 100% | 6.5 MiB/s | 33.5 KiB | 00m00s
[ 9/12] Installing pandoc-common-0:3.1. 100% | 75.8 MiB/s | 1.9 MiB | 00m00s
[10/12] Installing pandoc-0:3.1.11.1-32 100% | 466.1 MiB/s | 185.0 MiB | 00m00s
[11/12] Installing python-unversioned-c 100% | 11.8 KiB/s | 424.0 B | 00m00s
[12/12] Installing make-1:4.4.1-8.fc41. 100% | 16.7 MiB/s | 1.8 MiB | 00m00s
Complete!
$ make
pandoc --template template.html -o out/index.html src/index.md
pandoc --template template.html -o out/rules.html src/rules.md
src/news.bin >out/news.html
./table.py <src/score.csv | pandoc --template template.html --metadata title="score" - >out/score.html
./table.py <src/group-a.csv | pandoc --template template.html --metadata title="group-a" - >out/group-a.html
./table.py <src/group-b.csv | pandoc --template template.html --metadata title="group-b" - >out/group-b.html
cp main.css out/
Cleaning up project directory and file based variables 00:00
Job succeeded
Note that GitLab will mount the Git repository into the container first
and then execute the commands inside the clone.
The commands are executed with set -e
: the first failing command
terminates the whole pipeline.
Try to emulate the above run locally. Hint. Solution.
Other bits
Notice how using the GitLab pipeline is easy. You find the right image, specify your script, and GitLab takes care of the rest.
If you are unsure about which image to choose, official images are a good start. The script can have several steps where you install missing dependencies before running your program.
Recall that you do not need to create a virtual environment: the whole machine is yours (and would be removed afterwards), so you can install things globally.
There can be multiple jobs defined that are run in parallel (actually, there can be quite complex dependencies between them, but in the following example, all jobs are started at once).
The example below shows a fragment of .gitlab-ci.yml
that tests the project
on multiple Python versions.
# Default image if no other is specified
image: python:3.10
stages:
- test
# Commands executed before each "script" section (for any job)
before_script:
# To have a quick check that the version is correct
- python3 --version
# Install the project
- python3 -m pip install ...
# Run unit tests under different versions
unittests3.7:
stage: test
image: "python:3.7"
script:
- pytest --log-level debug tests/
unittests3.8:
stage: test
image: "python:3.8"
script:
- pytest --log-level debug tests/
unittests3.9:
stage: test
image: "python:3.9"
script:
- pytest --log-level debug tests/
unittests3.10:
stage: test
image: "python:3.10"
script:
- pytest --log-level debug tests/
Tasks to check your understanding
We expect you will solve the following tasks before attending the labs so that we can discuss your solutions during the lab.
Learning outcomes and after class checklist
This section offers a condensed view of fundamental concepts and skills that you should be able to explain and/or use after each lesson. They also represent the bare minimum required for understanding subsequent labs (and other courses as well).
Conceptual knowledge
Conceptual knowledge is about understanding the meaning and context of given terms and putting them into context. Therefore, you should be able to …
-
explain what is a container
-
compare container with a virtual machine and a process
-
explain in what situations can be leveraged container isolation
-
explain container life-cycle
-
explain why using virtual environments (or other types of sandboxin) inside a container is typically not needed
-
explain a difference between a running container and a container image
-
explain principles of continuous integration
-
explain advantages of using continuous integration
-
explain in broad sense how GitLab CI works
Practical skills
Practical skills are usually about usage of given programs to solve various tasks. Therefore, you should be able to …
-
start interactive Podman container
-
start service-style Podman container
-
expose container ports
-
mount a volume into a container
-
clean unused containers and images
-
setup GitLab CI for simple projects