Lab 03 - Docker


  • Understand what a software container is
  • Get familiar with the Docker environment
  • Learn how to build, publish, and deploy containers



Docker is a software container platform used for packaging and running applications both locally and on cloud systems, eliminating problems such as “it works on my computer”. Docker can therefore be seen as an environment that allows containers to run on any platform, and it is based on containerd. As a benefit, it provides faster compilation, testing, deployment, updating, and error recovery than the standard application deployment mode.

Docker provides a uniform development and production environment, where the compatibility of applications with the operating system is no longer a problem, and there are no more conflicts between the library/package versions on the host system. Containers are ephemeral, so an error or failure in one of them does not cause the entire system to crash. They help ensure strict consistency between the development environment and the production environment.

Docker also offers maximum flexibility. If, in a large project, we need new software tools because certain requirements change, we can pack them in containers and then link them very easily to the system. If we need to replicate the infrastructure to another medium, we can reuse Docker images saved in the registry (a kind of container repository). If we need to update certain components, Docker allows us to rewrite images, which means that the latest container versions will always be released.

Docker is a great work environment. As a matter of fact, most IDEs such as Visual Studio, VSCode, or IntelliJ have built-in support for debugging in Docker either by default or as a plugin. The reason why the most used IDEs offer this support is that Docker images represent a replicable and consistent work environment identical to the production one.

Images and containers

Docker containers are based on images, which are standalone lightweight executable packages that contain everything needed to run software applications, including code, runtime, libraries, environment variables, and configuration files. Images vary in size, do not contain full versions of operating systems, and are cached locally or in a registry. A Docker image has a union file system, where each change to the file system or metadata is considered a layer, with several such layers forming an image. Each layer is uniquely identified (by a hash) and stored only once.

A container is an instance of an image, that is, what the image becomes in memory when it is executed. It runs completely isolated from the host environment, accessing its files and ports only if it is configured to do so. Containers run native applications on the core of the host machine, performing better than virtual machines, which have access to the host's resources through a hypervisor. Each container runs in a discrete process, requiring as much memory as any other executable. From a file system standpoint, a container is an additional read/write layer over the image's layers.

In the image above (taken from the official Docker documentation), virtual machines run “guest” operating systems, which consume a lot of resources. The resulting image thus takes up a lot of space, containing operating system settings, dependencies, security patches, etc. Instead, containers can share the same kernel, and the only data that must be in a container image is the executable and the packages it depends on, which do not need to be installed on the host system at all. If a virtual machine abstracts hardware resources, a Docker container is a process that abstracts the base on which applications run within an operating system, and isolates operating system software resources (memory, network and file access, etc.).

Docker's Architecture

Docker has a client-server architecture, as shown in the image below (taken from the official Docker documentation). The Docker client communicates via a REST API (over UNIX sockets or over a network interface) with the Docker daemon (server), which is responsible for creating, running, and distributing Docker containers. The client and daemon can run on the same system or on different systems. A Docker registry is used to store images.


Docker is available in two versions: Community Edition (CE) and Enterprise Edition (EE). Docker CE is useful for developers and small teams who want to build container-based applications. On the other hand, Docker EE was created for enterprise development and IT teams that write and run critical business applications on a large scale. The Docker CE version is free, while EE is available with a subscription. In this lab, we will use Docker Community Edition. Docker is available on both desktop (Windows, macOS) and Cloud (Amazon Web Services, Microsoft Azure) or server (CentOS, Fedora, Ubuntu, Windows Server 2016, etc.) platforms.


The commands below are for Ubuntu. For other Linux variants (Debian, CentOS, Fedora), you can find more information on the official Docker documentation page.

To install Docker CE, you need one of the following versions of Ubuntu: Impish 21.10, Hirsute 21.04, Focal 20.04 (LTS), Bionic 18.04 (LTS). Docker CE has support for the following architectures: x86_64, amd64, armhf, arm64, and s390x (IBM Z).

The recommended Docker CE installation involves using the official repository, because all the subsequent updates are then installed automatically. When installing Docker CE on a machine for the first time, it is necessary to initialise the repository:

$ sudo apt-get update
$ sudo apt-get install ca-certificates curl gnupg lsb-release
$ curl -fsSL | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
$ echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Next, Docker CE can be installed:

