## Là gì

Docker đóng app cùng runtime và thư viện thành một **image**, rồi chạy image đó thành **container**. Container dùng kernel của host, nên lên trong vài giây. VM có kernel riêng, nên nặng hơn và boot lâu hơn.

Lệnh `docker` chỉ là client. Việc thật do daemon `dockerd` làm: pull image, tạo container, network, volume. Trên Windows, daemon chạy trong WSL2 qua Docker Desktop.

| Khái niệm | Hiểu nhanh |
| --- | --- |
| Image | Khuôn read-only, xếp bằng layer. Định danh bằng `repo:tag` |
| Container | Một lần chạy của image, thêm một layer ghi. Xóa container là mất layer đó |
| Registry | Kho image: Docker Hub, GHCR, ACR. Không phải chỗ app chạy |
| Volume | Chỗ giữ dữ liệu khi container bị xóa |
| Network | Cho container gọi nhau bằng tên |

## Để làm gì

- Máy dev, CI và server chạy cùng một image. Hết cảnh "máy tôi chạy được".
- Dựng database, Redis, queue cho môi trường local bằng một lệnh, không cài lên máy.
- Tạo artifact để deploy lên Kubernetes, App Service, Cloud Run.

## Lưu ý quan trọng

- **Pin tag.** Không ghi tag thì Docker lấy `latest`, và `latest` đổi mỗi lần có bản mới. Deploy bằng version hoặc git sha.
- **Container không giữ dữ liệu.** Database phải gắn volume vào thư mục data, trước khi có dữ liệu thật.
- **`localhost` trong container là chính container đó.** Gọi service khác bằng tên service trên cùng network.
- **`-p 8080:80` là host trước, container sau.** Publish chỉ mở cổng ra host, không giúp container khác gọi nhau.
- **Xóa container trước, rồi mới xóa image.** Image còn container trỏ tới, kể cả container đã dừng, thì `rmi` từ chối.
- **Không để secret trong image.** Không `ENV TOKEN=...`, không `COPY .env`. Ai pull image cũng đọc được qua `docker history` hoặc layer. Token lúc build đi qua `docker build --secret`. File `.env` ở trên host.
- **`docker run img arg` thay cả `CMD`.** `ENTRYPOINT` dạng mảng JSON giữ executable, arg nối phía sau. `CMD` lúc đó là argument mặc định.
- **File `.env` cạnh `compose.yaml` thay `${}` trong yaml.** Process chỉ thấy biến khi có `env_file` hoặc `environment`. `--env-file` của `docker run` thì đưa thẳng vào container.
- **Một container, một process chính.** Process đó chết thì container dừng. Đừng nhét systemd và vài service vào một image.
- **Không chạy app bằng root.** Image Node có sẵn user `node`, dùng `USER node`.
- **Sửa bằng `docker exec` không phải deploy.** Container tạo lại từ image sẽ mất sửa đó.
- **Option đứng trước tên.** `docker exec -u nginx web pwd` đúng. `docker exec web -u nginx pwd` thì `-u` thành argument của `pwd`.
- **`rm` xóa container, `rmi` xóa image.** Còn container trỏ image, kể cả đã dừng, thì `rmi` từ chối. Đừng `rmi -f` để bỏ qua bước đó.
- **Linux server: `docker-ce`, không `docker.io`.** Sau cài: `systemctl status docker`; chưa chạy thì `start`. Compose là plugin: `docker compose`.
- **`docker tag` chỉ thêm tên.** Cùng image ID, không pull lại. Tag nguồn phải đã có trên máy.
- **`docker cp` hai chiều.** Container chạy hoặc đã dừng. Không tự tạo thư mục cha của đích.
- **`docker commit` không thay Dockerfile.** Volume không vào image. Snapshot layer ghi, không lặp lại được như file build.
- **`docker network create` mặc định bridge.** User-defined mới có DNS theo tên. `--subnet` khi đề đòi CIDR. Publish `-p` không giúp container gọi nhau.

## Lệnh tổng quan

Option của Docker đứng trước tên container hoặc image. Phần sau tên là command và argument chạy bên trong.

```text
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
docker rm   [OPTIONS] CONTAINER [CONTAINER...]
docker rmi  [OPTIONS] IMAGE [IMAGE...]
```

Container:

```bash
docker run --name web -d -p 8080:80 nginx:1.27-alpine   # pull nếu thiếu, tạo, start
docker ps --filter name=web --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
docker ps -a --filter status=exited
docker logs --tail 20 -t web
docker exec web nginx -v                 # command + argument, không cần -it
docker exec -it web sh                   # shell thì cần -it
docker exec -u nginx -w /tmp web pwd     # user và workdir chỉ cho process này
docker stop -t 20 web                    # SIGTERM, chờ 20s, rồi SIGKILL
docker rm web                            # xóa container đã dừng
docker rm -f web                         # SIGKILL rồi xóa
docker rm -v tmp                         # xóa luôn anonymous volume
```

