+94

Dockerize ứng dụng chat realtime với Laravel, Nginx, VueJS, Laravel Echo, Laravel Reverb

Cập nhật gần nhất: 05/12/2024

Xin chào các bạn đã quay trở lại với series Học Docker và CICD của mình.

Chúc mọi người một mùa giáng sinh an lành ấm áp 😍😍

Mấy ngày vừa rồi tinh thần viết blog đang lên cao thì đôi mắt mình lại đình công sau 1 khoảng thời gian ngồi máy tính quá nhiều, hôm nay "cửa sổ tâm hồn" có vẻ tốt trở lại thì mình lại chầm chậm viết blog tiếp cho mọi người 😉

Dù làm gì cũng phải nhớ giữ sức khoẻ nhé mọi người, Tết nhất đến nơi rồi phải có sức khoẻ để còn đi làm lấy tiền đưa về biếu thầy bu 🤣 🤣

Ở bài trước mình đã hướng dẫn các bạn các Dockerize ứng dụng NodeJS với mysql, redis,... cùng với đó là các setup cho môi trường dev và production.

Ở bài này chúng ta sẽ chơi "hardcore" hơn bằng cách Dockerize ứng dụng Chat realtime với Laravel, VueJS, Laravel Reverb cùng với đó là setup Laravel Pulse/Telescope và Laravel Schedule Task nhé.

Sorry các bạn vì tên bài này mình để hơi dài, mục đích để khi đọc qua tiêu đề các bạn sẽ rõ hơn đồng thời cho những anh em search từ google thì cũng sẽ dễ tìm hơn nhé.

Setup

Các bạn clone source code ở đây nhé (nhánh master)

Ở bài này chúng ta sẽ chỉ quan tâm tới folder docker-laravel-realtime-chat-app nhé.

Tổng quan ứng dụng

Ứng dụng này đã được mình làm và deploy ở đây các bạn có thể vô vọc vạch: https://realtime-chat.jamesisme.com

Tổng quan:

  • Ứng dụng có các phòng chat để user có thể join vào
  • Mỗi phòng chat sẽ có 1 hộp (box) chat chung, tất cả user trong phòng có thể nhắn tin vào đây
  • Cùng với đó là danh sách user có ở trong phòng chat, click chọn vào 1 user bất kì để nhắn tin riêng vơi người đó
  • Cứ mỗi 1 phút sẽ tự động xuất hiện tin nhắn chào mừng của Bot (dùng Laravel Schedule Task)
  • Detect user đang gõ, hay đã xem tin nhắn,...
  • Các bạn có thể chọn biểu cảm tin nhắn như Facebook Messager: Love, Haha, Wow,..... 💪

Screenshot 2024-12-02 at 11.58.51 PM.jpg

Đàm đạo về phương án triển khai

Hình dung ban đầu

Ta cùng nhau hình dung ra những thành phần mà ta sẽ cần phải có cho app này nhé:

2.jpg

Sương sương thì ta bóc tách ra thành 6 thứ như sau:

  • Laravel App: main app xử lý các thể loại logic, views,...
  • Workers: vì message sẽ được đẩy vào queue để xử lý bất đồng bộ, do vậy ta cần có workers làm nhiệm vụ lấy message ra và xử lý
  • Reverb: Websocket server, chịu trách nhiệm nhận message và broadcast đi cho frontend
  • Scheduler: giống như cronjob, chạy các tác vụ vào giờ giấc được cài đặt sẵn (lát nữa ta sẽ có con Bot cứ 1 phút sẽ gửi các tin nhắn dạng hướng dẫn vào phòng chat)
  • MySQL: chắc chắn rồi ☺️, để lưu trữ dữ liệu
  • phpMyAdmin: cái này để ta có thể thao tác với data trong MySQL trực tiếp trên Web UI, rất tiện 😘

Cái này là mình đã phác thảo sẵn ra cho các bạn ăn sẵn luôn rồi đó nha 🤣🤣

Deploy theo kiểu truyền thống

Nếu các bạn đã xem bài Deploy ứng dụng chat realtime Laravel, VueJS trên Ubuntu, ở bài đó ta cũng deploy app chat y hệt, nhưng dùng cách ngày xưa đó là setup trực tiếp mọi thứ vào VM (VPS) luôn, thì quy trình sẽ vô cùng vất vả:

