0

Mình tự viết một app Docker ~8 MB cho Mac Apple Silicon

Chào mọi người, mình là tác giả của DockZ — một app Docker miễn phí, mã nguồn mở (Apache-2.0) cho Mac chip Apple Silicon. Có hai lý do khiến mình ngồi viết nó:

  1. Giấy phép. Docker Desktop yêu cầu trả phí khi dùng cho mục đích thương mại ở công ty lớn (trên 250 nhân viên hoặc doanh thu trên 10 triệu USD/năm). Mình muốn một công cụ dùng ở đâu cũng được, không phải nghĩ tới chuyện seat hay điều khoản.
  2. Nặng. Trên máy mình, Docker Desktop khá nặng — app ~1.5 GB, chạy nền liên tục, chỉ để chạy vài container. Mình muốn thứ gì đó nhẹ hơn nhiều nhưng vẫn có giao diện đàng hoàng, không chỉ CLI.

Kết quả là một app SwiftUI ~8 MB (file DMG 3.5 MB). Bài này kể cách nó hoạt động bên dưới và vài vấn đề mình không ngờ tới khi làm.

Kiến trúc tổng quan

 macOS                                    Alpine VM (Virtualization.framework)
┌─────────────────────────────┐          ┌───────────────────────────────┐
│ DockZ (SwiftUI)             │  vsock   │ dockerd (bản gốc, không sửa)  │
│  ├─ dashboard / monitor     │◀────────▶│ containerd, BuildKit          │
│  ├─ chuyển tiếp cổng        │          │                               │
│  └─ docker context "dockz"  │          └───────────────────────────────┘
└─────────────────────────────┘
        ▲
        │  docker / docker compose / buildx (dùng y như cũ)
  • Không fork Docker. Trong VM chạy đúng dockerd bản upstream. DockZ đăng ký một Docker context tên dockz, nên docker, Compose, BuildKit/buildx đều dùng như bình thường, không cần học lệnh mới.
  • Điều khiển qua vsock thay vì mạng ảo. Dashboard nói chuyện với Docker Engine API qua virtio-vsock — HTTP/1.1 thuần, mỗi luồng một kết nối, không mở cổng TCP nào trên Mac.
  • Tự chuyển tiếp cổng. DockZ theo dõi events API của Docker; container publish cổng nào thì cổng đó được mở tương ứng trên Mac, bind đúng địa chỉ Docker báo về (nên 0.0.0.0 mặc định thật sự truy cập được từ mạng LAN).

Phần nặng nhất do hypervisor của Apple và userland Linux chuẩn đảm nhận, nên bản thân app rất nhỏ: không kèm runtime Electron, image của VM là Alpine tối giản.

Quản lý Docker engine từ xa — khoá không bao giờ rời khỏi chip

Cùng một cửa sổ có thể quản lý các Docker engine khác: server qua SSH, server qua mutual TLS, hoặc socket của engine khác trên cùng máy (Colima, OrbStack, Docker Desktop).

SSH thì dễ: DockZ chạy docker system dial-stdio trên máy từ xa và nói chuyện với Engine API qua đường ống đó.

TLS thì thú vị hơn. Cách cấu hình thông thường để một file key.pem nằm trong ~/.docker — ai lấy được file trên Mac của bạn là có quyền tương đương root trên server. Mình muốn khoá client không thể bị lấy ra:

  • Private key được tạo ngay trong Secure Enclave của Mac (CryptoKit SecureEnclave.P256.Signing.PrivateKey), mỗi lần dùng cần Touch ID.
  • DockZ tạo CSR (PKCS#10) từ khoá đó; chỉ CSR rời khỏi máy. Bạn ký bằng CA của mình rồi dán certificate ngược vào app.
  • Network.framework của Apple chỉ nhận identity trong Keychain cho TLS client auth, nên DockZ dùng swift-nio-ssl với signing callback tuỳ biến: khi bắt tay TLS, NIO nhờ DockZ ký, và Secure Enclave làm việc ký đó.
  • CLI docker không dùng được khoá trong Secure Enclave, nên DockZ mở một socket relay cục bộ cho mỗi môi trường. CLI nói HTTP thường với socket; DockZ lo phần TLS. CLI không bao giờ thấy khoá.

Phiên làm việc mở khoá bằng Touch ID và tự khoá lại khi màn hình khoá, Mac ngủ, hoặc bạn chuyển sang môi trường khác.

Một cái bẫy đáng kể: docker exec và attach dựa vào TCP half-close — client đóng chiều ghi nhưng vẫn tiếp tục đọc. Bản relay đầu tiên của mình đóng cả hai chiều ngay khi một phía kết thúc, làm output bị cắt cụt mà không báo lỗi gì. Xử lý đúng half-close (cho phép phía kia đóng một nửa, rồi chỉ đóng chiều output sau khi đã ghi hết dữ liệu còn chờ) là hết.

Khi container "biến mất": cho VM tự hồi phục

Có một lỗi khá khó chịu: cứ vài ngày, toàn bộ container như biến mất khỏi dashboard. Xem console log thì thấy kernel trong VM bị oops ở đường truyền (TX) của virtio-vsock, sau đó ext4 bị deadlock và dockerd ngừng phản hồi — VM vẫn "đang chạy" nhưng vô dụng, có lúc cả mấy tiếng.

Đến giờ mình vẫn chưa xác định được lỗi gốc nằm ở driver trong VM hay phía host. Nên DockZ giờ coi VM như một service có giám sát:

  • Hỏng thì sập hẳn, đừng treo. Kernel trong VM chạy với panic_on_oops=1, softlockup_panic=1 và panic=10, nên kernel lỗi sẽ khởi động lại thay vì sống dở chết dở.
  • Kiểm tra sức khoẻ engine. DockZ gọi /_ping mỗi 15 giây; sau 4 lần thất bại kéo dài ít nhất 60 giây thời gian máy thức (Mac ngủ thì không tính), engine bị coi là không phản hồi và VM được khởi động lại.
  • Chống vòng lặp crash. Tối đa 3 lần tự khởi động lại trong 10 phút; quá mức đó DockZ dừng và báo cho bạn, không lặp mãi.
  • Giữ bằng chứng. Console log của 5 lần khởi động gần nhất được xoay vòng và giữ lại, để lần oops sau có cái mà điều tra.

Mình cũng giảm số kết nối vsock: tab Monitor trước đây hỏi stats theo chu kỳ, mỗi container mở một kết nối mới vài giây một lần. Giờ mỗi container giữ một luồng stats lâu dài, và dashboard cập nhật theo sự kiện của Docker thay vì hỏi lại mỗi 4 giây.

Một bug nhỏ làm app crash: con số -1 của Docker

Tab Monitor hiển thị dung lượng đĩa theo image, volume và build cache. Docker trả về -1 khi không biết kích thước. Giải mã giá trị đó vào kiểu số nguyên không dấu làm nó thành UInt64.max, và phép tính phía sau crash. Giờ mọi con số byte từ Docker đi qua đúng một hàm, giá trị âm được chuyển thành "không rõ". Bài học nhỏ: khi đọc JSON của hệ thống khác, các giá trị đặc biệt kiểu này nên được xử lý ở một chỗ duy nhất, đừng rải kiểm tra khắp nơi.

Ngoài ra còn có gì

  • Dashboard native: container nhóm theo Compose stack, đánh dấu container crash và health check lỗi; image, volume, network, registry, log trực tiếp; danh sách nào cũng có lọc và tìm kiếm.
  • Tab Monitor: CPU, RAM, mạng, đĩa theo từng container theo thời gian thực, thông số VM, dọn image/volume/build cache không dùng.
  • Máy ảo Linux kiểu Multipass (Alpine/Debian/Ubuntu), template k3s/kubeadm nhiều node chỉ một cú bấm, snapshot ổ đĩa VM bằng APFS copy-on-write.

Hạn chế (nói thật)

  • Chỉ chạy trên Mac Apple Silicon, macOS 15 trở lên.
  • Dự án còn trẻ.
  • Bản phát hành chưa được notarize, nên lần đầu mở macOS sẽ hỏi, bạn vào System Settings → Privacy & Security → Open Anyway một lần.

Dùng thử

brew tap nextage-soft/dockz https://github.com/nextage-soft/dockz
brew install --cask nextage-soft/dockz/dockz

Hoặc tải file DMG ở GitHub Releases.

Dự án vẫn đang phát triển tích cực. Nếu bạn đang dùng Docker Desktop, OrbStack hay Colima hằng ngày, mình rất muốn nghe chỗ nào bị lỗi, thiếu, hoặc rườm rà. Mọi đóng góp đều quý — báo lỗi, ý tưởng, thử trên các cấu hình VM/mạng khác nhau, hay code. Và nếu bạn từng gặp lỗi vsock trên Virtualization.framework, comment cho mình biết với nhé!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.