+109

Dockerize ứng dụng Laravel

Cập nhật gần nhất: 10/11/2024

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

Ở bài trước chúng ta đã cùng nhau dockerize ứng dụng VueJS và ReactJS cùng với đó các vấn đề liên quan trong quá trình làm quen với Docker

Ở bài này chúng ta sẽ cùng nhau dockerize một ứng dụng Laravel nhé (hình như ae đọc blog của mình toàn fan Laravel thôi 😄)

Bắt đầu thôi nào

Setup

Các bạn clone source code ở đây

Ở bài này ta sẽ chỉ cần quan tâm tới 1 folder trong source code bên trên đó là docker-laravel nhé 😉

Ở thư mục trên mình đã tạo sẵn cho các bạn một project Laravel rồi. Nếu máy các bạn có PHP, composer cài sẵn thì có thể chạy lên xem thử. Còn nếu không thì vẫn ok nhé, hãy cứ giữ "zin" cho máy gốc của các bạn và Docker sẽ lo hết 😉

Build Docker Image

Lắc não

Đối với project Laravel sẽ "hơi khó hơn" chút khi dockerize, do đó mình sẽ phân tích cho các bạn trước, rồi sau đó ta sẽ cùng cấu hình môi trường dựa vào phân tích nhé.

  • Để chạy được một project Laravel đơn giản (như bài này) ta cần tới PHP (đương nhiên 🤣) và composer (chưa nói tới DB, redis,....)
  • Để chạy được code PHP ta cần 1 bạn tên gọi là "handler", ý là code PHP không phải cứ thế ăn ngay chạy luôn được, cần phải có một anh bạn đảm nhận nhiệm vụ thực thi code PHP. Và ở thời điểm hiện tại thì PHP-FPM là phổ biến nhất, nên ta sẽ dùng PHP-FPM
  • Thường thường hay thực tế là mình thấy là hầu hết thì ta sẽ dùng cùng với một webserver như Apache hay Nginx trong việc vận hành ứng dụng PHP (Laravel). Vì Nginx hiện tại cực kì phổ biến và mạnh mẽ vượt trội nhiều so với Apache. Nên ta sẽ dùng Nginx nhé

Do đó bài này ta sẽ tách ứng dụng thành 2 phần như sau:

  • Một phần chứa PHP-FPM, trong đó có cài tất cả mọi thứ liên quan như composer, thư viện, setup, vì php-fpm đảm nhiệm vai trò chính trong việc chạy code
  • Một phần là webserver Nginx đóng vai trò như là 1 anh gác cửa, đứng ở bên ngoài, khi có request gửi đến anh gác cửa anh ấy sẽ làm một số nhiệm vụ và chuyển request vào cho php-fpm ở bên trong xử lý

Vì vậy lát nữa các bạn sẽ thấy ta có 2 services tương ứng ở file docker-compose.yml chứ không phải một như các bài trước đâu nha.

Câú hình Dockerfile

Ở bài này Nginx ta chưa cần cấu hình gì phức tạp nên ta sẽ không cần 1 Dockerfile cho bạn ý, mà lát nữa ta dùng trực tiếp image Nginx được build sẵn.

Ở đây ta chỉ cần viết cấu hình Dockerfile cho php-fpm nha

Ở folder docker-laravel các bạn tạo file Dockerfile với nội dung như sau:

# Set master image
FROM php:8.2-fpm-alpine

# Set working directory
WORKDIR /var/www/html

# Install PHP Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Copy existing application directory
COPY . .

Mình sẽ giải thích những điểm quan trọng ở file Dockfile nhé:

  • Đầu tiên ta bắt đầu từ image có tên php:8.2-fpm-alpine (mình lấy ở đây)
  • Tiếp theo ta chuyển đến đường dẫn /var/www/html
  • Tiếp đó ta cài composer, để ta có thể cài các thư viện php
  • Tiếp nữa là ta COPY toàn bộ folder hiện tại ở môi trường ngoài vào đường dẫn hiện tại trong Image, chính là WORKDIR