$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli

An easier method of installing Docker CE on Linux is to use the official script.

Windows and MacOS

Because Docker did not initially have native support for Windows and MacOS, Docker Toolbox was introduced, which can launch a virtualised Docker environment (more specifically, it uses a VirtualBox machine as the basis of the Docker environment). Recently, Docker Toolbox was marked as “legacy” and was replaced by Docker Desktop for Mac and Docker Desktop for Windows, which offer similar features with better performance. Furthermore, Windows Server 2016 and Windows 10 now support native Docker for the x86_64 architecture.

Testing the installation

To check if the installation was successful, we can run a simple Hello World container:

$ docker container run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
5b0f327be733: Pull complete
Digest: sha256:b2ba691d8aac9e5ac3644c0788e3d3823f9e97f757f01d2ddc6eb5458df9d801
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
For more examples and ideas, visit:

The execution output above shows us the steps Docker takes in the background when running this container. Specifically, if the image we want to run in a container is not available locally, it is pulled from the registry, and then a new container is created based on that image, where the desired application is running.

Running a container

We have seen above how we can run a Hello World in a simple container, but we can run containers from much more complex images. We can create our own image (as we will see later) or download an image from a public registry, such as Docker Hub. It contains public images, ranging from operating systems (Ubuntu, Alpine, Amazon Linux, etc.) to programming languages ​​(Java, Ruby, Perl, Python, etc.), web servers (NGINX, Apache), and more.

For this lab, we will use Alpine Linux, which is a lightweight Linux distribution (with a size of barely 5 MB). The first step is to download the image from a Docker registry (in our case, Docker Hub):

$ docker image pull alpine

To see all the images present on our system, we can run the following command:

$ docker image ls
REPOSITORY      TAG         IMAGE ID        CREATED         SIZE
alpine          latest      961769676411    4 weeks ago     5.58MB

It can be seen above that the downloaded image has the name alpine and the tag latest. An image tag is a label that generally designates the version of the image, and latest is an alias for the latest version, set automatically when no tag is explicitly specified.

Once the image is downloaded, we can run it in a container. One way to do this is by specifying a command to run inside the container (in our case, on the Alpine Linux operating system):

$ docker container run alpine ls -l
total 56
drwxr-xr-x    2 root     root     4096 Aug 20 10:30 bin
drwxr-xr-x    5 root     root      340 Sep 23 14:34 dev
drwxr-xr-x    1 root     root     4096 Sep 23 14:34 etc
drwxr-xr-x    2 root     root     4096 Aug 20 10:30 home

In the command given above, we can skip the container keyword altogether and only write docker run alpine ls -l.

In the previous example, Docker finds the specified image, creates a container from it, starts it, and then runs the command inside it. If we want interactive access inside the container, we can use the following command:

$ docker run -it alpine

If we want to see which containers are currently running, we can use the docker container ls command. If we want to see the list of all the containers we ran, we also use the -a flag:

$ docker container ls -a
CONTAINER ID        IMAGE          COMMAND        CREATED             STATUS                         NAMES
562c38bb6559        alpine         "/bin/sh"      17 seconds ago      Exited (0) 2 seconds ago       musing_dhawan
d21dacca2576        alpine         "ls -l"        56 seconds ago      Exited (0) 55 seconds ago      interesting_raman

To run an image in a background container, we can use the -d flag. At startup, the new container's ID will be displayed, which we can then later use to attach to the container, stop it, delete it, etc .:

$ docker run -d -it alpine
$ docker container ls
CONTAINER ID      IMAGE        COMMAND        CREATED             STATUS            PORTS     NAMES
50c4aeaa66d9      alpine       "/bin/sh"      5 seconds ago       Up 3 seconds                condescending_feynman
$ docker attach 50c4aeaa66d9
/ # exit
$ docker stop 50c4aeaa66d9
$ docker container ls
CONTAINER ID      IMAGE        COMMAND        CREATED             STATUS            PORTS     NAMES
$ docker rm 50c4aeaa66d9
$ docker container ls -a
CONTAINER ID      IMAGE        COMMAND        CREATED             STATUS            PORTS     NAMES

Creating an image