1.jpg

Ta sẽ phải cài tất cả các thứ vào thẳng máy ảo (VM - virtual machine, ta cũng hay gọi là VPS), việc cài thẳng vào VM này sẽ gây rất bất tiện:

  • tác động trực tiếp vào các file system của VM, có thể gây lỗi trong quá trình cài đặt và sử dụng, thậm chí có thể thay đổi vĩnh viễn một số thành phần
  • rất khó upgrade khi phải chạy nhiều app với nhiều dependencies khác nhau, ví dụ cái thì PHP7, cái PHP8, cái node 20, cái node 14,...
  • ...ti tỉ thứ khác

Vậy nên ta mới có bài này làm với Docker nè 🤣🤣

Deploy với Docker

Bài này mình viết từ khá lâu rồi, mình vẫn luôn review, update và xử lý các comment của các bạn, 😁, và vẫn gắng update sao cho phù hợp với hiện tại, tinh gọn, dễ hiểu và phù hợp với dự án thực tế nhất

Trước đây thì mình chọn kiến trúc deploy như sau:

3.jpg

Mỗi hình con cá 🐟 là 1 Docker container nha 😂

Kể ra thì nó chạy cũng oke trong 1 thời gian dài đấy, một phần mình bị ảnh hưởng bởi mô hình của công ty cũ ngày xưa, thấy chạy ổn thì follow theo.

Gần đây thì mình có review lại một số điểm bất tiện thì thấy như sau:

  • Kiến trúc bên trên ta vẫn có sự "couple" (gắn kết) giữa các thành phần như Laravel App - Task scheduler - Workers
  • việc dùng supervisor như cách deploy truyền thống bản chất là để quản lý process khi ta phải cài tất cả mọi thứ vào VM, còn đây là với Docker, chẳng phải mỗi 1 container cũng "gần như" một process rồi hay sao? Lại còn phải thêm Supervisor, rồi crontab, rồi gói mấy thằng đó chung vào 1 container làm gì? 🧐🧐

Khi làm thực tế chẳng phải ta thường sẽ deploy các thành phần thành các service riêng biệt, business riêng, dễ quản lý, lại dễ scale hơn hay sao?

Vậy nên mình đã refactor lại chút:

4.jpg

Ở trên các bạn thấy rằng mình đã tách hết ra mỗi thành phần là 1 container, trông kiến trúc rõ ràng hơn rất nhiều. Sau này chạy production cái nào nặng thì scale cái đó thôi, cũng tiện hơn rất nhiều😎

Do vậy nên là mình đã viết lại toàn bộ bài này, hiện tại là tháng 12/2024, các bạn đọc comment để ý nhé 😘

Ta chú ý rằng các service liên quan đến Laravel bao gồm: Main app, Reverb websocket server, worker, scheduler. Tất cả chúng có điểm chung là đều dùng PHP và cần dùng chung 1 source code 😉

Build Docker image

Đầu tiên, ở root folder project các bạn tạo cho mình Dockerfile nhé:

FROM php:8.3.9-fpm-alpine3.20 AS base
WORKDIR /app
# Add and Enable PHP-PDO Extenstions
RUN docker-php-ext-install pdo pdo_mysql pcntl
RUN docker-php-ext-enable pdo_mysql

FROM base AS reverb
CMD ["php", "artisan", "reverb:start"]

FROM base AS workers
CMD ["php", "artisan", "queue:work"]

Ở đây ta dùng multi-stage build nha, tức là chia Dockerfile thành nhiều stage, tí nữa với từng service ta sẽ target vào từng stage cụ thể.

Giải thích chút nha:

  • Ở trên ta có 1 stage base, ở đó ta cài extension để kết nối tới mysql từ PHP (Laravel)
  • tiếp theo ta có 2 stages là reverbworkers, chúng sẽ FROM từ base, lí do là vì chúng đều cần có đoạn setup extension

ủa vậy còn Main Laravel App và Scheduler đâu??? 🧐🧐

Thì 2 cái đó ta không cần custom CMD gì cả, mà dùng mặc định CMD của cái image php:8.3.9-fpm-alpine3.20, nó là:

CMD ["php-fpm"]

Các bạn có thể xem thêm ở đây: https://github.com/docker-library/php/blob/master/8.3/alpine3.20/fpm/Dockerfile#L259

