Skip to main content

6. Containerize một ứng dụng

Mục lục


Phần 5 đã nói qua vòng đời container, và có một lời hứa còn treo: cách bền vững thật sự để lưu dữ liệu là dùng volume, chưa nói kỹ ở đâu cả — phần này sẽ trả nợ đúng chỗ đó. Nhưng trước hết, quay lại việc chính: tự tay biến 1 ứng dụng thành Docker image, thay vì chỉ pull image có sẵn như các phần trước.

1. Containerize một ứng dụng — quy trình tổng quan

Docker xoay quanh việc lấy ứng dụng và chạy nó dưới dạng container. Quá trình cấu hình 1 ứng dụng để chạy được như container gọi là "containerize" (đôi khi gọi là "Dockerize"). Container luôn xoay quanh app — mục tiêu là làm cho việc build, ship, và run ứng dụng trở nên đơn giản.

Quy trình containerize 1 ứng dụng gồm 4 bước:

  1. Bắt đầu từ code ứng dụng.
  2. Viết 1 Dockerfile mô tả ứng dụng, dependency của nó, và cách chạy nó.
  3. Đưa Dockerfile này vào lệnh docker image build.
  4. Ngồi chờ Docker build ứng dụng thành 1 Docker image.
4 bước containerize 1 ứng dụng1. Codeứng dụng2. Dockerfilemô tả cách build3. dockerimage build4. DockerImageTừ đây sẵn sàng ship và chạy image này như 1 container

Sau khi containerize xong (đã thành 1 Docker image), bạn sẵn sàng ship và chạy nó như 1 container.


2. Lấy code & đọc hiểu Dockerfile

Ví dụ dưới đây containerize 1 web app Node.js đơn giản (quy trình với ứng dụng Windows cũng tương tự).

Lấy code ứng dụng

Giả sử bạn đã có sẵn source code 1 web app Node.js đơn giản trong thư mục dự án hello-web:

$ cd hello-web
$ ls -l
app.js Dockerfile package.json README.md test/ views/

Thư mục này chứa toàn bộ source code ứng dụng, cùng subdirectory cho view và unit test.

Đọc Dockerfile

Thư mục chứa code ứng dụng gọi là build context. Thường nên đặt Dockerfile ngay tại thư mục gốc của build context. Tên file phải viết đúng Dockerfile — chữ D viết hoa, liền 1 từ (dockerfile hay Docker file đều không hợp lệ).

💡 Hình dung: Dockerfile giống 1 công thức nấu ăn — liệt kê rõ nguyên liệu gốc (FROM), các bước sơ chế/nấu theo đúng thứ tự (RUN, COPY), và ghi chú cách trình bày món ăn khi dọn ra (ENTRYPOINT, EXPOSE). Ai cũng có thể cầm đúng công thức đó lên và ra được đúng 1 món giống hệt, bất kể nấu ở bếp nào.

Build context: toàn bộ thư mục hello-web/hello-web/ (build context)Dockerfileapp.jspackage.jsontest/, views/...toàn bộ thư mục được gửi đi, không chỉ DockerfileDocker daemonđọc Dockerfile, build imagedocker image build .Vì cả context bị gửi đi, thư mục càng gọn (ít file thừa) thì build càng nhanh
FROM alpine
LABEL maintainer="[email protected]"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]

Dockerfile có 2 mục đích chính: mô tả ứng dụng, và hướng dẫn Docker cách containerize nó (tạo ra 1 image chứa app bên trong). Đừng đánh giá thấp giá trị tài liệu hóa của Dockerfile — nó bắc cầu giữa dev và ops, và giúp rút ngắn thời gian onboard developer mới, vì nó mô tả chính xác ứng dụng và dependency của nó theo định dạng dễ đọc.

