Docker 101: Understanding Images, Containers, Volumes & Networks

Docker 101: Understanding Images, Containers, Volumes & Networks

What is Docker?

But it works on my machine – every software engineer at some point in their career. Programming is simple, but getting the same results everywhere isn't. You're about to deploy a website you've been working on for weeks – just to find out that, the deployment is harder than the actual programming. Is there a way for us to fix this? To never face dependency mismatches, rare quirks of a hosting server or anything of the sort?

There is, and it's called Docker.

Docker is a containerisation platform that allows you to build and share 'images'. Why is that so important? When you run an image, it becomes a 'container' that works identically regardless of the machine it's on. This solves the problem of dependency mismatches you face when moving code from development to production. That's how Docker saves the day. No more "it works on my machine".

Images

Base Image

A Dockerfile will often start with: FROM xyz.

xyz in this case being the base image. You can find base images on the Docker Hub. Base images can include: node, nginx, alpine (Linux), Ubuntu,... There is a lot of choice – and more often than not you'll find an image that suits your needs. E.g. if you're running a node back-end, the node image comes with npm installed. You can also start from absolute scratch with a base image such as Ubuntu but you'd need to do a lot of setting up.

Your own images

Docker images are the blueprint of a container. They include the necessary setup that is required for containers to work. Such as the base image on which they run, the directories, the files on them, the commands that need to be ran at build time and the ones at run time. These instructions are all packed together in a Dockerfile.

FROM NODE # base image

WORKDIR /app # working directory

COPY . . # copy ALL files from here to there

RUN npm install # install dependencies at build time

EXPOSE 80 # port 80 should be accessible

CMD ["node", "entry.js"] # execute this when the container runs

Something very important to consider is that every line in a Dockerfile is called a "layer". Layers build on top of one another. If a layer hasn't changed, it will be cached by Docker to increase performance. If you're developing a JavaScript application and the dependencies haven't changed why should docker perform the time-sensitive npm install again? It shouldn't. That's why it's essential to separate those tasks (copy package.json -> npm install and then copy over all of your files), if your source code changes, it won't need to run npm install anymore.

Commands

Command Explanation
FROM xyz Selects your base image
WORKDIR /app Sets your default work directory, e.g. where everything will be done in the container
COPY X [TO] Y Copy command, often used with dots. Copies everything from the host directory to the container's work directory
RUN This runs build time commands
EXPOSE Exposes a port on the container to the outside world
CMD The command that should be run when the container is executed

Containers

Once you're happy with your set-up, it's time to build your image. This can be done using the docker build -t myimagename . command in the folder where your Dockerfile is located.

You've built your image and now wish to run the image. This will create a container. This can be done through the docker run --name mycontainername myimagename.

If you've previously exposed a port however you still need to tell docker to which port it should map on your device, this can be done by adding the argument -p 3000:80. In this example the docker port we exposed will be mapped to the port 3000 on our machine.

However this is often not sufficient for most projects. That's where volumes and networks come into play.

Volumes

Volumes in Docker are a nice way of keeping your container's data saved.

Anonymous volume

As its name indicates this volume is anonymous, is assigned a random UUID and it can be defined inside of a Dockerfile however, this isn't recommended. Their primary use might be for dependencies. Although anonymous volumes aren't shared by default between containers, it is possible to do so manually. Anonymous volumes are managed by Docker hence only containers can edit the data that is saved on them. The path that is given is also the folder on the container host whose data will be saved.

VOLUME /data

Named volume

Named volumes are very similar to anonymous volumes except they cannot be defined in the Dockerfile. To create a named volume you must add -v volumename:/data to your docker run command. Once, again these are managed by Docker hence only containers can edit the data that is saved on them. However sharing this volume becomes easier as it has a name.

docker run -v volumename:/data node

Bind mount

The bind mount is of crucial importance to any developer. This is the only volume that isn't managed by Docker. Meaning you can alter your source code or data locally and it will be reflected inside of your container. This paired with your source code and a monitor that checks for changes and relaunches your app is a quality of life upgrade you never knew you needed. Similarly to the named volume you must once again do this via the command line by adding -v "/home/local/sourcecode:/code" this will assign your local folder to the /code folder on the container.

docker run -v "/home/sourcecode:/code"

Networks

Accessing the internet is something that docker allows you to do without a single problem. However, if you want your container to communicate with a service that is running on your machine things get a bit tricky.

localhost will not function in your code anymore. Instead use host.docker.internal this will automatically resolve to localhost.

If you're running multiple containers that need to interact with one another e.g. a MySQL server and a node application things get a bit tricky.

Docker has a handy 'network' command that allows you to create a network. If you add a container to a network you essentially allow them to find each other. The command to create a network is rather simple: docker create network <name>. To add one to a network you must specify this when running the container through this argument: --network <name>. Having done this on the containers that you want to communicate with each other, you won't have to enter an IP-address or anything of the sorts in your source code but rather the name of the container. Docker will automatically resolve this to an IP-address for you.

Next-up

In the second part of this series you will find out how we can go from lengthy cli commands to a configuration file (docker compose).