3. Docker#
Some short notes on using docker to create reproducible dev environments.
3.1. What is a Docker Image?#
A docker image can be thought of as a snapshot of a read-only virtual machine that contains everything needed to create a runnable virtual machine.
Docker images come in all shapes and sizes and can be downloaded from Docker
hub which is a huge online repository for Docker images. For example, the
Docker image for Ubuntu can be downloaded using
docker pull <image-name>
.
$ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
e96e057aae67: Pull complete
Digest: sha256:4b1d0c4a2d2aaf63b37111f34eb9fa89fa1bf53dd6e4ca954d47caebca4005c2
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
The images you have downloaded can be listed using docker images
.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest a8780b506fa4 8 days ago 77.8MB
Again, Docker images are read only. We use docker run
to create a
Docker container from the images which is essentially a runnable virtual
machine.
3.2. Docker Python Dev Environment#
On Docker Hub there are a number of python images on Docker hub. The
python:3.10-slim-buster
includes a minimal linux virtual machine with
python already installed. In this section, we will discuss how to use this
image to create a simple development environment.
3.2.1. TLDR#
To create an interactive shell from the python:3.10-slim-buster
image,
use the following command.
$ docker run -itd 3.10-slim-buster
38e12b993c037e2447cd63d4d7e5cb64d9c4c03e0df37041136627daab0db222
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
38e12b993c03 python:3.10-slim-buster "python3" 33 seconds ago Up 31 seconds condescending_galois
$ docker exec -it 38e12 /bin/bash
root@38e12b993c03:/# python
Python 3.10.8 (main, Oct 25 2022, 05:40:26) [GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
Above, we created a docker container with ID 38e12
. We then list the
running containers. Then we execute bash inside the container which gives us an
interactive shell from which we can run python.
This gives us a basic python environment inside a docker container.
In the next few sections, we explore the run command and its flags more deeply.
3.2.2. Explore Docker Run#
Docker run lets you run a command in a new container [DRUN].
$ docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
Docker run first buts the container from an image and then executes a command. For example, we can write hello world to the console after the container has been built.
$ docker run python:3.10-slim-buster echo Hello, World!
Hello, World!
We can use docker ps
to list all the running containers and
docker ps -a
to list all containers including those that have been
stopped.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e6289b05d40d python:3.10-slim-buster "echo Hello, World!" 4 minutes ago Exited (0) 4 minutes ago quirky_varahamihira
The container exited as soon as the command echo Hello, World
was
executed. We can execute any shell command here, in-fact we can start a bash
shell.
$ docker run python:3.10-slim-buster /bin/bash
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7677b3ae48c8 python:3.10-slim-buster "/bin/bash" 6 seconds ago Exited (0) 4 seconds ago competent_jennings
However, we see that the shell exits instantly. We need a way to keep the container running such that we can give input to the shell. We can do this using interactive mode.
3.2.3. Exlore Interactive Mode#
Interactive mode keeps the standard input (STDIN
) of a container open
and allows us to execute commands in the container. Containers can be run in
interactive mode by using the -i
or --interactive
flags.
$ docker run -i python:3.10-slim-buster /bin/bash
ls
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
Running the command above puts us in a bash shell withing the container that we
can interactive with. For example, above we executed the ls
command
which lists the files and directories of the current directory.
This shell may not look familiar, to get a more familiar shell, we have to
allocate a TTY to the container using the -t
flag.
$ docker run -it python:3.10-slim-buster /bin/bash
docker run -it python:3.10-slim-buster /bin/bash
root@c6b8389f4237:/# ls
bin dev home lib64 mnt proc run srv tmp var
boot etc lib media opt root sbin sys usr
root@c6b8389f4237:/# exit
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c6b8389f4237 python:3.10-slim-buster "/bin/bash" About a minute ago Exited (0) 55 seconds ago fervent_merkle
The -t
flag gives a more familiar shell. Note however that when we exit
the container, we see the container also stopped. To prevent this, we run the
container in detached mode.
3.2.4. Exlore Detached Mode#
By default, docker run
runs a container in foreground mode. This means
that when the containers command has been executed, the container is stopped.
We can run the container in the background by using the -d
flag.
$ docker run -it python:3.10-slim-buster
85ad08af58b567f35ca367b67f5ab09ba7d454e0506c9952c8cc06902f3ed496
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
85ad08af58b5 python:3.10-slim-buster "python3" 2 minutes ago Up 2 minutes determined_chatelet
Here, the docker run
command returns the ID of the container and from
docker ps
, we see the container is running. Note, we have not included
the /bin/bash
command here.
So, we have a container that is running in the background but we have not been
put into a shell in the container. To create a shell in the container, we use
the docker exec
command.
$ docker exec -it 85ad /bin/bash
root@85ad08af58b5:/# echo Hello, World!
Hello, World!
root@85ad08af58b5:/# python
Python 3.10.8 (main, Oct 25 2022, 05:40:26) [GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
We give docker exec
flags -it
which, like before, runs the
command interactively with a pseudo TTY. We then give the ID of the container
we want the command to be executed in and finally the command
/bin/bash
itself. This starts an interactive bash shell that we can use
for development.
When we exit the shell, the container will still be running in the background.
To stop the container, use the docker stop
command.
3.3. Volumes#
Suppose you are are starting a project and you want to do your development inside a docker container. You could create a container like in the previous section and do everything inside the container. In particular you could store the files for your project inside the docker container. However, getting the files from the container onto the host machine can be tricky.
Alternatively, you could attach your project files to the container using a volume. Think of this as sharing files and data between the host machine and the container. This means any changes made to the volume files inside the container will be reflected in the files on the host machine. Also, if the container is stopped or deleted, the files still exist on the host machine so no data will be lost.
Suppose we have a simple project on our system with one file main.py
.
$ pwd
/home/user/my_project
$ ls
main.py
The file main.py
is a simple script that prints some text.
# main.py
print("This is main.py")
Let us create a python docker container and attach the my_project
directory to the container. This is done with the -v
flag.
$ docker run -itd -v /home/user/my_project:/my_project python:3.10-slim-buster
1f181219691a3837f1830db590e88d8f1644be251b5915687db523096dff93fc
The argument passed to -v
contains two paths. The first path before the
colon is the path to the directory (or file) on the host machine that we want
to attach to the container. The second path after the colon is the path to the
location where we want the directory (or file) to be stored in the docker
container.
Now, if we execute bash in the container, we should be able to navigate to our
main.py
file.
$ docker exec -it 1f18 /bin/bash
root@1f181219691a:/# cd /my_project
root@1f181219691a:/my_project# ls
main.py
root@1f181219691a:/my_project# python main.py
This is main.py
root@1f181219691a:/my_project# touch created_in_container.py
root@1f181219691a:/my_project# ls
created_in_container.py main.py
root@1f181219691a:/my_project# exit
$ docker stop 1f18
1f18
$ docker rm 1f18
1f18
Above, we navigate to the /my_project
directory and run the
main.py
script. We also create a new file called
created_in_container.py
from within the container and exit the
container. Finally we stop and delete the container.
Since we attached the my_project
directory to the container using
volumes, the created_in_container.py
file will exist on the host
machine.
$ cd /home/user/my_project
$ ls
created_in_container.py main.py
Despite having stopped and deleted the container used to create
created_in_container.py
, the file still exists on the host machine. One
can use volumes to attached many files to a container such that changes made
inside the container are reflected on the host machine.