Docker Compose: build và image trên VPS khác gì?
Hiểu chính xác image chỉ pull tag, còn build tạo image từ Dockerfile. Tìm lý do compose up bỏ qua thay đổi Dockerfile và cách sửa trên VPS.
Docker Compose: build và image — câu trả lời ngắn
Trong file Docker Compose, image: chỉ định image cần pull từ registry, còn build: yêu cầu Compose build image trên máy này từ Dockerfile. Chỉ đặt image: thì Compose pull tag đó rồi chạy image. Chỉ đặt build: thì Compose build image tại đây và đặt tên dựa trên tên project cùng tên service. Đặt cả hai thì Compose build image cục bộ, sau đó gắn tag cho kết quả bằng tên trong image:. Đây là cách build một image rồi push image đó bằng tên bạn chọn.
Đó là toàn bộ khác biệt. Phần bên dưới giải thích cách các tùy chọn này hoạt động trên server. Nội dung này giả định Docker Engine và Compose plugin đã được cài đặt; chạy Docker trên VPS giải thích phần đó.
Ba dạng đầy đủ
Pull một tag đã được publish và chạy tag đó. Dockerfile không được dùng ở bất kỳ bước nào.
services:
web:
image: nginx:1.27
restart: unless-stopped
ports:
- "80:80"Build từ Dockerfile trong thư mục hiện tại. Không có gì được pull ngoài base image được nêu trong FROM.
services:
web:
build: .
restart: unless-stopped
ports:
- "80:80"Build cục bộ và gắn tag cho kết quả. Sau đó docker compose push có thể gửi đúng tag đó đến một registry.
services:
web:
build:
context: .
dockerfile: Dockerfile
image: registry.example.com/acme/web:1.4.2
restart: unless-stopped
ports:
- "80:80"context là thư mục được gửi đến builder. dockerfile được phân giải tương đối so với context đó, vì vậy context: . cùng với dockerfile: docker/prod.Dockerfile là cách dùng bình thường và đúng. Chạy docker compose images để xem tên image và image ID tương ứng với từng service container. Đây là cách nhanh nhất để xác nhận bạn thực sự đã viết theo dạng nào trong ba dạng này.
Vì sao docker compose up không build lại sau khi tôi thay đổi Dockerfile?
Vì up chỉ kiểm tra image có tồn tại hay không, không kiểm tra image còn mới hay không.
Khi Compose khởi động một service có phần build:, nó tìm image trong kho image cục bộ. Nếu đã có image với tên đó, Compose sẽ dùng image này. Nó không đọc Dockerfile, không so sánh các file mã nguồn và không kiểm tra timestamp nào. Đặc tả Compose quy định hành vi này bằng thuộc tính pull_policy, trong đó hành vi mặc định là chỉ build image khi image chưa tồn tại. Image đã tồn tại được xem là đủ điều kiện sử dụng.
Vì vậy, bạn sửa app.py, chạy docker compose up -d, thấy Compose báo container đang chạy, nhưng ứng dụng vẫn phục vụ mã nguồn cũ. Không có lỗi nào xảy ra nên cũng không có cảnh báo. Đây là nguyên nhân phổ biến nhất của tình huống “thay đổi của tôi không có hiệu lực” khi dùng Compose. Dấu hiệu rõ nhất là từ trạng thái Compose in bên cạnh tên container: container được Compose thay thế sẽ có trạng thái recreated hoặc started, còn container mà Compose quyết định giữ nguyên sẽ có trạng thái running.
Hai lệnh kiểm tra là đủ để xác định vấn đề. docker compose images in image ID mà mỗi container đang sử dụng; hãy ghi lại ID trước khi deploy và so sánh sau đó. docker image ls có cột CREATED; nếu image được tạo trước lần commit gần nhất thì đó là image cũ, bất kể script deploy đã in gì.
Các flag nào buộc build lại
docker compose up -d --buildbuild trước, sau đó tạo lại mọi container có image đã thay đổi. Đây là flag mà hầu hết mọi người đang tìm.docker compose build webbuild một service nhưng không khởi động gì. Dùng tiếpdocker compose up --no-deps -d webđể chỉ thay thế container đó và giữ các service còn lại trong stack tiếp tục chạy.docker compose build --no-cache webloại bỏ toàn bộ layer trong cache và build lại từ instruction đầu tiên.docker compose build --pullcố gắng pull phiên bản mới hơn của base image trongFROM. Vì vậy, tag có thể thay đổi nhưnode:22sẽ lấy nội dung hiện tại thay vì bản copy bạn đã tải vào tháng 3.docker compose up -d --force-recreatetạo lại container từ image mà chúng đang dùng. Flag này không bao giờ build. Dùng nhầm flag này khi bạn cần--buildlà một lỗi thường gặp và không giải quyết được vấn đề.
Bạn cũng có thể đưa quyết định này vào file. Theo cách diễn đạt trong Compose specification, pull_policy: build nghĩa là Compose sẽ build image và build lại nếu image đã tồn tại. Mỗi lần chạy up sau đó đều phải build, phù hợp khi dùng trên laptop nhưng hiếm khi phù hợp trên server.
services:
web:
build: .
image: registry.example.com/acme/web:dev
pull_policy: buildCần lưu ý thêm một tương tác. docker compose pull cũng cố gắng pull image cho các service có section build. Nếu pull thất bại, lệnh sẽ cho biết image phải được build thay thế. Truyền --ignore-buildable để âm thầm bỏ qua các service đó.
Cách build cache quyết định thời gian deploy
Mỗi instruction trong Dockerfile tạo ra một layer. Builder sẽ dùng lại layer trong cache khi instruction đó và các input của nó không thay đổi. Với COPY, input là nội dung của các file được copy. Khi một layer không còn trong cache, mọi layer phía sau nó đều phải build lại, vì mỗi layer được build trên filesystem do layer trước đó tạo ra.
Quy tắc này quyết định deploy mất vài giây hay vài phút. Hãy sắp xếp Dockerfile từ những phần ít thay đổi đến những phần thay đổi trong mỗi commit.
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]npm ci nằm phía trên COPY . .. Vì vậy, khi sửa một source file, install layer vẫn được giữ trong cache và quá trình build tiếp tục từ bước copy. Nếu đổi chỗ hai dòng đó, chỉ một thay đổi một ký tự cũng khiến mọi dependency được cài lại, vì COPY . . làm mất hiệu lực của layer mà npm ci dựa trên đó. Cấu trúc tương tự cũng áp dụng cho pip install -r requirements.txt và go mod download.
--no-cache là công cụ phù hợp khi bạn nghi ngờ một layer cũ đang che khuất bản sửa. Tuy nhiên, không nên dùng nó làm mặc định, vì nó xóa cơ chế reuse mà cách sắp xếp Dockerfile được thiết kế để tận dụng.
Image sets và Compose có thể override một giá trị: CMD trong Dockerfile là lệnh image chạy mặc định, còn key command: trong service sẽ thay thế lệnh đó. Cách command và entrypoint tương tác rất quan trọng ở đây, vì một override trong Compose có thể khiến image vừa build hoạt động giống hệt image cũ.
Build context và .dockerignore
context: . có nghĩa là Compose đóng gói thư mục đó và gửi đến builder trước khi chạy instruction đầu tiên. Mọi thứ bên trong đều được gửi, bao gồm .git và mọi data directory mà bạn để cạnh source. Nếu build dừng lâu ở bước chuyển build context trong khi project không thay đổi, context đang quá lớn.
File .dockerignore ở root của context sẽ loại trừ các path khỏi lần chuyển đó. Cú pháp gần giống .gitignore.
.git
node_modules
*.log
data/
.envCó 2 lợi ích. Dữ liệu cần chuyển nhỏ hơn nên mọi build đều bắt đầu nhanh hơn. Ngoài ra, COPY . . sẽ không thể copy .env vào image nữa; bất kỳ ai pull image đó cũng có thể đọc dữ liệu này.
Trường hợp build chậm dần theo thời gian thường là do bind mount. Named volume nằm bên ngoài project directory, nhưng bind mount như ./data:/var/lib/postgresql/data nằm bên trong build context. Vì vậy, build sẽ chậm hơn mỗi tuần khi database tăng lên. Thêm 1 dòng vào .dockerignore là xử lý được vấn đề này. Bind mount và named volume trình bày trade-off rộng hơn.
Build argument có một rủi ro tương tự nhưng nhỏ hơn. Các giá trị truyền qua args: hiển thị trong image history cho bất kỳ ai đang giữ image, vì vậy chỉ đặt version number ở đó, không bao giờ đặt token. Env file và secret trong Compose giải thích credential nên được đặt ở đâu.
Xây dựng trên VPS hay xây ở nơi khác rồi pull?
Xây dựng ngay trên máy chủ đang phục vụ traffic là lựa chọn mặc định vì đây là quy trình ngắn nhất: git pull, rồi docker compose up -d --build. Cách này phù hợp với một server nhỏ chưa có ai phụ thuộc vào. Nhưng nó không còn phù hợp vì 2 lý do có thể đo được và 1 vấn đề chỉ lộ ra vào ngày hệ thống gặp sự cố.
Bộ nhớ. Quá trình build chạy compiler và bundler ngay cạnh ứng dụng đang hoạt động, trong khi đây thường là các thành phần dùng nhiều bộ nhớ nhất trong stack. Trên VPS 1 GB, JavaScript bundler hoặc quá trình compile Rust thường là process lớn nhất trên máy. Khi kernel hết bộ nhớ, nó sẽ kill process lớn nhất: build dừng với Killed và exit status 137, hoặc database bị kill thay và site ngừng hoạt động giữa lúc deploy. dmesg -T | grep -i oom in ra dòng kill cùng tên process, nhờ đó bạn biết chính xác trường hợp nào đã xảy ra thay vì phải đoán.
Disk. Mỗi lần build để lại các layer, còn builder giữ cache riêng ngoài các image của bạn. docker system df hiển thị cả hai, và dòng build cache chỉ tăng lên. Dùng docker image prune để thu hồi dangling image và docker builder prune để thu hồi cached layer. Disk đầy không chỉ làm build dừng. Database cũng không ghi được nữa, và lỗi này gây thiệt hại lớn hơn nhiều so với một lần deploy chậm.
Khả năng tái lập. Image được build trên server chỉ tồn tại trên server đó. Muốn rollback, bạn phải checkout commit cũ rồi build lại. Kết quả build không được đảm bảo giống bản trước, vì base tag đã thay đổi và package mirror cũng thay đổi theo. Xây dựng ở nơi khác rồi push một tag sẽ biến rollback thành việc chỉnh sửa: trỏ image: vào tag trước đó rồi chạy docker compose up -d.
Cách tổ chức bền vững khá đơn giản. Continuous integration của bạn chạy build và push registry.example.com/acme/web:<git-sha>, còn file Compose trên VPS chứa image: mà hoàn toàn không có key build:. Khi đó, việc deploy chỉ còn 2 command và gần như không cần thêm bộ nhớ.
docker compose pull
docker compose up -dChạy docker login registry.example.com một lần trên server, sau đó Compose có thể pull các private tag.
Giữ lại phần build để dùng trong quá trình development thay vì xóa nó, trong một file do bạn tự đặt tên.
# compose.dev.yaml
services:
web:
build:
context: .
pull_policy: builddocker compose -f compose.yaml -f compose.dev.yaml up -d --buildĐặt tên file đó là compose.dev.yaml, không phải compose.override.yaml. Compose tự động load file override khi file này tồn tại, nên một file override bị copy nhầm lên server sẽ âm thầm khiến hệ thống build ở đó lần nữa. Layer nhiều file Compose giải thích cách merge xử lý từng key.
Bẫy kiến trúc khi build ở nơi khác
Một image chứa kiến trúc CPU mà nó được build cho. Nếu build trên laptop Apple Silicon, push, rồi pull tag đó lên VPS x86_64, Docker sẽ cảnh báo platform của image được yêu cầu không khớp với platform của host được phát hiện. Sau đó tiến trình dừng với exec format error. Lỗi này trông giống binary bị hỏng nhưng không phải vậy. Hãy build rõ ràng cho target:
docker buildx build --platform linux/amd64 \
-t registry.example.com/acme/web:1.4.2 --push .Mismatch tương tự cũng xảy ra theo chiều ngược lại nếu laptop của bạn dùng x86 nhưng bạn chạy trên VPS ARM thay vì VPS x86. Để CI build trên đúng kiến trúc mà bạn triển khai sẽ loại bỏ vấn đề này.
Cần kiểm tra gì sau khi deploy
docker compose imageshiển thị image và tag đang được sử dụng bởi từng container đang chạy. Image ID thay đổi là bằng chứng build mới đã được đưa vào sử dụng.docker compose confighiển thị file đã được gộp sau khi thay thế biến, để bạn đọc tên image cuối cùng mà Compose sẽ dùng trước khi chạy.docker compose logs -f webtrong 30 giây đầu sau khi chuyển đổi. Container khởi động rồi thoát sẽ bị restart lặp lại thay vì duy trì trạng thái chạy. Vòng lặp này sẽ không hiện rõ nếu bạn không kiểm tra.docker image lshiển thị cột CREATED. Nếu image cũ hơn commit gần nhất thì image đó chưa từng được build lại.
Nếu bạn vẫn đang tạo file mà các lệnh kiểm tra này sử dụng, kiến thức cơ bản về file Compose trên VPS trình bày các key liên quan, còn bảng tra cứu nhanh các lệnh Compose liệt kê những subcommand còn lại.
FAQ
Tôi có thể dùng build và image trong cùng một service không?
Có. Đây là cấu hình thông thường cho project bạn tự build. Compose build từ phần build: và gắn tag cho kết quả bằng giá trị của image:. Tag đó là giá trị docker compose push gửi lên registry và máy khác dùng để pull. Nếu không có key image:, Compose vẫn build, nhưng đặt tên image theo project và service. Compose cũng cảnh báo rằng thiếu thuộc tính này nên image không thể được push.
Vì sao docker compose up không nhận thay đổi trong Dockerfile của tôi?
Vì up chỉ kiểm tra xem đã có image mang tên đó hay chưa. Nếu image đã tồn tại, Compose khởi động image đó và không đối chiếu với Dockerfile hoặc các file source của bạn. Hãy chạy docker compose up -d --build, hoặc chạy docker compose build web rồi chạy docker compose up --no-deps -d web để thay thế một service. Đặt pull_policy: build cho service sẽ khiến mọi up đều rebuild, phù hợp với máy development.
--build khác --force-recreate như thế nào?
--build build lại image, sau đó recreate các container có image đã thay đổi. --force-recreate recreate container từ image hiện có, nên không thể nhận thay đổi code. Nếu thay đổi nằm trong source hoặc Dockerfile, --build là flag bạn cần dùng. --force-recreate dùng để reset chính container, chẳng hạn để xóa writable layer nhưng vẫn giữ nguyên image.
Tôi nên build Docker image trên VPS hay ở nơi khác?
Hãy build ở nơi khác rồi pull một tag sau khi máy đó bắt đầu phục vụ traffic. Build cạnh tranh memory với ứng dụng. Trên VPS nhỏ, kernel có thể xử lý việc này bằng cách kill process lớn nhất, có thể là process build hoặc database. Build cũng để lại cache trên disk mà không có thành phần nào tự dọn. Build trên server vẫn phù hợp với project nhỏ chưa có user. Sau này chuyển sang nơi khác cũng không tốn nhiều công nếu bạn giữ phần build: trong một file Compose chỉ dùng cho development.
Làm thế nào để ngăn Docker build cache chiếm đầy disk?
Chạy docker system df để xem image và build cache đang chiếm bao nhiêu dung lượng. docker builder prune xóa các layer đã cache, còn docker image prune xóa các image dangling do những lần build trước để lại. Thêm -a vào một trong hai lệnh sẽ dọn mạnh hơn và buộc lần build tiếp theo khởi động từ đầu. Không lập lịch chạy docker system prune -af --volumes trên server, vì --volumes sẽ xóa mọi volume hiện không được container nào sử dụng. Một stack đang dừng để bảo trì có thể lưu database trong đúng loại volume đó.