Week 7 Practical
Week 7 Practical
Week 7
Faculty of Computing, Engineering and
The Environment
Contents
1 Introduction .................................................................................................................................... 3
1.1 Task 1 – Familiarise yourself and check docker is working ..................................................... 3
1.2 Run Hello World Image ........................................................................................................... 4
2 Containerize an application............................................................................................................. 4
2.1 Prerequisites ........................................................................................................................... 5
2.2 Get the app ............................................................................................................................. 5
2.3 Build the app's image .............................................................................................................. 5
2.4 Start an app container............................................................................................................. 7
2.5 Summary ................................................................................................................................. 7
3 Update the application.................................................................................................................... 8
3.1 Update the source code .......................................................................................................... 8
3.2 Remove the old container ....................................................................................................... 8
3.3 Start the updated app container ............................................................................................. 9
3.4 Summary ................................................................................................................................. 9
3.5 Share the application .............................................................................................................. 9
3.6 Create a repository.................................................................................................................. 9
3.7 Push the image...................................................................................................................... 10
3.8 Run the image on a new instance ......................................................................................... 10
3.9 Summary ............................................................................................................................... 11
4 Persist the DB ................................................................................................................................ 11
4.1 The container's filesystem..................................................................................................... 12
4.1.1 See this in practice ........................................................................................................ 12
4.2 Container volumes ................................................................................................................ 13
4.3 Create a volume and start the container .............................................................................. 13
4.4 Verify that the data persists .................................................................................................. 14
4.5 Dive into the volume ............................................................................................................. 15
4.6 Summary ............................................................................................................................... 15
5 Use bind mounts ........................................................................................................................... 15
5.1 Quick volume type comparisons ........................................................................................... 16
5.2 Trying out bind mounts ......................................................................................................... 16
5.3 Development containers ....................................................................................................... 18
5.3.1 Run your app in a development container.................................................................... 18
5.3.2 Develop your app with the development container ..................................................... 19
5.4 Summary ............................................................................................................................... 20
1 Introduction
This practical is about docker and how to run, manage, and build docker containers. This includes
forwarding ports to the host and using persistent volumes to store data. These principles apply
anywhere containers are in use although the specific mechanism might change slightly. For example,
setting up persistent storage is done differently when a container is deployed by Kubernetes than
when done by Docker alone.
At a minimum you will need to know about the list of commands bellow and how to use them to
manage and build containers with docker. “run” in particular supports lots of options and it is worth
knowing at least at a high level what’s possible and what you might want to do.
1. How would you run a container in the background to keep your terminal free and keep it
running?
2. How do we reconnect to a container we started in the background to see its output?
3. If you have run a container in the background, how do you stop it?
4. How would you run another command in an already running container? (e.g., for debugging)
Once you have read up on the core commands you will need you can start using docker. In the
Windows PC’s in the lab docker is accessible via PowerShell, open a PowerShell terminal and run
“docker” and ensure you are presented with a command listing. If when using docker you see an
error message similar to “docker: error during connect: This error may indicate that the docker
daemon is not running.” It usually means you need to run docker desktop first. To start Docker
Desktop Search for Docker and select Docker Desktop in the search results.
If you need to install docker on your personal computer, follow the instructions given in the link Get
Docker | Docker Docs.
Using the information from the lecture slides and the commands you researched at the start; pull
and run the “hello-world” image to ensure docker is working and verify you get the success text from
the hello-world container. Remember it may be necessary to open the “Docker Desktop”
application first for it to start the necessary systems. Docker Desktop can provide a helpful UI to
view the state of your docker setup, images, and running containers. But do not lean on it too much
as knowing how to use the command line for management is an important skill.
Once the hello-world container has run you should see output like the following:
Tip: “docker run” will automatically pull the image you reference if it is not already downloaded
2 Containerize an application
For the rest of this task, you'll be working with a simple to do list manager that runs on Node.js. If
you're not familiar with Node.js, don't worry. This guide doesn't require any prior experience with
JavaScript.
2.1 Prerequisites
• View the contents of the cloned repository. You should see the following files and
sub-directories.
├── getting-started-app/
│ ├── package.json
│ ├── README.md
│ ├── spec/
│ ├── src/
│ └── yarn.lock
• Using a text editor or code editor, add the following contents to the Dockerfile:
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000
In the terminal, make sure you're in the getting-started-app directory. Replace /path/to/getting-
started-app with the path to your getting-started-app directory.
$ cd /path/to/getting-started-app
The expected output when you run the above command successfully.
The docker build command uses the Dockerfile to build a new image. You might have noticed that
Docker downloaded a lot of "layers". This is because you instructed the builder that you wanted to
start from the node:18-alpine image. But, since you didn't have that on your machine, Docker
needed to download the image.
After Docker downloaded the image, the instructions from the Dockerfile copied in your application
and used yarn to install your application's dependencies. The CMD directive specifies the default
command to run when starting a container from this image.
Finally, the -t flag tags your image. Think of this as a human-readable name for the final image. Since
you named the image getting-started, you can refer to that image when you run a container.
The . at the end of the docker build command tells Docker that it should look for the Dockerfile in
the current directory.
• Run your container using the docker run command and specify the name of the image you
just created:
The -d flag (short for --detach) runs the container in the background. The -p flag (short for --publish)
creates a port mapping between the host and the container. The -p flag takes a string value in the
format of HOST:CONTAINER, where HOST is the address on the host, and CONTAINER is the port on
the container. The command publishes the container's port 3000 to 127.0.0.1:3000 (localhost:3000)
on the host. Without the port mapping, you wouldn't be able to access the application from the
host.
• Add an item or two and see that it works as you expect. You can mark items as complete and
remove them. Your frontend is successfully storing items in the backend.
At this point, you have a running todo list manager with a few items.
If you take a quick look at your containers, you should see at least one container running that's using
the getting-started image and on port 3000. To see your containers, you can use the CLI or Docker
Desktop's graphical interface.
$ docker ps
2.5 Summary
In this section, you learned the basics about creating a Dockerfile to build an image. Once you built
an image, you started a container and saw the running app.
3 Update the application
In part 2, you containerized a todo application. In this part, you'll update the application and image.
You'll also learn how to stop and remove a container.
• In the src/static/js/app.js file, update line 56 to use the new empty text.
• Build your updated version of the image, using the docker build command.
docker: Error response from daemon: driver failed programming external connectivity on endpoint
laughing_burnell
(bb242b2ca4d67eba76e79474fb36bb5125708ebdabd7f45c8eaf16caaabde9dd): Bind for
127.0.0.1:3000 failed: port is already allocated.
The error occurred because you aren't able to start the new container while your old container is still
running. The reason is that the old container is already using the host's port 3000 and only one
process on the machine (containers included) can listen to a specific port. To fix this, you need to
remove the old container.
$ docker ps
• Use the docker stop command to stop the container. Replace <the-container-id> with the ID
from docker ps.
• Once the container has stopped, you can remove it by using the docker rm command.
$ docker rm <the-container-id>
Note: You can stop and remove a container in a single command by adding the force flag to
the docker rm command. For example: docker rm -f <the-container-id>
3.4 Summary
In this section, you learned how to update and rebuild a container, as well as how to stop and
remove a container.
Docker ID
A Docker ID lets you access Docker Hub, which is the world's largest library and community for
container images. Create a Docker ID for free if you don't have one.
Why did it fail? The push command was looking for an image named docker/getting-started, but
didn't find one. If you run docker image ls, you won't see one either.
To fix this, you need to tag your existing image you've built to give it another name.
Now run the docker push command again. If you're copying the value from Docker Hub, you can drop
the tagname part, as you didn't add a tag to the image name. If you don't specify a tag, Docker uses a
tag called latest.
• Now that your image has been built and pushed into a registry, try running your app on a
brand new instance that has never seen this container image. To do this, you will use Play
with Docker.
Note: Play with Docker uses the amd64 platform. If you are using an ARM based Mac with Apple
Silicon, you will need to rebuild the image to be compatible with Play with Docker and push the new
image to your repository.
To build an image for the amd64 platform, use the --platform flag.
Docker buildx also supports building multi-platform images. To learn more, see Multi-platform
images.
You should see the image get pulled down and eventually start up.
Tip
You may have noticed that this command binds the port mapping to a different IP address.
Previous docker run commands published ports to 127.0.0.1:3000 on the host. This time, you're
using 0.0.0.0.
Binding to 127.0.0.1 only exposes a container's ports to the loopback interface. Binding to 0.0.0.0,
however, exposes the container's port on all interfaces of the host, making it available to the outside
world.
For more information about how port mapping works, see Networking.
If the 3000 badge doesn't appear, you can select Open Port and specify 3000.
3.9 Summary
In this section, you learned how to share your images by pushing them to a registry. You then went to
a brand new instance and were able to run the freshly pushed image. This is quite common in CI
pipelines, where the pipeline will create the image and push it to a registry and then the production
environment can use the latest version of the image.
4 Persist the DB
In case you didn't notice, your todo list is empty every single time you launch the container. Why is
this? In this part, you'll dive into how the container is working.
4.1 The container's filesystem
When a container runs, it uses the various layers from an image for its filesystem. Each container also
gets its own "scratch space" to create/update/remove files. Any changes won't be seen in another
container, even if they're using the same image.
Note: If you use Windows and want to use Git Bash to run Docker commands, see Working with Git
Bash for syntax differences.
• Start an ubuntu container that will create a file named /data.txt with a random number
between 1 and 10000.
$ docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
In case you're curious about the command, you're starting a bash shell and invoking two commands
(why you have the &&). The first portion picks a single random number and writes it to /data.txt. The
second command is simply watching a file to keep the container running.
• Validate that you can see the output by accessing the terminal in the container. To do so,
you can use the CLI or Docker Desktop's graphical interface.
On the command line, use the docker exec command to access the container. You need to get the
container's ID (use docker ps to get it). In your Mac or Linux terminal, or in Windows Command
Prompt or PowerShell, get the content with the following command.
Tip: Docker Desktop (you can also do the above using Docker Desktop, but if you have don it using
command line then you do not need to do in Docker Desktop)
In Docker Desktop, go to Containers, hover over the container running the ubuntu image, and select
the Show container actions menu. From the drop-down menu, select Open in terminal.
You will see a terminal that is running a shell in the Ubuntu container. Run the following command to
see the content of the /data.txt file. Close this terminal afterwards again.
content_copy
$ cat /data.txt
• Now, start another ubuntu container (the same image) and you'll see you don't have the
same file. In your Mac or Linux terminal, or in Windows Command Prompt or PowerShell,
get the content with the following command.
• Go ahead and remove the first container using the docker rm -f <container-id> command.
Volumes provide the ability to connect specific filesystem paths of the container back to the host
machine. If you mount a directory in the container, changes in that directory are also seen on the
host machine. If you mount that same directory across container restarts, you'd see the same files.
There are two main types of volumes. You'll eventually use both, but you'll start with volume
mounts.
By default, the todo app stores its data in a SQLite database at /etc/todos/todo.db in the container's
filesystem. If you're not familiar with SQLite, no worries! It's simply a relational database that stores
all the data in a single file. While this isn't the best for large-scale applications, it works for small
demos. You'll learn how to switch this to a different database engine later.
With the database being a single file, if you can persist that file on the host and make it available to
the next container, it should be able to pick up where the last one left off. By creating a volume and
attaching (often called "mounting") it to the directory where you stored the data, you can persist the
data. As your container writes to the todo.db file, it will persist the data to the host in the volume.
As mentioned, you're going to use a volume mount. Think of a volume mount as an opaque bucket of
data. Docker fully manages the volume, including the storage location on disk. You only need to
remember the name of the volume.
• Stop and remove the todo app container once again with docker rm -f <id>, as it is still
running without using the persistent volume.
• Start the todo app container, but add the --mount option to specify a volume mount. Give
the volume a name, and mount it to /etc/todos in the container, which captures all files
created at the path. In your Mac or Linux terminal, or in Windows Command Prompt or
PowerShell, run the following command:
• Once the container starts up, open the app and add a few items to your todo list.
• Stop and remove the container for the todo app. Use Docker Desktop or docker ps to get the
ID and then docker rm -f <id> to remove it.
• Start a new container using the previous steps.
• Open the app. You should see your items still in your list.
• Go ahead and remove the container when you're done checking out your list.
"CreatedAt": "2019-09-26T02:18:36Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
"Name": "todo-db",
"Options": {},
"Scope": "local"
The Mountpoint is the actual location of the data on the disk. Note that on most machines, you will
need to have root access to access this directory from the host.
4.6 Summary
In this section, you learned how to persist container data.
A bind mount is another type of mount, which lets you share a directory from the host's filesystem
into the container. When working on an application, you can use a bind mount to mount source code
into the container. The container sees the changes you make to the code immediately, as soon as you
save a file. This means that you can run processes in the container that watch for filesystem changes
and respond to them.
In this chapter, you'll see how you can use bind mounts and a tool called nodemon to watch for file
changes, and then restart the application automatically. There are equivalent tools in most other
languages and frameworks.
5.1 Quick volume type comparisons
The following table outlines the main differences between volume mounts and bind mounts.
Host
location Docker chooses You decide
Mount
example type=volume,src=my-
(using -- volume,target=/usr/local/da type=bind,src=/path/to/data,target=/usr/local/
mount ) ta data
Populate
s new
volume
with
container
contents Yes No
Supports
Volume
Drivers Yes No
Note: If you use Windows and want to use Git Bash to run Docker commands, see Working with Git
Bash for syntax differences.
The --mount option tells Docker to create a bind mount, where src is the current working directory
on your host machine (getting-started-app), and target is where that directory should appear inside
the container (/src).
• After running the command, Docker starts an interactive bash session in the root directory
of the container's filesystem.
root@ac1237fad8db:/# pwd
root@ac1237fad8db:/# ls
bin dev home media opt root sbin srv tmp var
This is the directory that you mounted when starting the container. Listing the contents of this
directory displays the same files as in the getting-started-app directory on your host machine.
root@ac1237fad8db:/# cd src
root@ac1237fad8db:/src# ls
root@ac1237fad8db:/src# ls
• Open the getting-started-app directory on the host and observe that the myfile.txt file is in
the directory.
├── getting-started-app/
│ ├── Dockerfile
│ ├── myfile.txt
│ ├── node_modules/
│ ├── package.json
│ ├── spec/
│ ├── src/
│ └── yarn.lock
root@ac1237fad8db:/src# ls
That's all for a brief introduction to bind mounts. This procedure demonstrated how files are shared
between the host and the container, and how changes are immediately reflected on both sides. Now
you can use bind mounts to develop software.
5.3 Development containers
Using bind mounts is common for local development setups. The advantage is that the development
machine doesn’t need to have all of the build tools and environments installed. With a single docker
run command, Docker pulls dependencies and tools.
You can use the CLI or Docker Desktop to run your container with a bind mount.
Make sure you don't have any getting-started containers currently running.
node:18-alpine `
Tip: you can also do the above using Docker Desktop, but if you have don it using command line
then you do not need to do in Docker Desktop
Make sure you don't have any getting-started containers currently running.
Tip:Use the search filter to filter images and only show Local images.
In Host path, specify the path to the getting-started-app directory on your host machine.
• -dp 127.0.0.1:3000:3000 - same as before. Run in detached (background) mode and create a
port mapping
• -w /app - sets the "working directory" or the current directory that the command will run
from
• --mount "type=bind,src=$pwd,target=/app" - bind mount the current directory from the
host into the /app directory in the container
• node:18-alpine - the image to use. Note that this is the base image for your app from the
Dockerfile
• sh -c "yarn install && yarn run dev" - the command. You're starting a shell using sh (alpine
doesn't have bash) and running yarn install to install packages and then running yarn run
dev to start the development server. If you look in the package.json, you'll see that
the dev script starts nodemon.
• You can watch the logs using docker logs <container-id>. You'll know you're ready to go
when you see this:
nodemon -L src/index.js
[nodemon] 2.0.20
When you're done watching the logs, exit out by hitting Ctrl+C.
• Update your app on your host machine and see the changes reflected in the container.
In the src/static/js/app.js file, on line 109, change the "Add Item" button to simply say "Add":
Feel free to make any other changes you'd like to make. Each time you make a change and save a file,
the change is reflected in the container because of the bind mount. When Nodemon detects a
change, it restarts the app inside the container automatically. When you're done, stop the container
and build your new image using:
5.4 Summary
At this point, you can persist your database and see changes in your app as you develop without
rebuilding the image.
In addition to volume mounts and bind mounts, Docker also supports other mount types and storage
drivers for handling more complex and specialized use cases.