Đọc từng dòng:

  • FROM alpine — mọi Dockerfile đều bắt đầu bằng FROM, đây sẽ là base layer của image, mọi phần còn lại của app được thêm vào như các layer phía trên. Vì đây là app Linux, FROM phải trỏ tới 1 image gốc Linux (với app Windows, cần chỉ định image gốc Windows tương ứng, ví dụ microsoft/aspnetcore-build).
  • LABEL maintainer="..." — label là cặp key-value đơn giản, cách tốt để gắn thêm metadata tùy chỉnh vào image. Ghi rõ maintainer là best practice, để người dùng sau này có điểm liên hệ.
  • RUN apk add --update nodejs nodejs-npm — dùng package manager apk của Alpine để cài nodejs/nodejs-npm, tạo thêm 1 layer mới chồng lên base layer.
  • COPY . /src — copy toàn bộ file app từ build context vào image, tạo thêm 1 layer.
  • WORKDIR /src — đặt working directory cho các instruction phía sau; đây chỉ là metadata gắn vào image config, không tạo layer mới.
  • RUN npm install — cài dependency liệt kê trong package.json, chạy trong context của WORKDIR vừa đặt, tạo thêm 1 layer.
  • EXPOSE 8080 — ghi chú (document) rằng app expose service ở cổng TCP 8080 — chỉ là metadata, không tạo layer.
  • ENTRYPOINT ["node", "./app.js"] — đặt chương trình chính chạy khi container khởi động từ image này — cũng chỉ là metadata.

3. Build image, chạy & test

Build

$ docker image build -t web:latest .

Dấu . cuối lệnh báo Docker dùng working directory hiện tại của shell làm build context — nhớ luôn chạy lệnh này từ đúng thư mục chứa Dockerfile và code app.

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
web latest e33cdd8266d0 About a minute ago 55.6MB

Vậy là app đã được containerize! Dùng docker image inspect web:latest để xem lại toàn bộ cấu hình được set từ Dockerfile.

Chạy & test

$ docker container run -d --name c1 -p 80:8080 web:latest

-d chạy nền, -p 80:8080 map cổng 80 của host vào cổng 8080 bên trong container (nếu host đã có service khác dùng cổng 80, đổi sang cổng khác, ví dụ -p 5000:8080).

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
82a4f9c1d388 web:latest "node ./app.js" 2 minutes ago Up 2 minutes 0.0.0.0:80->8080/tcp c1

Mở trình duyệt, trỏ tới DNS name/IP của host — sẽ thấy trang web của app. Nếu không kết nối được: kiểm tra lại container đang chạy đúng port mapping (docker container ls), và kiểm tra firewall/network security có đang chặn cổng 80 trên host không.

Build → Run → Testimage build -t webtạo Docker Imagecontainer run -d -p80:8080container lsxác nhận Up + portBrowser:80Không kết nối được: quay lại bước container ls kiểm tra port, rồi kiểm tra firewall trên host

4. Nhìn kỹ hơn: instruction nào tạo layer, instruction nào chỉ tạo metadata

Dòng comment trong Dockerfile bắt đầu bằng #. Mọi dòng còn lại là instruction, dạng INSTRUCTION argument — tên instruction không phân biệt hoa/thường nhưng theo quy ước thường viết HOA để dễ đọc. docker image build đọc Dockerfile tuần tự từng dòng từ trên xuống.

Một số instruction tạo layer mới (FROM, RUN, COPY), số khác chỉ thêm metadata (EXPOSE, WORKDIR, ENV, ENTRYPOINT...). Nguyên tắc chung: nếu instruction thêm nội dung (file, chương trình) vào image → tạo layer mới; nếu chỉ là hướng dẫn cách build/chạy → chỉ tạo metadata.

Xem lại các instruction đã dùng để build image bằng docker image history:

$ docker image history web:latest

So khớp với docker image inspect web:latest (phần RootFS.Layers) sẽ thấy: dù output của docker image history trông như mọi dòng đều tạo layer, thực tế chỉ 4 layer có dữ liệu thật (ứng với các instruction FROM, RUN, COPY) — phần còn lại chỉ là metadata.

Dockerfile web:latest — instruction nào tạo layer thật?FROM alpineLayerLABEL maintainer="..."MetadataRUN apk add nodejs...LayerCOPY . /srcLayerWORKDIR /srcMetadataRUN npm installLayerEXPOSE 8080MetadataENTRYPOINT ["node", ...]Metadata8 instruction, nhưng chỉ 4 dòng "Layer" thật sự làm image nặng thêm

