วิธีติดตั้ง LEMP Stack ด้วย Docker Container สำหรับผู้เริ่มต้น

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร

ในบทความนี้เราจะติดตั้ง LEMP Stack ที่เป็นกลุ่มของ Open Source Software สำหรับเขียน Website ด้วยภาษา PHP บน Docker Container ซึ่งประกอบไปด้วย Software ดังต่อไปนี้

  • L = Linux OS
  • E = (E)Nginx Web Server
  • M = MariaDB
  • P = PHP

Apache vs Nginx

ปัจจุบันมี Web Server สองค่ายที่ได้รับความนิยมอย่างแพร่หลาย คือ Apache ซึ่งครองตลาดมาก่อน โดยมีการพัฒนามาตั้งแต่ในปี 1995 และ Nginx ที่เปิดให้ใช้งานเมื่อปี 2004 โดยนักพัฒนาชาวรัสเซีย ซึ่ง Web Server ทั้ง 2 ตัว สามารถทำงานได้บน Docker Container และใช้เวลาติดตั้งไม่ถึง 1 นาที!

ติดตั้ง Apache Web Server บน Docker ตามขั้นตอนด้านล่างครับ

  • Remote Login ไปยัง Cloud Server โดยใช้ ssh
ssh nc-user@labxx.cpsudevops.com
  • ติดตั้ง Apache Web Server โดยใช้ httpd:2.4.41-alpine Image ด้วยคำสั่ง docker run
docker run --name webserver1 -p 80:80 httpd:2.4.41-alpine
  • เปิดดู Website ที่รันบน Container โดยระบุ URL เป็นชื่อ Server ของเราเอง เช่น http://lab20.cpsudevops.com
  • กด Ctrl+C เพื่อ Stop Container
  • ดู Container ทั้งหมด
docker ps -a

ทีนี้เราจะ Config Docker-compose เพื่อใช้งาน Apache Web Server Container แทนการใช้คำสั่ง Docker run ครับ

  • สร้าง Project ใหม่ภายใน Folder httpd_dock ซึ่งประกอบด้วย ไฟล์ และ Folder ดังต่อไปนี้
.
|__ docker-compose.yml
|__ html/
   |__ index.html
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:
  httpd:
    container_name: httpd
    restart: unless-stopped
    image: httpd:2.4.41-alpine
    volumes:
      - ./html:/usr/local/apache2/htdocs/
    ports:
      - "80:80"

networks:
  default:
    external:
      name:
        web_network
  • แก้ไขไฟล์ index.html
Hello Apache Web Server
  • สร้าง Bridge network โดยตั้งชื่อเป็น web_network
docker network create web_network
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps
  • เปิดดู Website ที่รันบน Container โดยระบุ URL เป็นชื่อ Server ของเราเอง เช่น http://lab20.cpsudevops.com
  • ดู Volume ทั้งหมด

จะเห็นว่า Docker ไม่ได้สร้าง Volume ขึ้นมาใหม่ เนื่องจากเราใช้วิธี Mount Folder "html" ที่มีอยู่แล้วกับ Folder "/usr/local/apache2/htdocs/" ใน Apache Container

  • Remote ไปยัง bash shell ของ Apache Container ด้วยคำสั่ง docker exec -it แล้วดูไฟล์ใน Folder "html" ที่ถูก Mount
docker exec -it httpd /bin/bash
  • Stop/Delete Container ที่ docker-compose.yml ดูแล ด้วยคำสั่ง docker-compose down และลบ image ทั้งหมดด้วย parameter --rmi all
docker-compose down --rmi all

ต่อไปเราจะ Config Docker-compose เพื่อใช้งาน Nginx Web Server กันบ้างครับ

  • สร้าง Project ใหม่ภายใน Folder nginx_dock ซึ่งประกอบด้วย ไฟล์ และ Folder ดังต่อไปนี้
.
|__ docker-compose.yml
|__ static-html/
   |__ index.html
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:
  nginx:
    container_name: nginx
    restart: unless-stopped
    image: nginx:stable-alpine
    volumes:
      - ./static-html:/usr/share/nginx/html
    ports:
      - "80:80"

networks:
  default:
    external:
      name:
        web_network
  • แก้ไขไฟล์ index.html
Hello Nginx 
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • เปิดดู Website ที่รันบน Container โดยระบุ URL เป็นชื่อ Server ของเราเอง เช่น http://lab20.cpsudevops.com
  • Stop/Delete Container ที่ docker-compose.yml ดูแล ด้วยคำสั่ง docker-compose down และลบ image ทั้งหมดด้วย parameter --rmi all
