Tìm kiếm trang web

Cách dễ nhất để xây dựng một bộ công cụ phát triển cục bộ chỉ với một tệp


Mọi lập trình viên mà tôi biết đều đã trải qua cùng một chu kỳ. Bạn bắt đầu một dự án mới, và bạn cần một cơ sở dữ liệu. Vì vậy bạn tải PostgreSQL hoặc MySQL, thực hiện cấu hình, và quên mật khẩu mà bạn đặt chỉ sau năm phút. Vài tháng sau, bạn bắt đầu một dự án khác cần một phiên bản khác của cơ sở dữ liệu đó, hoặc có thể nó cần Redis, và đột nhiên máy chủ của bạn trở thành nghĩa địa của các dịch vụ mà bạn đã cài đặt một lần và quên mất.

Có nhiều cách để giải quyết vấn đề này (thật sự quá nhiều), nhưng đối với một môi trường phát triển cá nhân tại máy, giải pháp đơn giản và dễ bảo trì nhất là đặt mọi thứ trong một tệp Docker Compose duy nhất.

Giải phẫu một hộp công cụ phát triển cục bộ

Các dịch vụ mà hầu hết các môi trường phát triển cần

Khi tôi ngồi xuống để làm việc trên một ứng dụng web, có một vài dịch vụ mà tôi gần như luôn cần. Hầu hết các nhà phát triển đều dựa vào một bộ công cụ tương tự, ngay cả khi các yêu cầu chính xác khác nhau tùy dự án. Tôi cần một cơ sở dữ liệu để lưu trữ dữ liệu, thường là PostgreSQL. Tôi thường cần một kho lưu trữ giá trị khóa như Redis để lưu trữ tạm hoặc quản lý phiên. Nếu tôi đang làm việc với bất kỳ thứ gì liên quan đến công việc nền hoặc kiến trúc hướng sự kiện, tôi cần một trình nhắn tin như RabbitMQ và để thử nghiệm chức năng email trong quá trình phát triển, tôi muốn có thứ gì đó như MailHog, phần mềm này chặn các email gửi đi và hiển thị chúng cho tôi trong giao diện web thay vì thực sự gửi chúng đi.

Chúng ta sẽ xây dựng một tệp Docker Compose chạy tất cả các dịch vụ này cùng nhau. Tệp này sẽ trở thành bộ công cụ phát triển di động của bạn. Bạn có thể sao chép nó từ dự án này sang dự án khác, điều chỉnh các phiên bản, và có một môi trường nhất quán mọi lúc.

Đầu tiên, tạo một thư mục mới cho dự án của bạn và bên trong thư mục đó, tạo một tệp có tên là docker-compose.yml. Đây là tên tiêu chuẩn, và đó là những gì lệnh docker compose tìm kiếm theo mặc định.

1. Bắt đầu với cơ sở dữ liệu

Hãy bắt đầu với PostgreSQL. Đây là một định nghĩa dịch vụ cơ bản cho PostgreSQL:

services:
  postgres:
    image: postgres:18.2
    container_name: local_postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: myapp_dev
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

Có rất nhiều điều đang diễn ra trong khối nhỏ này, vì vậy hãy để tôi giải thích từng dòng. Khối dịch vụ là nơi bạn liệt kê tất cả các container mà bạn muốn chạy. Mỗi dịch vụ có một tên, trong trường hợp này là postgres.

Dòng hình ảnh cho Docker biết hình ảnh nào cần kéo từ Docker Hub. Tôi đã cố định nó ở postgres:18.2. Nếu bạn chỉ sử dụng postgres mà không có thẻ, bạn sẽ nhận được thẻ mới nhất, có nghĩa là môi trường của bạn có thể thay đổi bất ngờ khi một phiên bản lớn mới của PostgreSQL được phát hành (nó thường làm hỏng mọi thứ).

Tên_container là tùy chọn nhưng hữu ích. Nó đặt một tên cố định cho container. Nếu không có điều này, Docker sẽ tạo ra một tên ngẫu nhiên như postgres_1234abcd. Có một tên có thể dự đoán được sẽ giúp dễ dàng thực hiện các lệnh docker đối với container cụ thể đó khi bạn cần.