Tức là lát nữa ở docker compose, với service app và scheduler ta cứ target thẳng vào base mà không cần custom gì cả

Tiếp theo ta tạo file .dockerignore với nội dung như sau:

node_modules
npm-debug.log
.env*
Dockerfile*
docker-compose*
.git*
.docker
vendor
app.conf

Tiếp đó ta tạo file docker-compose.yml:

services:
  webserver:
    image: nginx:1.27.3-alpine
    ports:
      - "8000:80"
    working_dir: /app
    volumes:
      - ./:/app
      - ./nginx.app.conf:/etc/nginx/conf.d/default.conf
    restart: always

  app:
    build:
      context: .
      target: base
    volumes:
      - ./:/app
    restart: always
 
  reverb:
    build:
      context: .
      target: reverb
    volumes:
      - ./:/app
    restart: always

  workers:
    build:
      context: .
      target: workers
    volumes:
      - ./:/app
    restart: always

  db:
    image: mysql:8
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MYSQL_DATABASE=${DB_DATABASE}
      - MYSQL_USER=${DB_USERNAME}
      - MYSQL_PASSWORD=${DB_PASSWORD}
    volumes:
      - .docker/data/db:/var/lib/mysql
    restart: always

  scheduler:
    build:
      context: .
      target: base
    volumes:
      - ./:/app
    entrypoint: ["sh", "-c", "while true; do php artisan schedule:run; sleep 60; done"]
    restart: always

  phpmyadmin:
    image: phpmyadmin
    restart: always
    ports:
      - 8001:80
    environment:
      - PMA_HOST=${DB_HOST}
      - PMA_PORT=${DB_PORT}

Giải thích chút nha:

  • ở trên có 1 thứ mới mà mình hôm nay mới giới thiệu tới các bạn đó là target, bằng việc dùng targetdocker-compose.yml ta nói với Docker rằng: "ê ông, lúc build image thì ông build mỗi cái stage mà tôi đang target tới này thôi nhé, chứ đừng build cả Dockerfile nha" 😆
  • với nginx ta có mount 1 file nginx.app.conf vào container, tí nữa mình sẽ giải thích file đó là gì nhé
  • với service scheduler, mình có entrypoint, nó sẽ thay cho CMD và được chạy ngay lúc ta start container, ở đó các bạn thấy mình đơn giản là chạy task mỗi 60 giây
  • Với các service như dbphpmyadmin, chúng ta có khai báo vài biến môi trường, giá trị thực tế của chúng (ở trong dấu ngoặc ${}), sẽ được tham chiếu từ file .env mà tí nữa ta sẽ tạo nhé
  • Chỉ có 2 service là webserverphpmyadmin, được truy cập từ thế giới bên ngoài nên ta chỉ cần map port cho 2 cái đó thôi. Tí nữa mình sẽ giải thích về flow để ta rõ hơn nha

Tạo .env

Tiếp theo ta cần tạo file .env và update 1 số cấu hình nhen. Ta đơn giản là copy từ file .env.example:

cp .env.example .env

Giờ ta cần update 1 số thông tin ở file .env như sau:

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=realtime_chatapp
DB_USERNAME=myuser
DB_PASSWORD=myuserpass

DB_ROOT_PASSWORD=myrootpass

REVERB_HOST="reverb"

VITE_REVERB_HOST="localhost"

Các biến về DB_ nhìn phát chắc là ta hiểu luôn ý nhờ, học từ đầu series rồi mừ 😁, có gì thì comment cho mình nhé

REVERB_HOST là dùng để phía Laravel main app + worker connect tới Reverb websocket server, vì đây là connect trực tiếp từ container->container nên ta dùng luôn tên service chứ không cần map port ra localhost làm gì

Tiếp theo là VITE_REVERB_HOST, cái này được dùng ở Frontend connect tới Reverb websocket server, mà ở Frontend (browser) nó sẽ không biết service docker gì đâu nên ta phải dùng localhost

Cấu hình Nginx

Ở root folder project ta tạo file nginx.app.conf với nội dung như sau:

