เว็บเซิร์ฟเวอร์และโฮสติ้ง

How to setup Caddy with Let's Encrypt using Docker Compose

หากต้องการ web server พร้อมระบบจัดการขอและต่ออายุ certificate จาก Let’s Encrypt ให้อัตโนมัติ เราสามารถใช้ Caddy เป็นตัวช่วยได้ ซึ่ง Caddy รองรับ HTTP และ HTTPS และยังรองรับการทำงานแบบ reverse proxy สำหรับส่งต่อไปยังเว็บเซิร์ฟเวอร์ด้านในอีกด้วย

ภาพประกอบเบราว์เซอร์เชื่อมต่อผ่านตัวจัดการ certificate ไปยังเว็บเซิร์ฟเวอร์

ตัวอย่างนี้ใช้ Docker Compose รัน Caddy คู่กับ Nginx สำหรับแสดงเว็บตัวอย่าง ส่วน Nginx ไม่ต้องเปิดพอร์ตออกสู่ภายนอก เพราะ Caddy ติดต่อผ่านเครือข่ายของ Docker ได้โดยตรง

เตรียมโดเมนและพอร์ต

ก่อนเริ่ม ให้ติดตั้ง Docker และ Docker Compose บนเซิร์ฟเวอร์ แล้วเตรียมโดเมนที่ควบคุม DNS ได้ ในตัวอย่างใช้ example.com กับ www.example.com ซึ่งต้องเปลี่ยนเป็นชื่อโดเมนของเราเอง

ตั้ง DNS ของทั้งสองชื่อให้ชี้ไปยัง public IP ของเซิร์ฟเวอร์ โดยใช้ A record สำหรับ IPv4 และ AAAA record สำหรับ IPv6 ถ้ามี หากไม่ได้ใช้ IPv6 ให้ตรวจว่าไม่มี AAAA record เก่าที่ชี้ไปผิดเครื่อง

เปิด TCP พอร์ต 80 และ 443 จากอินเทอร์เน็ตมาที่เครื่องที่รัน Caddy รวมถึง firewall ของระบบและ firewall ของผู้ให้บริการ Cloud ถ้าผ่าน router ต้องตั้ง port forwarding ด้วย ตรวจด้วยว่าไม่มีโปรแกรมอื่นใช้สองพอร์ตนี้อยู่แล้ว

ถ้าใช้ DNS proxy หรือ CDN ต้องตั้งค่าให้คำขอยืนยันโดเมนส่งมาถึง Caddy ได้ด้วย และตรวจ HTTPS ระหว่าง proxy กับเซิร์ฟเวอร์ตามการตั้งค่าของบริการนั้น

Let’s Encrypt ยืนยันโดเมนอย่างไร

Let’s Encrypt จะตรวจว่าเราควบคุมโดเมนที่ขอใบรับรองได้จริง ผ่านขั้นตอนที่เรียกว่า ACME challenge การตรวจนี้เป็นการยืนยันสิทธิ์ควบคุมโดเมน ไม่ใช่การให้เจ้าของเว็บส่งบัตรประชาชนหรือพิมพ์รหัสผ่านเข้า Let’s Encrypt

Caddy เปิดวิธีต่อไปนี้ไว้โดยปริยายและเลือกใช้ตามที่ทำงานได้:

  • HTTP-01: Let’s Encrypt เรียกไฟล์ชั่วคราวใต้ /.well-known/acme-challenge/ ผ่าน HTTP พอร์ต 80 แล้วตรวจคำตอบที่ Caddy เตรียมไว้ เราไม่ต้องสร้างไฟล์เอง
  • TLS-ALPN-01: Let’s Encrypt เชื่อมต่อมาที่พอร์ต 443 และตรวจคำตอบผ่าน TLS วิธีนี้ต้องให้การเชื่อมต่อสำหรับ challenge มาถึง Caddy ได้

อีกวิธีคือ DNS-01 ซึ่งตรวจค่า TXT record ใต้ _acme-challenge ใช้ได้เมื่อไม่สะดวกเปิดพอร์ตสำหรับการยืนยัน และใช้กับ wildcard เช่น *.example.com ได้ แต่ต้องตั้งค่า DNS provider module และสิทธิ์ API ของผู้ให้บริการ DNS เพิ่มใน Caddy ตัวอย่างด้านล่างไม่ได้ตั้ง DNS-01

สร้างไฟล์ Docker Compose

