Docker Compose: build và image trên VPS khác gì?
Hiểu vì sao compose up vẫn chạy image cũ sau khi sửa Dockerfile: image chỉ pull tag, còn build tạo image mới trên VPS và cần rebuild đúng cách.
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 đó và 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 đó tag kết quả bằng tên trong image:. Đây là cách build image rồi push image đó lên registry bằng tên do bạn chọn.
Đó là toàn bộ khác biệt. Các phần bên dưới giải thích cách hoạt động của chú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 rồi 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 chỉ định 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 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 resolve tương đối so với context đó, nên 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 3 dạng trên.
Vì sao docker compose up không rebuild sau khi tôi thay đổi Dockerfile?
Vì up kiểm tra image có tồn tại hay không, không kiểm tra image có mới nhất hay không.
Khi Compose khởi động một service có section build:, nó tìm image trong image store cục bộ. Nếu đã có image mang tên đó, Compose sẽ sử dụng image này. Nó không đọc Dockerfile, không so sánh các file source và không kiểm tra timestamp. Compose specification định nghĩa quy tắc này bằng attribute pull_policy. Theo hành vi mặc định, Compose chỉ build image khi image chưa tồn tại. Image đã có đượ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 running nhưng ứng dụng vẫn chạy code cũ. Không có thao tác nào fail nên cũng không có cảnh báo. Đây là nguyên nhân phổ biến nhất của lỗi “thay đổi không có hiệu lực” khi dùng Compose. Dấu hiệu dễ nhận biết là status word Compose in 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ần 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 rồi so sánh sau đó. docker image ls có cột CREATED. Nếu image được tạo trước commit gần nhất thì đó là image cũ, bất kể deploy script đã in ra thông báo gì.
Các flag nào buộc rebuild
docker compose up -d --buildbuild trước, sau đó recreate 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 start service nào. Kết hợp vớidocker 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 webxóa mọi cached layer và build lại từ instruction đầu tiên.docker compose build --pullcố pull phiên bản mới hơn của base image trongFROM. Vì vậy, moving tag nhưnode:22sẽ lấy nội dung hiện tại thay vì bản copy bạn đã download vào tháng 3.docker compose up -d --force-recreaterecreate container từ image mà container đó đang sử dụng. Flag này không bao giờ build. Dùng nó khi bạn thực sự 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 của Compose specification, pull_policy: build có nghĩa là Compose build image và rebuild image nếu image đó đã tồn tại. Mỗi lần chạy up đều phải thực hiện build. Đây là điều bạn muốn trên laptop, nhưng hiếm khi là điều bạn muốn trên server.
services:
web:
build: .
image: registry.example.com/acme/web:dev
pull_policy: buildCần biết thêm một tương tác khác. docker compose pull cũng cố pull image cho các service có section build. Nếu pull thất bại, lệnh sẽ cho biết image cần được build thay thế. Truyền --ignore-buildable để bỏ qua các service đó một cách yên lặng.
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 cache, mọi layer phía sau cũng được 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 thành phần ít thay đổi đến những thành 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òn trong cache và quá trình build tiếp tục từ bước copy. Nếu đổi chỗ 2 dòng này, chỉ một thay đổi 1 ký tự cũng khiến mọi dependency được cài lại, vì COPY . . làm invalid layer mà npm ci phụ thuộc vào. 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 của mình. Đây không phải lựa chọn mặc định tốt, vì nó xóa khả năng 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 thiết lập: 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 y 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. Tất cả nội dung bên trong đều được gửi, bao gồm .git và mọi data directory bạn giữ cùng source. Nếu build bị dừng ở bước transferring context trong một 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 quá trình truyền này. Cú pháp gần giống .gitignore.
.git
node_modules
*.log
data/
.envCó 2 lợi ích. Dữ liệu truyền đi ít hơn nên mỗi build bắt đầu nhanh hơn. Ngoài ra, COPY . . không thể copy .env vào image nữa, nơi bất kỳ ai pull image đó cũng có thể đọc lại nội dung.
Trường hợp build chậm dần theo thời gian thường do bind mount. Named volume nằm bên ngoài project directory, nhưng bind mount như ./data:/var/lib/postgresql/data lại 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. Chỉ cần 1 dòng trong .dockerignore là xử lý được vấn đề này. Bind mount so với named volume trình bày đầy đủ hơn về sự đánh đổi này.
Build argument có một rủi ro tương tự, nhưng nhỏ hơn. Các value truyền qua args: hiển thị trong image history đối với bất kỳ ai đang có image đó. Vì vậy, hãy dùng build argument cho version number, không bao giờ dùng cho token. Env file và secret trong Compose giải thích nơi nên lưu credential.
Nên build trên VPS hay build ở nơi khác rồi pull?
Build 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 nó. Tuy nhiên, nó sẽ không còn phù hợp vì 2 lý do có thể đo lường được và 1 lý do chỉ lộ ra vào một ngày sự cố.
Bộ nhớ. Một lần build chạy compiler và bundler cùng với ứng dụng đang hoạt động. Đây thường là các tiến trình dùng nhiều bộ nhớ nhất trong hầu hết stack. Trên VPS 1 GB, JavaScript bundler hoặc lần compile Rust thường xuyên 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ào đó khiến 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 bên 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 để reclaim dangling image và docker builder prune để reclaim 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.
Tính tái lập. Một image 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. Build ở 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 ổn định rất đơ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 đó, deployment chỉ gồm 2 command và gần như không cần bộ nhớ.
docker compose pull
docker compose up -dChạy docker login registry.example.com một lần trên server. Từ đó Compose có thể pull private tag.
Giữ lại phần build cho môi trường 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. Vì vậy, một file override bị copy nhầm lên server sẽ âm thầm khiến server build lại. Gộp nhiều file Compose giải thích cách merge xử lý từng key.
Cạm bẫy kiến trúc khi build ở nơi khác
Image chứa kiến trúc CPU mà nó được build cho. Nếu build trên laptop Apple Silicon, push image, rồi pull tag đó vào 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 đã phát hiện. Sau đó process dừng với exec format error. Lỗi này trông giống binary bị hỏng nhưng thực tế 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 VPS ARM thay vì VPS x86. Để CI build trên đúng kiến trúc mà bạn deploy 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 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 đã merge sau khi thay thế biến, để bạn đọc được tên image cuối cùng mà Compose sẽ dùng trước khi chạy bất kỳ lệnh nào.docker compose logs -f webtrong nửa phút đầu sau khi swap. Container khởi động rồi thoát sẽ restart liên tục thay vì duy trì trạng thái running, và vòng lặp này thường không rõ nếu bạn không kiểm tra.docker image lshiển thị cột CREATED. Image cũ hơn commit gần nhất của bạn chưa từng được build lại.
Nếu bạn vẫn đang tạo file mà các bước kiểm tra này sẽ sử dụng, phần cơ bản về file Compose trên VPS trình bày các key liên quan, còn bảng tra 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 do bạn tự build. Compose build từ section build: rồi gắn tag cho kết quả bằng giá trị của image:. Tag đó là giá trị mà docker compose push gửi lên registry và máy khác pull về. Nếu không có key image:, Compose vẫn build, nhưng đặt tên image theo project và service, đồng thời cảnh báo rằng attribute bị thiếu khiến image không thể được push.
Tại sao docker compose up không nhận thay đổi trong Dockerfile?
Vì up chỉ kiểm tra xem image có tên đó đã tồn tại hay chưa. Nếu image đã tồn tại, Compose khởi động image đó và không bao giờ đối chiếu với Dockerfile hoặc source file của bạn. Hãy chạy docker compose up -d --build, hoặc chạy docker compose build web rồi docker compose up --no-deps -d web để thay thế một service riêng lẻ. Đặt pull_policy: build cho service sẽ khiến mỗi lần up đều build lại, 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 trong 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 tag khi máy đó cũng đang phục vụ traffic. Quá trình build cạnh tranh memory với application. Trên VPS nhỏ, kernel sẽ giải quyết việc này bằng cách kill process lớn nhất. Process đó có thể là quá trình build hoặc database của bạn. Build cũng để lại cache trên disk mà không có thành phần nào tự reclaim. Build ngay 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 sức nếu bạn giữ section build: trong Compose file chỉ dành cho development.
Làm thế nào để Docker build cache không là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 cached layer, còn docker image prune xóa các dangling image do những lần build trước để lại. Thêm -a vào một trong hai lệnh sẽ xóa mạnh tay hơn và buộc lần build tiếp theo bắt đầu từ trạng thái không có cache. 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 để maintenance có thể đang giữ database trong chính volume như vậy.