# Deploy bằng Docker

> Chạy Dockerfile của bạn hoặc một image công khai có sẵn sau tên miền, với phát hành không gián đoạn có health check, rollback, đường dẫn lưu trữ cố định có sao lưu, log và cô lập container.

Source: https://zopanel.net/vi/docs/docker-deploy  
Updated: 2026-10-09

Khi ứng dụng không phù hợp với các runtime có sẵn (cần phiên bản ngôn ngữ khác, thư viện hệ thống riêng, image của nhà cung cấp), ZoPanel có thể chạy nó dưới dạng container. Hệ thống build `Dockerfile` trong repository của bạn, hoặc chạy một image công khai có sẵn, chỉ mở container trên `127.0.0.1` và đặt nginx, SSL, tường lửa của website phía trước. Với ứng dụng cài một chạm như n8n hay Nextcloud, hãy dùng [Kho ứng dụng](/vi/docs/apps); cách đó không cần các quyền dưới đây.

## Điều kiện cần

Phải đáp ứng đủ các điều kiện sau, nếu không lần deploy sẽ bị từ chối:

| Điều kiện | Ai thiết lập | Cách làm |
| --- | --- | --- |
| Đã cài Docker | Quản trị viên | **Kho ứng dụng → Cài Docker**, hoặc **Thành phần → Docker**. |
| Máy chủ cho phép container tuỳ chỉnh | Root trên máy chủ | `zopanel ctl feature enable custom-docker` (xem bên dưới). |
| Tài khoản được chạy container riêng | Quản trị viên hoặc đại lý | Tài khoản thuộc quản trị viên toàn quyền, hoặc gói hosting của tài khoản bật **Ứng dụng Docker riêng** trong **Gói hosting** (mặc định tắt). Đại lý chỉ cấp được khi gói của chính đại lý có quyền này. |
| Gói có Git deploy | Quản trị viên hoặc đại lý | **Git deploy & ứng dụng** không bị tắt trong tính năng của gói. |

Bật container tuỳ chỉnh một lần, với quyền root:

```bash
sudo zopanel ctl feature enable custom-docker
```

Công tắc này chỉ có trên dòng lệnh của máy chủ, không có trên web panel, nên một phiên đăng nhập panel bị đánh cắp cũng không thể chạy container tuỳ ý.

## Deploy Dockerfile từ repository

1. Vào **Website → Thêm website**, chọn **Git deploy**, nhập **Địa chỉ repository**, **Nhánh** và, nếu `Dockerfile` nằm trong thư mục con, **Thư mục gốc**. Bấm **Tạo website**.
2. Nếu repository chỉ có `Dockerfile` (không có `package.json`, `requirements.txt`, `go.mod`…), nó được nhận diện là **Dockerfile** và build thành container.
3. Nếu hệ thống nhận diện ra framework khác, ghi chú "A Dockerfile was also found: switch the runtime to docker to build with it instead." sẽ hiện ra. Trong tab **Deploy**, tắt **Tự nhận diện** ở **Build & chạy**, đặt **Runtime** là **Dockerfile** (**Loại** chuyển thành **Container (Dockerfile)**) rồi bấm **Lưu & deploy**.

Build context là thư mục gốc: `Dockerfile` phải nằm ở đó, và cả nó lẫn `.dockerignore` phải là file thường, không phải symbolic link. ZoPanel chạy `docker build --pull` nên base image được làm mới ở mỗi lần build, và truyền `PORT` làm build argument (khai báo `ARG PORT` nếu cần). Ở chế độ này không dùng lệnh cài đặt, build hay chạy: hãy đặt chúng trong `Dockerfile`. Lần build chạy quá 45 phút sẽ bị dừng và lần deploy thất bại.

## Deploy image có sẵn