server {
    listen 80;
    index index.php index.html;

    root /app/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
 
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }
 
    error_page 404 /index.php;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_hide_header X-Powered-By;
    }

    location /reverb {
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header SERVER_PORT $server_port;
        proxy_set_header REMOTE_ADDR $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";

        # trailing slash is important to remove /echo from the url
        proxy_pass http://reverb:8080/;
    }
 
    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Chú ý ở trên khi với code PHP thì ta sẽ redirect request vào cho service app để service app có PHP-FPM làm việc, còn với /reverb thì ta điều hướng công việc tới service http://reverb:8080 nha

Nói chung đoạn này cấu hình cũng khá cơ bản của Nginx thôi, ta chỉ cần chú ý là đây là môi trường Docker nên khi proxy pass request ta phải dùng service name 😘

Chạy ứng dụng

Âu cây setup thế thôi nhỉ, giờ ta chạy lên coi mặt mũi nó thế nào 🚀🚀

docker compose up -d --build

Chú ý rằng bình thường ta hay build image trước với docker build... nhưng ở bài này ta cấu hình phần build image trực tiếp ở docker-compose.yml với build > context

Screenshot 2024-12-04 at 9.51.14 PM.jpg

Âu câyyy, giờ ta mở trình duyệt ở địa chỉ http://localhost:8000 vàaaaaa PÒMMMM:

Screenshot 2024-12-04 at 9.52.36 PM.jpg

Ủa cái gì zị anh em??????

Ầu thì ra ta không có folder vendor, cái này là nơi lưu trữ các dependencies cho app Laravel (PHP, như kiểu node_modules của JS ấy)

Cài dependencies (composer install, npm install,...)

Như ở bài Dockerize VueJS mình có nhắc tới dùng container tạm thời để cài dependencies thay vì cài trực tiếp vào image.

Việc này sẽ giúp giảm size của image xuống, vì việc cài dependencies này không xảy ra thường xuyên mà ta thường chỉ làm 1 lần lúc đầu khi setup project.

Ở đây ta sẽ vừa setup dependencies cho PHP vừa cho Frontend (VueJS) luôn đó

Các bạn chạy lần lượt các command sau nhé (tuỳ vào hệ điều hành của các bạn là gì mà chọn cho đúng nha ☺️):

# MacOS + Linux
docker run --rm -v $(pwd):/app -w /app composer:2.8.3 install

docker run --rm -v $(pwd):/app -w /app node:22-alpine npm install

docker run --rm -v $(pwd):/app -w /app node:22-alpine npm run build


# If Windows see below:

# Git bash
docker run --rm -v "/$(pwd)":/app -w //app composer:2.8.3 install

docker run --rm -v "/$(pwd)":/app -w //app node:22-alpine npm install

docker run --rm -v "/$(pwd)":/app -w //app node:22-alpine npm run build

# PowerShell
docker run --rm -v "$(pwd):/app" -w /app composer:2.8.3 install

docker run --rm -v "$(pwd):/app" -w /app node:22-alpine npm install

docker run --rm -v "$(pwd):/app" -w /app node:22-alpine npm run build

# Command Prompt
docker run --rm -v "%cd%:/app" -w /app composer:2.8.3 install

docker run --rm -v "%cd%:/app" -w /app node:22-alpine npm install

docker run --rm -v "%cd%:/app" -w /app node:22-alpine npm run build

Giải thích:

  • Đầu tiên ta tạo 1 container tạm thời từ image compose (có cài sẵn composer) và ta chạy composer install
  • Tiếp đó ta tạo 1 container từ image node (22) và chạy npm install
  • Cuối cùng là ta build phần code VueJS với npm run build

Note cho bạn nào dùng Windows:

  • nếu bạn chạy docker run ... npm install và gặp lỗi ENOSYS: Function not implemented thì bạn khởi động lại Docker, nếu vẫn không được thì bạn cần khởi động lại máy nhé. Đây là 1 issue của Docker liên quan tới file system trên Windows được mô tả ở đây
  • nếu sau khi khởi động lại mà vẫn xảy ra các lỗi liên quan tới npm (docker run ...npm install, npm run...), thì các bạn chạy trực tiếp các command npm ở môi trường gốc nhé (tất nhiên điều này yêu cầu máy gốc các bạn có NodeJS)

Giây phút của sự thật

Sau khi hoàn thành ta quay lại trình duyệt và F5:

Screenshot 2024-12-04 at 9.57.56 PM.jpg

Ầu, quên chưa generate key (APP_KEY.env vẫn đang trống), ahihi 😁😁

Trước khi chạy command tạo key mình xin lưu ý: mọi command php artisan ... từ giờ ta sẽ luôn chạy dưới dạng docker compose exec app php artisan.... (để ý phần đầu nhé). Vì sao? Vì khi chạy bên ngoài thì Laravel sẽ dựa vào bối cảnh là môi trường ngoài để chạy, nên những command như migrate sẽ bị lỗi. Nên để đồng bộ và cho các bạn quen thì ta luôn chạy với docker compose exec... nhé

Chúng ta chạy command sau để tạo key đồng thời tạo DB và seed luôn nhé

docker compose exec app php artisan key:generate

docker compose exec app php artisan migrate --seed

Sau đó ta load lại trình duyệt và...... BÙM 💥

Screenshot 2024-12-04 at 10.04.15 PM.jpg

Tạo 2 tài khoản 2 mở 2 tab zô nghịch tí coi nàooooooooo 💪💪

Screenshot 2024-12-04 at 10.06.27 PM.jpg

Ủa tin nhắn gửi đi thì có vẻ lưu vào DB rồi, F5 thấy ok, mà chả có tí realtime nào zị nhỉ???? 🧐🧐 Danh sách user đang online cũng trống rỗng luôn?????

Vọc vạch

Giờ ta check xem các container có oke không nhé:

docker compose ps

>>>
NAME                                            IMAGE                                        COMMAND                  SERVICE      CREATED          STATUS          PORTS
docker-laravel-realtime-chat-app-app-1          docker-laravel-realtime-chat-app-app         "docker-php-entrypoi…"   app          19 minutes ago   Up 19 minutes   9000/tcp
docker-laravel-realtime-chat-app-db-1           mysql:8                                      "docker-entrypoint.s…"   db           19 minutes ago   Up 19 minutes   3306/tcp, 33060/tcp
docker-laravel-realtime-chat-app-phpmyadmin-1   phpmyadmin                                   "/docker-entrypoint.…"   phpmyadmin   19 minutes ago   Up 19 minutes   0.0.0.0:8001->80/tcp
docker-laravel-realtime-chat-app-reverb-1       docker-laravel-realtime-chat-app-reverb      "docker-php-entrypoi…"   reverb       19 minutes ago   Up 13 minutes   9000/tcp
docker-laravel-realtime-chat-app-scheduler-1    docker-laravel-realtime-chat-app-scheduler   "sh -c 'while true; …"   scheduler    19 minutes ago   Up 19 minutes   9000/tcp
docker-laravel-realtime-chat-app-webserver-1    nginx:1.27.3-alpine                          "/docker-entrypoint.…"   webserver    19 minutes ago   Up 19 minutes   0.0.0.0:8000->80/tcp
docker-laravel-realtime-chat-app-workers-1      docker-laravel-realtime-chat-app-workers     "docker-php-entrypoi…"   workers      19 minutes ago   Up 5 minutes    9000/tcp

Có vẻ mọi thứ đều oke mà ta?? Các service đều Up hết

Ê........ Sao 2 cái là reverbworkers lại có thời gian Up ít hơn những cái còn lại nhờ???? 🧐🧐

Ta thử check logs workers:

docker compose logs workers

Screenshot 2024-12-04 at 10.11.38 PM.jpg

Ở trên ta để ý rằng, các log mới nhất của Workers thì có vẻ oke, nhưng kéo lên trên thì có lỗi không tìm thấy table trong database. Cái này ta đoán là lúc mới start lên thì worker nó không connect tới DB được, sau đó nó bị Docker restart và sau khi ta migrate database thì nó chạy ngon

Tiếp theo ta check tới Reverb nhé:

docker compose logs reverb

Screenshot 2024-12-04 at 10.13.23 PM.jpg

Ầu, log toàn lỗi, khả năng là thanh niên này không được Docker restart rồi 🧐🧐

Thực tế là bởi vì cách vận hành của Reverb, nó sẽ tiếp tục chạy kể cả khi có lỗi, miễn là nó start lên là được php artisan reverb:start, còn sau đó thì nó vẫn throw lỗi bình thường.

Vậy giờ cái ta cần làm là restart lại Reverb để nó connect đến DB cho đúng. Vì giờ DB của ta có data rồi mà

À ngoài vấn đề này ra thì ví dụ DB của ta sau này to đùng, mỗi lần up mấy một lúc thì nếu đoạn đó các service khác kết nối tới sẽ bị báo Connection refused nữa á 😪