Vài lưu ý thêm:

  • Nên dùng image từ repository chính thức với FROM, vì chúng thường theo best practice và ít lỗ hổng bảo mật đã biết hơn.
  • Nên FROM từ image càng nhỏ càng tốt — giảm attack surface đã nói ở Phần 5 (image càng nhỏ, càng ít package/service đi kèm, càng ít điểm có thể bị khai thác).
  • Quy trình build thực chất: dựng 1 container tạm → chạy instruction của Dockerfile bên trong container đó → lưu kết quả thành 1 layer image mới → xóa container tạm. Lặp lại cho từng instruction tạo layer.

5. Multi-stage build — giữ image production nhỏ gọn

Với Docker image, to là xấu: to nghĩa là chậm, khó làm việc cùng, và attack surface lớn. Vì vậy image nên nhỏ gọn — chỉ nên ship lên production đúng những gì cần để chạy app.

Vấn đề là: giữ image nhỏ vốn không dễ. Cách viết Dockerfile ảnh hưởng rất lớn tới kích thước image cuối — ví dụ mỗi RUN đều tạo 1 layer mới, nên thường nên gộp nhiều lệnh vào chung 1 RUN (nối bằng && và xuống dòng bằng \). Vấn đề khác: không dọn dẹp sau khi build — build tool/dependency chỉ cần lúc build lại vô tình bị đóng gói theo cả vào image production.

Trước đây có cách giải quyết gọi là builder pattern: cần ít nhất 2 Dockerfile — 1 bản Dockerfile.dev (base image lớn, kéo theo build tool, build app), build ra image + container tạm, rồi 1 bản Dockerfile.prod (base image nhỏ hơn) copy app đã build từ container tạm đó sang — toàn bộ phải nối lại bằng 1 script riêng. Khả thi, nhưng khá phức tạp.

Multi-stage build giải quyết đúng vấn đề này mà không cần thêm độ phức tạp: chỉ cần 1 Dockerfile duy nhất, chứa nhiều instruction FROM — mỗi FROM là 1 build stage riêng, có thể COPY artifact từ stage trước đó sang.

💡 Hình dung: giống nấu ăn trong 1 bếp nhà hàng bừa bộn — nồi niêu, dao thớt, rau vụn vương vãi khắp nơi trong lúc nấu — nhưng khi bưng ra bàn cho khách, chỉ đúng 1 đĩa thức ăn sạch sẽ được mang ra, không ai bưng theo cả đống nồi niêu bẩn. Stage builder chính là cái bếp bừa bộn đó; stage production chỉ nhận đúng "đĩa thức ăn" (file binary đã build xong), bỏ lại toàn bộ "bếp" phía sau.

Ví dụ

FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api

FROM alpine:latest AS production
RUN adduser -D appuser
COPY --from=builder /out/api /app/api
USER appuser
EXPOSE 8080
ENTRYPOINT ["/app/api"]

Dockerfile này có 2 instruction FROM — tương ứng 2 build stage, đánh số nội bộ từ 0, nhưng đã được đặt tên gợi nhớ riêng:

  • Stage builder: dùng image golang:1.22-alpine (vài trăm MB, kèm sẵn toàn bộ Go toolchain) — tải dependency, rồi compile code Go thành 1 binary tĩnh duy nhất (CGO_ENABLED=0 để không phụ thuộc thư viện C động của hệ thống). Kết quả là 1 image rất lớn chứa cả compiler lẫn source code, chỉ dùng để build chứ không phải để chạy production.
  • Stage production: dùng image alpine:latest — chỉ vài MB, nhỏ hơn hẳn stage build. RUN adduser -D appuser tạo 1 user thường (không phải root), rồi USER appuser chuyển toàn bộ instruction phía sau (và cả tiến trình app lúc container chạy) sang chạy dưới quyền user đó — best practice bảo mật: nếu app bị khai thác lỗ hổng, kẻ tấn công cũng chỉ có quyền của user thường, không có quyền root bên trong container. Sau đó dùng COPY --from=builder để chỉ lấy đúng file binary đã build xong từ stage trước — không copy theo compiler, source code, hay bất kỳ dependency build-time nào.
Multi-stage build: 2 FROM, 1 DockerfileStage "builder"golang:1.22-alpine+ toàn bộ Go toolchain+ source code~ 380MB (chỉ để build)Stage "production"alpine:latest+ đúng 1 file binary+ user thường (không phải root)~ 14.8MB (ship lên production)COPY --from=builderChỉ đúng "đĩa thức ăn" (binary) đi qua — toàn bộ "bếp" (compiler, source) ở lại stage builder

