dockerized-web

มาทำเว็บยุ่งๆ ให้สวยงามด้วย Docker กันครับ

Published on
5 mins read

สวัสดีครับทุกคน วันนี้จะมาเล่าประสบการณ์การ renovate เว็บ rayriffy.com จากเดิมที่เป็น standalone กลายมาเป็น Docker กันนะครับ

ตอนที่ 1: ออกแบบโครงสร้างของระบบ

ระบบที่วางเอาไว้คือ จะให้ Traffic ทั้งหมดไปที่ container ที่ชื่อว่า proxy โดย proxy จะเป็นตัวกลางสื่อสารระหว่าง Network ภายนอกกับ Network ภายใน

ส่วนด้านในก็จะเป็นเว็บต่างๆ โดยจัดไว้แบบหนึ่งเว็บต่อ container แล้วก็มีของจุกจิกอื่นๆนิดหน่อยเช่น php-fpm คือ...ต้องเข้าใจนะว่าผมสาย Laravel ผสม Vue.js ซึ่ง Laravel มันก็ต้องใช้ php-fpm เหมือนกัน แล้วทั้งหมดนี้ก็จะอยู่ใน backend Network ที่ไม่สามารถเข้าถึงได้โดยตรงจากภายนอก

จากตรงนี้ง่ายๆเลยคือมีแค่ proxy เท่านั้นที่ออกคุยกับโลกภายนอกได้

ตอนที่ 2: เตรียมตัวก่อนเริ่มงาน

โปรเจคสุด Masterpiece นี้สามารถหาดูได้บน GitHub นะครับ จุ๊บๆ

แน่นอนว่าเราต้องลง Docker CE และ Docker Compose ให้เรียบร้อยก่อน โดยจะแปะ tutorial ไว้ให้

หลังลงเสร็จแล้วก็มาต่อกันได้เลย ไฟล์หลักที่เราจะทำงานกันคือ docker-compose.yml โดยเริ่มต้นเราก็จะมาสร้าง Network ให้กับระบบเราก่อน ซึ่งจะมีอยู่ 2 อันคือ frontend กับ backend

version: '3'

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

ตอนที่ 3: เริ่มสร้าง proxy อันน่ารักของเรากัน~

อย่างแรกก่อนที่จะมี proxy เลยคือเราต้องมี SSL Certificate ก่อน โดยผมก็ไปหา Images ที่สามารถสร้าง SSL ด้วย Let's Encrypt ได้ แถมทำให้เองได้กับ Cloudflare API ด้วย!? เลยจัดซะเลย

services:
  certbot:
    image: adferrand/letsencrypt-dns:2.5.3
    container_name: certbot
    restart: unless-stopped
    env_file:
      - ./certbot/build/env
    volumes:
      - ./certbot/dist/domains.conf:/etc/letsencrypt/domains.conf
      - ./tmp/letsencrypt:/etc/letsencrypt
    networks:
      - backend

จาก config ผมได้ volume folder ของ /etc/letsencrypt ไปใส่ที่ ./tmp/letsencrypt อันนี้จะเอาไว้ให้ proxy เรียก SSL certificate มาใช้ และคราวนี้ก็จะสังเกตุเห็นว่าผมมีการอ้างอิงไฟล์ 2 ตัวคือ env กับ domains.conf โดยมันจะมีเหตุผลของมันอยู่

env จะเป็น Environment Variables ที่เอาไว้ใช้ Build โดยหลักๆที่ตั้งคือ Email และรายละเอียดของ Cloudflare

LETSENCRYPT_USER_MAIL=example@example.com
LEXICON_PROVIDER=cloudflare
LEXICON_CLOUDFLARE_USERNAME=example@example.com
LEXICON_CLOUDFLARE_TOKEN=example

domains.conf จะเป็นไฟล์ที่บอกว่าจะให้สร้าง SSL ของโดเมนไหนบ้าง โดยผมตั้งให้ทำ Wildcard SSL ให้กับ rayriffy.com และทุก subdomain ของ rayriffy.com

*.rayriffy.com rayriffy.com

คราวนี้ก็ต่อด้วย proxy โดยผมเลือกที่จะใช้ NGINX เป็น Web Server

version: '3'

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

services:
  certbot:
    image: adferrand/letsencrypt-dns:2.5.3
    container_name: certbot
    restart: unless-stopped
    env_file:
      - ./certbot/build/env
    volumes:
      - ./certbot/dist/domains.conf:/etc/letsencrypt/domains.conf
      - ./tmp/letsencrypt:/etc/letsencrypt
    networks:
      - backend
  proxy:
    image: nginx:1.15.3-alpine
    container_name: proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf/web:/etc/nginx/conf.d
      - ./nginx/conf/module:/etc/nginx/snippets
      - ./tmp/letsencrypt:/etc/letsencrypt
    depends_on:
      - certbot
    networks:
      - frontend
      - backend

คราวนี้ใน volumes ก็จะมีพวกไฟล์ config ของแต่ละ domain อยู่ที่ ./nginx/conf/web โดยตัวอย่างง่ายๆจะประมาณนี้

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name blog.rayriffy.com;

  include snippets/_ssl.conf;

  location / {
    proxy_pass         http://web-blog-rayriffy-com;
    proxy_redirect     off;
    proxy_set_header   Host $host;
    proxy_set_header   X-Real-IP $remote_addr;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Host $server_name;
  }
}
server {
  listen 80;
  listen [::]:80;

  server_name blog.rayriffy.com;

  return 301 https://blog.rayriffy.com$request_uri;
}