Vậy nên để đảm bảo vấn đề này không xảy ra ta setup chút HEALTHCHECK, và start các service theo thứ tự phù hợp sao cho cả kiến trúc trơn tru nha.

Trước khi làm thì ta tưởng tượng ra giao tiếp giữa các service của chúng ta tí nha:

5.jpg

  • Ở trên ta có thể thấy rằng MySQL là trung tâm của cả kiến trúc
  • Request đi từ trình duyệt vào đến Nginx, Nginx lại forward request vào Laravel Main App và Reverb Websocket server (dựa vào path mà khi nãy ta khai báo ở nginx.app.conf
  • Khi có 1 message gửi lên Laravel App thì nó sẽ lưu vào DB, Workers sẽ liên tục poll data từ database về (ở bảng jobs), và thực hiện, mỗi job xử lý xong thì nó sẽ bắn sang bên Reverb Websocket server để Reverb bắn ngược lại cho phía trình duyệt
  • Task Scheduler, thì chạy như cronjob, mỗi phút lại bắn ra 1 message (là 1 event vào bảng jobs)

Oke rồi giờ ta sửa lại docker-compose.yml như sau:

services:
  webserver:
    image: nginx:1.27.3-alpine
    ports:
      - "8000:80"
    working_dir: /app
    volumes:
      - ./:/app
      - ./nginx.app.conf:/etc/nginx/conf.d/default.conf
    restart: always
    depends_on:
      - app
      - reverb

  app:
    build:
      context: .
      target: base
    volumes:
      - ./:/app
    restart: always
    depends_on:
      db:
        condition: service_healthy # Wait for the db service to be healthy (using the healthcheck defined below)
        restart: true # Restart the service if db stops

  reverb:
    build:
      context: .
      target: reverb
    volumes:
      - ./:/app
    restart: always
    depends_on:
      db:
        condition: service_healthy
        restart: true

  workers:
    build:
      context: .
      target: workers
    volumes:
      - ./:/app
    restart: always
    depends_on:
      reverb:
        condition: service_started
        restart: true # Restart the service if reverb stops

  db:
    image: mysql:8
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MYSQL_DATABASE=${DB_DATABASE}
      - MYSQL_USER=${DB_USERNAME}
      - MYSQL_PASSWORD=${DB_PASSWORD}
    volumes:
      - .docker/data/db:/var/lib/mysql
    restart: always
    healthcheck:
      test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD # in docker compose to reference an environment variable, you need to use $$ instead of $
      interval: 10s       # Check every 10 seconds
      timeout: 5s         # Timeout for each check
      retries: 3          # Consider unhealthy after 3 consecutive failures
      start_period: 30s   # Wait 30 seconds before starting health checks

  scheduler:
    build:
      context: .
      target: base
    volumes:
      - ./:/app
    entrypoint: ["sh", "-c", "while true; do php artisan schedule:run; sleep 60; done"]
    restart: always
    depends_on:
      db:
        condition: service_healthy
        restart: true

  phpmyadmin:
    image: phpmyadmin
    restart: always
    ports:
      - 8001:80
    environment:
      - PMA_HOST=${DB_HOST}
      - PMA_PORT=${DB_PORT}
    depends_on:
      db:
        condition: service_healthy
        restart: true

Ở trên mình đã thêm vào HEALTHCHECK cho db, các service phụ thuộc vào db thì phải chờ cho nó ở trạng thái service_healthy mới start, với restart=true thì nếu db restart thì các service liên quan cũng restart theo để tạo connection mới

HEALTCHECK là gì thì hiểu đơn giản nó là 1 command ta chạy liên tục để check xem trạng thái của container có healthy không, cái này mình sẽ nói kĩ hơn ở các bài sau nha 😘

Với workers, vì nó phụ thuộc cả vào reverbdb, mà reverb cũng phụ thuộc vào db, nên ta chỉ cần khai báo nó phụ thuộc vào reverb là đủ

Giờ ta restart lại project nhé:

docker compose down

docker compose up -d

Sau khi start ta sẽ thấy các service liên quan phải chờ cho db healthy mới start 😎

Sau đó ta quay lại trình duyệt, F5, vàaaaaaa........ Vẫn không thấy realtime đâu 😂😂, check console thấy báo như trên

Screenshot 2024-12-04 at 10.55.20 PM.png

Ở đây ta có thấy 2 điều lạ:

  • sao nó lại connect tới localhost:8080?? Cả kiến trúc của ta chỉ có webserver chạy ở cổng 8000, và phpMyAdmin8001
  • Thứ nữa là sao cái path của nó không bắt đầu bằng /reverb?? 🧐

Ahihi mình quên, ở .env ta cần phải sửa lại như sau:

VITE_REVERB_PORT=8000

VITE_REVERB_PATH="/reverb"

Ở trên ta cần update biến VITE_REVERB_PORT, dùng cổng 8000, vì khi này là ta sẽ connect vào webserver, rồi từ đó proxy qua reverb.

Tiếp đó ta cần thêm vào biến VITE_REVERB_PATH, để cái path nó được chính xác.

Oke rồi vì đây là những thay đổi phía frontend nên ta cần build lại:

# MacOS + Linux
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm run build


# If Windows see below:

# Git bash
docker run --rm -v "/$(pwd)":/app -w //app node:22-alpine npm run build

# PowerShell
docker run --rm -v "$(pwd):/app" -w /app node:22-alpine npm run build

# Command Prompt
docker run --rm -v "%cd%:/app" -w /app node:22-alpine npm run build

Vì ta đã mount volume từ môi trường gốc vào trong container nên các bạn có thể sửa trực tiếp code PHP và sẽ thấy thay đổi nhé. Chú ý nếu sửa code VueJS thì nhớ là phải chạy npm run build như khi nãy nhen 😉

Giờ ta restart lại project nha:

docker compose down
docker compose up -d

Âu cây, giờ ta quay lại trình duyệt F5, mở 2 tab login 2 account và chat với nhau sẽ thấy mọi thứ ngon lành nha, các bạn tự thẩm nhé 😎😎

Screenshot 2024-12-04 at 11.46.09 PM.jpg

Ta để ý xem cứ mỗi phút thì Bot có gửi message không nha

Ta có thể xem Laravel Telescope để monitor phần request khi click vào Telescope:

Screenshot 2024-12-04 at 11.47.15 PM.jpg

Click vào Pulse sẽ show thông tin kết nối websocket các thứ:

Screenshot 2024-12-04 at 11.47.23 PM.jpg

phpMyAdmin được truy cập từ địa chỉ http://localhost:8001, ta login với DB_USERNAME + DB_PASSWORD khai báo ở .env:

Screenshot 2024-12-04 at 11.48.56 PM.jpg

Screenshot 2024-12-04 at 11.49.04 PM.jpg

Kết bài

Phùuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu........ Bài cũng dài ý chứ bộ 😁😁, hàng tỉ kiến thức mới, mắt mờ, đầu óc quay cuồng. Nhưng nếu so với phiên bản trước đây của bài blog này thì mình đã thu gọn đi rất nhiều rồi á: https://gist.github.com/maitrungduc1410/d6adc5345c70dbbcebb9788ab16b1229

Ê mà đến cuối bài kể ra chúng ta mới sửa có vài files từ source ban đầu của mình á 😂😂

Screenshot 2024-12-04 at 11.53.10 PM.jpg

Có Docker phát tiện liền, triển khai phát một 😎😎

Trong bài có rất nhiều kiến thức liên quan đến Linux, mình không thể giải thích từng tí từng tí một cho các bạn được, nhưng các bạn có thể tự copy paste và search google là sẽ có hết thông tin nhé.

Qua bài này hi vọng các bạn đã hiểu được cách Dockerize một project Laravel full options 😘, đầy đủ các thứ như DB, Websocket, Queue Job, Cronjob, ... trong sẽ như thế nào nhé. Từ đó vận dụng và áp dụng vào thực tế cho phù hợp nhé. Đồng thời ta cũng để ý là dù cho ta dùng nhiều thứ MySQL, Nginx, phpMyAdmin, PHP, Composer NodeJS... thế nhưng toàn bộ đều được chạy trong container Docker, môi trường gốc của ta vẫn "trinh nguyên" nhé 😎😎

Nếu có vấn đề gì thì các bạn cứ để lại comment cho mình nha.

Cám ơn các bạn đã theo dõi. Hẹn gặp lại các bạn vào những bài sau 👋👋


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí