Docker
Docker is the most important tool in our tech stack. Almost everything, with the exception of CAPEv2, uses Docker.
CAPEv2 does not use Docker as it needs to communicate with KVM/Qemu kernel drivers, which are not easily accessible via Docker.
Some of the more common Docker commands to be familiar with:
docker rundocker pulldocker builddocker composedocker compose updocker compose downdocker compose pulldocker compose build
docker logsdocker execdocker imagesordocker image lsdocker container lsordocker psdocker rm
Why use Docker?
Ease of use
After dockerization of an application, it can be deployed on every device regardless of operating system or version. This means that the same application is guaranteed to behave in a similar fashion regardless of whether it was running on Windows or Linux, on staging environments or on production environments. Essentially, any device that can run Docker is now able to run the application.
Take note, however, that data within the Docker containers are considered ephemeral. To ensure that the data is persistent across different containers, use Docker volumes.
Security
In the unfortunate event of an attack, the scope of the attack is still limited as the hacker can only access resources within the Docker container itself. For instance, even if a hacker gets remote code execution via the frontend, only the frontend container is affected - it is significantly harder to perform a Docker escape if configured properly. Additional docker run flags such as --read-only also disallow modifications of files other than those specified.
Greater developer experience
The ability to run the docker containers across different platforms means each developer can develop on their own machine with its own configurations. Even developer environments that run on Windows are able to run applications that are normally only available on Linux. Docker container also allows for mounting of local files into the docker container, allowing for hot-reloading.
Hot-reloading of files on Windows hosts in Linux containers require a different technique to watch for file changes as there are no Windows-native file events that are sent. Instead, files are continuously polled at regular intervals to check for updates to trigger a reload of the application. This technique is also known as polling.
Isolation of environments
This is the main reason why most developers go to Docker - the ability to isolate environments. With Docker, you have full control over the environment variables of each Docker container, while still sharing data between multiple Docker containers in the form of volumes. You can even have different versions of Python with different Python packages installed, all running on the same host.
Small resource usage
Docker containers take extremely little space, and run on extremely little resources, only requiring as much resources as your application requires. Combined with the ability to push and pull from Docker registries, it makes it extremely easy to perform a fresh deployment of an application on any device.
How to use Docker?
There are 2 main ways to run docker - using docker run, or using a docker compose file. To build a Docker container, you need to write a Dockerfile for each application that you want to run in the Dockerfile.
I will not be covering how to write a Dockerfile, as there are numerous tutorials available for that. Instead, here are some useful resources on writing a good Dockerfiles to create as small a container as possible.
- Caching Docker builds
- Multi-stage builds
- Using the smallest base image (generally Alpine-based images are used)
Docker networking
To gain full mastery of Docker networking, you must understand the host that localhost or 127.0.0.1 points to. localhost on your host refers to the host itself (most likely the Windows machine that you are working on), whereas localhost in your Docker container refers to the docker container itself. It is also in perspective of the Docker container that it is running on.
If you wish for the Docker container to communicate with the host itself, there are 2 solutions:
- Using host networking
- Not recommended, as it defeats the purpose of Docker networking
- Only works on Linux
- Use
host.docker.internal, and Docker networking will resolve it to your host IP address - Using your host IP address manually (not recommended as your IP address is ephemeral)
For inter-container communication, each container must be within the same Docker network to be able to communicate with one another. (You can do so in the Docker compose file by specifying the network section.) You can reference each container using the container name as the hostname, and the Docker network will resolve them accordingly.
There are many instances where you will still be unable to connect from one container to another, especially on non-standard ports, on Linux machines. This is often due to the firewall. Ensure that your firewall is disabled, or there is a corresponding rule to allow that traffic through. (e.g. ufw disable)
Developing with Docker
When developing with Docker, it is important to note the differences in files in your host filesystem vs the docker container filesystem. Some files are mounted via Docker volumes into the containers - therefore changing the file contents on your host will also change the contents in the container filesystem. On the other hand, there are files that are strictly unique to the container filesystem. Changing the file contents in your container will not change the file contents on your host system.
For instance, the package.json files are not mounted into the container filesystem. To add an npm module in the project, you might run pnpm add <package_name> on your host. However, upon hot reloading, your application raises an error:
Module not found: Can't resolve <package_name>
Reason
The node_modules folder and the package.json file is not updated within the container. Running npm add on your host will install the package on your host filesystem, NOT the container filesystem.
To resolve this, a rebuild of the container is necessary for the node_modules folder to contain the added package.
This error is not unique to installation of new packages. For instance, modification of specific files such as tsconfig.json and prisma files will not trigger a hot reload and require a restart/rebuild of the container.