Docker
Introduction
Post này sẽ tập trung nói về Docker, image, container, docker compose. Nếu đã làm quen với các khái niệm thì bạn có thể nhảy vào các phần Example luôn để thực hành.
Glossary
- namespaces, cgroups: feature exists in Linux kernel
- daemon: a background process that handles requests for services such as print spooling and file transfers, and is dormant when not required.
- Dangling image: an image that is no longer tagged, appears in
<none>:<none>
- Ephemeral serverless backend: backend is so highly virtualized that the concept of a host or node no longer has any meaning - your container simply runs, and you don’t need to care about the how or where.
Architecture of Docker
Advantages of Docker
- Separate your applications from your infrastructure, từ đó có thể deploy nhanh chóng hơn.
- Immutable server: toàn bộ server phụ thuộc vào dockerfile -> có thể dễ dàng thay đổi bằng cách sửa dockerfile.
So sánh Virtualization vs Containerlization
Virtualization:
- Về tài nguyên: khi chạy máy ảo phải cung cấp dung lượng ổ cứng, cũng như ram cho máy ảo đó.
- Về thời gian: khởi động, shutdown khá lâu.
Containerlization:
- Trên máy chủ vật lý sẽ sinh ra được nhiều máy con (giống virtualization) nhưng tốt hơn ở chỗ các máy con (Guest OS) đều dùng chung phần nhân của máy mẹ (Host OS) và chia sẻ tài nguyên máy mẹ. -> Khi nào cần tài nguyên thì cấp, cần bao nhiêu thì cấp bấy nhiêu -> tận dung tài nguyên tốt hơn. -> chỉ cần license cho host os.
At a high level, hypervisors perform hardware virtualization - they carve up physical hardware resources into virtual versions called VMs. On the other hand, containers perform OS virtualization - the carve OS resources into virtual versions called containers.
Containers are less secure and provide less workload isolation than VMs. Technologies exist to secure containers and lock them down, but some of them are prohibitively complex.
Installation
Cài đặt bản Community Docker, với Mac và Win cần cài Docker Desktop (VM để chạy docker). Có thể cần thêm user account vào group Docker để chạy không cần sudo.
docker version
Components of Docker
2 Major components:
- Docker client:
- Client gọi Daemon thông qua local IPC/Unix socket ở /var/run/docker.sock
- Docker engine:
- implements the runtime, API and everything else required to run Docker.
- modular in design, built from many small specialized tools. (đây là kết quả của quá trình refactor nhằm chia nhỏ docker daemon).
Inside Docker Engine:
- Docker daemon: some major functionality still exists: image management, image builds, the REST API, authentication, security, core networking, and orchestration
- runc: has a single purpose is to create containers. It’s a standalone container runtime tool. (tự exit sau khi tạo xong).
- containerd: manage container lifecycle operations (start, stop, pause, rm,…) and some extended functionality (for images, volumes and networks) to make it easier to use in other projects.
- shim:
- Keeping any STDIN and STDOUT streams open so that when the daemon is restared, the container doesn’t terminate due to pipes being closed.
- Reports the container’s exit status back to the daemon.
→ entire container runtime is decoupled from the Docker daemon. → “daemonless containers” → có thể upgrade daemon mà không cần tắt container.
Working with Images
An image is just a bunch of loosely-connected read-only layers, with each layer comprising one or more files. Images don’t contain kernel - all containers running on a Docker host share access to the host’s kernel. One image can have several tags (tags point to the same image id).
Multi-architecture Images: hầu hết các official image trên Docker Hub đều hỗ trợ multi-architecture mà không cần khai báo architecture.
Examples
docker image ls
docker pull [REPO:TAG]
docker search [QUERY_STRING]
# show all images
docker images
docker image inspect [IMAGE]
# delete image, you need to delete all dependent containers first
docker rmi <image>
# Delete all dangling images
docker image prune
# Delete all unused images (not currently used by any containers)
docker image prune -a
# Delete all images
docker image rm $(docker ls -q) -f
# RUN CONTAINER FROM AN IMAGE
docker run [IMAGE]
docker run -it [IMAGE] # start a container
docker run -it [IMAGE] [CMD] # override the default command
# options:
# -i : interaction mode
# -d : detach mode
# -v <outside_volume>:<inside_volume> : mount volume
# -e MY_VAR=my_value : set envvars.
Working with Containers
Docker sinh ra để chạy application, chứ không chạy để host OS, nếu không application nào thì nó sẽ tự động exit. Docker sẽ chạy ở hai chế độ attach và detach. Và docker cũng sẽ không listen ở stdin theo mặc định (non-interact mode).
Kill the main process in the container will kill the container. Container cũng hoạt động giống VM ở khoản nếu stop thì dữ liệu trong container vẫn được giữ. (điều này cũng ko quá quan trọng vì có biện pháp tốt hơn cho vấn đề này đó là sử dụng volume).
Examples
docker ps # show running containers
docker ps -a # show all containers
docker start/stop [CONTAINER]
# stop here mean sending a SIGTERM signal to the main process (PID 1). After 10s, if it doesn't exit, it will receive a SIGKILL.
# execute a command
docker exec [CONTAINER] [COMMAND]
# E.g: docker exec -it my_container /bin/bash
# -> re-attach to the container by creating another bash shell.
docker attach [CONTAINER]
# show full details of a container
docker inspect [CONTAINER]
docker logs [CONTAINER]
docker rm [CONTAINER]
# delete a running container (sending SIGKILL)
docker rm -f [CONTAINER]
# Press Ctrl + P Q to exit the container without terminating it.
Restart Policies
This is a form of self-healing that enables Docker to automatically restart container after certain events or failures have occurred. The policy is applied per container.
3 policies:
- always
- unless-stopped: khi docker daemon khởi động mà container ở trạng thái Stopped thì sẽ ko được restart.
- on-failed: khởi động khi nhận được non-zero exit code.
docker run -it --restart always --name my_container my_image /bin/bash
docker update --restart=no my_container
You can see the restartCount
if you inspect that container.
Commit the changes from container into a new image
In general, it is better to use dockerfile to manage and maintain images. However, during development process, the very first initialized images are often not sufficient of requirements or bug-free. Hence, the final stable versions of an image need to commited. This commit operation will not include any data contained in volumes mounted inside the container.
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
Options
options | default | description |
---|---|---|
–author, -a | author | |
–change, -c | apply Dockerfile instruction to the create image | |
–message, -m | commit message | |
–pause, -p | true | pause the container during commit |
Note:
- As the –pause value is true by default, the container will be paused during the process of commiting. This should curb the risk of corrupting data during the process of creating new image. If it is undesired, set it to false.
- The –change option will apply Dockerfile instructions to the image that is created including supported fundamental instructions CMD, ENTRYPOINT, ENV, EXPOSE, LABEL, ONBUILD, USER, VOLUME, WORKDIR
Examples
# commit with --change
docker commit --change "ENV DEBUG=true" [CONTAINER]
# or
$ docker commit --change='CMD ["apachectl", "-DFOREGROUND"]' -c "EXPOSE 80" [CONTAINER]
Containerizing an app
Containers are all about making apps simple to build, ship and run.
Build context: the directory containing the application and dependencies. -> your dockerfile should be keeped in the root directory of the build context.
Dockerfile
Dockerfile là một file text giúp Docker tạo image. Mỗi dòng sẽ bao gồm 2 phần: instruction và argument. Sau mỗi dòng lệnh, một layer sẽ được tạo ra để cache. -> Không mất công tạo lại toàn bộ khi có sai sót.
Tất cả image phải base trên một base image khác (OS).
Example:
FROM alpine
LABEL maintainer="nigelpoulton@hotmail.com"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
CMD ["exe1", "param1"]
# CMD exe1 param1
ENTRYPOINT ["exe2"]
Example: Python app
FROM python:3.8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update
RUN apt-get install ffmpeg -y
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/
Build new image
docker build [BUILD_CONTEXT]
docker build [BUILD_CONTEXT] -t [IMAGE_NAME:TAG]
# E.g:
# docker build -t my_img:latest.
docker push [IMAGE]
Best practices: Khi tạo Docker image thì luôn để những thành phần thay đổi nhiều xuống bên dưới. Ở ví dụ trên, cần copy requirements.txt vào trước cũng vì thế.
Việc có nhiều instruction sẽ dẫn đến việc có nhiều layer -> tăng size của image. Để hạn chế, có thể gộp các lệnh RUN làm 1 với &&. Cái thứ 2 là thường mình không clean các file không cần thiết (building tools) sau khi build xong.
-> Giải pháp là sử dụng multi-stage build: Một dockerfile có nhiều stage, với stage cuối cùng chỉ cần copy các file cần thiết (production-related) từ stage trước đó vào để chạy.
- Runtime environments are smallers and more secure
Ngoài ra, cũng có nhiều tutorial hướng dẫn tối ưu hoá quá trình build (squashing, no-install-recommends,…).
Ngoài ra, nên xem thêm phần Best Practices ở cuối bài.
.dockerignore
Thường thì sẽ ignore folder .git và .env
Docker Compose
Thay vì chạy từng lệnh docker run
riêng lẻ kia thì ta có thể sử dụng Docker compose (phù hợp với các ứng dụng microservices). Về cơ bản thì là tạo một file docker-compose.yml
, trong đó liệt kê các service kèm theo setting cần tạo.
- Các service trong docker-compose có thể tìm thấy nhau thông qua Docker Compose link, sử dụng service name làm hostname.
Docker Compose links extend Docker links. Docker links only allow communication. Docker Compose links also implement load balancing and set the start order so that the dependent DOcker containers start first.
docker-compose up/down/restart
docker-compose ps
docker-compose top
# delete stopped containers and networks only.
docker-compose rm
# scale horizontally service
docker-compose up --scale <service>=<number>
Lúc này, tên của các container sẽ có prefix là tên của build context directory và numeric suffix chỉ instance number (vì Compose hỗ trợ scaling).
Ví dụ:
version: "3.8"
services:
web-fe:
build: .
command: python app.py
ports:
- target: 5000
published: 5000
networks:
- counter-net
volumes:
- type: volume
source: counter-vol
target: /code
redis:
image: "redis:alpine"
networks:
counter-net:
networks:
counter-net:
volumes:
counter-vol:
Networks: tell Docker to create new networks. By default, Compose will create bridge
networks. These are single-host networks that can only connect containers on the same Docker host. However, you can use drive
property to specify different network types.
networks:
over-net:
driver: overlay
attachable: true
Volumes: tell Docker to create new volumes.
Docker Network
Khi tạo container thì nó sẽ có thể có 3 chế độ network: bridge (default), none, host
# bridge
docker run ubuntu
# none
docker run ubuntu --network
Bridge: private internal network created by docker on the host. All containers attach to this network, thường sẽ có dải 172.17.0.xxx. Các container có thể kết nối với nhau thông qua dải này.
Host: tự động mapping port, sử dụng host network -> Không thể sử dụng container trùng port.
None: cô lập.
User-defined networks
Docker có thể chia nhiều private internal network.
docker network create --driver bridge --subnet 182.18.0.0/16 <network_name>
docker network ls
Embedded DNS
Docker hỗ trợ relsove IP thông qua container name. DNS server chạy ở 172.17.0.11
Docker volumes and persistent data
Docker lưu file tại /var/lib/docker
. Docker hoạt động theo layered architecture. Các lớp image sẽ được tạo thông qua dockerfile, khi đó nó sẽ trở thành read-only. Khi chạy lệnh docker run
, Docker sẽ tạo thêm một lớp container layer, lớp này hỗ trợ read-write, nhưng không persistent.
Giả sử nếu mình copy source code vào trong image khi tạo dockerfile thì khi mình chỉnh sửa code, thực ra Docker đã tạo cho mình một bản copy từ Image layers trong Container layers. Khi tắt container đi thì những thứ nằm trong Container layers sẽ bị xóa sạch.
Để tạo persistent storage, thì mình cần tạo volume và kết nối với container.
docker volume create <volume_name>
Khi đó volume sẽ được tạo ở trong /var/lib/docker/volumes/<volume_name>
. Mình mount volume vào trong container bằng cách:
docker run -v <volume_name>:<internal_endpoint> <image>
Ngoài ra, thì volume không nhất thiết phải ở trong thư mục mặc định của Docker, mình cũng có thể mount volume ngoài bằng cách specific absolute path của nó. (Cách này gọi là bind mounting).
Sử dụng option -v
đã cũ rồi, giờ người ta hay sử dụng --mount
:
docker run --mount type=bind,source=<external_volume>,target=<internal_volume> <image>
Container orchestration
Phần này xứng đáng có một post riêng để bàn, vậy nên mình sẽ không đi sâu trong post này. Tìm hiểu chi tiết tại đây
Container orchestration: solution that contain a set of tools and scripts that help monitoring, deploying containers efficiently.
- Docker Swarm
- Kubernetes (most popular)
- Mesos
Docker Swarm
Docker Swarm is 2 main things:
- An enterprise-grade secure cluster of Docker hosts
- An engine for orchestrating microservices apps
Kubernetes
Kubernetes is the most popular tool for deploying and managing containerized apps. Kubernetes has a pluggable container runtime interface (CRI) that makes it easy to swap-out Docker for a different container runtime. → Docker has been replaced by containerd as the default container runtime in Kubernetes.
Best Practices
Bring source code into Docker container
Có 3 cách phổ biến để đưa source code vào trong docker container:
- Sử dụng Git Clone
- Sử dụng COPY
- Sử dụng Volume
Đầu tiên là sử dụng git clone. Ưu điểm là viết dockerfile khá dễ hiểu, hết🙂 Nhược điểm là mỗi lần build image đều phải rebuild lại, không đảm bảo được bảo mật khi truyền vào credentials (có cách work around cơ mà sẽ phức tạp hơn). Cách này chỉ nên dùng với các project dạng open-source.
Cách thứ hai là sử dụng COPY, nhược điểm vẫn là phải rebuild lại image (khi code thay đổi). Ưu điểm hơn là không phải cài Git, bảo đảm bảo mật hơn. Cách này khá phổ biến, chuyên dùng để tạo production image. Người nhận chỉ cần chạy dockerfile thay vì phải lấy code về.
Cách cuối cùng là sử dụng Volume, thay vì copy code vào trong image thì mình chỉ cần mount thư mục project vào trong docker. Mọi thay đổi ở code sẽ hiện diện luôn trong container (server có thể hot reload được). Cách này thì phù hợp làm development image, giảm thời gian đi build image rất rất nhiều.
Dockerize for Python project
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
PYTHONDONTWRITEBYTECODE=1
: không cho Python tạo các file .pyc trong image- Container chỉ chạy 1 lần, việc generate ra .pyc không có ý nghĩa gì cả.
PYTHONUNBUFFERED=1
: không cho Python sử dụng buffer khi đẩy ra STDOUT.- Đề phòng khi crash thì Python sẽ output toàn bộ ra STDOUT, thay vì giữ lại ở buffer.
- Có thể xem được STDOUT realtime.
Dockerize for Django and Celery
Tách riêng phần task và scheduler của Celery ra khỏi service Backend. -> Chỉ cần restart service task và scheduler nếu có thay đổi thay vì phải restart toàn bộ backend. -> Có thể chia nhỏ làm nhiều queue với priority khác nhau. -> Có thể có cơ chế tracking để tự động restart service. Sẽ thử sau này.
Ví dụ:
version: '3'
services:
db:
image: postgres:9.6.5
volumes:
- postgres_data:/var/lib/postgresql/data/
redis:
image: "redis:alpine"
web:
build: .
command: bash -c "python /code/manage.py migrate --noinput && python /code/manage.py runserver 0.0.0.0:8000"
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- db
- redis
celery:
build: .
command: celery -A proj worker -l info
volumes:
- .:/code
depends_on:
- db
- redis
celery-beat:
build: .
command: celery -A proj beat -l info
volumes:
- .:/code
depends_on:
- db
- redis
volumes:
postgres_data:
References:
- https://www.revsys.com/tidbits/celery-and-django-and-docker-oh-my/
Cache pip install
when building image
Bản chất là sử dụng cache volume để cache thư mục /root/.cache/pip
trong container khi chạy pip install lúc build. Cache volume này sẽ do BuildKit quản lý.
RUN --mount=type=cache,target=/root/.cache/pip pip install --default-timeout=2000 -r requirements.txt
Use multiple docker-compose files for multiple environments
Mặc định thì docker compose sẽ đọc 2 file docker-compose.yml
và docker-compose.override.yml
. Thế nên có thể hiểu là lấy docker-compose.yml
làm file chứa thiết lập mặc định và docker-compose.override.yml
làm thiết lập cho môi trường local/dev.
Một ví dụ về cách chia docker-compose theo môi trường:
$ tree configurations
.
├── assets-minio
│ ├── README.md
│ ├── docker-compose.override.yml
│ ├── docker-compose.yml -> ../docker-compose.base.yml
├── assets-built-in
│ ├── docker-compose.override.yml
│ └── docker-compose.yml -> ../docker-compose.base.yml
├── assets-s3
│ ├── README.md
│ ├── docker-compose.override.yml
│ └── docker-compose.yml -> ../docker-compose.base.yml
...
└── docker-compose.base.yml
References:
- https://pspdfkit.com/blog/2018/how-to-manage-multiple-system-configurations-using-docker-compose/
Một container chỉ nên chạy 1 process, nếu không thì nó đi ngược lại với ý tưởng sử dụng Docker để chia tách các process. -> không nên có background services hoặc daemons trong Docker container.
Mỗi container chứa file system riêng.
References
- Docker Tutorial for Beginners - YouTube
- https://viblo.asia/p/docker-chua-biet-gi-den-biet-dung-phan-1-lich-su-ByEZkWrEZQ0
- Docker Official Docs
- Sách Docker Deep Dive
Contributors
- minhdq99hp $\dagger$
- misaki
Comments