1. Tạo website (ví dụ **Node / Proxy**) hoặc mở website có sẵn, rồi vào tab **Deploy**.
2. Ở **Nguồn**, chọn **Docker image có sẵn**.
3. Nhập **Image**, ví dụ `nginx:1.27` hoặc `ghcr.io/owner/app:1.2.0`, rồi bấm **Deploy**.

Quy tắc với image:

- Chỉ image công khai (Docker Hub, GHCR hoặc registry công khai khác). Registry trên `localhost` hoặc địa chỉ IP bị từ chối.
- Tên viết thường, có thể kèm tag và digest (`app@sha256:…`), tối đa 255 ký tự.
- Nên ghi rõ tag. Mỗi lần deploy đều pull lại image, nên tag thay đổi như `latest` có thể khác nhau giữa các lần deploy.

ZoPanel biến image thành một bản release (một `Dockerfile` một dòng `FROM` image đó), nên nó có cùng health check, chuyển đổi không gián đoạn và rollback như Dockerfile tự build.

## Cổng

Container phải lắng nghe trên một cổng HTTP. ZoPanel chọn cổng theo thứ tự:

1. biến môi trường `PORT`, nếu bạn đặt;
2. `EXPOSE` đầu tiên trong `Dockerfile` (với image có sẵn là cổng TCP đầu tiên image khai báo);
3. `8080`.

Bên trong container, `PORT` được đặt bằng cổng đó. Bên ngoài, cổng được mở trên `127.0.0.1:<cổng nội bộ>` (hiển thị là **Cổng nội bộ** trong tab **Deploy**) và nginx chuyển tên miền tới đó. Ngoài cổng này, không có gì của container truy cập được từ internet.

```dockerfile
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci && npm run build
ENV PORT=3000
EXPOSE 3000
USER node
CMD ["node", "server.js"]
```

Ứng dụng bên trong nên lắng nghe `0.0.0.0:$PORT`, không phải `127.0.0.1`: trong container, `127.0.0.1` là chính container đó.

## Biến môi trường

Thêm biến trong thẻ **Biến môi trường** rồi bấm **Lưu & deploy lại**. Tối đa 200 biến, giá trị một dòng. Biến được truyền vào container khi khởi động qua một file chỉ root đọc được, không bao giờ nằm trên dòng lệnh.

- Giá trị được truyền nguyên văn. Đừng bọc trong dấu nháy (**Dán .env** tự bỏ dấu nháy bao ngoài).
- Lúc build, một biến chỉ tới được các bước `RUN` nếu `Dockerfile` khai báo nó bằng `ARG` (xem bên dưới). Các biến còn lại chỉ có khi container chạy.

### Biến lúc build

Khi `Dockerfile` khai báo `ARG TEN_BIEN` (hoặc `ARG TEN_BIEN=mặc_định`) và panel có biến `TEN_BIEN`, ZoPanel truyền nó cho `docker build` làm build argument. Log tác vụ liệt kê tên các biến được truyền (`Build arguments from the environment variables: …`), không bao giờ ghi giá trị.

```dockerfile
FROM node:22-alpine AS build
ARG NEXT_PUBLIC_API_URL
WORKDIR /app
COPY . .
RUN npm ci && npm run build
```

- Chỉ những tên được khai báo mới được truyền, nên các biến khác (mật khẩu database, API key) không bao giờ bị build vào image.
- **Giá trị build argument xem được trong lịch sử image:** `docker history` hiển thị chúng ở mọi bước `RUN` sau `ARG`, và những gì một bước ghi ra từ chúng nằm lại trong layer đó. Đừng khai báo bí mật bằng `ARG`; hãy đọc chúng từ biến môi trường lúc chạy. Dùng `ARG` cho cấu hình công khai như URL API công khai hay cờ tính năng.
- `ARG` đã khai báo mà panel không có biến tương ứng thì nhận giá trị mặc định trong `Dockerfile`.
- Các tên có thể thay đổi cách lệnh `docker` của máy chủ chạy không bao giờ được truyền, ví dụ `PATH`, `HOME`, `LD_*`, `DOCKER_*`, `BUILDKIT_*`, `GO*`, `SSL_*` và các biến proxy (`HTTP_PROXY`…). Log tác vụ liệt kê các tên bị bỏ qua. `PORT` luôn là cổng của container.

