Triển khai & Vận hành

Docker Compose cho app web kèm database: ví dụ từng bước cho người mới

Một file điều khiển container web và cơ sở dữ liệu
Trong bài này

    Ở bài trước, bạn đã viết Dockerfile cho một app web nhỏ, build thành Image và chạy thử một Container. Bài này dùng chính app và Dockerfile đó để chạy cả app và MySQL cùng lúc bằng Docker Compose, chỉ với một lệnh (docker compose up). Bạn cần có sẵn Docker; nếu chưa cài thì cài đặt theo hướng dẫn của Docker trước. Thư mục my-app của bạn lúc này đã có sẵn các file của app và Dockerfile từ bài trước, chỉ thiếu ba file của bài này là docker-compose.yml, .env và .gitignore:

    my-app/
    ├── Dockerfile
    ├── .dockerignore
    ├── .env
    ├── .gitignore
    ├── docker-compose.yml
    ├── package.json
    └── server.js

    Trong cây thư mục trên, package.json và server.js là code của app, Dockerfile là công thức build Image, .dockerignore giữ file thừa ngoài Image, còn .gitignore giữ .env ngoài Git. Cách tạo từng file của app có trong bài viết Dockerfile, bài này chỉ thêm file Compose, file .env và file .gitignore.

    Docker Compose là gì?

    Chạy một Container bằng docker run thì ổn, nhưng app thật thường cần thêm database. Gõ từng lệnh dài cho từng Container vừa mệt vừa dễ nhầm. Docker Compose giải quyết việc này bằng một file YAML (YAML là định dạng văn bản mà thụt lề thể hiện cấu trúc) mô tả tất cả các Container cùng lúc.

    Trong file đó, mỗi khối cấu hình được gọi là một service, ví dụ service web và service db dưới đây. Khi chạy, Compose tạo một Container từ mỗi service. Với từng service, bạn ghi rõ dùng Image nào, mở cổng nào, gắn Volume nào. Sau đó chỉ cần một lệnh duy nhất (docker compose up) để khởi động toàn bộ hệ thống. Compose dùng lại Dockerfile và Image của bạn, nó không thay thế chúng. Các lệnh trong bài dùng dạng docker compose (có khoảng trắng), là dạng hiện tại của Docker Compose.

    Một file Docker Compose điều khiển container web và container cơ sở dữ liệu
    Một file Compose điều khiển cả web và database.

    Viết docker-compose.yml cho web + database

    Tạo file docker-compose.yml trong thư mục dự án với nội dung dưới đây. File mô tả hai service: web là app của bạn, còn db là cơ sở dữ liệu MySQL.

    services:
      web:
        build: .
        ports:
          - "3000:3000"
        environment:
          DB_HOST: db
          DB_USER: root
          DB_PASSWORD: ${DB_PASSWORD}
          DB_NAME: appdb
        depends_on:
          - db
      db:
        image: mysql:8.0
        environment:
          MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
          MYSQL_DATABASE: appdb
        volumes:
          - db-data:/var/lib/mysql
    volumes:
      db-data:

    Giải thích file docker-compose.yml

    • web build Image từ Dockerfile ở thư mục hiện tại, dấu . trong build: . nghĩa là thư mục hiện tại. Dòng "3000:3000" ánh xạ cổng 3000 của máy vào cổng 3000 của Container, nên bạn mở localhost:3000 là tới app.
    • environment của web truyền thông tin kết nối database cho app. Hãy để ý dòng DB_HOST: db: trong Compose, tên service cũng chính là tên máy (hostname). Compose tự đặt các service vào chung một Network, nên app chỉ cần gọi db là tìm thấy MySQL. Từ trong Container của app, localhost trỏ về chính Container đó, nên app không thể dùng localhost để tới MySQL; localhost:3000 chỉ dùng khi bạn mở trình duyệt trên máy của mình. DB_USER: root là tài khoản app dùng để đăng nhập MySQL, còn DB_NAME: appdb là tên database app cần dùng, phải trùng với tên database mà db tạo ra dưới đây.
    • depends_on yêu cầu Compose khởi động db trước web. Nó chỉ quyết định thứ tự khởi động, chứ không chờ MySQL sẵn sàng nhận kết nối, nên lần chạy đầu app có thể báo lỗi kết nối, bạn đợi MySQL khởi động xong rồi tải lại trang (xem mục lỗi dưới).
    • db dùng luôn Image MySQL 8.0 trên Docker Hub, Compose sẽ tải về nếu máy bạn chưa có, nên bạn không cần viết Dockerfile cho database.
    • MYSQL_ROOT_PASSWORD đặt mật khẩu cho tài khoản root, còn MYSQL_DATABASE tạo sẵn database tên appdb. Hai biến này chỉ có tác dụng trong lần đầu MySQL khởi tạo dữ liệu, khi Volume còn trống; nếu Volume đã có dữ liệu từ trước thì đổi mật khẩu hay tên database trong file .env cũng không thay đổi dữ liệu hiện có.
    • volumes trong db gắn Volume db-data vào thư mục /var/lib/mysql, nơi MySQL lưu dữ liệu trong Container, nhờ vậy dữ liệu vẫn còn khi Container bị dừng hay bị xóa, trừ khi bạn xóa luôn Volume bằng down -v (xem dưới).
    • volumes ở cuối file khai báo Volume db-data để Compose tạo và quản lý.

    Một lưu ý về DB_USER: root: bài này để app đăng nhập bằng tài khoản root cho gọn, vì ví dụ chỉ chạy trên máy của bạn. Với app thật, bạn nên tạo một tài khoản riêng chỉ có quyền trên database đó, thay vì đưa tài khoản quản trị cho app.

    Bạn để ý là mật khẩu không được ghi thẳng trong file Compose. File Compose thường được đưa lên Git cùng code, còn file .env thì không, nên mật khẩu nằm trong .env. Cách này chỉ tách mật khẩu ra khỏi file Compose; bản thân file .env vẫn chứa mật khẩu ở dạng văn bản thuần, nên bạn phải giữ nó ngoài Git và ngoài Image.

    Khi đọc file Compose, Compose thay ${DB_PASSWORD} bằng giá trị trong file .env nằm cùng thư mục rồi mới đưa cho các Container. Cách app đọc các biến môi trường này đã giải thích ở bài viết Dockerfile. Tạo file .env với nội dung:

    DB_PASSWORD=mat-khau-vi-du

    mat-khau-vi-du chỉ là mật khẩu minh họa, khi làm thật bạn hãy đổi sang mật khẩu mạnh. Vì cả hai service cùng dùng mật khẩu này nên bạn chỉ cần khai báo một lần.

    Để mật khẩu không lên Git, bạn thêm .env vào .gitignore. Nếu thư mục chưa có file .gitignore, bạn tạo file đó với một dòng duy nhất:

    .env

    Về phía Image thì bạn không cần lo thêm: file .dockerignore ở bài Dockerfile đã có dòng .env, nên lệnh COPY . . không chép file này vào Image. Nếu bạn bỏ qua file .dockerignore thì mật khẩu sẽ lọt vào Image, nên bạn kiểm tra lại file đó trước khi build.

    Chạy, kiểm tra và dừng

    Trước khi khởi động, bạn kiểm tra file Compose viết đã đúng chưa:

    docker compose config -q

    Lệnh này chỉ kiểm tra cú pháp và việc thay biến môi trường, không khởi động gì cả. Nếu nó không báo lỗi thì bạn chạy các lệnh sau ngay trong thư mục dự án:

    docker compose up -d --build   # build và khởi động ở chế độ chạy nền
    docker compose ps              # xem trạng thái các container
    docker compose logs -f web     # xem log của app (Ctrl+C để thoát)

    Trong khối lệnh trên, up nghĩa là tạo và khởi động các Container đã mô tả trong file; -d nghĩa là chạy nền, để cửa sổ lệnh không bị giữ bởi tiến trình đang chạy; --build nghĩa là build lại Image trước khi khởi động. ps liệt kê trạng thái các Container, còn logs -f web hiện log của app theo thời gian thực, web ở đây là tên service nên lệnh chỉ hiện log của app, bấm Ctrl+C để thoát khỏi chế độ xem log.

    Sau khi chạy, docker compose ps phải liệt kê cả hai service web và db đang chạy, và log của web phải có dòng App chạy ở cổng 3000. Nếu ps không hiện đủ hai service thì bạn xem log của service còn thiếu để biết nó dừng ở đâu.

    Bạn mở localhost:3000, lần này bạn sẽ thấy lời chào kèm giờ lấy từ MySQL, tức là app đã kết nối được với database. Nếu lần đầu trang báo Chưa kết nối được database: ... thì MySQL chưa khởi động xong: bạn đợi rồi tải lại trang. App ví dụ kết nối lại database mỗi lần có yêu cầu nên tải lại là đủ; với app thật, bạn cần cơ chế thử lại hoặc healthcheck (xem mục lỗi dưới).

    Khi làm xong và muốn dừng cả hệ thống, bạn chạy lệnh sau ngay trong thư mục dự án:

    docker compose down            # dừng và xóa container, giữ lại volume

    Sau khi docker compose down, dữ liệu vẫn nằm trong Volume. Riêng docker compose down -v thì khác: lệnh này xóa luôn Volume, tức là mất dữ liệu, nên chỉ dùng khi bạn thật sự muốn làm lại từ đầu.

    docker compose down -v  # xóa luôn Volume, mất dữ liệu

    Thay đổi code rồi chạy lại

    Khi sửa code, bạn không cần đụng tới database. Cứ sửa server.js như bình thường rồi chạy lại docker compose up -d --build. Compose sẽ build lại Image và tạo lại Container cho web, còn db vẫn chạy tiếp và dữ liệu trong Volume không mất, trừ khi bạn chạy docker compose down -v.

    Lỗi thường gặp khi mới dùng Docker Compose

    • App chạy trước khi database sẵn sàng. Bạn mở localhost:3000 và trang báo Chưa kết nối được database: ... dù Container db đang chạy. Nguyên nhân là depends_on chỉ quyết định thứ tự khởi động, chứ không chờ MySQL sẵn sàng nhận kết nối. App ví dụ kết nối lại database mỗi lần có yêu cầu nên bạn chỉ cần đợi MySQL khởi động xong rồi tải lại trang. Với app thật, bạn thêm healthcheck cho db và condition: service_healthy trong depends_on, để Compose chỉ khởi động web khi database đã sẵn sàng nhận kết nối, cách làm này có trong tài liệu ở mục Nguồn dưới đây.
    • Thụt lề YAML sai. Compose báo lỗi khi đọc file. YAML dùng khoảng trắng để thể hiện cấp bậc và cấm dùng phím Tab để thụt lề: một phím Tab lẫn vào cũng đủ làm hỏng cấu trúc. Bạn kiểm tra file bằng lệnh docker compose config -q ở trên, rồi sửa các dòng sai thụt lề về đúng số khoảng trắng và chỉ dùng phím cách.

    Câu hỏi thường gặp

    Docker Compose có thay thế Docker không?

    Không. Compose vẫn dùng Docker để chạy Container, nó chỉ giúp bạn mô tả và điều khiển nhiều Container cùng lúc bằng một file duy nhất.

    Có dùng Docker Compose cho môi trường production được không?

    Có thể, nhưng bài này chỉ dùng mật khẩu minh họa và cấu hình cho máy của bạn. Muốn đưa lên production, bạn cần tách riêng bí mật khỏi code, sao lưu Volume định kỳ và theo dõi log, những việc ngoài phạm vi bài này.

    Đổi mật khẩu trong .env mà MySQL vẫn dùng mật khẩu cũ?

    Đúng vậy, nếu Volume db-data đã có dữ liệu từ lần chạy trước. Các biến MYSQL_ROOT_PASSWORD và MYSQL_DATABASE chỉ có tác dụng khi MySQL khởi tạo dữ liệu lần đầu. Muốn dùng mật khẩu mới, bạn tạo lại Volume bằng docker compose down -v rồi chạy lại, và nhớ là lệnh này xóa hết dữ liệu cũ.

    Kết luận

    Vậy là bạn đã dùng Docker Compose để chạy cả app web và MySQL cùng lúc, và biết cách giữ dữ liệu trong Volume.

    Ở bài tiếp theo, mình sẽ giới thiệu các lệnh Docker hay dùng nhất và cách gỡ lỗi khi Container không chạy như mong đợi: Các lệnh Docker hay dùng và lỗi thường gặp (sẽ ra mắt sau).

    Nguồn tham khảo

    Các liên kết dưới đây đã được kiểm chứng ngày 05/11/2026:

    Gnurt

    Lập trình viên full-stack

    Trao đổi về dự án của bạn

    Bình luận

    Chưa có bình luận nào. Hãy là người đầu tiên!