Diving with the Whale III - Persisting Data & Dockerfiles

Ahmed AymanAhmed Ayman
7 min read

Remember when I said No persisting data with a bold font? well, I lied! ๐Ÿ˜…

Persisting Data

Containers are designed to be ephemeral and isolated from the host machine. This creates problems when data is involved.

But, there are two ways around it.

1. Mapping Volumes

Just the same as we mapped ports we can map directories from the host machine to a container directory, so anything that happens here happens there and vice versa.

Let's say we want to run the Ubuntu image and map its documents directory to a d

docker run -v :

if the container directory does not exist, it'll create one for you

after you run the command anything you put in this directory on the host machine appears in the container directory that's mapped to it, and vice versa!

pwd
# /home/ahmed 
# create directory contained 
mkdir contained
# docker run -v <host-directory>:<container-directory> <image>
docker run -it -v /home/ahmed/contained:/mnt/contained-vol ubuntu
# the proper way is to put it under /mnt/ but thats not really a rule
# -it to give us an interactive shell inside the container
# now check if the directory is actually there
cd /mnt/contained-vol/
# and WALAH!
# now let's create a file says "Hello From The CONTAINER Side" inside the container
echo "Hello From The CONTAINER Side" > hello.msg
# now let's head back to our host machine and check the message we got from inside this container
# _______________________________________________________________________________
# fire up a new terminal 
# get inside the shared directory
cd /home/ahmed/contained/
ls
# hello.msg
# yaay it's there
# let's read it
cat hello.msg
# Hello From The CONTAINER Side
# NICE! let's say hi back
echo "Hi From The HOST Side!" > hi-back.msg
# let's head back to our container shell session to check if we got the message
# _______________________________________________________________________________
# get back to the container terminal
ls 
# hello.msg  hi-back.msg
# let's read it, shall we?
cat hi-back.msg
# Hi From The HOST Side!

Now what happens if we stop and remove the container? can you guess?
Wild Idea: The data will persist!

Let's do it

# exit your container shell session 
exit

# display all running containers
docker ps 
# no thing running righ not

# display all containers
docker ps -a
# CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS                      PORTS     NAMES
# 043808ca3998   ubuntu    "/bin/bash"   14 minutes ago   Exited (0) 25 seconds ago             gallant_pascal

# remove the container
docker rm gallant_pascal
# gallant_pascal

# display all containers again
docker ps -a
# CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS                      PORTS     NAMES

# the container got removed
# now let's check our files in the contained directory
# get inside the shared directory
cd /home/ahmed/contained/
ls 
# hello.msg  hi-back.msg
# both of the files still exists! 
# we managed to persist data from docker container!

2. Docker Volumes

  • the second solution is Docker Volumes, which is the preferred one

  • These are volume mounts that are managed Docker

    1. CREATE

      Creates a new volume with the given name

    # docker volume create <name>
    docker volume create contained
  1. LS

    Lists all the volumes on your system

    docker volume ls
    # DRIVER    VOLUME NAME
    # local     contained
  1. INSPECT

    Gets info about the given volume

    docker volume inspect contained
    [
     {
        "CreatedAt": "2021-05-04T03:40:33+02:00",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/contained/_data",
        "Name": "contained",
        "Options": {},
        "Scope": "local"
      }
    ]
  1. PRUNE

    Removes all unused local volumes

    docker volume prune
  1. RM

    Removes one or more volumes

    docker volume rm contained

So how to attach a volume to a container?

# create a new-volume named contained
docker volume create contained 

# docker run -mount type=volume,source=<volume_name>,target=<mount-point-inside-the-container> <image>
docker run -it --mount type=volume,source=contained,target=/mnt/mounted-vol ubuntu

# the proper way is to put it under /mnt/ but thats not really a rule
# -it to give us an interactive shell inside the container
# now check if the directory is actually there
cd /mnt/contained-vol/

# and WALAH!
# now let's create a file says "Hello From The CONTAINER Side" inside the container
echo "HELLO FROM THE CONTAINER, I'M ATTACHED TO THE VOLUME \"CONTAINED\"" > hello.msg

# now let's head back to our host machine and check the message we got from inside this container
# _______________________________________________________________________________
# fire up a new terminal 
# get the mount point by inspecting the volume 
docker volume inspect contained | grep -i "mountpoint"
# "Mountpoint": "/var/lib/docker/volumes/contained/_data"

# now we know where the shared data is
# let's get in there
cd /var/lib/docker/volumes/contained/_data
ls
# hello.msg

# read the file content
cat hello.msg
#HELLO FROM THE CONTAINER, I'M ATTACHED TO THE VOLUME "CONTAINED"

# NICE! let's say hi back
echo "Hi From The HOST Side!" > hi-back.msg

# let's head back to our container shell session to check if we got the message
# _______________________________________________________________________________
# get back to the container terminal
ls 
# hello.msg  hi-back.msg
# let's read it, shall we?
cat hi-back.msg
# Hi From The HOST Side!

TASK

DO THIS TASK USING THE TWO OPTIONS, VOLUME MAPPING AND DOCKER VOLUMES

setup a script to send a log file (create a cron tab for example or copy log files every 1 minute) to the mapped volume or the docker volume.

Here we are! Docker Files

Dockerfile Reference

Docker File Best Practices

Docker files are specific instructions on how to build the docker image, things like libraries, environments, system updates, project files, and everything Docker Engine needs to build up the Docker image.

Diving%20with%20the%20Whale%20-%20Docker%20Day%20III%20-%20Persistin%20e00176bef9d94dea943699bd2f4cfee1/Dockerfile.jpg

Docker File Anatomy - Commands

Usually, the naming convention is Dockerfile or Dockerfile.ubuntu

FROM

MUST BE THE FIRST COMMAND IN THE DOCKERFILE

ONLY ONE FROM COMMAND

Specify the image to base your image on so you won't have to start from scratch. For example in the above Dockerfile, we're basing our image on a Python image that would have most of the libraries already installed and the environment is ready, so we don't need to do this from scratch!

FROM ubuntu:18.04

RUN

Runs Linux commands inside the container

For example, what is the first thing you do when you acquire a new Linux machine? YES, you update and upgrade the repository! so that's a thing you would normally want the docker image to run as a command

RUN sudo apt update

LABEL <key=value>

adding metadata like the author's data or info about the image

LABEL Author="Ahmed"

CMD [<"command">, <"argument-1">, ..]

ONLY ONE CMD COMMAND

Actually, I lied, you can have more than one CMD command, but only the last one will get executed!

Sets defaults for running a container

it tells the engine exactly how to run a container from this image if new commands are specified ( if any commands are specified it will overwrite the CMD command)
Only the last CMD will get

CMD ["python3", "app.py"]

ADD <host file(s)(or)directory(or)URL*> <path in the image*>

Copies files into the image from the host machine or remote URL into the image

ADD "https://www.notion.so/Diving-with-the-Whale-Docker-Day-III-97af6b7cda5249ab82df3656ea18e1da" /Documents

COPY <host file(s)(or)directory*> <path in the image*>

Copies files into the image from the host machine or remote URL into the image

COPY ~/myapp /

ENV <variable*>

Sets environment variables inside the image

ENV DATABASE_USER="dbuser"
# we can use or don't use the = sign
ENV DATABASE_HOST "host.com"

EXPOSE <port*/protocol>

Expose a port to the outer world!

for example, if we want to run a web app we need to expose port 80 for HTTP and perhaps port 443 for HTTPS

EXPOSE 80/tcp

ENTRYPOINT <command*>

Configure the container to run as an executable, only one ENTRYPOINT Is allowed.

ENTRYPOINT ["/bin/bash"]

WORKDIR

Sets the path of the working directory this changes the working directory to the path you're giving to it

WORKDIR /var/www/html
# now all the next commands (until the next RUN cd or WORKDIR) will be executed inside this dir

it's the same as

# RUN cd <path>
RUN cd /var/www/html

BUILD IT!

Diving%20with%20the%20Whale%20-%20Docker%20Day%20III%20-%20Persistin%20e00176bef9d94dea943699bd2f4cfee1/200.gif

now from our Dockerfile, we want to build the image that we can run to get a running container, so how can we build a Docker file

# change your directory to the path where your Dockerfile is
cd project/
# run the build command
docker build -t flask-app:latest .
# -t to specify a tag for your image 
# flask-app:latest is the tag we chose
# . is the path of the directory contains the docker file you want to build

TASK

  • Create a bash script that says hello from the container!

  • Then create a docker file based on Ubuntu, copy the script into the container and make the script run once you run the container.

  • Build the image!

  • Run and test your first image

  • Errors? debug and find the issue

  • Run again

  • Congratulations You Created Your First Docker Image! ๐ŸŽ‰

Advanced Topics

Create your own base image from scratch

0
Subscribe to my newsletter

Read articles from Ahmed Ayman directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ahmed Ayman
Ahmed Ayman

Software Engineer @Bosta