## Health check và phát hành không gián đoạn

Mỗi bản release khởi động ở container thứ hai, trên cổng của slot rảnh. Bản đó được đưa lên khi **Đường dẫn health check** (mặc định `/`) trả về mã HTTP dưới 500 trong vòng 120 giây. Sau đó nginx chuyển sang container mới và container cũ được xoá sau vài giây. Nếu build lỗi, container thoát hoặc không đạt health check, 25 dòng log cuối của nó được ghi vào log tác vụ và container đang chạy vẫn tiếp tục phục vụ.

## Rollback

Thẻ **Các bản release** giữ 5 bản gần nhất, mỗi bản giữ image đã build. **Rollback** chạy image của bản được chọn ở slot rảnh, chờ health check rồi chuyển, không build lại. Image của các bản cũ hơn bị xoá cùng bản release.

## Log và điều khiển

- **Log ứng dụng** hiển thị 400 dòng log mới nhất của container kèm thời gian; bật **Trực tiếp** để theo dõi. Docker xoay vòng log container (3 file, mỗi file 10 MB).
- Thẻ trạng thái hiển thị tình trạng container, **RAM** và **Cổng nội bộ**. Các nút **Khởi động lại**, dừng và bật tác động lên container đang chạy.
- Log build (đầu ra của `docker build`) của từng lần nằm ở **Tác vụ**.

## Giới hạn và dữ liệu

| Giới hạn | Giá trị |
| --- | --- |
| RAM | 1024 MB mỗi container |
| CPU | 1 CPU |
| Số tiến trình | 512 |
| Khởi động lại | Tự khởi động lại, trừ khi đã bị dừng |

Trên máy chủ mà Docker dùng cgroup driver systemd (mặc định trên Ubuntu 22.04/24.04 và Debian 12/13), container còn chạy trong nhóm tài nguyên của tài khoản, nên giới hạn của gói cũng được áp dụng.

### Quá trình build

`docker build` chạy trong Docker daemon, không chạy bằng user Linux của tài khoản. Với cgroup driver systemd, các container của những bước `RUN` khi build được đặt vào nhóm tài nguyên của tài khoản (`--cgroup-parent`), nên giới hạn CPU và RAM của gói áp dụng cho quá trình build giống như cho ứng dụng đang chạy: bước nào cần nhiều RAM hơn gói cho phép sẽ bị dừng (`Killed` trong log tác vụ). Cơ chế này hoạt động với BuildKit (thành phần `buildx` của Docker) lẫn với trình build cũ mà Docker dùng khi chưa cài `buildx`. Những phần không bị giới hạn của gói chi phối:

- tải base image, gửi build context và xuất image, do chính Docker daemon thực hiện;
- dung lượng đĩa của image và build cache (nằm trong `/var/lib/docker`, không tính vào quota của tài khoản).

Mọi lần build đều bị dừng sau 45 phút.

### Đường dẫn lưu trữ cố định

Hệ thống file của container được tạo lại ở mỗi lần deploy và rollback, nên mọi thứ ghi bên trong sẽ mất, trừ các **đường dẫn lưu trữ cố định**. Trong tab **Deploy**, ở **Build & chạy**, nhập chúng vào **Đường dẫn lưu trữ cố định (trong container)**, cách nhau bằng dấu phẩy, rồi bấm **Lưu & deploy**:

```text
/app/data, /app/uploads
```

- Mỗi đường dẫn là một thư mục tuyệt đối bên trong container, tối đa 10. Chỉ dùng chữ, số, `.`, `_`, `-` và `/`; không có `..`, không có thư mục nằm trong một thư mục khác của danh sách.
- Thư mục hệ thống bị từ chối: `/proc`, `/sys`, `/dev`, `/etc`, `/run`, `/boot`, `/bin`, `/sbin`, `/lib*`, `/usr/bin`, `/usr/sbin`, `/usr/lib*` và mọi thứ bên trong, cùng chính các thư mục `/`, `/usr`, `/usr/local`, `/usr/share`, `/usr/src`, `/var`, `/var/lib`, `/var/cache`, `/var/log`, `/home`, `/opt`, `/root`, `/tmp`, `/mnt` và `/media` (thư mục con của chúng, như `/var/lib/postgresql/data` hay `/usr/src/app/uploads`, thì được).
- Mỗi đường dẫn là một thư mục trên máy chủ trong `/var/lib/zopanel-apps/git-<user>-<domain>/`, chỉ root và các container của website này truy cập được. Nó được gắn vào cùng đường dẫn ở cả hai slot release: trong lúc deploy, container cũ và mới cùng dùng nó vài giây, điều mà database dạng file như SQLite xử lý được.
- Thư mục mới ban đầu trống và che mọi thứ image có ở đường dẫn đó, nên hãy dùng thư mục dữ liệu, không dùng thư mục chứa code. Thư mục thuộc về `USER` của image khi đó là ID dạng số (`USER 1000`); khi image chạy bằng root, nó thuộc root trong container; khi `USER` là tên (`USER node`), mọi user trong container đều ghi được. Dữ liệu đã có giữ nguyên chủ sở hữu.
- Giống dữ liệu ứng dụng trong Kho ứng dụng, nó không tính vào quota đĩa của tài khoản.
- Dữ liệu vẫn còn khi bạn đổi danh sách đường dẫn. Nó chỉ bị xoá cùng website khi bạn chọn xoá file của website, và khi xoá tài khoản.
- Khi chuyển tài khoản sang máy chủ khác, các đường dẫn được tạo lại ở máy chủ mới nhưng trống: hãy tự chép dữ liệu.

Các thư mục này sao lưu được giống dữ liệu của ứng dụng trong Kho ứng dụng: thẻ **Sao lưu** xuất hiện trong tab **Deploy** khi ứng dụng có đường dẫn lưu trữ cố định. Ai quản lý website cũng sao lưu ngay được; quản trị viên đặt lịch (hằng ngày hoặc hằng tuần, số bản giữ lại), khôi phục và xoá. Khi sao lưu, container tạm dừng trong lúc chép; khi khôi phục, container bị dừng, dữ liệu được thay rồi container chạy lại. Bản sao lưu nằm trong `/var/backups/zopanel-apps/` trên máy chủ, tách riêng khỏi bản sao lưu tài khoản.

Với dữ liệu dùng chung với dịch vụ khác, database trên máy chủ khác hoặc object storage, ví dụ [Lưu trữ S3](/vi/docs/apps) của ZoPanel đã gắn tên miền (truy cập qua HTTPS cổng 443), vẫn là nơi phù hợp hơn.

## Cô lập

Container của khách chạy mã mà máy chủ không kiểm soát, nên ZoPanel giới hạn nó:

- **Mạng:** container không mở được kết nối tới địa chỉ private, shared, loopback hay link-local (kể cả dịch vụ metadata của cloud tại `169.254.169.254`), cũng không kết nối được tới container khác. Trên chính máy chủ, container chỉ tới được các cổng công khai: web (80, 443), mail (25, 465, 587) và DNS. MySQL, PostgreSQL, Redis và panel của máy chủ không truy cập được từ container. Quá trình build image cũng chịu cùng quy tắc.
- **Quyền:** mọi Linux capability bị bỏ, trừ `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID`, `SETGID`, `NET_BIND_SERVICE` và `KILL`; bật `no-new-privileges`; áp dụng profile seccomp và AppArmor mặc định của Docker.
- **User namespace:** root trong container tương ứng với một dải ID không có đặc quyền trên máy chủ.
- **Không truy cập máy chủ:** không có Docker socket, không gắn thư mục nào của máy chủ ngoài các đường dẫn lưu trữ cố định của chính website.