docker-compose down --rmi all

การจะเลือกใช้ Web Server เจ้าไหนนั้น มีปัจจัยหลายอย่าง เมื่อพิจารณาจากความสามารถในการรองรับ Client Request แล้ว พบว่า Nginx สามารถรองรับ Client Request ได้พร้อมกันจำนวนมาก โดยใช้ทรัพยากร เช่น RAM และ CPU ต่ำ มันจึงมีความเร็วมากกว่า Apache พอสมควร ทำให้เกิดความนิยมเพิ่มขึ้น โดยเฉพาะจากชุมชนชาว DevOps ครับ

สถิติจาก W3Techs
สถิติจาก W3Techs

Nginx + php

การที่ Nginx สามารถรองรับ Client Request ได้พร้อมกันจำนวนมากนั้นเป็นเพราะมีการตัด Feature อย่างเช่น การประมวลผลภาษา Script ออก แต่ในการใช้งาน Web Server เรามักจะต้องมีการประมวลผลแบบ Dynamic content ที่เขียนขึ้นจากภาษา Script เช่น php ด้วย  นั่นทำให้เราต้องส่งต่อการรันไฟล์ php ไปให้ php FPM (FastCGI Process Manager) Container ประมวลผลแทน

เราจะ Config Nginx และ php FPM Container ตามขั้นตอนด้านล่างครับ

  • สร้าง Project ใหม่ภายใน Folder lemp_dock ซึ่งประกอบด้วย ไฟล์ และ Folder ดังต่อไปนี้
.
|__ docker-compose.yml
|__ html/
|   |__ index.php
|__ nginx
   |__ conf/
   |  |__ nginx.conf
   |__ conf.d/
      |__ default.conf
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:
  php:
    container_name: lemp_php
    image: php:7.4-fpm-alpine
    restart: unless-stopped
    volumes:
      - ./html/:/var/www/html
    expose:
      - "9000"

  nginx:
    container_name: lemp_nginx
    image: nginx:stable-alpine
    restart: unless-stopped
    volumes:
      - ./html/:/var/www/html

      - ./nginx/conf/nginx.conf:/etc/nginx/conf/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro

    ports:
      - "80:80"

networks:
  default:
    external:
      name:
        web_network
  • แก้ไขไฟล์ index.php
<?php phpinfo();?>
  • แก้ไขไฟล์ nginx.conf