So far, we only ran containers based on existing images, but now we will see how we can create and publish our own application. During the three Docker labs, we will go through an entire cloud application hierarchy. For this particular lab, we will start from the bottom level of this hierarchy, which is represented by the containers. Above this level, there are the services, which define how the containers behave in production, and at the highest level there is the service stack, which defines the interactions between services. The sources for this example can be found in the flask_app folder of the lab archive.

In this example, we will create a web application using Flask (as studied in lab 1) which displays a random picture each time its main page is accessed. The application's code can be found in a file called, which looks like this:
from flask import Flask, render_template
import random
app = Flask(__name__)
images = [
def index():
    url = random.choice(images)
    return render_template('index.html', url=url)
if __name__ == "__main__":"")

As you can see in the Python file, the web page is based on a template found in the index.html file, which should be located in the templates folder:

    <style type="text/css">
      body {
        background: black;
        color: white;
      div.container {
        max-width: 500px;
        margin: 100px auto;
        border: 20px solid white;
        padding: 10px;
        text-align: center;
      h4 {
        text-transform: uppercase;
    <div class="container">
      <h4>Which Avenger are you?</h4>
      <img src="{{url}}" width="450"/>

We also need a requirements.txt file, where we specify the Python packages to be installed in the image we are creating:


An image is defined by a file called Dockerfile, which specifies what happens inside the container we want to create, where access to resources (such as network interfaces or hard disks) is virtualised and isolated from the rest of the system. With this file, we can specify port mappings, files that will be copied to the container when it is run, and so on. A Dockerfile is somewhat similar to a Makefile, and each line in it describes a layer in the image. Once we have defined a correct Dockerfile, our application will always behave identically, no matter in what environment it is run. An example of a Dockerfile for our application is as follows:

FROM alpine:edge
RUN apk add --update py3-pip
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
COPY /usr/src/app/
COPY templates/index.html /usr/src/app/templates/
CMD ["python3", "/usr/src/app/"]

In the above file, we have the following commands:

  • FROM - specifies an image which our new image is based on (in our case, we start from a basic Alpine image found on Docker Hub, where we will run our Flask app)
  • COPY - copies files from a local directory to the image we are creating
  • RUN - runs a command (in the example above, we first install the pip Python package installer, then we install the Python packages listed in the requirements.txt file, i.e., Flask)
  • EXPOSE - exposes a port outside the container
  • CMD - specifies a command that will be run when the container is started (in this case, we run with Python).

When setting a base image using FROM, it is recommended that we explicitly specify the version of the image instead of using the latest tag, as the latest version may no longer be compatible with our components in the future.

The EXPOSE statement does not actually expose the port given as a parameter. Instead, it functions as a kind of documentation between the developer who builds the image and the developer who runs the container, in terms of which ports to publish. To publish a port when running a container, we need to use the -p flag of the docker run command (as will be seen below).

Finally, we end up with the following file structure:

$ tree
├── requirements.txt
└── templates
    └── index.html

To build an image for our Flask application, we run the command below in the current directory (the -t flag is used to tag the image):

$ docker build -t testapp .
Sending build context to Docker daemon  6.144kB
Step 1/8 : FROM alpine:edge
Step 2/8 : RUN apk add --update py3-pip
Step 3/8 : COPY requirements.txt /usr/src/app/
Step 4/8 : RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
Step 5/8 : COPY /usr/src/app/
Step 6/8 : COPY templates/index.html /usr/src/app/templates/
Step 7/8 : EXPOSE 5000
Step 8/8 : CMD python3 /usr/src/app/

To check if the image was created successfully, we use the following command:

$ docker images
REPOSITORY          TAG              IMAGE ID            CREATED             SIZE
testapp             latest           21a2e1b319ac        2 minutes ago       62.9MB

We can get more details about the new image using the following command:

$ docker image inspect testapp
        "Id": "sha256:d722f3da27d0d0e7d8cf9130738bbdb43a79204cddd4c0a9dba20becb4c0d3eb",
        "RepoTags": [
        "Parent": "sha256:d4f707536bf6b93836d7eda20edc7ccfba5a071e3c8a0d932c020b4c6b23ca00",
        "Comment": "",
        "Created": " 2019-09-23T14:41:51.204728682Z",
        "Container": "d9fec234255480ada84b772eb1e4b722b33fa262bc9481688920cba01f6d7d5d",
        "ContainerConfig": {
            "Hostname": "d9fec2342554",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "5000/tcp": {}
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
            "Cmd": [
                "#(nop) ",
                "CMD [\"python3\" \"/usr/src/app/\"]"
            "ArgsEscaped": true,
            "Image": "sha256:d4f707536bf6b93836d7eda20edc7ccfba5a071e3c8a0d932c020b4c6b23ca00",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": [],
            "Labels": {}
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 62900300,
        "VirtualSize": 62900300,

The image can now be found in the local Docker image registry and can be run with the following command:

$ docker container run -p 8888:5000 testapp
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on all addresses (
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on
 * Running on (Press CTRL+C to quit) - - [04/May/2022 10:09:12] "GET / HTTP/1.1" 200 - - - [04/May/2022 10:09:13] "GET / HTTP/1.1" 200 -

In the command given above, it can be seen that we followed the documentation and exposed port 5000 using the -p flag, as discussed previously. However, in this particular case, we chose to map the container's port 5000 to our host's port 8888.

By accessing the address from a web browser, we will see the web application we have created.

Publishing an image to a registry

Previously, we created a Docker image that we ran locally in a container. In order to be able to use the image created in any other system, it is necessary to publish it, i.e., to upload it to a registry in order to be able to deploy containers based on it in production. A registry is a collection of repositories, and a repository is a collection of images (similar to GitHub, except that, in a Docker registry, the code is already built). There are many registries for Docker images (Docker Hub, Gitlab Registry, etc.), but in this lab we will use the public Docker registry, since it is free and pre-configured.

We will start from the previous application. The first step in publishing an image is to create an account at Next, logging in from the local machine is done by the following command:

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to to create one.
Login Succeeded

We can specify the username and password directly in the command, so the above command can be written as (where the default server, if we choose to omit that particular parameter, is Docker Hub):

$ docker login [–u <USER> –p <PASSWORD>] [SERVER]

Before publishing the image to the registry, it must be tagged with the username/repository:tag format. The tag is optional, but it is useful because it denotes the version of a Docker image. We use the following command to tag an image (in the example below, the user is mobylab, the repository is iapp, and the tag is example):

$ docker tag testapp mobylab/iapp:example
$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED              SIZE
testapp                          latest              74254b15e6ba        About a minute ago   62.9MB
mobylab/iapp                     example             74254b15e6ba        About a minute ago   62.9MB
alpine                           edge                f96c4363411f        4 weeks ago          5.58MB

Once the image is tagged, it can be published to the registry:

$ docker push mobylab/iapp:example

From this point on, the image will be visible on, where it can be pulled and run on any host, server or cloud system:

$ docker run -p 8888:5000 mobylab/iapp:example
Unable to find image 'mobylab/iapp:example' locally
example: Pulling from mobylab/iapp
cc5efb633992: Pull complete 
cd0af7ebab8a: Pull complete 
41c55a3da379: Pull complete 
a779b27637f8: Pull complete 
dfaeccf28d0c: Pull complete 
805843c75452: Pull complete 
Digest: sha256:25af18fb4ffa9bf439e90bd4baee9adf0ab1e2999a44aeaa216ebf0454201ce8
Status: Downloaded newer image for mobylab/iapp:example
 * Serving Flask app 'app' (lazy loading)

Useful commands

We encourage you to read further from the official Docker site, as the commands shown here are the bare minimum you need in order to be able to work with Docker. In reality, there are many more commands, each with a variety of arguments.


$ docker <COMMAND> --help  # shows complete information about a command
$ docker version           # shows Docker's version and other minor details
$ docker info              # shows complete Docker information
$ docker system prune      # clears space by deleting unused components

Image interaction

$ docker image pull <IMAGE>.      # downloads an image to the local cache
$ docker build -t <TAG> .         # builds an image from a Dockerfile located in the current folder
$ docker image ls                 # lists the images in the local cache
$ docker images                   # lists the images in the local cache
$ docker image rm <IMAGE>         # deletes an image from the local cache
$ docker rmi <IMAGE>              # deletes an image from the local cache
$ docker image inspect <IMAGE>    # shows information about an image

Container interaction

$ docker container run <IMAGE> [COMMAND]    # runs a container and optionally sends it a starting command
$ docker container run -it <IMAGE>          # runs a container in interactive mode
$ docker container run -d <IMAGE>           # runs a container in the background (as a daemon)
$ docker exec -it <IMAGE> <COMMAND>         # starts a terminal in a running container and executes a command
$ docker container ls                       # lists all running containers
$ docker container ls -a                    # lists all containers that were run or are running
$ docker container inspect <ID>             # shows information about a container
$ docker attach <ID>                        # attaches to a container
$ docker stop <ID>                          # stops a container
$ docker restart <ID>                       # restarts a container
$ docker rm <ID>                            # deletes a container
$ docker ps                                 # lists running containers
$ docker logs <ID>                          # shows logs from a container
$ docker top <ID>                           # shows the processes running in a container

The difference between the exec and attach commands (which might appear similar) is that attach associates a terminal to a container, which means that, when we exit that terminal, we also exit the container. This is not the case for the exec command.

Working with a registry

$ docker login [–u <USER> –p <PASSWORD>] [SERVER]   # logs a user into a registry
$ docker tag <IMAGE> <USER/REPOSITORY:TAG>          # tags an image for registry push
$ docker push <USER/REPOSITORY:TAG>                 # pushes an image to a registry


00. [00p] Getting started

For the tasks in this lab, you will be using the files found in this archive.

Prior to solving the next tasks, you need to install Docker, using the instructions specific to your operating system that can be found here or in the official documentation.

If, for any reason, you cannot (or do not wish to) install Docker on your computer, you can use Play with Docker, which is an online framework that offers free Docker virtual machines. All you need is a Docker Hub account that you will use to log in. Once logged in, you will be faced with a dashboard as shown in the image below.

By pressing the “add new instance” button, you get terminal access to a virtual machine that already has the Docker runtime installed. You can either write your commands in the dashboard terminal, or you can use SSH to connect to the virtual machine by using the command shown on the dashboard (marked in red in the image below).

01. [10p] Testing the Docker installation


  1. start a container that runs the official hello-world image and check that everything functions correctly

02. [10p] Running containers


  1. pull the busybox image from the official Docker registry to the local cache
  2. run a busybox container that executes the uptime command
  3. run an interactive busybox container; once you enter it, run the command wget, then exit
  4. run a busybox detached interactive container (daemon); once you have started it, attach to it and run the id command, then exit
  5. delete all containers and images created at the previous points

03. [20p] Building an image


  1. go to the flask_app folder from the lab archive
  2. modify the images array in the file to make the application display other images of your choice
  3. modify the templates/index.html file to display a different title related to your chosen images (i.e., change line 22)
  4. build a Docker image entitled myflaskimage based on the provided Dockerfile
  5. run a container based on your image on port 8888
  6. check http://localhost:8888 to see if your container was started successfully

If you are solving this lab on Play with Docker, you can access an exposed port by clicking on the port button as shown in red in the image below.

04. [30p] Writing a Dockerfile


  1. go to the node_app folder from the lab archive, which contains the source files for a simple NodeJS application
  2. write a Dockerfile that will be used to create an image by following the steps below:
    1. start from the latest version of the official NodeJS image, node:15.11.0-stretch
    2. copy the file package.json from the local host to the current directory in the new image (./); this file is intended to specify the dependencies of the NodeJS application (for example, the Express.js framework)
    3. run the command npm install to install the dependencies in the file from the previous step
    4. copy the source file server.js to the working directory /usr/src/app/
    5. expose port 8080
    6. set the command to run the application; you will run the node binary to execute the /usr/src/app/server.js file
  3. use the previously written Dockerfile to create an image called mynodeimage
  4. start a container running image mynodeimage on port 12345 in detached (daemon) mode
  5. verify that your application works correctly by going to http://localhost:12345 (you should see a “Hello World” text)

05. [30p] Publishing an image


  1. create a Docker Hub account
  2. publish one of your previous images (either the Flask app, or the NodeJS app) to your Docker Hub registry
  3. run a container using the image from the registry

ii/labs/s2/03.txt · Last modified: 2022/05/04 16:58 by radu.ciobanu
CC Attribution-Share Alike 3.0 Unported Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0