Nếu không áp dụng được các quy tắc này, ZoPanel không build và không khởi động container.

## Xử lý sự cố

| Thông báo | Nguyên nhân và cách xử lý |
| --- | --- |
| `this account's package does not allow its own Docker apps (Dockerfile or image); ask the administrator` | Bật **Ứng dụng Docker riêng** trong gói của tài khoản, hoặc deploy từ tài khoản của quản trị viên toàn quyền. |
| `Dockerfile deployments are disabled; enable them on the server with: zopanel ctl feature enable custom-docker` | Chạy lệnh này với quyền root trên máy chủ. |
| `Docker is not installed (install it from the App Store page)` | Cài Docker từ **Kho ứng dụng** hoặc **Thành phần**. |
| `no Dockerfile in the build directory` | `Dockerfile` phải nằm trong **Thư mục gốc** (tên phân biệt hoa thường). |
| `docker build failed: …` | Một bước build bị lỗi; log tác vụ có đầu ra chi tiết. Các bước tải dữ liệu từ địa chỉ private bị chặn. `Killed` ở một bước nghĩa là nó cần nhiều RAM hơn gói của tài khoản cho phép. |
| `docker build did not finish within 45 minutes` | Làm build nhanh hơn (context nhỏ hơn nhờ `.dockerignore`, ít bước hơn), hoặc build image ở nơi khác rồi deploy dưới dạng image có sẵn. |
| `persistent path … is not allowed` / `must be an absolute path inside the container` | Dùng thư mục tuyệt đối của ứng dụng như `/app/data`, không dùng thư mục hệ thống. |
| Mất dữ liệu sau khi deploy | Dữ liệu được ghi ngoài các đường dẫn lưu trữ cố định. Thêm thư mục đó vào **Đường dẫn lưu trữ cố định (trong container)**. |
| Ứng dụng không ghi được vào đường dẫn lưu trữ cố định | Thư mục được tạo khi image còn chạy bằng user khác, hoặc ứng dụng cần file mà image có sẵn ở đường dẫn đó (thư mục mới ban đầu trống). Dùng `USER` dạng số, hoặc `chown` thư mục trong entrypoint chạy bằng root (container giữ quyền `CHOWN`). |
| `cannot pull … (public images only): …` | Image hoặc tag không tồn tại, hoặc là image riêng tư. |
| `image must look like nginx:1.27 or ghcr.io/owner/app:v2` | Dùng tên image viết thường. |
| `the container did not answer on port … within 120s (set PORT or EXPOSE to the port it listens on)` | Ứng dụng lắng nghe cổng khác hoặc lắng nghe `127.0.0.1` bên trong container. Đặt `PORT`, hoặc lắng nghe `0.0.0.0:$PORT`. |
| `the container exited during startup` | Đọc log container trong log tác vụ: thường do thiếu biến hoặc lệnh kết thúc ngay. |
| `Docker runs without user-namespace isolation; restart the ZoPanel agent to enable it` | Chạy `sudo systemctl restart zopanel-agent` trên máy chủ. |
| `the network isolation for containers could not be set up (iptables); Docker apps are not started without it` | Không cài được quy tắc tường lửa. Kiểm tra iptables hoạt động và Docker đang chạy. |
| Ứng dụng không kết nối được database | Container không tới được database của chính máy chủ, kể cả qua IP công khai. Dùng database trên máy chủ khác, truy cập qua địa chỉ công khai. |

## Xem thêm

- [Git deploy](/vi/docs/git-deploy)
- [Kho ứng dụng và lưu trữ S3](/vi/docs/apps)
- [Gói hosting và giới hạn](/vi/docs/packages-limits)
- [Bảo mật](/vi/docs/security)
- [Tài liệu Dockerfile](https://docs.docker.com/reference/dockerfile/)