Dòng ports ánh xạ một cổng trên máy chủ của bạn với một cổng bên trong container. Định dạng là "host:container". PostgreSQL lắng nghe trên cổng 5432 bên trong container theo mặc định. Bằng cách ánh xạ nó đến cùng một cổng trên máy chủ của bạn, bạn có thể kết nối với cơ sở dữ liệu sử dụng localhost:5432 từ bất kỳ công cụ nào chạy trên laptop của bạn, giống như thể bạn đã cài đặt PostgreSQL một cách nguyên bản.

Phần môi trường thiết lập các biến môi trường bên trong container. Hình ảnh PostgreSQL tìm kiếm các biến cụ thể này để cấu hình người dùng mặc định, mật khẩu và cơ sở dữ liệu. Tôi đã sử dụng các giá trị đơn giản ở đây chỉ để trình diễn.

Phần volumes rất quan trọng vì các container theo mặc định là tạm thời. Nếu bạn xóa container, dữ liệu của bạn cũng sẽ bị xóa (vĩnh viễn, không có thùng rác hay recycle bin). Dòng postgres_data: /var/lib/postgresql/data tạo một volume có tên là postgres_data và gắn nó vào đường dẫn bên trong container nơi PostgreSQL lưu trữ các tệp dữ liệu. Điều này có nghĩa là cơ sở dữ liệu của bạn vẫn tồn tại khi container được khởi động lại hoặc thậm chí bị xóa. Chúng ta sẽ định nghĩa volume ở cuối tập tin.

Cuối cùng, restart: unless-stopped thông báo cho Docker tự động khởi động lại container nếu nó bị sập, nhưng không khởi động lại nếu bạn tắt nó bằng tay.

2. Thêm Redis

Tiếp theo, hãy thêm Redis. Redis đơn giản hơn so với PostgreSQL. Nó không có hệ thống người dùng phức tạp hoặc cần các biến môi trường để cài đặt.

redis:
    image: redis:8.6.1
    container_name: local_redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    restart: unless-stopped

3. Tích hợp RabbitMQ

RabbitMQ phức tạp hơn một chút vì nó có giao diện quản lý. Chúng tôi muốn truy cập giao diện đó trong trình duyệt của mình.

rabbitmq:
  image: rabbitmq:4.2-management-alpine
  container_name: local_rabbitmq
  ports:
    - "5672:5672"
    - "15672:15672"
  environment:
    RABBITMQ_DEFAULT_USER: guest
    RABBITMQ_DEFAULT_PASS: guest
  volumes:
    - rabbitmq_data:/var/lib/rabbitmq
  restart: unless-stopped

Hiện tại có hai cổng được ánh xạ. Cổng 5672 là cổng AMQP chính mà ứng dụng của bạn sẽ sử dụng để kết nối với RabbitMQ để gửi và nhận tin nhắn. Cổng 15672 là cổng HTTP cho giao diện quản lý web. Bạn có thể mở trình duyệt của mình đến http://localhost:15672 và đăng nhập bằng thông tin đăng nhập mặc định guest/guest để xem các hàng đợi, các exchange và tin nhắn.



4. Bắt email với MailHog

MailHog là một công cụ chạy một máy chủ SMTP giả và bắt giữ mọi tin nhắn được gửi đến nó. Nó cung cấp giao diện web để xem những email bị bắt giữ đó. Nó vô cùng hữu ích để kiểm tra các quy trình đăng ký, đặt lại mật khẩu và email thông báo mà không vô tình gửi thư rác cho người dùng thực sự.

  mailhog:
    image: minidocks/mailhog
    container_name: local_mailhog
    ports:
      - "1025:1025"
      - "8025:8025"
    restart: unless-stopped

Ở cuối tệp docker-compose.yml của bạn, bạn cần khai báo các volume được đặt tên mà bạn đã sử dụng:

volumes:
  postgres_data:
  redis_data:
  rabbitmq_data:

Điều này cho Docker biết để tạo và quản lý các volume này. Docker lưu trữ dữ liệu thực tế ở đâu đó trên hệ thống tệp của máy chủ của bạn, nhưng bạn không cần phải biết vị trí đó.

5. Tổng hợp mọi thứ lại

# Local Dev Toolbox
services:
  postgres:
    image: postgres:18.2
    container_name: local_postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: myapp_dev
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:8.6.1
    container_name: local_redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    restart: unless-stopped

  rabbitmq:
    image: rabbitmq:4.2-management-alpine
    container_name: local_rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    restart: unless-stopped

  mailhog:
    image: minidocks/mailhog
    container_name: local_mailhog
    ports:
      - "1025:1025"
      - "8025:8025"
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  rabbitmq_data:

Lưu tệp đó vào thư mục dự án của bạn.

Chạy bộ công cụ của bạn

1. Mở một terminal trong thư mục chứa tệp docker-compose.yml của bạn. Để khởi động mọi thứ, chạy:

docker compose up -d 

Cờ -d chạy các container ở chế độ tách rời, có nghĩa là chúng chạy ở nền, và bạn sẽ nhận lại dấu nhắc lệnh trên terminal của mình.

2. Để xem những gì đang chạy, sử dụng:

docker compose ps

Điều này cho bạn thấy trạng thái của từng container, các cổng mà chúng được ánh xạ tới, và chúng đã chạy trong bao lâu.



3. Khi bạn hoàn thành công việc trong ngày, bạn có thể dừng các dịch vụ mà không phá hủy chúng:

docker compose stop

Điều này dừng các container nhưng giữ nguyên chúng và các volume dữ liệu của chúng. Bạn có thể khởi động lại chúng sau với lệnh docker compose start hoặc docker compose up.

4. Nếu bạn muốn xóa hoàn toàn các container nhưng giữ lại các volume dữ liệu của mình, hãy sử dụng:

docker compose down

5. Để xóa mọi thứ, bao gồm cả các volume và tất cả dữ liệu bên trong chúng, hãy thêm cờ -v:

docker compose down -v



Các lựa chọn thay thế cho Docker Compose

Cài đặt dịch vụ trực tiếp trên hệ thống của bạn

Liệu một tệp Compose có phải là cách duy nhất để xây dựng bộ công cụ phát triển địa phương không? Tất nhiên là không. Cũng giống như có vô số các bản phân phối Linux, có nhiều cách để tạo ngăn xếp phát triển.

Cách tiếp cận truyền thống nhất là cài đặt mọi thứ trực tiếp trên hệ điều hành của bạn. Bạn cài đặt PostgreSQL từ trình quản lý gói, kích hoạt dịch vụ và tiếp tục. Theo nghĩa rộng hơn, thậm chí việc cài đặt một thứ gì đó như build-essential trên hệ thống dựa trên Debian đã là khởi đầu của một ngăn xếp phát triển. Bạn đang kéo các trình biên dịch, các tệp tiêu đề và các công cụ cốt lõi vào hệ điều hành cơ bản của mình để phần mềm có thể được biên dịch và chạy tại chỗ.

Điều này hoạt động tốt cho các trường hợp đơn giản, nhưng mọi thứ trở nên rối loạn khi các dự án bắt đầu khác nhau. Nếu một ứng dụng yêu cầu PostgreSQL 18 và một ứng dụng khác phụ thuộc vào PostgreSQL 16, bạn buộc phải xoay sở nhiều phiên bản trên các cổng khác nhau hoặc cài đặt lại các phiên bản khác nhau. Việc quản lý chúng như các dịch vụ hệ thống gốc trở nên phiền phức. Việc gỡ bỏ chúng sau này hiếm khi sạch sẽ như mong đợi, và các tệp cấu hình thường vẫn phân tán khắp hệ thống.

Các trình quản lý phiên bản như asdf

Các công cụ như asdf rất tuyệt vời để quản lý các môi trường chạy ngôn ngữ và giúp việc chuyển đổi giữa các phiên bản khác nhau của Python, Node.js hoặc Ruby trở nên rất mượt mà. Đối với mã ứng dụng, điều này thực sự giải quyết một vấn đề thực sự.

Tuy nhiên, cơ sở dữ liệu, trình trung gian tin nhắn và máy chủ thư không vừa vặn gọn gàng trong mô hình đó. Chúng không phải là các nhị phân runtime nhẹ mà bạn có thể thay thế bằng một lớp trung gian. Chúng yêu cầu thư mục dữ liệu lưu trữ, các tiến trình nền và tài nguyên hệ thống. Bạn có thể biên dịch chúng thủ công, nhưng điều đó sẽ làm mất đi mục tiêu của việc có một thiết lập nhanh chóng và có thể tái tạo.

Các công cụ tự động hóa như Ansible và Taskfiles