Điểm quan trọng: COPY --from chỉ lấy đúng phần liên quan tới production, không kéo theo rác build. Và tất cả chỉ cần 1 Dockerfile duy nhất — không cần thêm tham số gì đặc biệt cho docker image build.

$ docker image build -t api:latest .
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
golang 1.22-alpine 8f3e2a1b9c0d 2 weeks ago 358MB
<none> <none> 6598db3cefaf 3 mins ago 381MB
alpine latest 9e1a4c8d0f2e 1 week ago 7.4MB
api latest 3dc0d5e6223e 1 min ago 14.8MB

Dòng cuối (api:latest) chính là image production cuối cùng — nhỏ hơn hẳn image build phía trên (14.8MB so với hơn 380MB), vì nó dựa trên base nhỏ (alpine:latest) và chỉ mang theo đúng 1 file binary cần thiết từ stage build. Kết quả: 1 image production nhỏ gọn, tạo ra chỉ bằng 1 Dockerfile, 1 lệnh docker image build bình thường, và không cần thêm bất kỳ script phụ nào.

Multi-stage build là tính năng mới từ Docker 17.05 tại thời điểm sách gốc viết — hiện đã là kỹ thuật tiêu chuẩn để build image production nhỏ gọn.

Theo kinh nghiệm thực tế, phần lớn Dockerfile viết ẩu không phải vì thiếu kiến thức mà vì bỏ qua đúng thói quen ở đây: gộp RUN cẩn thận, dùng multi-stage build ngay từ đầu thay vì "để sau tối ưu", và luôn tự hỏi "cái gì trong image này thực sự cần lúc chạy, chứ không chỉ lúc build". Một image nhẹ vài chục MB thay vì vài trăm MB không chỉ đỡ chi phí lưu trữ/băng thông — nó còn kéo theo attack surface nhỏ hơn, pull/deploy nhanh hơn, và ít bất ngờ hơn khi debug production.


6. BuildKit, .dockerignore & HEALTHCHECK — build nhanh hơn, image đáng tin hơn

BuildKit — builder mặc định từ Docker 23.0

Mọi ví dụ docker image build ở trên thực chất đã chạy qua BuildKit — builder engine mới, thay thế hẳn "classic builder" (legacy builder) cũ, và là mặc định kể từ Docker 23.0 (2023). Classic builder giờ gần như đã nghỉ hưu. Khác biệt cốt lõi: classic builder build tuần tự từng instruction, đúng thứ tự viết trong Dockerfile; BuildKit phân tích Dockerfile thành 1 dependency graph, rồi chạy song song những bước không phụ thuộc lẫn nhau — build nhanh hơn đáng kể, nhất là với multi-stage build có nhiều stage độc lập.

💡 Hình dung: classic builder giống 1 đầu bếp duy nhất làm tuần tự từng món trong thực đơn, món sau chỉ bắt đầu khi món trước xong hẳn. BuildKit giống cả 1 dây chuyền bếp — bước nào không phụ thuộc bước khác (sơ chế rau, ướp thịt) được làm song song ngay từ đầu, chỉ những bước thực sự cần chờ nhau (nấu chín rồi mới trình bày) mới xếp hàng tuần tự.

BuildKit còn hỗ trợ cache mount — cache riêng cho package manager (npm, pip, apt...) sống sót qua nhiều lần build, tách biệt hẳn khỏi layer cache thông thường:

# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm install
COPY . .

Dòng # syntax=docker/dockerfile:1 ở đầu file là cách khai báo tường minh "Dockerfile này cần cú pháp BuildKit" — nên có mặt trong mọi Dockerfile mới, kể cả khi không dùng tính năng nâng cao nào, để luôn nhận được bản parser mới nhất bất kể phiên bản Docker Engine đang chạy là gì. --mount=type=cache,target=/root/.npm giữ lại cache của npm giữa các lần build khác nhau — lần build sau, dependency đã tải trước đó không cần tải lại, dù layer COPY package*.json phía trên đã invalidate cache thông thường.

.dockerignore — đừng gửi thừa vào build context

