What is Docker
Docker is a system which manages containers. It can do sooo much and it’s your best friend when it comes to DevOps. Reading the following sections will make it clear just how awesome Docker is.
What are containers you ask
Containers are isolated boxes where applications can run. They are like a virtual machine in that they’re isolated and completely separate. This means that if malware got into a container it shouldn’t be able to poison the host system. However containers are much slimmer. They share the kernel with the host and then only run the bare bones they need which makes them super lightweight compared to a virtual machine.
What are images?
Images are blueprints for a container. The are essentially templages for what files to copy over, what packages to install, commands to run etc to make the environment just right for your application to run. Images are created from Dockerfiles.
Registries
Registries hold images and allow you to share and pull images. Often times you’ll want your image to start with a base image and then build on top of that. For instance Ubuntu which has lots of packages already installed ready to go and is configured in an easy to use way. You can then add to it. Let’s take a look at a Dockerfile to consolidate this:
# This file is called Dockerfile
# Not Dockerfile.config or Dockerfile.txt just Dockerfile
# It typically lives in the root of your project
FROM node:lts-alpine
EXPOSE 3000
USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app
COPY ./package.json .
RUN npm install
COPY . .
CMD [ "npm", "run", "dev" ]
This one works from the base image of node:lts-alpine. This is an image which already has node installed and configured. Alpine is a very stripped down and bare bones distribution of Linux.
It then says EXPOSE 3000 – this doesn’t really do anything (for our purposes) but is more of a note for those running the Docker container to know it runs on port 3000.
It then sets the user to node, sets the working directory (basically the home directory where it’ll start) to /home/node/app before copying the package.json file into the workdir and installing the dependencies. Finally it copies everything from the current working directory of the host machine to the workdir on the docker container. This will be project files for node.
Last but not least it has a CMD command is basically the command to run automatically when the container starts up.
As you can see commands are broken up into a list. Basically wherever you’d add a space you put it into a list. For instance
echo Hello Worldwould becomeCMD ["echo", "Hello", "World"]
Building your image
We could now build this into an actual image we can use and reference (like we did node:lts-alpine) and then run it.
To build your image go into the root of your project where your Dockerfile lies and run sudo docker build -t nodeapp ..
This will automatically look for a Dockerfile in the current working directory because you specified sudo docker build . and . means the current directory. It’ll then build the image. We used the -t flag to tag the image with a name of “nodeapp” but we could call it anything.
Running your Docker container
There’s a couple of different ways to run a Docker container. The first is with docker cli and the second is with docker compose.
To run your node app with the docker cli we’ll run sudo docker run -p 3000:3000 nodeapp. This will run the app and show us the output from the CMD. If you press Ctrl+C or close your terminal it will interrupt the program and it will stop. To run this in the background use the -d flag. This stands for “detached” so sudo docker run -p 3000:3000 -d nodeapp. Fantastic!
Ports
What is the -p 3000:3000 about though? Well it’s rather simple. We spoke about ports earlier and this is what they are. The value on the left is the port on our host machine. The value on the right is the port running inside the Docker container itself. Our node app is running on port 3000 within the Docker container however by default it can’t access our machine because Docker containers are closed off and isolated. What’s great about this is we can effectively port forward port 3000.
The great thing about this method is that we can actually map the ports. Although it’s running on port 3000 inside our docker container, this doesn’t mean it has to run on port 3000 on our machine. We could run sudo docker run -p 8080:3000 -d nodeapp and it would map port 3000 inside the docker container to port 8080 on our host machine. This means that if we went to “localhost:8080” on our host machine we’d see our node development app. How cool is that!?
SUPER IMPORTANT Docker bipases your firewall by default. Not your router or Cloud firewall but your device one. It’s quite complex to explain why but there’s a simple patch. When you publish ports prefix then with the loopback address
127.0.0.1:. For instancesudo docker run -p 127.0.0.1:8080:3000 -d nodeapp. This binds it to localhost and ensures it’s only running locally on your machine. What it’s saying is “Publish me to 127.0.0.1:8080” or “localhost:8080”. Remember these are loopback addresses which point to your machine’s IP and are only used locally. It then says “map this 127.0.0.1:8080 to port 3000 in the Docker container”. Without the127.0.0.1an implict IP of0.0.0.0is applied. This is basically a wildcard and it means “expose me everywhere”.
Docker compose
Docker compose allows us to set up all this in a file and then run it. Let me explain.
Let’s create a file in our projects root directory called docker-compose.yaml.
Inside it is the following:
# docker-comopose.yaml
---
services:
myamazingapp:
image: nodeapp
ports:
- 8080:3000
Now all we have to run is sudo docker compose up and it’ll find our compose file and then start the container up! How miraculous!
We can even do sudo docker compose up -d to run it in detached mode.
Container name DNS routing
For those of you already using Docker this is magic! Let’s say we have a node app with postgres. The backend speaks to Postgres and then Postgres sends the data back to the node backend which forwards it onto the client. How do we get two docker containers (the node backend and the postgres instance) to communicate easily and securely?
Well you might think of the following solution:
---
services:
nodeapp:
image: mynodeapp
ports:
- 127.0.0.1:3000:3000
restart: unless-stopped
database:
image: postgres
ports:
- 127.0.0.1:5432:5432
Now I’d like to stress that this isn’t actually how you setup Postgres, you need to no more than that but this is for demontration purposes.
In your node app code you could have node connect to 127.0.0.1:5432 or localhost:5432 right? Well the answer is no. Because the containers are isolated so even though you publish the ports to the host you wouldn’t be able to do this because the Docker container can’t access your machines localhost, it has it’s own because it’s isolated right.
There is a workaround:
---
services:
nodeapp:
image: mynodeapp
ports:
- 127.0.0.1:3000:3000
restart: unless-stopped
network_mode: "host"
database:
image: postgres
ports:
- 127.0.0.1:5432:5432
network_mode: "host"
Here we’ve specified network_mode: "host" which says “The docker container now has the same network as the host”. This means it can access everything on the host and when we reference 127.0.0.1 it will be the loopback address of the host machine. These two containers can now communicate 😀
But that is a horrendous idea so never do this!! There’s also Docker IP addresses which sometimes are necessary but that’s generally a bad idea for JS developers purposes and docker networking can get rather complicated.
Why are we blocking up ports on our host system? Docker can actually communicate using something called Docker networks.
Docker networks are very special, they allow containers added to them to speak to each other. So this means we could add nodeapp and postgress to the same docker network and then they’d be able to speak to each other internally without having to poke holes in our host system.
To create a network in Docker run sudo docker network create mynetworkname. I won’t get too much into this but you can do a lot with networks!!
Now we can add both the Docker containers to the network and they can communicate via their names:
---
services:
nodeapp:
image: mynodeapp
ports:
- 127.0.0.1:3000:3000
restart: unless-stopped
networks:
- mynetworkname
database:
image: postgres
ports:
- 127.0.0.1:5432:5432
networks:
- mynetworkname
networks:
mynetworkname:
type:
external: true
Now they’re part of the same network. Yay \o/
They can now speak to each other. So the nodeapp could connect to database:5432 and that would take it straight to the database container and the port Postgres is running on. In simple terms Docker uses a DNS router here to lookup database in the network and replace it with the IP of the container.
We can now remove the expose Postgres port as they can speak to each other now.
---
services:
nodeapp:
image: mynodeapp
ports:
- 127.0.0.1:3000:3000
restart: unless-stopped
networks:
- mynetworkname
database:
image: postgres
networks:
- mynetworkname
networks:
mynetworkname:
type:
external: true
Now technically we don’t need the to define the networks here because both containers are in the same docker compose file. When you run docker compose it automatically adds all the containers in a compose file to a shared network. So technically the following would be suffice:
---
services:
nodeapp:
image: mynodeapp
ports:
- 127.0.0.1:3000:3000
restart: unless-stopped
database:
image: postgres
However…it’s good practice to define your networks.
You can define new networks in your Docker compose file. It’s also possible to add a single container to multiple networks.
Now we don’t have to expose our Postgres instance at all, it’s secure and only accessible by the node backend container. Brilliant.
Some additional notes
There’s so much more you can do with Docker such as restarting automatically if it fails (basically if the node app or the CMD it’s running stops or crashes), and sooooo much more. Please read this Docker guide for a full, comprehensive rundown on Docker.
What’s a DNS server?
Earlier we mentioned about Docker DNS routing shinanigans. But what is a DNS server?
Domain Name Servers (DNS) are simple concepts. Essentially when you go to a site such as “google.com” your computer doesn’t know where the server that serves “google.com” is. As a result it sends “google.com” to a DNS server which has a list of all the domains and their respective IP addresses. It then looks up the domain “google.com”, finds the associated IP address and then sends it back to your machine. Your machine then makes a request to Google’s server via it’s IP address and requests the web page.
In practice they get very complicated!! Fun fact, DNS servers will share IP addresses. There’s not much else on the internet that shares IP addresses.
Awesome self hosted
Meet Awesome Self hosted. A list of several easy to host applications and services for almost anything you could ever need. They’re all free too. Now you know Docker settings these up is as simple as sudo docker compose up -d.
There’s a nice looking site https://awesome-selfhosted.net or a GitHub README with a markdown list at https://github.com/awesome-selfhosted/awesome-selfhosted.
Let’s try hosting that Enclosed software we mentioned earlier. Add this Docker compose file somewhere:
services:
enclosed:
image: corentinth/enclosed
ports:
- 127.0.0.1:8787:8787
volumes:
- ./data:/app/.data
restart: unless-stopped
Now just run sudo docker compose up -d and checkout “localhost:8787”. If you’re running a server then go you’d need to expose 8787 in your firewall; or just run the docker container without 127.0.0.1 but that’s really bad as we’re bypassing our firewall and if we look at our firewall in future we might think everything is closed off when really this container is exposing port 8787. Therefore we should run it with 127.0.0.1 and then just do sudo ufw allow 8787/tcp. I like to add the /tcp part to restrict it just to TCP traffic (which http and https traffic is) however this is not necessary at all.
Now if you navigate to your server’s public IP address with port 8787 such as http://89.823.442.11:8787 then you’ll see your Enclosed instance running 😀
Didn’t you tell us not to expose ports!?
Yes I did! You shouldn’t be doing it this way but I think it’s important for you to understand this first. Let’s look at some more secure, convenient and clean ways of doing this. Look at reverse proxies and cloudflare tunnels. See you there!