Ô không thấy CMD đâu cả ??? Ở bài đầu thấy nói là cần phải có CMD ở cuối Dockerfile thì mới chạy được cơ mà???

Chúng ta cùng vào tận gốc rễ image php:8.2-fpm-alpine ở đây nhé. Kéo xuống tận cùng và các bạn thấy có những dòng sau:

EXPOSE 9000
CMD ["php-fpm"]

Ở trên các bạn có thể thấy là bản thân image php:7.2-fpm-alpine đã chạy CMD cho chúng ta rồi, nên khi chạy Docker sẽ thấy là "container vẫn luôn trong trạng thái hoạt động", đó là lí do vì sao ở Dockerfile của chúng ta ta không cần CMD nữa

Đồng thời các bạn chú ý cho mình dòng bên trên EXPOSE 9000, chú ý cho mình dòng đó, lát mình giải thích nhé 😉

Build Image

Và vẫn như thường lệ như các bài khác, để build image thì ta chạy command sau nhé:

docker build -t learning-docker/laravel:v1 .

Screenshot 2024-11-10 at 12.15.50 PM.png

Cấu hình docker-compose

Tiếp theo, ta tạo file docker-compose.yml với nội dung như sau nhé:

services:

  #PHP Service
  app:
    image: learning-docker/laravel:v1
    restart: unless-stopped
    volumes:
      - ./:/var/www/html

  #Nginx Service
  webserver:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "8000:80"
    volumes:
      - ./:/var/www/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf

Nom cũng ná ná các bài trước nhỉ 😄

Nhưng cũng cần giải thích một số chỗ ở trên cho các bạn thắc mắc nhé:

  • ở service app mình có mount (hay hiểu đơn giản hơn đó là ánh xạ) 1 volume từ folder hiện tại vào bên trong container ở đường dẫn /var/www/html, để làm gì thì các bạn xem ở cuối bài nhé
  • ở service webserver ta vừa mount folder hiện tại vào bên trong /var/www/html đồng thời cũng mount thêm file nginx.conf ở folder hiện tại vào bên trong container ở đường dẫn /etc/nginx/conf.d/default.conf. Vì để nginx có thể hiểu được php và vận hành cho đúng thì ta cần thêm 1 chút config nhỏ để khi được khởi chạy, Nginx sẽ đọc file config và vận hành cho đúng, file config đó sẽ được ta định nghĩa ở phần tiếp theo nha

Cấu hình Nginx

Mọi bài trước build xong Image là chạy được rồi nhưng bài này chúng ta cần phải cầu hình Nginx thêm chút nữa, vì lát nữa ta dùng Nginx như webserver đấy nha 😊

Ở folder gốc docker-laravel chúng ta tạo file nginx.conf với nội dung như sau:

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

    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;

    root /var/www/html/public;

    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 / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

Ở trên các bạn chú ý dòng fastcgi_pass app:9000;: khi có request gửi đến nginx, nginx sẽ chuyển request đó tới PHP-FPM đang lắng nghe ở host tên là app và port 9000.

Giải thích chút nhé :

Host tên là app ở đâu ra vậy? đó chính là địa chỉ của service app tên này sẽ phải trùng khớp với tên của service ta định nghĩa ở file docker-compose nhé

Cổng 9000 ở đâu ra đây: cái đó thì là ở setup của PHP FPM họ đã set sẵn cho ta: https://github.com/docker-library/php/blob/master/8.2/alpine3.20/fpm/Dockerfile

EXPOSE 9000 đơn giản nhằm mục đích muốn nói là: app của tôi sẽ chạy ở port này đó, cái này chỉ là "mô tả" (description) thôi chứ không phải chắc chắn nhé. Với EXPOSE thì người khác sử dụng image sẽ hiểu được là à, container sẽ chạy ở port 9000