Phần 6 §2 đã nói: docker image build gửi toàn bộ thư mục (build context) tới daemon, không chỉ đúng Dockerfile. Nếu thư mục dự án có node_modules/, .git/, file .env chứa secret, hay log cũ hàng trăm MB — tất cả đều bị gửi đi dù Dockerfile không hề dùng tới, làm chậm build và có nguy cơ rò rỉ secret vào image nếu vô tình COPY . ..

.dockerignore (đặt cùng cấp Dockerfile, cú pháp giống .gitignore) loại các file/thư mục đó khỏi build context trước khi gửi đi:

node_modules
.git
.env
*.log
Dockerfile.dev

⚠️ Lưu ý: .dockerignore không mã hóa hay bảo vệ gì cả — nó chỉ ngăn file lọt vào build context. Secret thật sự (API key, mật khẩu DB...) không nên nằm trong thư mục project ở dạng plaintext dù có .dockerignore hay không — dùng Docker secret hoặc biến môi trường bơm lúc runtime.

HEALTHCHECK — để Docker tự biết container có "sống" đúng nghĩa không

docker container ls chỉ báo STATUS: Up khi tiến trình PID 1 còn chạy — nhưng "còn chạy" không có nghĩa là còn phục vụ được: 1 web server có thể vẫn chạy (Up) trong khi bị deadlock, hết connection pool, hay treo hoàn toàn. HEALTHCHECK giải quyết đúng khoảng trống này — khai báo 1 lệnh Docker tự chạy định kỳ bên trong container để xác nhận app còn phục vụ được thật sự, không chỉ "chưa chết":

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -q --spider http://localhost:8080/health || exit 1
  • --interval=30s: 30 giây kiểm tra 1 lần.
  • --timeout=3s: lệnh check phải trả lời trong 3 giây, quá hạn tính là fail.
  • --retries=3: fail liên tiếp 3 lần mới đổi trạng thái sang unhealthy.
$ docker container ls
# Output minh họa
CONTAINER ID IMAGE STATUS PORTS
82a4f9c1d388 web:latest Up 5 minutes (healthy) 0.0.0.0:80->8080/tcp

(Output minh họa — cột STATUS hiện thêm (healthy)/(unhealthy)/(starting) ngay bên cạnh Up; các công cụ orchestration như Swarm/Kubernetes dựa vào đúng trạng thái này để quyết định có nên route traffic vào container hay tự động thay thế nó.)


7. Volume — lưu dữ liệu bền vững đúng cách

Phần 5 đã chứng minh container có thể giữ dữ liệu qua stop/start — nhưng đó không phải cách khuyến nghị để lưu dữ liệu thật sự quan trọng, vì dữ liệu lúc đó vẫn nằm chết dí trong writable layer của đúng 1 container: xóa container (docker container rm) là mất trắng, và không container nào khác chia sẻ được dữ liệu đó. Volume giải quyết đúng vấn đề này — tách hẳn dữ liệu ra khỏi vòng đời container.

💡 Hình dung: writable layer của container giống 1 cuốn sổ tay cá nhân — mất cuốn sổ (xóa container) là mất luôn nội dung, và không ai khác đọc chung được. Volume giống 1 tủ hồ sơ đặt riêng ngoài phòng — nhân viên (container) nào cũng mở tủ ra đọc/ghi được, nhân viên nghỉ việc (container bị xóa) thì tủ hồ sơ vẫn còn nguyên đó cho người tiếp theo.

Named volume vs bind mount

Docker có 2 cách chính để đưa dữ liệu bền vững vào container:

  • Named volume: Docker tự tạo và quản lý hẳn 1 vùng lưu trữ riêng (mặc định nằm ở /var/lib/docker/volumes/ trên Linux) — không cần biết chính xác nó nằm ở đâu trên host, chỉ cần gọi đúng tên.
  • Bind mount: mount thẳng 1 thư mục có sẵn trên host (đường dẫn tuyệt đối do bạn chỉ định) vào bên trong container — thường dùng khi cần chia sẻ code đang phát triển (live-reload) hoặc truy cập file host đã có sẵn từ trước.
# Named volume — Docker tự quản lý nơi lưu trữ
$ docker volume create db-data
$ docker container run -d --name db \
--mount source=db-data,target=/var/lib/mysql \
mysql:8

# Bind mount — chỉ định thẳng thư mục host
$ docker container run -d --name dev \
--mount type=bind,source="$(pwd)"/src,target=/app/src \
myapp:dev
$ docker volume ls
# Output minh họa
DRIVER VOLUME NAME
local db-data

