14 minute read

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

Architecture

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).

Architecture2

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

Docker 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.ymldocker-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

Contributors

Comments