
You are using Containers right?
You are using Containers right?
What are Containers?
Containers are like (but not actually) little virtual machines. It is important to note that these are not actually virtual machines, but if you’re familiar with that idea of basically running multple systems on the same computer device, then this is similar, but it is like a really lightweight way of doing virtual machines.
Containers are a vendor neutral way of describing the environment, you may be more familiar with the name and brand Docker which can be credited with driving the popularity of containerisation.
So they are not Virtual Machines, what are they…
Containers are a ways to describe, run and manage highly specific environments, it can be as simple as telling your container provider that you want to run a MySQL server, the container engine (such as Docker) will go away and find the latest stable docker image which can, in almost all cases, include everything you need to run an instance of MySQL, such as a very light Linux installation, the MySQL app and some common ports or services active/open/etc. It can also get very complex very quickly and we might dive in to some of that complexity as we go through this article.
A Container is a standard unit or Package for delivering applications with a virtual environment to run that application. So it is basically like buying yourself a laptop that you only run a single application from it. Or perhaps a Raspberry Pi, that you may buy, setup and leave running on your home network to act as a Proxy Server for example.
Standard Unit…
By using Containers, we have the ability to ensure the application looks and acts the same everytime we deploy it (be that to Production, Test, Development, etc). You can be very specific in how you define the container and by being specific, you can ensure that each time you build or deploy that application, it should look and act the same regardless of who deploys it or where it was deployed, specifically, wroking on a MySQL container that lives on top of a MacOS powered computer should be the same as if I deployed the container on a Windows computer or even some virtual machine living inside a cloud, how it acts, how it looks and how you interact with it will be the same regardless of the host operating system.
but it isn’t a Virtual Machine?
On a Linux laptop, a Windows laptop or a MacOS laptop. I could use something like Oracle VirtualBox as a hypervisor on those laptops, the hypervisor looks and acts the same (or very similar) on each of those machines, and then I can install a Virtual Machine in each of those hypervisors, like, for example, an Ubuntu server, within which I install MySQL. In theory I should have the same experience regardless of the host operating system (Windows, Linux, MacOS) and in fact I probably will have the same experience. Underlying system hardware may have some impact on the experience but in most cases I won’t notice it.
So from this point of view a Virtual Machine and a Container deployment are very similar. However, the way virtualisation works is that it replicates your system hardware, so the installation of Ubuntu on the Mac sees a CPU, it sees some RAM, it sees the sound device, the network device, etc. Often the Virtual Machine implements a virtual version of the hardware, so looking through a Windows Device Manager in a (Windows) virtual machine will likely look different from the device manager on the host machine, but the virtual machine is based on a virtual copy of your hardware. Running an application inside a virtual machine involves a lot more setup in most cases (to clarify, you can find many pre-built virtual machines where the configuration has been taken care of for you). Virtual Machines also use a lot more resources, typically. Generally, looking after Virtual Machines is a more admin intensive task than looking after containers.
Deploying a container usually includes using a highly optimised linux installation, if you are deploying MySQL, then MySQL doesn’t need to know about hardware related to sound-cards, for example, so the highly optimized verion of Linux will not attempt to recreate that hardware environment within the container. This is one of the reasons it is so easy to move a container to an entirely different system.
Benefits of Containers
Easy
They are easy to setup and startup. You pull your base image(s) and run the container.
Rollback
They’re also quick and easy to rollback. So perhaps we pull an image at version 2.1.4, it has been working fine for months, but the developer has moved to image 2.1.5, we can pull that image, however, perhaps that version doesn’t work withour application, we can roll back very quickly to version 2.1.4 giving us time to work on a fix for 2.1.5 or giving the developer time to work on a fix that we might use in a future build.
Portability
A container works almost the same no matter what the host looks like or where that host is located.
Microservices
This is where we use individual containers for loosly coupled services. So we could use a container that implements the entire app, that is completely fine for most users/applications. But we could also split our application into multiple containers, like perhaps one to deal with the frontend and one to deal with the backend, this not only allows us to upgrade parts of the system independantly but it allows us to scale parts of the system that are under the greatest load.
A previous employer of mine used multiple frontend containers based on their customers, so they could upgrade an individual customers implementation of the application without impact other customers, for example. As it happens, they also used this approach to upsell, different versions of their application included features that were only available to customers that subscribed to a specific version of the application which was an interesting way to provide those differences.
Agility
I already mentioned scale, indeed, a well defined container allows for instances to be deployed or dropped as needed, this not only increases speed (or agility) to respond to external events, but it can also allow you to reduce Cloud subscription costs as you don’t need to pay for computing power that you don’t use and when you do need that extra power, you’re only paying for it for the duration required (i.e. perhaps you need to ramp up computing power just after a TV advert goes out, perhaps for the next hour or so you see a lot more traffic on a website, but then the traffic falls back, or perhaps you see seasonal surges, again, ramping up for a seasonal shopping event but dropping those containers when the traffic returns to normal can be a very cost effective way to manage your environment).
Snapshots
A Container Image is basically a Snapshot of a configured piece of software, again, this might make more sense to those that are familiar with virtual machines, where we may take snapshots of the system before or after a major event, in fact as I write this, I recall Windows including something like this in some of its operating systems, the idea being that you could always revert to a ‘safe’ image or a restore point. A container image is kind of the same thing, someone has configured a system and then taken a snapshot of it, when you call that image, you call that snapshot of a working system or application.
Of course just because it worked for the system builder does not ensure it will work for your very specific use case, so you may need to go on and do more configuration but those configurations form part of the description or the recipe of your application.
Pulling a standard container image and making no further changes to it might well be enough to meet your needs, but often, there will be some configuration or other images that combine to make the application you want, in this case you build a recipe card that might pull several images, each of which is a snapshot of a working application or component.
Immutability
One interesting thing is that a container image is immutable, which is to say, it is read-only, you can’t change the image. That doesn’t mean you can’t customise it, you can. But you can’t pull XYZ v2.4, uninstall bits of it and replace them with YYZ v1.2. Again, this does not mean you cannot customise the images, you definitely can do that, but what you have now, is not XYZ v2.4.
Layers
What you end up with is likely a collection of images, that are layered on top of one another and when combined, they provide you with an application or environment as required.
In the following example, I am pulling something called nginx
@rnddave ➜ /workspaces/containers (main) $ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
dad67da3f26b: Pull complete
3b00567da964: Pull complete
56b81cfa547d: Pull complete
1bc5dc8b475d: Pull complete
979e6233a40a: Pull complete
d2a7ba8dbfee: Pull complete
32e44235e1d5: Pull complete
Digest: sha256:6784fb0834aa7dbbe12e3d7471e69c290df3e6ba810dc38b34ae33d3c1c05f7d
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
Each of these lines:
dad67da3f26b: Pull complete
Is a layer in a pre-defined image.
Container images are made up of multiple layers, each of which corresponds to a step in the image’s creation (like FROM, RUN, COPY, etc. in a Dockerfile). These layers are:
- Cached and reused across images when possible
- Pulled individually during docker pull
- Stacked together to form the final image
Dockerfile
A Dockerfile is like the recipe card that describes how to build the environment you need. Whilst it is not always true today, each instruction in a Dockerfile could be another layer within your container. Traditionally, this was the case, each instruction was a new layer, but it doesn’t really work like that now, some instructions will create a new layer, some will not.
In the example above, I pulled the nginx container image, which consisted of many layers. Nginx is an opensource application and we can go and have a look at the dockerfile that was called when I asked for nginx:
👉 https://github.com/nginxinc/docker-nginx
We can find and review the dockerfile, it describes the installation such as installing a slim version of Debian, setting up users and groups as well as any other required applications etc.
Dockerfile common commands:
## 🔧 Basic Instructions
| Command | Description |
|---------------|-------------|
| `FROM` | Specifies the base image (e.g., `FROM node:18`) |
| `RUN` | Executes a command in a new image layer (e.g., `RUN apt-get update`) |
| `CMD` | Sets the default command to run when a container starts (e.g., `CMD ["node", "index.js"]`) |
| `COPY` | Copies files/folders from host into the image (e.g., `COPY . /app`) |
| `ADD` | Like `COPY`, but can also handle URLs and extract tar files |
| `WORKDIR` | Sets the working directory for subsequent commands |
| `EXPOSE` | Documents which port the container listens on (e.g., `EXPOSE 3000`) |
| `ENV` | Sets environment variables (e.g., `ENV NODE_ENV=production`) |
---
## 🛠 Advanced & Helpful
| Command | Description |
|---------------|-------------|
| `ENTRYPOINT` | Configures the main container command (can be combined with `CMD`) |
| `ARG` | Defines build-time variables (e.g., `ARG VERSION=1.0`) |
| `VOLUME` | Creates a mount point for external volumes |
| `LABEL` | Adds metadata (e.g., `LABEL maintainer="you@example.com"`) |
| `USER` | Sets the user to run commands as (e.g., `USER node`) |
| `HEALTHCHECK` | Defines a command to check if the container is healthy |
---
## 🧪 Example Dockerfile
```Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
Docker History
We can also use docker history
to understand what happened:
@rnddave ➜ /workspaces/containers (main) $ docker history nginx
IMAGE CREATED CREATED BY SIZE COMMENT
1e5f3c5b981a 2 months ago CMD ["nginx" "-g" "daemon off;"] 0B buildkit.dockerfile.v0
<missing> 2 months ago STOPSIGNAL SIGQUIT 0B buildkit.dockerfile.v0
<missing> 2 months ago EXPOSE map[80/tcp:{}] 0B buildkit.dockerfile.v0
<missing> 2 months ago ENTRYPOINT ["/docker-entrypoint.sh"] 0B buildkit.dockerfile.v0
<missing> 2 months ago COPY 30-tune-worker-processes.sh /docker-ent… 4.62kB buildkit.dockerfile.v0
<missing> 2 months ago COPY 20-envsubst-on-templates.sh /docker-ent… 3.02kB buildkit.dockerfile.v0
<missing> 2 months ago COPY 15-local-resolvers.envsh /docker-entryp… 389B buildkit.dockerfile.v0
<missing> 2 months ago COPY 10-listen-on-ipv6-by-default.sh /docker… 2.12kB buildkit.dockerfile.v0
<missing> 2 months ago COPY docker-entrypoint.sh / # buildkit 1.62kB buildkit.dockerfile.v0
<missing> 2 months ago RUN /bin/sh -c set -x && groupadd --syst… 118MB buildkit.dockerfile.v0
<missing> 2 months ago ENV DYNPKG_RELEASE=1~bookworm 0B buildkit.dockerfile.v0
<missing> 2 months ago ENV PKG_RELEASE=1~bookworm 0B buildkit.dockerfile.v0
<missing> 2 months ago ENV NJS_RELEASE=1~bookworm 0B buildkit.dockerfile.v0
<missing> 2 months ago ENV NJS_VERSION=0.8.10 0B buildkit.dockerfile.v0
<missing> 2 months ago ENV NGINX_VERSION=1.27.5 0B buildkit.dockerfile.v0
<missing> 2 months ago LABEL maintainer=NGINX Docker Maintainers <d… 0B buildkit.dockerfile.v0
<missing> 2 months ago # debian.sh --arch 'amd64' out/ 'bookworm' '… 74.8MB debuerreotype 0.15
Where you see this <missing>
it just means that we don’t know what the original layer id was, but it doesn’t really matter, so don’t get hung up on that bit.
What is Docker?
Docker is A Container Engine, a way to manage containers, it is not the only way, but they helped make this popular and a lot of what Docker did in the early days have gone in to something called the Open Container Initiative.
Docker is very popular and probably the biggest name that I know of in this space. Docker has a service called the Docker Daemon, this talks to another daemon called the Container Daemon. A daemon is basically a server or service.
Here are some common Docker CLI commands:
## 📦 Images
| Command | Description |
|-------------------------------------|-------------|
| `docker pull nginx` | Download an image from Docker Hub |
| `docker build -t myapp .` | Build an image from a Dockerfile in the current directory |
| `docker images` | List all local images |
| `docker rmi image_name` | Remove a local image |
| `docker tag myapp myuser/myapp` | Tag an image for pushing to a registry |
| `docker push myuser/myapp` | Push an image to a registry |
---
## 🚀 Containers
| Command | Description |
|--------------------------------------------------|-------------|
| `docker run nginx` | Run a container from the nginx image |
| `docker run -it ubuntu bash` | Run a container interactively with a shell |
| `docker run -d -p 8080:80 nginx` | Run nginx in detached mode and map port 80 to 8080 |
| `docker ps` | List running containers |
| `docker ps -a` | List all containers (including stopped ones) |
| `docker stop container_id` | Stop a running container |
| `docker start container_id` | Start a stopped container |
| `docker restart container_id` | Restart a container |
| `docker rm container_id` | Remove a container |
| `docker exec -it container_id bash` | Open an interactive shell in a running container |
| `docker logs container_id` | View logs from a container |
---
## 🗂 Volumes & Networks
| Command | Description |
|------------------------------------|-------------|
| `docker volume create myvolume` | Create a volume |
| `docker volume ls` | List volumes |
| `docker network ls` | List Docker networks |
| `docker network create mynet` | Create a custom network |
---
## 🧹 Cleanup
| Command | Description |
|-----------------------------------|-------------|
| `docker system prune` | Remove all unused containers, networks, images, and volumes |
| `docker image prune` | Remove unused images |
| `docker container prune` | Remove stopped containers |
| `docker volume prune` | Remove unused volumes |
---
## 🧪 Compose (if using docker-compose)
| Command | Description |
|----------------------------------|-------------|
| `docker compose up` | Start services defined in `docker-compose.yml` |
| `docker compose up --build` | Build and start services |
| `docker compose down` | Stop and remove containers, networks, etc. |
| `docker compose ps` | List containers managed by Compose |
---
✅ **Tip**: Use `docker --help` or `docker COMMAND --help` to explore more options.
Docker CLI Flags
Some of the common flags we may pass with our Docker CLI commands:
## 🧱 `docker run` – Run a Container
| Flag | Description |
|-------------------------------|-------------|
| `-d` | Run container in detached mode (in background) |
| `-it` | Interactive terminal (often used with shells) |
| `--rm` | Automatically remove container when it exits |
| `--name mycontainer` | Assign a custom name to the container |
| `-p 8080:80` | Map host port 8080 to container port 80 |
| `-v volume_name:/path` | Mount a volume |
| `--network mynetwork` | Connect to a specific Docker network |
| `--env VAR=value` or `-e` | Set environment variable inside container |
| `--entrypoint /bin/sh` | Override the image’s default entrypoint |
| `--hostname myhost` | Set a custom hostname for the container |
| `--cap-add NET_ADMIN` | Add Linux capabilities (advanced) |
---
## 🏗 `docker build` – Build an Image
| Flag | Description |
|-------------------------------|-------------|
| `-t myimage:tag` | Tag image with a name (and optional version) |
| `-f Dockerfile.dev` | Use a specific Dockerfile |
| `--build-arg VAR=value` | Pass a build-time variable |
| `--no-cache` | Don’t use cache when building |
| `--platform linux/amd64` | Set the target platform (useful for cross-platform builds) |
| `.` | Build context (usually the current dir) |
---
## 📋 `docker ps` – List Containers
| Flag | Description |
|-------------|-------------|
| `-a` | Show all containers (not just running) |
| `-q` | Show only container IDs |
| `--format` | Format the output using a Go template |
---
## 🧪 `docker exec` – Run Command in a Running Container
| Flag | Description |
|---------|-------------|
| `-it` | Interactive terminal (useful for shells) |
| `-u` | Run as a specific user (e.g., `-u root`) |
---
## 🧹 `docker system/image/container/volume prune`
| Flag | Description |
|-----------|-------------|
| `-f` | Force prune without confirmation prompt |
| `--all` | Also remove dangling AND unused images or containers |
---
## 🧪 `docker compose` (v2+ syntax)
| Flag | Description |
|----------------------|-------------|
| `--build` | Rebuild images before starting services |
| `--profile dev` | Use specific profile(s) |
| `-f file.yml` | Use a custom Compose file |
| `--env-file .env` | Use a specific env file for variables |
| `--detach` or `-d` | Run services in the background |
---
✅ **Tip**: Most Docker commands support `--help` for a complete list of flags, like:
```bash
docker run --help
I think we will wrap this up for now as we have covered a lot, I will probably write some more docker articles in the weeks that follow. I hope you found something useful in this post.