worker_processes 1;
daemon off;
events {
    worker_connections 1024;
}
error_log   /var/log/nginx/error.log warn;
pid         /var/run/nginx.pid;
http {
    include /etc/nginx/conf/mime.types;
    default_type application/octet-stream;
    log_format main '$remote_addr - $remote_user [$time_local] "$request"'
    '$status $body_bytes_sent "$http_referer"'
    '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log main;
    sendfile on;
    #tcp_nopush on;
    keepalive_timeout 65;
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
    # tells the server to use on-the-fly gzip compression.
    include /etc/nginx/conf.d/*.conf;
}
  • แก้ไขไฟล์ default.conf
server {
   charset utf-8;
   client_max_body_size 128M;
   listen 80; ## listen for ipv4
   #listen [::]:80 default_server ipv6only=on; ## listen for ipv6
   
   root /var/www/html;
   index index.php;

   location / {
       # Redirect everything that isn't a real file to index.php
       try_files $uri $uri/ /index.php$is_args$args;
   }

   # uncomment to avoid processing of calls to non-existing static files by Yii

   #location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
   #    try_files $uri =404;
   #}

   #error_page 404 /404.html;

   location ~ \.php$ {
       include fastcgi_params;
       fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
       fastcgi_pass php:9000;
       try_files $uri =404;
   }

   location ~ /\.(ht|svn|git) {
       deny all;
   }
}

เราได้ตั้งค่า Nginx ให้รันใน Folder /var/www/html

root /var/www/html;

และกำหนด index file โดยใช้คำสั่ง

index index.php;

ส่วนที่จะรัน php เราได้กำหนด fastcgi_pass php:9000 เพื่อให้ Nginx ส่ง Script ไปยัง php FPM ซึ่งเรา Config ให้เปิด Port 9000 ไว้ใน php Container ของไฟล์ docker-compose.yml ด้วย

fastcgi_pass php:9000;
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Containers ที่รันทั้งหมด ตามที่ docker-compose.yml ดูแล
docker-compose ps
  • เปิดดู Website ที่รันบน Container โดยระบุ URL เป็นชื่อ Server ของเราเอง เช่น http://lab20.cpsudevops.com

Nginx + php + MariaDB

ในการสร้าง Web Application เรามักจะต้องมีการจัดเก็บข้อมูลลง Database

MySQL เป็น RDBMS แบบ Open Source Platform ที่ได้รับความนิยมมาก

แต่เมื่อปี ค.ศ. 2008 MySQL ได้ถูกซื้อไปโดย Oracle ทำให้ชุมชนชาว MySQL กังวลใจกับอนาคตของ MySQL ที่อยู่ในมือ Oracle

Michael Widenius ผู้ก่อตั้ง MySQL จึงได้ "Fork" Source Code ของ MySQL เพื่อนำไปพัฒนาต่อในชื่อ MariaDB โดยในปัจจุบันมีผู้ใช้งาน MariaDB ซึ่งเป็น Open Source Platform จำนวนมาก อย่างเช่น Wikipedia, WordPress.com และ Google เป็นต้น

เราจะ Config MariaDB Container ตามขั้นตอนด้านล่างครับ

  • สร้าง Folder และไฟล์ ใน Project lemp_dock รวมทั้ง Download "titanic.sql" จาก Gitlab Server ดังต่อไปนี้
.
|__ docker-compose.yml
|__ html/
|   |__ index.php
|__ nginx
|  |__ conf/
|  |  |__ nginx.conf
|  |__ conf.d/
|     |__ default.conf
|__ mariadb/
|  |__ data/
|  |__ initdb/
|  |  |__ tinanic.sql
|  |__ backup/
|__ php/
   |__ Dockerfile
git clone http://gitlab.cpsudevops.com/nuttachot/titanic.git
สร้าง Folder php ภายใน lemp_dock/ เพื่อเก็บ Dockerfile
  • Copy  ไฟล์ titanic.sql จาก Folder "tinatic" ไปยัง Folder "initdb" แล้วลบ Folder ที่ Clone มาทิ้ง ด้วยคำสั่ง rm -rf titanic
cp titanic.sql ../
cd ..
rm -rf titanic
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:
  php:
    container_name: lemp_php
    build: php/
    restart: unless-stopped
    volumes:
      - ./html/:/var/www/html
    expose:
      - "9000"
    depends_on:
      - db

  nginx:
    container_name: lemp_nginx
    image: nginx:stable-alpine
    restart: unless-stopped
    volumes:
      - ./html/:/var/www/html

      - ./nginx/conf/nginx.conf:/etc/nginx/conf/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro

    ports:
      - "80:80"
      
  db:
    container_name: lemp_mariadb
    image: mariadb:latest
    restart: unless-stopped
    volumes:
      - ./mariadb/initdb/:/docker-entrypoint-initdb.d
      - ./mariadb/data/:/var/lib/mysql/
    environment:
      - MYSQL_ROOT_PASSWORD=devops101
      - MYSQL_DATABASE=devops_db
      - MYSQL_USER=devops
      - MYSQL_PASSWORD=devops101

networks:
  default:
    external:
      name:
        web_network

เราจะทดลองนำไฟล์ "titanic.sql" มา Import ลงใน Mariadb โดยเมื่อรัน Container แล้ว Mariadb จะ Import ไฟล์ ".sql" ใน Folder /docker-entrypoint-initdb.d ให้อัตโนมัติถ้ามันพบว่าภายใน /var/lib/mysql/ ของ lemp_mariadb Container ยังไม่มีข้อมูล

volumes:
  - ./mariadb/initdb/:/docker-entrypoint-initdb.d
  - ./mariadb/data/:/var/lib/mysql/
Volume ของ lemp_mariadb Container ใน docker-compose.yml
  • แก้ไข Dockerfile โดยติดตั้ง mysqli เพื่อเรียก Mariadb จาก php
FROM php:7.4-fpm-alpine

RUN docker-php-ext-install mysqli
  • แก้ไขไฟล์ index.php
<?php
   $servername = "db";
   $username = "devops";
   $password = "devops101";

   $dbhandle = mysqli_connect($servername, $username, $password);
   $selected = mysqli_select_db($dbhandle, "titanic");
   
   echo "Connected database server<br>";
   echo "Selected database";
?>
  • สร้าง php Image ด้วยคำสั่ง docker-compose build
docker-compose build
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Containers ที่รันทั้งหมด ตามที่ docker-compose.yml ดูแล
  • เปิดดู Website ที่รันบน Container โดยระบุ URL เป็นชื่อ Server ของเราเอง เช่น http://lab20.cpsudevops.com
ขอขอบคุณ Nipa.Cloud ที่ให้การสนับสนุน Environment ในการเรียนการสอน
รายวิชา Dev-Ops and Cloud Engineering 101