Các bạn chú ý sự khác nhau giữa mapping port ở các bài trước và EXPOSE port ở bài này nhé:

  • Mapping port nhằm giúp thế giới bên ngoài giao tiếp được với container
  • EXPOSE port chỉ đơn thuần là 1 dạng "chỉ dẫn, mô tả", và không bắt buộc phải có. Cái này là "có còn hơn không" thôi 😄
  • Ở thế giới bên ngoài sẽ không gọi được vào service app ở port 9000 nhưng webserver thì có thể vì mặc định khi chạy lên với docker compose thì chúng ở trong cùng 1 network

Chạy project

À trước khi chạy các bạn tạo file .env đã nha:

cp .env.example .env

Xong xuôi tất cả cấu hình rồi thì giờ ta chạy project thôi nhé. Các bạn chạy command sau:

docker compose up -d

Sau đó mở trình duyệt và ta sẽ thấy như sau:

Screenshot 2024-11-10 at 12.18.42 PM.png

Đó là bởi vì ta chưa chạy composer install, lí do vì sao không chạy ngay lúc build image thì các bạn lại xem ở cuối bài nhé.

Và command này phải được chạy ở container của service app nhé, vì ở bên webserver chỉ có trách nhiệm "gác cửa" thôi, không có php hay composer gì cả, nếu ta chạy ở đó sẽ báo lỗi command not found đấy 😉

Để chạy composer install ta có 2 cách:

  1. Chui vào trong container app
  2. Đứng ở ngoài và chạy

Ta sẽ dùng cách 2 cho tiện nhé, các bạn chạy command sau:

docker compose exec app composer install

Chờ command chạy xong ta quay lại F5 trình duyệt và thấy:

Screenshot 2024-11-10 at 12.19.43 PM.png

Đó là vì ta chưa generate key cho project, nhưng đừng bấm vào nút "generate app key" nhé, ta làm bằng tay cho "nguy hiểm" 🤣, các bạn chạy command sau để generate key nhé:

docker compose exec app php artisan key:generate

Sau đó ta quay lại trình duyệt F5 lần nữa và....:

Screenshot 2024-11-10 at 12.22.42 PM.png

Lại lỗi nữaaaaaaa 😠😠

Lí do là vì ở .env ta để DB_CONNECTION=sqlite nên mặc định Laravel sẽ tìm ở database/database.sqlite . và không thấy có, giờ ta cần tạo nó nhé:

docker compose exec app touch database/database.sqlite

Sau đó ta quay lại trình duyệt F5 lần nữa:

Screenshot 2024-11-10 at 12.24.41 PM.png

Vẫn lỗi???? Sắp hết kiên nhẫn rồi à nha 😡😡

Lí do ở đây là giờ ta chưa migrate để tạo các bảng trong database, giờ ta chạy nha:

docker compose exec app php artisan migrate

Screenshot 2024-11-10 at 12.26.04 PM.png

Và ta lại quay trở lại trình duyệt và F5:

Screenshot 2024-11-10 at 12.26.31 PM.png

Pòm pòm chíu chíu lên rồi 🎉🎉

Nếu bạn gặp lỗi Permission Denied

Bởi vì với service app thì php-fpm nó được chạy bằng user www-data với userId:groupId82:82, ta có thể kiểm chứng bằng cách chạy:

docker compose exec app sh
top

Các bạn sẽ thấy như sau:

Screenshot 2024-11-10 at 12.29.03 PM.png

Sau đó ta kiểm tra userId:groupId:

cat /etc/passwd

Screenshot 2024-11-10 at 12.30.04 PM.png

Nhưng vì ta đang mount toàn bộ code ở môi trường gốc vào trong service app do vậy toàn bộ code thực tế trong container sẽ ăn theo permission với môi trường ngoài, và trong trường hợp này rất có khả năng code ở môi trường ngoài đang không phải thuộc sở hữu của userId:groupId=82:82

Do đó để fix lỗi này thì các bạn làm như sau, ở môi trường ngoài ta chạy:

