Cách mình sử dụng Docker thời gian qua
Xin chào, mình là một NodeJS developer, đã đi làm được một thời gian. Mình chưa từng viết blog trước đây, nhưng giờ mình sẽ bắt đầu làm điều này. Mình nghĩ đây sẽ là một cách để mình vừa củng cố kiến thức cho bản thân, và cũng có thể sẽ mang lại một ít giá trị (mình hi vọng thế hehe). Trong bài này, mình sẽ nói về cách mà mình đã dùng Docker thời gian qua, cũng không quá cao siêu, nhưng mình thấy nó phù hợp với bối cảnh của mình.
Trước tiên, mình ôn lại một vài khái niệm trong Docker đã nhé.
Docker là gì? Các thành phần cốt lõi
Nói một cách dễ hiểu, Docker là một nền tảng giúp bạn đóng gói ứng dụng và các phụ thuộc của nó vào một môi trường cô lập. Nhờ đó, ứng dụng của bạn có thể chạy một cách nhất quán ở bất cứ đâu, từ máy tính của bạn cho đến server cloud. Giải quyết được vấn đề “work on my machine”.
Để làm được điều đó, Docker có một kiến trúc gồm các thành phần:
- Docker Client: Đây là giao diện dòng lệnh (CLI) mà bạn sử dụng hàng ngày để gõ các lệnh như
docker run
,docker build
, v.v. - Docker Daemon (Server): "Bộ não" của Docker, chạy ngầm trên máy chủ và lắng nghe các lệnh từ Client. Daemon sẽ thực hiện mọi công việc như xây dựng Image, chạy Container, quản lý Network, v.v.
- Docker Registry: Nơi lưu trữ và chia sẻ các Docker Images. Docker Hub chính là Registry công khai lớn nhất.
- Docker Host: Chính là máy chủ vật lý hoặc máy ảo nơi Docker Daemon đang chạy.
Các Docker Object :
- Image: Tương tự như một "Class" chỉ đọc. Nó chứa mọi thứ cần thiết để chạy một ứng dụng: mã nguồn, libs/deps, runtime, hệ điều hành tối giản.
- Container: "Object" được tạo ra từ một Image. Đây là môi trường chạy thực tế của ứng dụng, có thể khởi động, dừng, xóa, v.v.
- Volume: Một cơ chế để lưu trữ dữ liệu một cách độc lập với vòng đời của Container. Dữ liệu trong Volume sẽ không bị mất đi khi Container bị xóa.
- Network: Giúp các Container có thể giao tiếp với nhau hoặc với thế giới bên ngoài.
Một điểm quan trọng cần làm rõ là Docker không phải là máy ảo (Virtual Machine). Máy ảo tạo ra một hệ điều hành riêng biệt, hoàn chỉnh, chạy độc lập trên phần cứng được ảo hóa. Điều này khiến nó nặng và tốn tài nguyên.
Ngược lại, Docker sử dụng hệ điều hành hiện có của máy chủ (Host OS). Nó tận dụng hai tính năng mạnh mẽ của Linux là cgroups và namespaces để tạo ra một môi trường ảo hóa nhẹ hơn rất nhiều.
- Namespaces: giúp cô lập môi trường bên trong container. Mỗi container có một "không gian tên" riêng cho các process, network, file system, v.v., khiến chúng hoạt động độc lập và không ảnh hưởng lẫn nhau.
- cgroups (Control Groups): giúp giới hạn tài nguyên mà mỗi container có thể sử dụng (CPU, RAM, I/O), đảm bảo một container không thể chiếm hết tài nguyên của hệ thống.
Chính nhờ cách hoạt động này, Docker chỉ chứa một phiên bản "lite" của hệ điều hành, giúp các container khởi động cực nhanh và tiết kiệm tài nguyên hơn hẳn so với máy ảo.
Cách mình dùng Docker trong công việc
Thông thường, mọi người sẽ viết một Dockerfile để đóng gói toàn bộ mã nguồn, thư viện và môi trường vào một Docker Image. Sau đó, họ sẽ đẩy Image đó lên Registry và triển khai. Nhưng mình chọn một cách tiếp cận khác: Không build Image chứa mã nguồn.
Thay vào đó, mình chỉ dùng một Docker Image có sẵn môi trường runtime tương thích. Khi cần triển khai, mình sẽ dựng một Container từ Image đó, sau đó sử dụng Docker Volume để mount thư mục mã nguồn đã được build sẵn từ bên ngoài Docker Host vào bên trong Container.
Cách làm này mang lại một số lợi ích rõ ràng, đặc biệt trong giai đoạn phát triển và triển khai nhanh:
- Tốc độ triển khai cực nhanh: Không cần tốn thời gian để build lại Image mỗi khi có thay đổi mã nguồn. Toàn bộ quá trình build được thực hiện trên máy local, sau đó chỉ cần
scp
mã nguồn đã build lên server, mount vào container và restart. Toàn bộ quy trình này được tự động hóa trong một bash script, giúp tiết kiệm rất nhiều thời gian. - Linh hoạt trong debug: Một ưu điểm lớn là có thể sửa lỗi trực tiếp trên server. Nếu phát hiện một bug nhỏ, chỉ cần ssh vào server, sửa file mã nguồn trong thư mục đã mount, và container sẽ tự động cập nhật hoặi chỉ cần restart lại container mà không cần thực hiện lại toàn bộ quy trình build/deploy.
Nhược điểm: Khi mở rộng, mọi thứ trở nên phức tạp
Tuy nhiên, cách làm này cũng có những hạn chế đáng kể, đặc biệt khi dự án lớn dần:
- Không thể scale một cách dễ dàng: Vì mã nguồn không được đóng gói trong Image, không có một "production-ready image" để triển khai lên nhiều server một cách đồng nhất. Khi cần scale, phải đảm bảo mã nguồn trên mỗi server đều được cập nhật và đồng bộ, điều này rất dễ dẫn đến sai sót.
- Rủi ro về tính nhất quán: Mỗi khi có cập nhật mã nguồn, phải chạy lại script trên từng server để update. Nếu bỏ sót, mỗi server có thể chạy một phiên bản mã nguồn khác nhau, gây ra sự không nhất quán và khó khăn trong việc debug.
- Phát sinh downtime: Việc restart container để nhận mã nguồn mới đôi khi gây ra một khoảng downtime nhỏ, không phù hợp với các ứng dụng yêu cầu tính sẵn sàng cao.
- Không tận dụng được sức mạnh của Docker: Cách làm này chưa thực sự phát huy hết lợi thế về tính bất biến (immutability) và khả năng quản lý của Docker, đặc biệt khi kết hợp với các công cụ orchestrator như Kubernetes.
Lời kết
Trên đây là các mà mình hay dùng Docker, mình nghĩ nó cũng phổ biến. Mặc dù cách này mang lại nhiều thuận tiện trong giai đoạn develop và triển khai nhanh, thậm chí có thể dùng cho production, nhưng mình tin rằng việc xây dựng một quy trình CI/CD bài bản, đóng gói ứng dụng vào Docker Image vẫn là con đường tốt nhất cho một ứng dụng production-ready.
All rights reserved