Image và registry:

```bash
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl start docker
docker pull nginx:1.27-alpine
docker tag nginx:1.27-alpine nginx:lab
docker images
docker cp ./index.html web:/usr/share/nginx/html/index.html
docker commit web nginx:from-container
docker build -t my-app:1 .
docker build --secret id=npmrc,src="$HOME/.npmrc" -t my-app:1 .
docker run --rm --env-file .env my-app:1
docker run --rm --entrypoint echo my-app:1 hello
docker inspect --format '{{json .Config.Env}}' web
docker tag my-app:1 ghcr.io/org/my-app:1.4.0
docker push ghcr.io/org/my-app:1.4.0
docker rmi hello-world                   # xóa theo tag
docker rmi hello-world alpine:3.20       # xóa nhiều image
```

Volume và network:

```bash
docker volume create data
docker run --rm -v data:/data alpine:3 ls /data     # named volume
docker run --rm -v "$PWD":/app alpine:3 ls /app     # bind mount thư mục host
docker network create app-net
docker network create --driver bridge --subnet 172.28.0.0/16 app-cidr
docker network ls --filter type=custom
docker run -d --name api --network app-net my-app:1
docker run --rm --network app-net alpine:3 wget -qO- http://api:3000
```

Compose:

```bash
docker compose up -d --build
docker compose ps
docker compose logs -f web
docker compose exec db psql -U app
docker compose down             # giữ volume
docker compose down --volumes   # xóa cả dữ liệu
```

## Cú pháp cần nhớ

Dockerfile Node, copy lockfile trước để cache layer `npm ci`:

```dockerfile
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
ENTRYPOINT ["node"]
CMD ["server.js"]
```

Cache gói và token lúc build không vào layer. Dòng `# syntax=docker/dockerfile:1` đứng đầu file thì `RUN --mount` mới được hiểu:

```dockerfile
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci
```

| Chỉ thị | Ý nghĩa |
| --- | --- |
| `FROM` | Image gốc. Nhiều `FROM` là multi-stage build |
| `COPY` | Copy file từ build context, hoặc từ stage (`COPY --from`) |
| `ADD` | Thêm URL HTTPS/Git và tự giải nén tar. File thường dùng `COPY` |
| `RUN` | Chạy lệnh lúc build, tạo layer mới. `--mount=type=cache` hoặc `type=secret` không vào layer |
| `USER` | User chạy process |
| `ENV` | Biến đóng trong image. `docker run -e` và `--env-file` ghi đè cho một container |
| `EXPOSE` | Chỉ là metadata. Mở cổng thật bằng `-p` |
| `ENTRYPOINT` | Executable dạng mảng JSON. Argument của `docker run` nối phía sau |
| `CMD` | Lệnh mặc định, hoặc argument mặc định của `ENTRYPOINT`. Chữ sau tên image thay cả dòng này |

`docker run img extra` với chỉ `CMD` thì chạy `extra`. Với `ENTRYPOINT ["node"]` và `CMD ["server.js"]` thì chạy `node extra`.

File `.env` trên host, dạng `TÊN=giá`, không có `export`. `.gitignore` và `.dockerignore`. `docker run --env-file .env` đưa vào container.

YAML của file Compose: mapping tra theo tên (`services`, `web`), list giữ thứ tự (`ports`, `command`, mỗi phần tử một dòng `- `). Thụt bằng dấu cách, cùng cấp thì cùng cột. Sau dấu hai chấm của khóa có một dấu cách: `image: nginx:1.27-alpine`. `#` là chú thích đến hết dòng.

`compose.yaml` app + Postgres:

```yaml
services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      DB_HOST: db
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db-password
    secrets:
      - db-password
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
volumes:
  pgdata:
secrets:
  db-password:
    file: ./secrets/db-password.txt
```

File mật khẩu nằm ngoài Git. Cách tạo ở bài Compose.

Tên image đầy đủ: `registry/namespace/repo:tag`. `nginx:alpine` là dạng ngắn của `docker.io/library/nginx:alpine`.

`.dockerignore` tối thiểu: `node_modules`, `.git`, `.env`, `dist`.

`docker init` trên Docker Desktop sinh `.dockerignore`, `Dockerfile`, `compose.yaml`, `README.Docker.md`. Đó là bản nháp. Đọc lại tag, `USER` và `CMD` trước khi up. File bị ghi đè không lấy lại được.