sudo chwown -R 82:82 .

Sau đó chạy lại docker-compose down và up đi là sẽ oke

Lỗi này không xảy ra trên MacOS, nhưng đúng là ta nên đổi permission code ở môi trường ngoài cho khớp với user thực tế chạy trong container để tránh lỗi sau này

Câu hỏi liên quan

Tại sao không chạy composer install ngay lúc build image?

Câu trả lời là các bạn có thể để composer install ở trong Dockerfile để command đó được chạy ngay lúc build image. Nếu như thế thì ta cần phải comment đoạn mount volume của service app lại và mọi thứ sẽ chạy bình thường

Nhưng ở các bài sau ta sẽ cần sửa code trực tiếp ở bên ngoài và bên trong container sẽ phải được cập nhật lại để ta thấy được thay đổi trên trình duyệt luôn, như thế thì ta lại phải mount volume của service app.

Ừ thế mount volume của service app thì vấn đề gì xảy ra?

Khi ta mount volume của service app từ bên ngoài vào trong container thì khi container được khởi tạo, toàn bộ file từ bên ngoài sẽ được ghi đè vào bên trong container, dẫn tới việc folder vendor (do composer install mà có) sẽ bị biến mất trong container, vì ở môi trường ngoài ta đâu có vendor

Do đó ở bài này mình cho các bạn làm quen luôn và từ các bài sau thì ta đều chạy composer install sau chứ không cho vào file Dockerfile nhé các bạn

Build lại image và lúc "chui" vào container app tôi thấy có file nginx.conf, có cần thiết?

Đúng là service app không cần gì đến file nginx.conf, service app đảm nhận trách nhiệm thực thi code PHP, còn service nginx mới cần đến file đó.

Mặc định khi build image sẽ lấy toàn bộ file ở folder hiện tại và build, nếu các bạn muốn bỏ đi một hoặc một số file/folder nào đó thì ta cần tạo ra một file tên là .dockerignore, vai trò của nó thì y như .gitignore vậy, và cú pháp cũng viết cũng xêm như thế.

Ví dụ ở bài của chúng ta, ta tạo file .dockerignore với nội dung như sau:

nginx.conf

Thì khi build image file nginx.conf sẽ được bỏ qua và container app của chúng ta khi chạy sẽ không có file đó nữa, giảm size của image xuống

Nhưng nếu giờ các bạn thử build lại:

docker build -t learning-docker/laravel:v2 .

Sau đó check image thì lại thấy size tăng lên:

REPOSITORY                    TAG       IMAGE ID       CREATED             SIZE
learning-docker/laravel       v2        49a779a5576c   2 seconds ago       140MB
learning-docker/laravel       v1        61e4828a92c0   16 minutes ago      86.1MB

Ủa ủa????!!!!!! 🤔🤔🤔

Lí do là vì ở đầu bài ta đã chạy composer install, nó sinh ra folder vendor khá là nặng giống kiểu node_modules phía Javascript vậy:

Screenshot 2024-11-10 at 12.33.06 PM.png

Do đò với folder này chúng ta nên cho nó vào .dockerignore, vì nó cũng có thể bị ảnh hưởng bởi môi trường (giống như node_modules) vậy, ta sửa lại .dockerignore như sau nhé:

vendor
Dockerfile
docker-compose.yml
nginx.conf
.env*

Ở trên ta loại bỏ các files/folders không cần thiết cho image. Giờ các bạn thử build lại image sẽ thấy size giảm đi nhé 💪

Các bạn nhớ file .dockerignore này nhé, ta sẽ dùng rất nhiều đấy

docker build -t learning-docker/laravel:v3 .

docker images

>>>
REPOSITORY                 TAG    IMAGE ID       CREATED           SIZE
learning-docker/laravel    v3     66e70786dbbd   4 seconds ago     86.5MB
learning-docker/laravel    v2     49a779a5576c   2 minutes ago     140MB
learning-docker/laravel    v1     61e4828a92c0   18 minutes ago    86.1MB