$ docker volume inspect db-data
# Output minh họa (rút gọn)
[
{
"Name": "db-data",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/db-data/_data"
}
]

(Output minh họa — Mountpoint là đường dẫn thật trên host nơi dữ liệu volume nằm; với bind mount thì không có entry tương ứng trong docker volume ls, vì bind mount không phải 1 volume do Docker quản lý.)

Volume sống ngoài vòng đời containerContainer "db" (chạy)mount db-dataVolume "db-data"/var/lib/docker/volumes/...Container "db" (đã rm)❌ không còn tồn tạiVolume "db-data"✅ dữ liệu vẫn còn nguyênContainer tới rồi đi, volume vẫn ở đó — mount lại vào container mới bất cứ lúc nào

Xóa container không tự xóa volume nó dùng (đây chính là điểm mấu chốt của "bền vững") — muốn dọn volume phải chủ động:

$ docker container rm -f db
$ docker volume rm db-data
# Output minh họa
db-data

(Output minh họa — docker volume rm chỉ echo lại đúng tên volume vừa xóa; nếu volume đang được 1 container khác dùng, lệnh sẽ báo lỗi tương tự cách docker image rm chặn xóa image còn container tham chiếu.)

Có thể khai báo volume ngay trong Dockerfile bằng instruction VOLUME /var/lib/mysql — đây cũng chỉ là metadata (không tạo layer), báo cho người dùng image biết thư mục nào nên được mount volume vào, nhưng không tự tạo volume nào cho tới khi container thực sự chạy.

Theo kinh nghiệm vận hành thực tế, sai lầm phổ biến nhất với volume không phải là quên dùng nó, mà là quên rằng docker system prune -a --volumes hay docker container rm -f $(docker container ls -aq) (đã thấy ở Phần 5) xóa luôn cả volume không còn container nào tham chiếu nếu thêm cờ --volumes — 1 lệnh dọn dẹp tưởng vô hại có thể xóa mất dữ liệu production nếu chạy nhầm môi trường. Luôn kiểm tra kỹ docker volume ls trước khi chạy bất kỳ lệnh dọn dẹp hàng loạt nào.

Phần tiếp theo rời khỏi 1 host đơn lẻ, bước sang quy mô cluster: Swarm mode — cách Docker tự động hóa việc chạy, scale, và tự chữa lành hàng chục container trải trên nhiều máy.


Danh sách lệnh & instruction chính

Lệnh/InstructionCông dụng
docker image build -t <tên>:<tag> .Đọc Dockerfile và containerize app; -f cho phép chỉ định tên/vị trí Dockerfile khác mặc định
docker image history <image>Xem lại các instruction đã dùng để build image
FROMChỉ định base image — thường là instruction đầu tiên
RUNChạy lệnh bên trong image lúc build, mỗi RUN tạo 1 layer mới
COPYCopy file vào image dưới dạng 1 layer mới (thường dùng để copy code app)
EXPOSEGhi chú network port ứng dụng dùng (metadata, không tạo layer)
ENTRYPOINTĐặt chương trình mặc định chạy khi container khởi động (metadata)
COPY --from=<stage>Trong multi-stage build, copy artifact từ 1 stage trước đó sang
HEALTHCHECKKhai báo lệnh Docker tự chạy định kỳ để xác nhận app còn phục vụ được, không chỉ "còn chạy"
docker volume create <tên> / ls / rmTạo / liệt kê / xóa named volume
--mount source=<vol>,target=<path>Gắn volume (hoặc type=bind,source=<host-path> để bind mount) vào container lúc run

Các instruction khác còn có LABEL, ENV, ONBUILD, CMD...


Nguồn tham khảo

Nguồn gốc: Bài viết lấy cảm hứng từ sách "Docker Deep Dive" — tác giả Nigel Poulton (bản phát hành version 4, 3/10/2017), Part 2: The Technical Stuff — chapter 8 "Containerizing an App". Nội dung được viết lại theo cách hiểu riêng, ví dụ minh họa (web app Node.js, multi-stage build Go) là ví dụ tự soạn, không sao chép từ sách gốc. Muốn đọc bản đầy đủ hoặc ủng hộ tác giả, có thể mua sách tại leanpub.com/dockerdeepdive.