Containerization with Docker
Containerization
Containerization is a process that consists of packaging code and all of its dependencies, so it can be moved from one execution environment to another and executed quickly and reliably.
It is a type of OS virtualization, through which applications run and are isolated in what are called “containers”.
Benefits
There are many benefits to using virtualization:
- Portability
- Scalability
- Faster deployment
- High productivity
- Enhanced security
- Continuity (if one fails, it does not affect the others)
Differences with virtualization
Virtualization uses software to create an abstraction layer over the computer hardware, so they can be divided and used as if there were multiple computers (called VMs). Each VM has its own OS.
With virtualization, you can run multiple computers under the same real computer, but with containerization, you can run multiple services under the same OS on a single computer.
Furthermore, virtualization is a heavyweight operation, in comparison to containerization, which is a lightweight operation.
On one hand, virtualization provides complete isolation, on the other hand, containerization provides less isolation from the host OS.
But the most important difference is that virtualization is not portable, where as containerization was built to be portable.
Docker
Docker is the most well-known containerization tool. It goes by the keywords “develop”, “ship” and “run” anywhere.
It provides what is called Docker Platform, that allows for container development, testing and deployment.
Docker uses a client-server architecture, so the client and the daemon can be in different servers.
The architecture components are:
- Docker Host: Includes the Docker daemon (one of the most core components), which handles Docker Objects such as images, containers, networks and volumes.
- Docker Client: Transmits the commands to the Docker deamon, which executes it.
- Docker Registries: They store Docker Images that can be downloaded.
- Docker Objects: They are the following:
- Image: Read-only template.
- Container: Created when an image is run.
- Volume: Store the persistent data of the cointainers.
- Network: How the isolated containers communicate with one another. It has several network drivers.
- Docker Engine: The core part of the Docker system. It is an application that follows a client-server architecture. It has several components:
- Server: The daemon.
- REST API: To communicate with the Docker daemon.
- Docker CLI: Client used to enter Docker commands.
Installation
For Ubuntu systems, this is the webpage with the installation instructions.
The following is a script that does everything automatically, with no configuration required:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
To get the configuration of the system, run docker info
.
Images
To create a Docker Image, you can write a Dockerfile using a simple syntax that defines the steps required to set everything up.
Here is a list of the basic commands to use and manipulate images:
docker image build
: Create an image from a Docker file.docker image history
: Display an image’s history.docker image inspect
: Display detailed information about one or more images.docker image ls
: Lists the images.docker image prune
: Removes unused images.docker image pull
: Downloads an image from a registry.docker image push
: Pushes an image to a registry.docker image rm
: Deletes one or more images.docker image save
: Saves an image to a .tar archive.
Containers
docker container ls
: Show the running containers.docker container stop <container_name>
: Stop the container named <container_name>.
Networks
docker network ls
: List the networks.
Network drivers
Docker has several network drivers available:
- Bridge: Interconnects containers running under the same daemon.
- Host: The container shares the hosts network, but it does not have its own IP address.
- Overlay: Connects two or more Docker daemon hosts to form a distributed network. It can be used with encryption enabled so the communications are secure.
- Macvlan: Some legacy apps demand to be connected to the physical network. This driver can be used to assign a MAC address to each container’s virtual network interface.
- None: Used to completely disable the network on a container.
Docker usage examples
Build a custom image from a Dockerfile on the current working directory:
docker build -t new_image_name .
Run a Python3 program that’s on the current working directory on a Python3 container and remove the container when the program finishes:
docker run --rm -v$(pwd):/src python:3 python/src/program.py
Start a Python3 container in iteractive mode (open the interpreter shell):
docker run --rm -it -v$(pwd):/src python:3
Start a Python3 container and open a Bash shell:
docker run --rm -it -v$(pwd):/src python:3 /bin/bash
Create a Dockerfile from the base image of Python3 and add numpy to it, then build the image from the Dockerfile of the current working directory (.):
cat > Dockerfile << EOF
FROM python:3
RUN pip3 install numpy
EOF
docker build -t <new_image_name> .
Start an Nginx container to serve files of the current working directory, forwarding the port 80 of the host to the port 80 of the container. As Nginx is a web server, it will keep running until we stop it:
docker run --rm -v$(pwd):/usr/share/nginx/html -p 80:80 nginx:latest
The same as the last one, but run the container in background mode (-d for daemon):
docker run --rm -v$(pwd):/usr/share/nginx/html -p 80:80 -d nginx:latest
Open a shell on a container running in the background:
docker exec -it <container_name> /bin/bash
Create a network, a Mysql container in the background (setting the environment variable MYSQL_ROOT_PASSWORD to ‘root’) and a Node.js container with an open Bash shell. Note that Docker automatically sets up DNS entries so the containers can communicate with each other using their names:
docker network create <network_name>
docker run --rm -d --name mysql --net <network_name> -e MYSQL_ROOT_PASSWORD='root' mysql:5.6
docker run --rm -it --name nodejs --net <network_name> nodejs:8 /bin/bash
Docker Compose
The best way to describe Docker Compose is “Docker on steroids”, is a tool that allows users to define and run multi-container Docker applications.
It uses a .yaml file that has the configuration for your application services. Once that gets defined, you can start everything with one command.
It is compatible with the following environments:
- Production
- Staging
- Development
- Testing
- Continuous Integration
Steps to use Docker Compose
These are the steps to start using Docker Compose:
- Create a Dockerfile.
- Create docker-compose.yml
- Run
docker-compose up
.
The following is a docker-compose.yml example file:
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
- logvolume01:/var/log
links:
- redis
redis:
image: redis
volumes:
logvolume01: {}
Docker Compose commands
Here is a list of the basic Docker Compose commands:
docker-compose build
: Build or rebuild services.docker-componse create
: Create containers for a service.docker-compose down
: Stop and remove containers and networks.docker-compose kill
: Force stop service containers.docker-compose ls
: List running compose projects.docker-compose pause
: Pause the services.docker-compose ps
: List containers.docker-compose restart
: Restart containers.docker-compose rm
: Removed stopped service containers.docker-compose start
: Start services.docker-compose stop
: Stop services.docker-compose up
: Create and start containers.docker-compose up -d
: Create and start containers in the background.docker-compose up -f docker-compose.json
: Create and start containers defined in a JSON file.
Examples
Example 1
You can use a JSON file instead of a YML file, but in that case you have to specify to Docker Compose the file that you want to use.
Start a Redis and a Mysql container. Note that with Docker Compose, the containers that you start at the same time, they will be connected to the same network automatically.
docker-compose.yml file:
version: '3'
services:
redis:
container_name: redis
image: redis:3.2.12
volumes:
- ./redis:/data
restart: always
mysql:
container_name: mysql
image: mysql:5.6
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- ./mysql:/var/lib/mysql
restart: always
And then run docker-compose -d up
to start the containers in the background.
Example 2
Start an Nginx server and a Node.js application, and configure the Nginx to act as a proxy for the nodejs application.
lb.conf file (for Nginx):
upstream serv{
server nodejs:3000;
}
server {
listen 8080;
location / {
proxy_pass http://serv;
}
}
docker-compose.yml file:
version: '3'
services:
nodejs:
container_name: nodejs
image: node:latest
volumes:
- ./app:/opt/app
# Run as soon as the container starts
command: node /opt/app/app.js
nginx:
container_name: nginx
image: nginx:latest
# Port forwarding
port:
- "1313:8080"
volumes:
- ./nginx/lb.conf:/etc/nginx/conf.d/lb.conf
# Wait until the nodejs service has started, then start the nginx service
depends_on:
- nodejs
And then run docker-compose -d up
to start the containers in the background.
Example 3
The same as the last one, but now use a Dockerfile to build a custom image for the nodejs application, and we start a bunch of nodejs applications and set a load balancer with Nginx.
lb.conf file (for Nginx):
upstream serv{
server ex3_nodejs_1:3000;
server ex3_nodejs_2:3000;
server ex3_nodejs_3:3000;
server ex3_nodejs_4:3000;
}
server {
listen 8080;
location / {
proxy_pass http://serv;
}
}
Dockerfile:
FROM node:latest
docker-compose.yml file:
version: '3'
services:
nodejs:
#container_name: nodejs
# Use the Dockerfile in the current working directory (.) to build the container image
build: .
volumes:
- ./app:/opt/app
# Run as soon as the container starts
command: node /opt/app/app.js
nginx:
container_name: nginx
image: nginx:latest
# Port forwarding
port:
- "1313:8080"
volumes:
- ./nginx/lb.conf:/etc/nginx/conf.d/lb.conf
# Wait until the nodejs service has started, then start the nginx service
depends_on:
- nodejs
And then run docker-compose -d up --scale nodejs=4
to start the containers in the background, and start 4 containers of the nodejs application.
Docker Swarm Mode
Swarm Mode is a cluster management and orchestration tool that’s embedded right in the Docker Engine. If you know what Kubernetes is, the best way to describe Swarm Mode is to think about it as a Kubernetes alternative that’s easier to use and that I would recommend for simple applications that are not too complex to manage.
Comparing it to Docker Compose, and its ability to define and run containers, you can define and run Swarm service stacks.
If you want detailed information, check the official page.
Key concepts
Node
A node is an instance of a Docker Engine that takes part in the swarm. You can have one or more on a single computer, but usually they are distributed across multiple machines. There are two types:
- Manager node: Dispatches the work (tasks) to the worker nodes. By default, they act as worker nodes too.
- Worker node: Receives and execute tasks dispatched from the manager nodes. It notifies the manager of the current state of operation periodically.
Service
It is the definition of the tasks that will get executed.
A service specification dictates the container image to use and the commands to execute inside the running containers.
In the replicated services model, the swarm manager sends replica tasks to the worker nodes, and for global services, the swarm runs one task for the service on every node of the cluster.
Task
It is made of a container and the commands to run inside said container. It is the minimum unit of swarm. Once a task gets assigned to a node, it can only run on the assigned node.
Load balancing
The manager node uses ingress load balancing, so the services you want are made available to the Internet. If a port is not specified, is uses a port from the range 30000-32767.
Like Docker Compose, Swarm Mode automatically assigns DNS records to each service, and uses internal load balancing to distribute requests between the services based on the DNS names.
Getting started
In order to be able to use Swarm Mode there are several prerequisites that need to be satisfied:
- Three Linux hosts that can communicate over a network, with Docker installed.
- The IP address of the manager. You should use a fixed IP address for the manager as all nodes in the swarm need to connect to it.
- The ports 2377, 7946 and 4789 (and 50 if you plan to use
--opt encrypted
to use secure connections between the machines) open and allowed by default.
Creating a swarm
Once those requirements are met, you can create a swarm. To do so, connect to the manager node and run the following command:
docker swarm init --advertise-addr <manager_IP>
That will output something like:
Swarm initialized: current node (xi3zvyg3wgnv04jz0z36b9kyn) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-0yjkvziblj2nuwz35cl9uc7evx3p93onhbxan66gz7wsvwi7tp-02kyqwtgpjotdbzqh0bu1nj2g 10.10.10.10:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
If you clear the terminal, you can get the join command for a worker by running docker swarm join-token worker
.
Now we can run the following command to check the status of the nodes:
docker node ls
It will output something like (note the *, it means that Docker is connected to it):
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
xi3zvyg3wgnv04jz0z36b9kyn * ubuntu Ready Active Leader 20.10.8
Adding worker nodes to the swarm
You can run the join command provided when the swarm was initialized on the worker nodes to join the swarm. In my case:
docker swarm join --token SWMTKN-1-0yjkvziblj2nuwz35cl9uc7evx3p93onhbxan66gz7wsvwi7tp-02kyqwtgpjotdbzqh0bu1nj2g 10.10.10.10:2377
And it will output:
This node joined a swarm as a worker.
Now, on the manager node, we can run docker node ls
, and we should see the new node there:
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
xi3zvyg3wgnv04jz0z36b9kyn * ubuntu Ready Active Leader 20.10.8
yox9u6ux9aom7w5mtal5woqog ubuntuserver Ready Active 20.10.8
Deploying a service to the swarm
From the manager node, create a service:
docker service create --replicas 1 --name test alpine ping docker.com
That will create a service named “test”, with 1 replica, using the image python tagged 3 that runs the command ping docker.com
.
Now you can inspect the services running with the command docker service ls
.
Inspecting a service
Run the following command to get the output in JSON format:
docker service inspect <ID>
Add --pretty
to the above command to get a more readable output.
To see which nodes are running the service, run docker service ps <service_name | service_ID>
. Either way, it will output something like:
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
7ihrx5jn31fr test.1 alpine:latest ubuntu Running Running 5 minutes ago
Scaling the service
Scaling a service is really easy with Swarm Mode, just run docker service scale <service_name | service_ID>=<number_of_replicas>
.
It will output:
test scaled to 3
overall progress: 3 out of 3 tasks
1/3: running [==================================================>]
2/3: running [==================================================>]
3/3: running [==================================================>]
verify: Service converged
Now if we list the containers of the service with docker service ps <service_name | service_ID>
, we get:
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
7ihrx5jn31fr test.1 alpine:latest ubuntu Running Running 11 minutes ago
z6ctn65c04h6 test.2 alpine:latest ubuntuserver Running Running 32 seconds ago
ahmdt70a0je5 test.3 alpine:latest ubuntuserver Running Running 32 seconds ago
Deleting a service
Run the command docker service rm <service_name | service_ID>
.
It will take a few seconds (30s) for the containers to be cleaned up. You can check the state with the command docker container ls
or with docker ps
.
More detailed info
Check these pages: