So far we've been focusing a lot on running containers and haven't much dug into building them. This is on purpose because most of benefit of containers for developers comes from the running of containers. If you learn one thing, it should be how to run them. In fact I'll event venture to say that most developers really only ever need to know how to run them. But you, you're going to learn how to write them. It's an extra superpower.

That said, let's learn to build our own containers. We'll again be using Docker for this though there are other ways to do this. Docker has a special file called a Dockerfile which allows you to outline how a container will be built. Each line in a Docker file is a new a directive of how to change your Docker container.

A big key with Docker container is that they're supposed to be disposable. You should be able to create them and throw them away as many times as necessary. In other words: adopt a mindset of making everything short-lived. There are other, better tools for long-running, custom containers.

The (imperfect) analogy that people use sometimes is your containers should be "cattle, not pets". You design containers so you can easily create and destroy them as much as necessary. The analogy here is that you name your pets and take special care of them whereas you have a thousand cattle and can't name or take special care of them, just the herd.

Let's make the most basic Dockerfile ever. Let's make a new folder, maybe on your desktop. Put a file in there called Dockerfile (no extension.) In your file, put this.

The most basic Dockerfile-based Container

FROM node:20

CMD ["node", "-e", "console.log(\"hi lol\")"]

⛓️ Link to the Dockerfile

The first thing on each line (FROM and CMD in this case) are called instructions. They don't technically have to be all caps but it's convention to do so so that the file is easier to read. Each one of these instruction incrementally changes the container from the state it was in previously, adding what we call a layer.

Let's go ahead and build our container. Run (from inside of the directory of where your Dockerfile is)

docker build .

You should see it out put a bunch of stuff and it'll leave you with the hash of an image. After each instruction, you'll see a hash similar to the ones we've been using for the IDs for the containers. You know why that is? It's because each one of those layers is in-and-of themselves a valid container image! This ends up being important later and we'll discuss it in a bit.

Our container has two instructions in its Dockerfile, but actually it has many, many more. How? The first instruction, FROM node:20 actually means start with the node container. That container itself comes from another Dockerfile which build its own container, which itself comes from another Dockerfile, which comes ultimately from the Debian image.

This is something very powerful about Docker: you can use images to build other images and build on the work of others. Instead of having to worry about how to install Debian and all the necessary items to build Node.js from its source, we can just start with a well-put-together image from the community.

Okay, so we start with node:20 and then we add the CMD instruction. There will only ever be one of these in effect in a Dockerfile. If you have multiple it'll just take the last one. This is what you want Docker to do when someone runs the container. In our case, we're running node -e "console.log('hi lol')" from within the container. node -e, if you don't know, will run whatever is inside of the quotes with Node.js. In this case, we're logging out hi lol to the console.

You can put CMD node -e "console.log('hi lol')" as that last line and it'll work but it's not the preferred way of doing it. This won't actually go through bash which itself is simpler and usually safer. I do it this way because the docs strongly encourage you to do it this way.

So, in essence, our containers nabs a node:20 container and then when we have it execute a node command when you run it. Let's try it. Grab the hash from your build and run

docker run <ID>

It's a little inconvenient to always have to refer to it by ID, it'd be easier if it had a name. So let's do that! Try

docker build . --tag my-node-app ## or -t instead of --tag
docker run my-node-app

Much easier to remember the name rather than a hash. If you want to version it yourself, you can totally do this:

docker build -t my-node-app:1 .
docker run my-node-app:1

Now change your Dockerfile so that it logs out wat instead of hi lol. After you do that.

docker build -t my-node-app:2 .
docker run my-node-app:2
docker run my-node-app:1

You can version your containers and hold on to older ones, just in case!