In this tutorial we will demonstrate how to build a container image and how to run a docker container, as well as how to persist data and exposing host ports to reach the container ports.
I will assume that you have docker installed.
Running a Container
Let’s start with the basics, running a container. To run a container, we instantiate a container instance from a container image. There are thousands of container images on a container registry such as docker hub, a example that we will be working with will be the ubuntu official image.
In the following command:
docker run -it ubuntu:latest bash
We are running a container from the ubuntu image, and we are using the latest version by specifying the image with it’s tag:
ubuntu:latest, we are also specifying that we want to attach the bash shell to a interactive shell with
-it which is short for
Once we run our command we will be placed inside the container, then we run the
hostname command to see the containers hostname:
docker run -it ubuntu:latest bash # hostname 0e7af919daa5
Because we attached the
bash shell to our session our container is running while we are inside the container, as soon as we exit the container, the container will be stopped and not be in a running state anymore. We can verify the behaviour by running
docker ps -l to see the info of the last running container:
# exit docker ps -l CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0e7af919daa5 ubuntu:latest "bash" 54 seconds ago Exited (0) 8 seconds ago stoic_bhabha
As you can see the container id matches the hostname, and that will always be the default behaviour of a container.
Running a Container in the Background
If the image we want to run a container from, does not have a process that allows the container to remain running, we can specify the command, something like
sleep 120 and run the container in detached mode. We will also name the container with
--name detached-container and once we run a container with a specific name, and we want to run it again, it will error due to the container name already exists, therefore we will pass the flag
--rm so it deletes the container on exit, so we dont have to manually do it:
docker run --rm -itd --name detached-container ubuntu:latest sleep 120 131f7e78903c6b8d40989148a4ec0f04f9def9db997564c56fdba897dec5df80
Now because we ran the container in detached mode, which is the
-itd, we can verify that the container is running, by using
ps and specifying a name filter:
docker ps -f name=detached-container CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 131f7e78903c ubuntu:latest "sleep 120" 2 seconds ago Up 2 seconds detached-container
Because we specified the command as
sleep 120 the container will only be running for 120 seconds, then it will exit. So let’s run the container again and specify a larger number to keep it running for longer:
docker run --rm -itd --name detached-container ubuntu:latest sleep 600
Now since its running, we can
exec into the container, we can either exec using the container name
--name or by specifying the container id. Let’s exec using the container id, therefore we need to get the id first:
docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f243d803cbd8 ubuntu:latest "sleep 600" About a minute ago Up About a minute detached-container
Now we can exec into the container by passing the container id:
docker exec -it f243d803cbd8 bash
Now we are placed inside the container, and we can use
ps and you will notice the sleep command is currently running:
root@f243d803cbd8:/# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 2516 592 pts/0 Ss+ 22:14 0:00 sleep 600 root 6 1.0 0.0 4116 3584 pts/1 Ss 22:17 0:00 bash root 14 0.0 0.0 5904 3096 pts/1 R+ 22:17 0:00 ps aux
exit from the container, you can similarly exec into the container by specifying the name that you provided for the container:
docker exec -it detached-container bash
Containers are stateless by design, so if you run a container and write data to the containers filesystem and you exit, that data is lost. Technically you can revive the data by starting the stopped container and commit the container id to the image, but it feels like a hack to me, and I treat the containers ephemeral.
A way to persist the data is by using volumes, or in the docker run commoand it will be with
-v which you will map the host’s filesystem path to the containers filesystem path.
So let’s say you want to persist data in the containers path under
/data, you will map the host path, lets say
/Users/ruan/data to the containers path
/data, so when you write content inside the container in
/data the data gets written on
/Users/ruan/data on the host, so when you exit the container and run another container with the same volume mapping, the data will be persisted.
Let’s run a container with the mentioned volume mappings:
docker run --rm -it -v /Users/ruan/data:/data ubuntu bash
Once we are inside the container, write data to the persisted location
/data and write data to a location that is not persisted, like
/tmp, then exit the container:
root@1fd34a6e57d7:/# echo "hi" > /data/test1.txt root@1fd34a6e57d7:/# echo "hi" > /tmp/test2.txt root@1fd34a6e57d7:/# exit
Now run a new container with the same volume mappings, and you should see the persisted directory’s data is persisted:
docker run --rm -it -v /tmp/data:/data ubuntu bash root@193d7f987a32:/# cat /data/test1.txt hi
And the non-persisted location is not accessible:
root@193d7f987a32:/# cat /tmp/test2.txt cat: /tmp/test2.txt: No such file or directory
exit the container, we can see that the file is present on our host’s filesystem:
cat /Users/ruan/data/test1.txt hi
You get different docker storage drivers which can make use of cloud block storage such as EBS, etc.
Exposing a Port
So let’s say you browsed on docker hub and found the nginx docker image, and you would like to run the container and access the nginx container port which will be port
80, from the host which is running docker.
We will be making use of exposed ports, which is referenced with
-p, you will bind the host port that you are connecting to, to the container port. So let’s say you want to access the Nginx container from your workstation on
http://localhost:80 to traverse to the container port on
80, your configuration will look like
But in this case port 80 is already running on my laptop, so we will open port 8080 to connect to the container on port 80:
docker run --rm -it -p 8080:80 nginx:latest
Now we can access the nginx container from our host on port
80, because the container is running in the foreground, I will open a new terminal tab, and use curl to test the connectivity:
curl -I http://localhost:8080/ HTTP/1.1 200 OK Server: nginx/1.19.9 Date: Mon, 05 Apr 2021 22:41:00 GMT Content-Type: text/html Content-Length: 612 Last-Modified: Tue, 30 Mar 2021 14:47:11 GMT Connection: keep-alive ETag: "606339ef-264" Accept-Ranges: bytes
And on the terminal where we ran the docker command, we can see the logs:
172.17.0.1 - - [05/Apr/2021:22:41:00 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.68.0" "-"
Building a Docker Image
So let’s say you want to run a slim base image, such as alpine, but you want to add files to the container or install packages, it’s not really practical to run the container and install packages, because when you exit your container you will lose your changes.
The answer is to build a docker image to the desired state of how you want to package up your container, then you can reuse it as many times as possible.
In this example we will build a basic container that just runs a bash command on the startup when we run the container, we first need to create a
Dockerfile which acts as the file with the instructions on how the container image will be built:
# we are using the alpine base image as a starting point FROM alpine:latest # install bash RUN apk --no-cache add bash # copy the bash script COPY run.sh /bin/run.sh # this will run when the container runs CMD ["/bin/bash", "/bin/run.sh"]
run.sh bash script:
echo "hello, world!"
Build the container image with the tag
docker build -f ./Dockerfile -t myfirstcontainerimage:v1 .
Im the above command if the
Dockerfile is in the current working directory, you don’t need to specify the
Dockerfile as it will expect it as that as default, then we specify the tag we build the container image as, and we specify the context directory where it should build from with
. which is the current directory.
Once our image has been built we can verify by running:
docker images REPOSITORY TAG IMAGE ID CREATED SIZE myfirstcontainerimage v1 31eb211e6355 4 seconds ago 7.76MB
As we can see our container image that exists on our local workstation is only 7.76MB small.
(Optional): If you sign up with docker hub, you can then log in with your credentials
docker login and push this image to the public container registry, but then when you build the image, you will tag it as the username and repo that you have on docker hub, as example:
-t myusername/myrepo:v1 and then push with
docker push myusername/myrepo:v1.
Run a container from the built container image:
docker run -it myfirstcontainerimage:v1 hello, world!
Containers are amazing, and you can speed up the process to get your environment running in no time, to find more docker images head over to:
And for most of the images, they are either documented on the page, or on their github repository.