Docker overview
Tags: docker, containers
Published: 2023-03-10 10:14:21

## Overview

Docker là một open platform dùng cho việc phát triển, chuyển giao và run application.

Docker cung cấp khả năng đóng gói và chạy app trong một môi trường isolated gọi là **container**. Do đó chúng ta có thể chạy nhiều containers
đồng thời trên một máy mà không lo ngại chúng sẽ xung đột với nhau hay với chính host machine.

## Docker Architecture


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i9e1p96gd1gcbz17il2c.png)

**Docker daemon** (dockerd): là một process chạy ngầm. dockerd quản lý *docker objects*: images, containers, networks và volumes.
Một dockerd có thể giao tiếp với dockerd khác (trên host khác) để quản lý docker *services*.
Dockerd cung cấp API để **docker client** có thể tương tác (e.g. quản lý objects) với nó.

Docker objects:

* **Images**: một image là template chứa các instructions (lệnh) dùng để tạo Docker containers. Thông thường, một image sẽ được based từ một
image khác và thêm vào đó phần customization. Ví dụ build một image based từ image *ubuntu* nhưng có cài thêm Apache server và application.
* **Containers**: một container là một runnable instance của image. Container là *isolated* với container khác và với host machine. Bạn có thể
kiểm soát tính *isolated* của container network và storage với container khác và với host machine. 

Khi một container bị removed thì mọi thay đổi và trạng thái (state, e.g. file, data, ...) không được lưu lại vào persistent storage, sẽ biến mất.

**Docker client** hay *docker* là một command line interface (CLI) dùng để tương tác với *dockerd* thông qua các command (sử dụng API)
ví dụ như *docker run* (tạo container). Docker client có thể tương tác với nhiều *dockerd* (cấu hình DOCKER_HOST).