Ở bên trên phần cấu hình Dockerfile, đoạn giải thích lí do không cần CMD, vậy tại sao điều tương tự không xảy ra ở bài Dockerize ứng dụng NodeJS?

Nếu các bạn để ý ở bài Dockerize ứng dụng NodeJS ở file Dockerfile ta vẫn phải có CMD, mặc dù khi vọc cấu hình của image node:22-alpine ở đây ta cũng đã thấy người ta có viết CMD [ "node" ]

Thế tại sao bài này ta lại không cần ??

VÌ lí do ở bài Dockerize ứng dụng NodeJS ta cần chạy CMD là npm start, và CMD đó sẽ "ghi đè" lại CMD mà image node:22-alpine khai báo sẵn

Còn ở bài này thì ta không có yêu cầu gì đặc biệt và ta có thể sử dụng CMD mặc định  image php:8.2-fpm-alpine cung cấp

Hơi lằng nhằng một chút các bạn để ý cho mình đó 😁

Bài về nhà

Có 1 bí mật đó là trong bài này ta hoàn toàn có thể không dùng tới Dockerfile, vì settings khá đơn giản, ta có thể chỉ cần dùng docker-compose.yml là đủ để chạy project lên được

Các bạn thử làm xem và comment cho mình biết kết quả nhé 😘

Laravel + Vue/React + Vite khi dev local

Giờ nếu khi dev local mà ta cũng muốn dùng Docker thì như nào ta? Từ đầu đến giờ bài này là hướng dẫn các chạy Laravel khi deploy với Nginx thì đúng hơn 🙄🙄

Thì hiện tại Laravel đã support Vite, dev frontend cực cháy cực nhanh 😎😎. Các bạn xem thêm ở đây nha: https://laravel.com/docs/11.x/vite (có thể kết hợp với cách dùng container tạm thời ta đã nói ở bài Dockerize project Vue/React đó)

Tổng kết

Lại thêm một bài với rất nhiều thứ mới, 😊.

Ở bài này các bạn có thêm khá nhiều khái niệm mới, không chỉ mỗi Docker mà giờ là cả Linux và Webserver, đọc blog mà có khi đầu óc đang quay cuống 😄.Có nhiều đoạn mình nghĩ nát óc để giải thích nhưng cũng không chắc là đã dễ hiểu cho các bạn. Bài này mình sẽ còn phải review và edit nhiều nhiều để giúp bạn đọc dễ hiểu hơn.

Mình khuyên là các bạn cứ từ từ bình tĩnh, ban đầu cứ "chấp nhận" và dùng nó, sau đó vừa làm vừa học và tìm hiểu thêm và nhận ra chân lý nhé 😉

Qua bài này có một số nội dung quan trọng mình muốn nói như sau:

  • EXPOSE PORT, phân biệt EXPOSE PORT với Mapping Port
  • Dùng .dockerignore để bỏ đi file nào ta không muốn khi build image

Cá nhân mình thấy dockerize ứng dụng Laravel khá là lằng nhằng, nhưng khi các bạn đã quen rồi thì nó sẽ thực sự hữu ích, không chỉ dạy cho ta kiến thức về Docker mà còn cả những kiến thức liên quan đến Linux và webserver (nginx). Project Laravel này mới chỉ có Hello World, đến bài có MySQL, Redis, Queue, Horizon, Schedule task,... thì lúc đó mới thật sự là thách thức 💪

Ở bài này chúng ta vẫn chưa làm phần support cho việc sửa code trực tiếp từ bên ngoài và thấy thay đổi ngay trên trình duyệt, ta cứ đi từ từ, chầm chậm các bạn nhé 😘

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

Source code bài này mình để ở đây (nhánh complete-tutorial nhé)

Cám ơn các bạn đã theo dõi và hẹn gặp lại các bạn vào các 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í