Một lựa chọn khác là sử dụng các khung tự động hóa và trình chạy tác vụ. Các công cụ như Ansible cực kỳ mạnh mẽ trong việc cung cấp máy chủ. Tương tự, các trình chạy tác vụ như Task cũng cho phép bạn điều phối các chuỗi lệnh phức tạp.

Đối với một máy phát triển cục bộ, tuy nhiên, việc viết các playbook để đảm bảo PostgreSQL, Redis và RabbitMQ được cài đặt và cấu hình đúng có thể cảm thấy không cân xứng với vấn đề.

Sử dụng Docker trực tiếp

Giải pháp kỹ thuật gần nhất với Compose là sử dụng lệnh docker run thô. Đối với một dịch vụ đơn lẻ, điều này có thể gọn gàng và hiệu quả. Một lệnh khởi động một container, ánh xạ một cổng, và bạn đã hoàn tất.

Độ phức tạp tăng nhanh khi nhiều dịch vụ được thêm vào. Các ánh xạ cổng, các volume được đặt tên cho dữ liệu tồn tại lâu dài, biến môi trường cho thông tin xác thực, chính sách khởi động lại và mạng tùy chỉnh đều phải được chỉ định thủ công. Khi các dịch vụ cần giao tiếp với nhau, việc nhớ từng cờ trở nên dễ gây lỗi. Thành thật mà nói, không ai có thể nhớ hết những lệnh dài đó!

Sau ba hoặc bốn container, nhiều nhà phát triển cuối cùng phải sao chép các lệnh dài từ một tài liệu hoặc gói chúng vào một script shell (tôi đã từng như vậy). Ở giai đoạn đó, script giống như một phiên bản viết tay và kém dễ đọc hơn của tệp docker-compose.yml.

Tại sao Compose vẫn thực tế

Docker Compose chuyển đổi những lệnh docker run dài và mang tính mệnh lệnh đó thành một cấu hình có cấu trúc và khai báo. Tất cả các dịch vụ được định nghĩa trong một tệp duy nhất và mạng lưới giữa các container được tạo tự động. Mạng lưới, thậm chí trong trường hợp của Compose, có thể trở nên phức tạp, nhưng đó là câu chuyện cho một ngày khác, vì phần lớn thời gian, bạn sẽ không phải xử lý nó.

Lợi thế thực sự của Compose không chỉ là tính độc quyền mà còn là sự rõ ràng và khả năng tái tạo mà nó mang lại. Đối với hầu hết các ngăn xếp phát triển cục bộ, một tệp Compose tạo ra sự cân bằng thực tiễn giữa sự đơn giản và kiểm soát, mà không làm thay đổi vĩnh viễn hệ điều hành cơ bản của bạn xung quanh mọi dự án mà bạn thử nghiệm.


Tại sao các môi trường phát triển dưới dạng container lại tiết kiệm nhiều thời gian

Sử dụng tệp Docker Compose để tạo một hộp phát triển cục bộ giúp tiết kiệm thời gian mà bạn sẽ phải mất để cài đặt, cấu hình và khắc phục sự cố các dịch vụ gốc. Khi một dự án hoàn thành, bạn có thể xóa thư mục dự án, hạ các container xuống, và sẽ không còn dấu vết nào còn lại trên hệ thống. Tôi không đề xuất rằng Compose là lựa chọn khách quan tốt nhất trong mọi trường hợp. Tuy nhiên, về mặt dễ thiết lập và hiệu quả thời gian, nó khó bị đánh bại đối với các nhà phát triển mới bắt đầu tổ chức môi trường cục bộ của mình. Những sở thích có xu hướng phát triển khi kinh nghiệm tăng lên và các yêu cầu trở nên chuyên biệt hơn.

Theo thời gian, quy trình làm việc của riêng tôi đã thay đổi. Tôi dần chuyển sang các công cụ như asdf và Podman Pods cho một số dự án nhất định, đặc biệt là khi tôi ưu tiên một trải nghiệm không cần daemon và tích hợp chặt chẽ hơn với hệ thống máy chủ. Tuy nhiên, ý tưởng cốt lõi vẫn giữ nguyên: nhóm các dịch vụ liên quan lại với nhau, tách chúng khỏi hệ điều hành cơ sở, và làm cho toàn bộ ngăn xếp có thể tái tạo được.

Bài viết liên quan