**Docker registries** là nơi lưu trữ *docker images*. [Docker hub](https://hub.docker.com/) là một public registries, ai cũng dùng được.
Bạn cũng có thể tạo private registry của mình. Docker được cấu hình tìm kiếm docker images từ Docker Hub. Bạn có thể pull image từ registry
về máy local hay push image của mình lên registry.


## Get started

Để bắt đầu với Docker, thực hiện theo bài hướng dẫn ở link dưới đây:

https://docs.docker.com/get-started

## Build docker images

Docker builds images bằng cách đọc các lệnh từ **Dockerfile** - một file text chứa tất cả các lệnh để build image.

Format của Dockerfile:


```sh
# Comment
INSTRUCTION arguments
```


Các lệnh không phân biệt hoa thường. Nhưng convention thì nên viết lệnh UPPERCASE để phân biệt với tham số.

rMột Dockerfile **phải bắt đầu** bằng một lệnh **FROM** để chỉ định *Base image* và từ đó dùng cho các lệnh tiếp theo.

**FROM**

`FROM [--platform=<platform>] <image> [AS <name>]`

* `FROM` có thể xuất hiện nhiều lần trong Dockerfile trong trường hợp sử dụng *build stage*
* `AS name` gán tên cho build stage và có thể được dùng cho lệnh `FROM` sau đó, ví dụ `COPY --from<name>`
* `--platform` chỉ định platform của image, ví dụ: linux/amd64, linux/arm64, hay windows/amd64. Mặc định là platform của máy.


**RUN**

Lệnh RUN dùng để chạy bất kỳ commands nào trong một layer mới của image hiện tại và commit kết quả.

Lệnh RUN có 2 forms:

* `RUN <command>` (*shell* form, the command được chạy trong một shell, mặc định trong Linux là `/bin/sh -c` và `cmd /S /C` trong Windows)
* `RUN ["executable", "param1", "param2", ..]` (*exec* form)


**CMD**

`CMD` có 3 forms:

* `CMD ["executable", 'param1", "param2", ..]` (*exec* form)
* `CMD ["param1", "param2", ..]`(như là *default parameters* cho `ENTRYPOINT`)
* `CMD command param1 param2 ..` (*shell* form)

Có duy nhất một lệnh `CMD` trong một Dockerfile. Nếu có nhiều thì chỉ lệnh `CMD` cuối cùng có tác dụng.

**Mục đích chính** của `CMD` là **cung cấp chương trình mặc định khi chạy container**. Nếu không có thì bạn phải chỉ định
`ENTRYPOINT` instruction.

Nếu `CMD` được sử dụng để cung cấp default cho `ENTRYPOINT`, thì cả hai lệnh cần được khai báo dưới dạng ARRAY (`["abc", "xyz",..]`)

Khác với *shell* form, thì *exec* form sẽ không gọi command shell, đồng nghĩa với xử lý shell thông thường không xảy ra. Ví dụ
`CMD ["echo", "$HOME"]` sẽ không thực hiện thay thế biến môi trường `$HOME`. Nếu muốn xử lý shell thì hoặc sử dụng *shell* form hay 
chạy shell trực tiếp, ví dụ như `CMD [ "sh", "-c", "echo $HOME"]`.

Khi sử dụng *exec* form và chạy trực tiếp shell thì shell sẽ sử lý thay thế biến môi trường, chứ không phải docker.

Khi được sử dụng ở dạng *shell* hay *exec*, `CMD` được command được chạy khi running image (chạy container).

Nếu sử dụng *shell* form của `CMD`, thì `<command>` sẽ được chạy với `/bin/sh -c`:

```sh
FROM ubuntu
CMD echo "this is the shell form cmd." | wc -
```

Nếu muốn chạy `<command>` without shell, bạn phải sử dụng array form và chỉ định path tới executable.

```sh
FROM ubuntu
CMD ["/ur/bin/wc", "--help"]
```

Nếu muốn container chạy same executable every time, bạn cần sử dụng lệnh `ENTRYPOINT` (có thể combination với `CMD`).

Nếu bạn chỉ định command khi run container từ command `docker run <IMAGE> [COMMAND]` thì command này sẽ overwrite default
được chỉ định trong `CMD`.

Không nhầm lẫn `RUN` với `CMD`. `RUN` thực sự chạy command và commit kết quả; `CMD` không chạy gì cả tại thời điểm build.


**EXPOSE**

`EXPOSE <port> [<port>/<protocol>...]`

`EXPOSE` instruction báo rằng container sẽ listen nhưng ports nào lúc chạy. Bạn có thể chỉ định protocol là TCP hay UDP, 
mặc định là TCP. `EXPOSE` thực tế không publish port, muốn publish port, sử dụng `-p` khi chạy container `docker run`.

```sh
EXPOSE 80/tcp
EXPOSE 80/udp
```

Publish port lúc chạy container:

`$ docker run -p 80:80/tcp -p 80:80/udp`


**ENV**

`ENV <key>=<value> ...`

`ENV` instruction set biến môi trường, giá trị của các biến này sẽ ở môi trường của các instruction phía sau.
Các biến này được persist khi container chạy. Bạn có thể xem giá trị của chúng bằng `docker inspect`, 
và thay đổi giá trị `docker run --env <key>=<value>` (-e is short hand).

Nếu một biến môi trường chỉ được sử dụng khi build mà không phải cho the final image, hãy cân nhắc set giá trị
trong command:

`RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...` 

Hay là sử dụng `ARG`, sẽ không peristent trong image cuối cùng.

```sh
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y ...
```


**ADD**

ADD có 2 forms:

```sh
ADD [--chown=<user>:<group>] [--checksum=<checksum>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"] 
```

Form thứ hai cần khi path có chứa white spaces.

`ADD` instruction thực hiện copy files, directories hay remote file URLs từ `<src>` và thêm chúng vào filesystem của image
tại path `<dest>`.

Nhiều `<src` có thể được chỉ định. Nếu là files hay directories thì path của chúng được quy ước là tương đối với thư mục khi build (build context).

`<dest>` là đường dẫn tuyệt đối hay tương đối với `WORKDIR`, chỉ định source được copy vào chỗ nào của container.

Add "test.txt" to `<WORKDIR>/relativeDir`:

`ADD test.txt relativeDir`

Hay sử dụng absolute path

`ADD test.txt /absoluteDir`

Tất cả file và thư mục mới được tạo với a UID và GID 0, trừ khi sử dụng option `--chown`, user and group can be name string or integer id.
Name sẽ được lookup từ `/etc/passwd` và `/etc/group`.

Notes: nếu build bằng cách pass `Dockerfile` through STDIN (`docker build - < somefile`), khi đó không có build context, `Dockerfile` chỉ
có thể chứa URL based của `ADD`. Có thể pass một compressed archive through STDIN (`docker build - < archive.tar.gz`), `Dockerfile` nằm ở
root của archive and phần còn lại của archive sẽ được sử dụng như là context của build.

`ADD` tuân theo các rules sau:

* `<src>` path phải nằm trong `context` của build, bạn không thể `ADD ../something /somthing` bởi vì bước đầu tiên khi build
`docker build` là send context directory (và subdirectories) cho `docker daemon`.
* Nếu `<src>` là a URL và `<dect>` không kết thúc với `/`, thì file download được từ URL sẽ được copy tới `<dect>`.
* Nêu `<src>` là a URL và `<dect>` kết thúc với `/`, thì file download được sẽ lưu tại `<dect>/<filename>`. Ví dụ
`ADD http://example.com./foobar /` sẽ lưu file tại `/foobar`.
* Nếu `<src>` là một directory, toàn bộ contents của directory được copy, bao gồm của filesystem metadata
* Nếu nhiều `<src>` được dùng, sử dụng trực tiếp hay wildcard, thì `<dect>` phải là một directory, và phải kết thúc với `/`.
* Nếu `<dect>` không kết thúc với `/`, nó sẽ được coi là một file thông thường và nội dung của `<src>` sẽ được lưu tại `<dect>`
* Nếu không có `<dect>` thì nó sẽ được tạo cùng với tất cả các thư mục còn thiếu trong path của nó.


**COPY**

`COPY` có 2 forms:

```sh
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
```

Form thứ hai được dùng khi path có chứa whitespace.

Đặc biệt là `COPY` nhận một flag `--from=<name>` được sử dụng để chỉ định source location trỏ tới `build stage` phía trước
(tạo ra bời `FROM.. AS <name>`).

`COPY` hoạt động giống `ADD`, nhưng khác là `COPY` không tự động extraction và không dùng với URL.


**ENTRYPOINT**

The *exec* form (preferred):

`ENTRYPOINT ["executable", "param1", "param2"]`

The *shell* form:

`ENTRYPOINT command param1 param2`

`ENTRYPOINT` dùng để cấu hình executable cho container.

Command line arguments của `docker run <image>` sẽ được appended sau các phần trong *exec* form của `ENTRYPOINT`, và cũng 
override tất cả các phần của `CMD`.

*shell* form hạn chế (prevent) mọi `CMD` và `run` command arguments. `ENTRYPOINT` dạng shell sẽ chạy command như là subcommand
của `/bin/sh -c`, và không pass signals. Điều này có nghĩa là *executable* process không phải là process `PID 1` của containter
và sẽ *không nhận* Unix signals - process này sẽ không nhận `SIGNTERM` từ `docker stop <container>`.

Chỉ có lệnh `ENTRYPOINT` cuối cùng trong `Dockerfile` là có tác dụng. 

Ví dụ *exec* form của `ENTRYPOINT`

```sh
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]
```

Khi chạy container, `top` là process duy nhất.

`docker run -it --rm --name test <image>`

Liệt kê process

`docker exec test ps aux`

Process chạy sẽ là `top -b -c`, nếu chạy với arguments

`docker run -it --rm --name test <image> -H`

Process chạy sẽ là `top -b -H`