config ตัวนี้จะบังคับให้ redirect ทุก HTTP Request ไป HTTPS แล้วส่ง proxy ไปที่ http://web-blog-rayriffy-com โดย web-blog-rayriffy-com จะเป็นชื่อ container ที่จะ deploy ต่อค่อยเอาไว้มาอธิบาย แต่ concept คือไม่จำเป็นต้องกำหนด IP เพราะ Docker จะจัดการ DNS ทุกอย่างไว้ให้แล้วตามชื่อ container ขอแค่ให้เชื่ออยู่ใน Network เดียวกันก็พอ

ส่วนตั้งค่า SSL จะตั้งยังไงก็ตั้งกันเองเลยเต็มที่

อ่อลืมอธิบายเรื่องเกี่ยวกับ depends_on

ตอนที่ 4: ลงเว็บ (ของจริงล่ะๆ)

มาลงเว็บกันเลย เริ่มตั้งแต่ docker-compose.yml

version: '3'

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

services:
  certbot:
    image: adferrand/letsencrypt-dns:2.5.3
    container_name: certbot
    restart: unless-stopped
    env_file:
      - ./certbot/build/env
    volumes:
      - ./certbot/dist/domains.conf:/etc/letsencrypt/domains.conf
      - ./tmp/letsencrypt:/etc/letsencrypt
    networks:
      - backend
  php-fpm-72:
    build:
      context: ./php-fpm/build/72
      dockerfile: Dockerfile
    container_name: php-fpm-72
    networks:
      - backend
    volumes:
      - ./web/data/html:/web
  proxy:
    image: nginx:1.15.3-alpine
    container_name: proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf/web:/etc/nginx/conf.d
      - ./nginx/conf/module:/etc/nginx/snippets
      - ./tmp/letsencrypt:/etc/letsencrypt
    depends_on:
      - certbot
    networks:
      - frontend
      - backend
  web-blog-rayriffy-com:
    build:
      context: ./web/build/html
      dockerfile: Dockerfile
    container_name: web-blog-rayriffy-com
    restart: unless-stopped
    environment:
      - SERVER_NAME=blog.rayriffy.com
      - PHP_BACKEND=php-fpm-72
      - ROOT=/web/blog.rayriffy.com
    depends_on:
      - php-fpm-72
    volumes:
      - ./web/data/html/blog.rayriffy.com:/web/blog.rayriffy.com
    networks:
      - backend

services ที่มาเพิ่มจะมีอยู่ 2 อันคือ เว็บอันนึง และ PHP-FPM อีกอันนึง ซึ่ง PHP-FPM เรา build เอาเองสดๆ ไปดู Dockerfile เอาเอง จะไม่อธิบาย แต่ง่ายๆคือ php-fpm จะฟังอยู่ที่ port 9000 และอย่าลืม volume เว็บให้ path เหมือนกับ container เว็บด้วย

ส่วนตัวเว็บเราก็ Build เองสดๆเหมือนกัน โดยมีโครงสร้างแบบนี้

Dockerfile

FROM nginx:1.15.3-alpine
COPY site-template.conf /etc/nginx/conf.d/site.template
CMD sh -c "envsubst '\$SERVER_NAME \$ROOT \$PHP_BACKEND' < /etc/nginx/conf.d/site.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"

site-template.conf

server {
  listen 80;
  listen [::]:80;
  index index.html index.php;
  server_name ${SERVER_NAME};
  root ${ROOT};

  error_log /var/log/nginx/${SERVER_NAME}.error.log error;

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

  location ~ \.php$ {
    fastcgi_pass ${PHP_BACKEND}:9000;
    fastcgi_split_path_info ^(.+\.php)(/.*)$;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param DOCUMENT_ROOT $realpath_root;
    fastcgi_intercept_errors off;
    fastcgi_hide_header X-Powered-By;

    fastcgi_buffer_size 128k;
    fastcgi_buffers 256 16k;
    fastcgi_busy_buffers_size 256k;
    fastcgi_temp_file_write_size 256k;

    include fastcgi_params;
    # Prevents URIs that include the front controller. This will 404:
    # http://domain.tld/index.php/some-path
    # Remove the internal directive to allow URIs like this
    internal;
  }
}

เรารับ environment SERVER_NAME ROOT PHP_BACKEND จาก docker-compose.yml เพื่อเอามาวางในไฟล์ site-template.conf ข้อดีของการทำแบบนี้คือภายใน Dockerfile ตัวเดียว จะสามารถใช้กับ domain อื่นๆได้อีก ไม่จำเป็นต้องเขียนทีละอัน

เราก็ volume data เว็บทั้งหมดแล้วกำหนด ROOT ให้ถูก ตั้ง SERVER_NAME ให้ดี แล้วก็บอก PHP_BACKEND ที่ต้องการให้ fast_cgi ใช้

ตอนที่ 5: ลองรันมันดู

คำสั่งเดียวง่ายๆ

$ docker-compose up

แค่นี้ก็ได้ blog.rayriffy.com แล้วแบบง่ายๆ

สรุป

คราวนี้ก็จะได้มาแล้ว 1 domain ที่เหลือแค่ทำแบบเดิมคล้ายๆกันไปเรื่อยๆจนเสร็จตามที่ต้องการ :) ถ้าต้องการดูอะไรที่ละเอียดกว่านี้ก็ลองไปดู GitHub ผมแล้วขุดๆคุ้ยๆดู หวังว่าจะได้ concept การทำเว็บแบบ Dockerized ของผมกันนะครับ

ว่างๆก็ลองเอาไป Implement กับเว็บของคุณเองได้นะครับ ขอบคุณครับ