สร้างโฟลเดอร์สำหรับตัวอย่างนี้

mkdir -p caddy-https/caddy caddy-https/site
cd caddy-https

สร้างไฟล์หน้าเว็บตัวอย่าง

printf '%s\n' '<h1>Caddy HTTPS is working</h1>' > site/index.html

สร้างไฟล์ compose.yaml ดังนี้

services:
  caddy:
    image: caddy:2.11.7-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy:/etc/caddy:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - web

  web:
    image: nginx:stable-alpine
    restart: unless-stopped
    volumes:
      - ./site:/usr/share/nginx/html:ro

volumes:
  caddy_data:
  caddy_config:

caddy_data เก็บใบรับรองและข้อมูลบัญชี ACME ส่วน caddy_config เก็บข้อมูล config ของ Caddy ไว้ใน Docker volume จึงยังอยู่เมื่อสร้าง container ใหม่ ควรสำรอง volume เหล่านี้ไว้ และอย่าลบ volume หากยังต้องการใช้ข้อมูลเดิม

ตั้งค่า Caddy

สร้างไฟล์ caddy/Caddyfile โดยเปลี่ยนอีเมลและชื่อโดเมนให้เป็นของเรา

{
    email [email protected]
    acme_ca https://acme-v02.api.letsencrypt.org/directory
}

example.com, www.example.com {
    reverse_proxy web:80
}

อีเมลใช้เป็นข้อมูลติดต่อของบัญชี ACME ส่วน acme_ca ในตัวอย่างกำหนดให้ขอใบรับรองจาก Let’s Encrypt โดยตรง และ reverse_proxy web:80 ส่งคำขอไปยัง service ชื่อ web ใน Docker ไม่ต้องใส่ IP ของ container

เมื่อ DNS และพอร์ตพร้อมแล้ว ให้เริ่ม container

docker compose up -d

ดูสถานะและ log ของ Caddy

docker compose ps
docker compose logs -f caddy

Caddy จะเริ่มขอใบรับรองและยืนยันโดเมนให้อัตโนมัติ ถ้ายังออกใบรับรองไม่สำเร็จ ให้ดูข้อความใน log แล้วตรวจ DNS พอร์ต และ firewall ก่อน

ตรวจ HTTPS และการต่ออายุ

ลองเปิด https://example.com และ https://www.example.com จากเครือข่ายภายนอก หรือใช้คำสั่งต่อไปนี้โดยเปลี่ยนชื่อโดเมน

curl -I http://example.com
curl -I https://example.com
curl -I https://www.example.com

คำขอ HTTP ควรถูกส่งต่อไป HTTPS ส่วน HTTPS ต้องเชื่อมต่อได้โดยไม่เกิดข้อผิดพลาดเรื่องใบรับรอง จากนั้นตรวจวันหมดอายุและผู้ออกใบรับรองได้ด้วย

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

คำสั่งนี้แสดงข้อมูลใบรับรองที่เครื่องปลายทางส่งมา หากมี CDN หรือ proxy อยู่ด้านหน้า อาจเห็นใบรับรองของ CDN แทนใบรับรองที่ Caddy ใช้กับเซิร์ฟเวอร์ต้นทาง

Caddy จะตรวจและต่ออายุใบรับรองให้เองขณะ service ทำงาน โดยไม่ต้องตั้ง cron หรือรัน Certbot แยก แต่ DNS พอร์ตและการตั้งค่า challenge ต้องยังใช้งานได้ตอนต่ออายุด้วย ถ้าแก้ caddy/Caddyfile ภายหลัง ให้ตรวจ config แล้ว reload

docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

หากออกใบรับรองไม่ผ่าน ลองตรวจ A/AAAA record ว่าชี้เครื่องถูกตัว, พอร์ต 80/443 เข้าถึงได้จากภายนอก, ไม่มีโปรแกรมอื่นจับพอร์ตอยู่ และ proxy หรือ firewall ไม่ขวาง challenge เวลาทดลองหลายครั้งสามารถใช้ Let’s Encrypt staging เพื่อลดโอกาสชน rate limit แล้วเปลี่ยน acme_ca กลับเป็น production เมื่อพร้อมใช้งานจริง โดยใบรับรอง staging ไม่ได้รับความเชื่อถือจากเบราว์เซอร์

อ้างอิง: Caddy Automatic HTTPS, Let’s Encrypt Challenge Types, Caddy Docker image

EOF.← กลับไปอ่านบทความอื่น