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).