SSD Nodes Learn Hosting plans →
คู่มือ Matt Connorโดย Matt Connor

วิธีรัน 5 แอปด้วย Traefik v3 บน Docker Compose ตัวเดียว

เรียนรู้วิธีตั้งค่า Traefik v3 เพื่อรัน 5 แอปภายใต้ IP เดียวผ่าน Docker Compose พร้อมการทำ Host rule และ TLS อัตโนมัติ รวมถึงวิธีแก้ปัญหาไฟล์ acme.json ที่มักบล็อกการออกใบรับรอง

หนึ่ง IP, ห้าแอป, หนึ่งพอร์ต 443

VPS ของคุณมี public IPv4 address เพียงหนึ่งหมายเลขและ TCP port 443 เพียงหนึ่งพอร์ต คุณต้องการรัน Gitea, staging copy ของแอป, internal dashboard, status page และ webhook receiver บนเซิร์ฟเวอร์เครื่องเดียว โดยใช้ 5 โดเมนเนม Reverse proxy คือกระบวนการที่ทำหน้าที่ถือครองพอร์ต :80 และ :443 คอยอ่าน header Host ในทุกคำขอที่เข้ามา แล้วส่งต่อไปยัง container ที่ถูกต้อง Traefik สามารถทำหน้าที่นี้ได้ รวมถึงการขอและต่ออายุ certificate ให้กับแต่ละโดเมนเนมโดยอัตโนมัติโดยที่คุณไม่ต้องรัน certbot ด้วยตนเอง Nginx และ Caddy ก็สามารถจัดการ 5 โดเมนเนมนี้ได้ดีเช่นกัน ดังนั้นหากคุณยังไม่ได้ตัดสินใจเลือก ควรเปรียบเทียบ proxy ทั้งสามตัวในด้านการจัดการ certificate และภาระในการตั้งค่าต่อแอป ก่อนที่จะเชื่อมต่อทุกอย่างเข้ากับตัวใดตัวหนึ่ง

สิ่งที่ทำให้ Traefik แตกต่างจาก block server {} ของ nginx คือแหล่งที่มาของการตั้งค่า สำหรับ nginx คุณต้องแก้ไขไฟล์แล้ว reload ใหม่ และวงจรชีวิตของ certificate ยังคงเป็นงานแยกต่างหาก ซึ่งเป็นขั้นตอนเดียวกับที่คุณทำเมื่อ ออก certificate ของ Let's Encrypt ด้วย certbot บน nginx โดยที่ตัวตั้งเวลาต่ออายุจะอยู่นอกเหนือการทำงานของ web server โดยสิ้นเชิง แต่ Docker provider ของ Traefik จะคอยเฝ้าดู Docker event stream และอ่าน labels จาก container ของคุณ: เพียงแค่เริ่ม container ที่มี label กฎ Host() ระบบก็จะสามารถเข้าถึงได้ภายในหนึ่งวินาที และเมื่อหยุด container เส้นทางนั้นก็จะหายไป นี่คือกับดักเช่นกัน เพราะการตั้งค่าที่อยู่ใน labels จะกระจายตัวอยู่ใน 5 จุดพร้อมกัน และหากใส่ label ผิด ระบบจะไม่มีการแจ้งเตือนใดๆ container จะเพียงแค่ไม่ถูก route และ Traefik จะไม่แสดงข้อความผิดพลาดใดออกมา

คำนามหลักสี่ประการ

  • Entrypoints คือซ็อกเก็ตที่คอยรับการเชื่อมต่อ คุณจะต้องกำหนดค่าสองรายการ ได้แก่ web บนพอร์ต :80 และ websecure บนพอร์ต :443
  • Routers ทำหน้าที่จับคู่คำขอ (Host(...)) และเชื่อมโยงเข้ากับ service โดยจะมีการร้องขอใบรับรองแยกตาม router ผ่านทาง tls.certresolver
  • Services คือส่วน backend ซึ่งประกอบด้วยคอนเทนเนอร์และพอร์ตที่คอนเทนเนอร์นั้นรับการเชื่อมต่ออยู่ ภายใน เครือข่าย Docker
  • Middlewares คือส่วนที่อยู่ระหว่าง router และ service ทำหน้าที่จัดการงานต่างๆ เช่น basic auth, การกำหนด IP allow-lists, การเขียน header ใหม่ และการทำ redirects

คำนามทั้งสี่นี้เป็นชื่อที่ Traefik ใช้เรียกงานที่คุณต้องเขียนด้วยตนเองในกรณีอื่น: router เปรียบเสมือน server_name, service คือเป้าหมายของ proxy_pass และ middlewares คือคำสั่งจัดการ header และการยืนยันตัวตนที่คุณต้องตั้งค่าเองเมื่อคุณ สร้าง server block ของ nginx reverse proxy ทีละบรรทัด

การตั้งค่าแบบคงที่ (static configuration เช่น entrypoints, providers, ACME) จะถูกส่งผ่านทางบรรทัดคำสั่งของ Traefik หรือในไฟล์ traefik.yml ซึ่งการเปลี่ยนแปลงค่าเหล่านี้จำเป็นต้องรีสตาร์ท Traefik ส่วนการตั้งค่าแบบไดนามิก (dynamic configuration เช่น routers, services, middlewares) จะได้รับมาจาก label ของคอนเทนเนอร์และสามารถโหลดใหม่ได้ทันทีโดยไม่ต้องรีสตาร์ท การสับสนระหว่างการตั้งค่าทั้งสองประเภทนี้เป็นสาเหตุทั่วไปที่ทำให้เกิดปัญหา "ตั้งค่า flag ไปแล้วแต่ไม่มีผล"

ไฟล์ compose

เครือข่าย Docker ร่วมที่ชื่อ proxy คือหัวใจหลัก Traefik จะเข้าถึงคอนเทนเนอร์ได้ก็ต่อเมื่อทั้งสองอยู่บนเครือข่ายเดียวกันเท่านั้น

name: edge

networks:
  proxy:
    name: proxy

services:
  traefik:
    image: traefik:v3.7
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=proxy
      - --entryPoints.web.address=:80
      - --entryPoints.websecure.address=:443
      - --entryPoints.web.http.redirections.entryPoint.to=websecure
      - --entryPoints.web.http.redirections.entryPoint.scheme=https
      - --certificatesresolvers.le.acme.email=you@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      # while you iterate, point at staging so a mistake costs nothing:
      # - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
      - --api.dashboard=true
      - --log.level=INFO
      - --accesslog=true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=le
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.middlewares=dashboard-auth
      - traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS

  gitea:
    image: gitea/gitea:1  # major-only pin keeps this demo copy-pasteable; pin an exact release in production
    restart: unless-stopped
    volumes:
      - ./gitea:/data
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.gitea.rule=Host(`git.example.com`)
      - traefik.http.routers.gitea.entrypoints=websecure
      - traefik.http.routers.gitea.tls.certresolver=le
      - traefik.http.services.gitea.loadbalancer.server.port=3000

เริ่มจาก docker compose up -d แล้วตามด้วย docker compose logs -f traefik แอปพลิเคชันเพิ่มเติมแต่ละตัวจะเป็นสำเนาของบล็อก gitea โดยมีชื่อเราเตอร์ของตัวเอง มี Host() ของตัวเอง และพอร์ตภายในของตัวเอง สำหรับ การติดตั้ง Nextcloud บน Docker พร้อม TLS และการสำรองข้อมูล สามารถนำมาใส่ในลักษณะเดียวกันได้ โดยให้ลบการประกาศพอร์ตที่เผยแพร่ออก เชื่อมต่อเข้ากับ proxy แล้วปล่อยให้ label ของเราเตอร์จัดการชื่อโฮสต์และใบรับรอง

มีรายละเอียด 5 ประการที่สำคัญดังนี้

exposedByDefault=false จะทำให้คอนเทนเนอร์ถูกซ่อนจาก Traefik จนกว่าจะมีการระบุ traefik.enable=true หากไม่ใส่ไว้ คอนเทนเนอร์ทุกตัวที่คุณเริ่มทำงาน รวมถึง postgres ชั่วคราวที่คุณรันเพื่อตรวจสอบบางอย่าง จะถูกสร้างเส้นทาง (route) ให้โดยอัตโนมัติ

providers.docker.network=proxy ใช้บอก Traefik ว่าควรใช้เครือข่ายใดเมื่อคอนเทนเนอร์เชื่อมต่อกับเครือข่ายหลายแห่ง หากละเว้นไว้ Traefik อาจเลือก IP ของคอนเทนเนอร์ผิด ซึ่งจะแสดงผลเป็นข้อผิดพลาด 502 ที่ดูเหมือนเป็นความผิดพลาดของแอปพลิเคชัน

loadbalancer.server.port=3000 คือพอร์ตที่อยู่ ภายใน คอนเทนเนอร์ เช่น Gitea ที่ฟังคำขออยู่ที่พอร์ต 3000 สังเกตว่าไม่มีคอนเทนเนอร์แอปพลิเคชันใดเผยแพร่พอร์ตออกมาเลย มีเพียง Traefik เท่านั้นที่ทำ

การเปลี่ยนเส้นทาง (redirect) บน entrypoint web จะเป็นแบบถาวรโดยค่าเริ่มต้น ดังนั้นคำขอ GET แบบข้อความธรรมดา (สิ่งที่เบราว์เซอร์ส่งมา) จะได้รับสถานะ 301 ไปยัง HTTPS ส่วน Traefik จะตอบกลับด้วย 308 เฉพาะสำหรับวิธีอื่น เช่น POST เพื่อให้วิธีดังกล่าวคงอยู่หลังการเปลี่ยนเส้นทาง พอร์ต 80 จะยังคงเปิดอยู่เสมอ เนื่องจาก ACME HTTP challenge จำเป็นต้องใช้ และผู้ใช้ที่พิมพ์ชื่อโฮสต์เปล่าๆ ก็จำเป็นต้องใช้เช่นกัน

การเบิ้ล $$ ในค่า hash ของ basic-auth คือการ escape ใน Compose ไม่ใช่การพิมพ์ผิด ให้สร้างค่าดังกล่าวด้วย htpasswd -nbB admin 'your-password' (จากแพ็กเกจ apache2-utils) จากนั้นให้เบิ้ล $ ทุกตัวที่มี

ใบรับรองและกับดักของ acme.json

tlschallenge=true เลือกใช้ TLS-ALPN-01: Let's Encrypt จะเชื่อมต่อเข้ามายังเซิร์ฟเวอร์ของคุณผ่านพอร์ต 443 และ Traefik จะตอบรับการตรวจสอบภายใน TLS handshake อีกทางเลือกหนึ่งคือ HTTP-01 ซึ่งใช้พอร์ต 80 ให้เปลี่ยนบรรทัด tlschallenge ในรายการ command: ของ Traefik เป็นสองบรรทัดนี้:

      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

ใช้ได้ทั้งสองวิธี ทั้งสองวิธีต้องการให้ DNS สาธารณะสำหรับชื่อโฮสต์ชี้มาที่ VPS ของคุณเรียบร้อยแล้ว เพื่อให้ผู้ออกใบรับรองสามารถตรวจสอบชื่อและเชื่อมต่อจากภายนอกได้ ให้สร้างเรกคอร์ด A (และ AAAA) ก่อน ตรวจสอบด้วย dig +short git.example.com แล้วจึงเริ่มทำงาน Traefik

นี่คือกับดักที่ทำให้หลายคนเสียเวลาไปทั้งคืน Traefik จะเก็บคีย์บัญชี ACME และใบรับรองทั้งหมดไว้ในไฟล์ acme.json หากไฟล์นั้นอนุญาตให้กลุ่มหรือผู้อื่นอ่านได้ Traefik จะบันทึกข้อความแจ้งเตือนที่ใกล้เคียงกับข้อความนี้ลงใน log และยกเลิกการทำงานของ resolver ทันที มันยังคงทำงานและทำ routing ต่อไปได้ แต่จะไม่มีการร้องขอใบรับรองใดๆ ทำให้ทุกชื่อโฮสต์ถูกเสิร์ฟด้วย TRAEFIK DEFAULT CERT:

ERR The ACME resolve is skipped from the resolvers list error="unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600" resolver=le

วิธีแก้ไขที่สะอาดที่สุดคือวิธีข้างต้น: ให้ bind-mount เฉพาะ ไดเรกทอรี แล้วปล่อยให้ Traefik สร้างไฟล์ขึ้นมาเองด้วยสิทธิ์ที่ถูกต้อง หากคุณสร้าง acme.json ด้วย touch ค่า umask ของคุณอาจทำให้ไฟล์มีสิทธิ์เป็น 644 ให้แก้ไขบนโฮสต์ด้วยคำสั่งนี้:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

สำรองข้อมูลไดเรกทอรีนั้นไว้พร้อมกับ volume ของแอปพลิเคชัน การทำข้อมูลหายครั้งหนึ่งอาจยังพอรับได้เพราะสามารถขอใบรับรองใหม่ได้ แต่หากคุณลบข้อมูลทิ้งทุกครั้งที่ redeploy คุณจะติดข้อจำกัดการออกใบรับรองสูงสุด 5 ฉบับต่อชุดชื่อโฮสต์เดิมภายใน 7 วัน

ให้ใช้ staging CA ในระหว่างการทดสอบ ให้เอาคอมเมนต์ออกจากบรรทัด caserver เพื่อทดสอบจนกว่าทุกเส้นทางจะทำงานได้ถูกต้อง จากนั้นค่อยใส่คอมเมนต์กลับและลบ acme.json เพื่อให้ระบบร้องขอใบรับรองจริง Let's Encrypt สำหรับการใช้งานจริงอนุญาตให้ขอใบรับรอง ซ้ำ ได้ 5 ฉบับต่อสัปดาห์สำหรับชุดชื่อโฮสต์เดียวกัน และจะจำกัดการร้องขอหากการตรวจสอบล้มเหลวซ้ำๆ สำหรับชื่อเดิม ส่วน staging จะออกใบรับรองที่ไม่น่าเชื่อถือ ซึ่งเบราว์เซอร์ของคุณจะแจ้งเตือน และคำเตือนนั้นคือสัญญาณว่าการตั้งค่าสำเร็จ โดยมีข้อจำกัดที่ผ่อนปรนกว่ามาก

แดชบอร์ดคือส่วนควบคุม ไม่ใช่ตัวอย่างสาธิต

คู่มือเริ่มต้นใช้งานส่วนใหญ่ตั้งค่า --api.insecure=true ซึ่งเปิดใช้งานแดชบอร์ดบนพอร์ต 8080 โดยไม่มีการยืนยันตัวตน หากรันบนเครื่องที่มี Public IP ข้อมูลโครงสร้างการทำ Routing, ชื่อ Hostname, ชื่อ Middleware และพอร์ตของ Backend จะถูกเปิดเผยแก่ทุกคนที่สแกนเข้ามา

Label บนบริการ traefik ด้านบนคือทางเลือกที่ปลอดภัยกว่า โดยการทำ Routing แดชบอร์ดเหมือนกับแอปพลิเคชันอื่น ผ่านชื่อ Hostname จริง บนโปรโตคอล TLS และอยู่หลัง basicauth ส่วน service=api@internal คือตัวเชื่อมต่อ Router เข้ากับ API ภายในของ Traefik คุณสามารถเพิ่มความปลอดภัยได้อีกขั้นด้วยการต่อพ่วง IP allow-list โดยประมวลผลจากซ้ายไปขวา หาก IP ของออฟฟิศคุณเป็นแบบ Dynamic ให้กำหนดช่วง IP เป็น Subnet ที่ได้รับจาก WireGuard VPN ที่คุณโฮสต์เองบน VPS เดียวกัน แล้วเข้าถึงแดชบอร์ดผ่านอุโมงค์ VPN เท่านั้น:

- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth

การใช้รหัสผ่าน basicauth ร่วมกันเพียงชุดเดียวจะเริ่มไม่ปลอดภัยเมื่อคุณมีแอปพลิเคชันที่ต้องการบัญชีแยกกันถึง 5 รายการ ซึ่งช่อง Middleware เดียวกันนี้สามารถใช้ forwardauth เพื่อส่งต่อการตัดสินใจยืนยันตัวตนไปยัง Authentik ซึ่งเป็นเซิร์ฟเวอร์ Single Sign-On ที่คุณโฮสต์เอง ส่งผลให้แดชบอร์ดและทุกเส้นทางที่เกี่ยวข้องอยู่หลังการล็อกอินเพียงจุดเดียวที่คุณสามารถเพิกถอนสิทธิ์ได้จากที่เดียว

Docker socket มีสิทธิ์เป็น root

/var/run/docker.sock เป็น API ที่สามารถสร้างคอนเทนเนอร์ซึ่งเมาท์ / จากโฮสต์ได้ การเข้าถึงส่วนนี้เทียบเท่ากับการมีสิทธิ์ root บนเครื่อง และ Traefik จำเป็นต้องใช้เพื่ออ่านค่า labels

ให้คง :ro ไว้บนจุดเมาท์ แต่ต้องเข้าใจให้ชัดเจนว่าสิ่งนี้ช่วยอะไรได้บ้าง: มันทำให้ ไฟล์ socket เป็นแบบอ่านได้อย่างเดียวเท่านั้น แต่มันไม่ได้หยุดคำขอ POST ที่ส่งไปยัง Docker API ผ่านช่องทางนี้ วิธีป้องกันที่แท้จริงคือการไม่ให้ Traefik เข้าถึง socket โดยตรง แต่ให้ใช้พร็อกซีสำหรับกรองคำขอคั่นกลางไว้:

  dockerproxy:
    image: tecnativa/docker-socket-proxy   # pin the current tag
    restart: unless-stopped
    environment:
      CONTAINERS: 1
      NETWORKS: 1
      POST: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy

ให้ลบ volume ของ socket ออกจาก Traefik และชี้ provider ไปยังพร็อกซีแทน:

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik จะยังคงมีสิทธิ์อ่านข้อมูลคอนเทนเนอร์และเครือข่ายได้ แต่จะสูญเสียความสามารถในการสร้างสิ่งต่างๆ ไป

ไฟร์วอลล์ พอร์ต และกฎที่ทุกคนมักเข้าใจผิด

เปิดพอร์ตไว้สองพอร์ต พร้อมกับ SSH:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

พอร์ตที่เผยแพร่โดย Docker จะข้ามการทำงานของ ufw Docker จะแทรกกฎ iptables ของตนเองเข้าไป ซึ่งจะถูกประมวลผลก่อน chain ของ ufw ดังนั้นคอนเทนเนอร์ที่เริ่มทำงานด้วย ports: ["3000:3000"] จะสามารถเข้าถึงได้จากอินเทอร์เน็ตแม้ว่าจะมีการตั้งค่า ufw deny ไว้ก็ตาม วิธีป้องกันคือการจัดการเชิงโครงสร้าง ไม่ใช่การตั้งค่าไฟร์วอลล์: ให้เผยแพร่พอร์ตจาก Traefik เท่านั้น และกำหนดให้คอนเทนเนอร์อื่นทั้งหมดใช้ networks: [proxy] โดยไม่ต้องกำหนดค่าอื่นเพิ่มเติม หากมีบริการใดที่จำเป็นต้องเข้าถึงโฮสต์จริงๆ ให้ bind บริการนั้นไว้กับ loopback ด้วย "127.0.0.1:3000:3000"

การแก้ไขปัญหา: ข้อผิดพลาดที่คุณจะพบจริง

404 page not found ซึ่งแสดงโดย Traefik หมายความว่าไม่มี router ใดตรงกับคำขอ โดยเรียงลำดับความเป็นไปได้ดังนี้: container ขาด traefik.enable=true (โดยที่ตั้งค่า exposedByDefault=false ไว้แล้ว), กฎ Host() ไม่ตรงกับชื่อที่คุณพิมพ์, ชื่อ router ใน label หนึ่งไม่ตรงกับอีก label หนึ่ง (routers.gitea.rule และ routers.gitea.entrypoints ต้องเป็นคำเดียวกัน), หรือคุณครอบชื่อโฮสต์ด้วยเครื่องหมาย single quote โดย Traefik v3 ต้องใช้ backtick หรือ double quote ที่มีการ escape (\") ภายในตัวจับคู่ (matcher) และจะปฏิเสธการใช้ single quote

502 Bad Gateway หมายความว่า router จับคู่ได้แล้วแต่ไม่สามารถเข้าถึง backend ได้ สาเหตุส่วนใหญ่คือ container ไม่ได้อยู่ในเครือข่าย proxy ให้ตรวจสอบ docker inspect -f '{{json .NetworkSettings.Networks}}' gitea อีกสาเหตุที่เป็นไปได้คือ loadbalancer.server.port ไม่ถูกต้อง เช่น คุณระบุพอร์ตที่เผยแพร่ออกไป (published port) หรือแอปพลิเคชันฟังการเชื่อมต่อที่พอร์ตอื่น โดย log จะระบุความพยายามในการเชื่อมต่อไว้ที่: dial tcp 172.18.0.5:8080: connect: connection refused

เบราว์เซอร์แจ้งเตือน และใบรับรองออกให้แก่ TRAEFIK DEFAULT CERT หมายความว่าไม่มีใบรับรองสำหรับชื่อโฮสต์นั้น และ Traefik ได้แสดงใบรับรอง self-signed ที่เป็นค่าเริ่มต้นออกมา ให้ตรวจสอบบรรทัด ACME:

unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.com

DNS ยังไม่ชี้มาที่เซิร์ฟเวอร์ ให้แก้ไข record, รอจนกว่า TTL จะหมดอายุ แล้วรีสตาร์ท Traefik

Invalid response from http://git.example.com/.well-known/acme-challenge/... ในขั้นตอน HTTP challenge หมายความว่าพอร์ต 80 ไม่สามารถเข้าถึง Traefik จากภายนอกได้ ซึ่งมักเกิดจาก firewall ระดับผู้ให้บริการที่อยู่หน้า VPS ไม่ใช่ ufw

ใบรับรองไม่ออก และ DNS ของคุณอยู่บน Cloudflare โดยเปิดสถานะ orange cloud ไว้ Cloudflare จะทำ TLS termination ที่ edge ของตนเอง ทำให้ TLS-ALPN-01 ไม่สามารถผ่านเข้ามาได้ ให้ตั้งค่า record เป็น DNS-only ในระหว่างการออกใบรับรอง หรือเปลี่ยนไปใช้ DNS-01 challenge ด้วย API token ทั้งนี้ DNS-01 เป็นวิธีเดียวที่สามารถออกใบรับรองแบบ wildcard ได้

เกิด Redirect loop หมายความว่ามีบางอย่างที่อยู่หน้า Traefik ทำ TLS termination ไปแล้วและส่งต่อข้อมูลแบบ plaintext มาที่พอร์ต :80 ทำให้การตั้งค่า redirect ของ entrypoint ส่งข้อมูลกลับไปที่ HTTPS อีกครั้ง ให้ลบการตั้งค่า redirect ออกหนึ่งจุด

การรักษาให้ระบบทำงานอย่างต่อเนื่อง

Docker unit ต้องถูกตั้งค่าให้เริ่มทำงานตอนบูต (systemctl is-enabled docker) และ restart: unless-stopped จะช่วยให้ stack กลับมาทำงานได้อีกครั้งหลังรีบูต หากต้องการการจัดการที่ชัดเจน การใช้ systemd unit ขนาดเล็กที่รัน docker compose -f /srv/edge/compose.yml up -d พร้อมด้วย RemainAfterExit=yes จะช่วยให้คุณมี systemctl status edge และสามารถควบคุมลำดับการทำงานได้

ให้ระบุเวอร์ชันของ Traefik แบบเจาะจง (traefik:v3.7 ห้ามใช้ latest) และระบุ branch ที่ยังมีการออก release อยู่ ณ วันที่ 2026-10-12 Traefik ระบุว่า v3.7 เป็น branch เดียวในตระกูล v3 ที่ยังได้รับการสนับสนุนอยู่ โดย v3.5 ได้ออก release สุดท้ายคือ v3.5.6 ไปเมื่อวันที่ 7 พฤศจิกายน 2025 ซึ่ง Docker provider ของเวอร์ชันนั้นถูกกำหนดค่า API version ไว้ที่ 1.24 แบบตายตัว ทำให้ Docker Engine 29 ขึ้นไปปฏิเสธการเชื่อมต่อด้วย client version 1.24 is too old ส่งผลให้ไม่มี container ใดถูกส่งข้อมูลผ่านได้ Traefik 3.6.1 ได้เพิ่มความสามารถในการเจรจาต่อรอง Docker API version เข้ามา ซึ่งเป็นเหตุผลว่าทำไมไฟล์ compose ด้านบนจึงใช้ v3.7 การอัปเกรดจาก v2 ไป v3 มีการเปลี่ยนแปลงไวยากรณ์ของ rule และชื่อของ provider หากมีการทำ latest โดยอัตโนมัติ ระบบจะโหลด config ที่ไม่รองรับแล้วขึ้นมาทำงานทันที การอัปเกรดควรทำอย่างตั้งใจ: อ่านบันทึกการย้ายระบบ, ปรับเลขเวอร์ชัน, docker compose up -d traefik และคอยตรวจสอบ log หากคุณยังใช้ tag ของ v2 อยู่ คู่มือการย้ายระบบจาก Traefik v2 ไป v3 จะอธิบายขั้นตอนการเปลี่ยนชื่อทั้งหมด, โหมดความเข้ากันได้ และวิธีการย้อนกลับที่ยังคงรักษา certificate ของคุณไว้ได้

สำรองข้อมูล ./letsencrypt และ volume ข้อมูลของแต่ละแอปพลิเคชันไว้ Traefik ไม่ได้เก็บสถานะอื่นใดที่คุณไม่สามารถสร้างใหม่ได้จากไฟล์ compose

สิ่งที่มักจะติดขัดเมื่อขยายระบบ

ขีดจำกัดแรกไม่ใช่ปริมาณการรับส่งข้อมูล แต่เป็นตัวเซิร์ฟเวอร์เอง การใช้ Traefik หนึ่งตัวบน VPS หนึ่งเครื่องถือเป็นจุดเดียวที่อาจทำให้ระบบล่ม (single point of failure) สำหรับแอปพลิเคชัน 5 ตัว และเนื่องจาก acme.json เป็นการจัดเก็บข้อมูลแบบไฟล์ หากมี Traefik สองอินสแตนซ์เขียนข้อมูลพร้อมกันจะทำให้ไฟล์เสียหายได้ การขยายระบบ (scaling out) หมายถึงการย้ายที่จัดเก็บใบรับรอง (certificate) ออกจากไฟล์ หรือการทำ TLS termination ที่จุดอื่นแทน

ประเด็นที่สองคือการเชื่อมต่อที่คงอยู่เป็นเวลานาน (long-lived connections) เช่น Server-sent events, การอัปโหลดไฟล์ขนาดใหญ่ และไคลเอนต์ที่ตอบสนองช้า มักจะติดปัญหาเรื่อง timeout ของจุดรับคำขอ (entrypoint) โดยมี --entryPoints.websecure.transport.respondingTimeouts.readTimeout รวมถึง writeTimeout และ idleTimeout เป็นตัวปรับตั้งค่า ส่วน WebSockets นั้นสามารถใช้งานได้ทันทีโดยไม่ต้องตั้งค่าเพิ่มเติม

ประเด็นที่สามคือพื้นที่จัดเก็บข้อมูล --accesslog=true จะเขียน log ลง stdout และ Docker driver แบบ json-file จะเก็บข้อมูลนั้นไว้ตลอดไปหากไม่มีการจำกัดขนาด ควรตั้งค่า logging.options.max-size ในบริการ Traefik หรือเขียน access log ลงไฟล์แล้วทำการหมุนเวียนไฟล์ (rotate) แทน

ทั้งหมดนี้ไม่จำเป็นต้องใช้ระบบ orchestrator แต่จำเป็นต้องมีเซิร์ฟเวอร์ที่คุณควบคุมได้ มี IP จริง และเปิดพอร์ต 80 กับ 443 สู่สาธารณะ โดยมี VPS ขนาดเล็กเพียงเครื่องเดียวก็เพียงพอต่อความต้องการทั้งหมดแล้ว

FAQ

ฉันยังจำเป็นต้องใช้ certbot หากใช้งาน Traefik อยู่หรือไม่?

ไม่จำเป็น Traefik มี ACME resolver ที่ทำหน้าที่ร้องขอและต่ออายุใบรับรองสำหรับทุก hostname ที่กำหนดเส้นทางไว้ โดยจะจัดเก็บข้อมูลทั้งหมดไว้ใน acme.json ส่วน Certbot ยังคงเป็นเครื่องมือที่เหมาะสมในกรณีที่ nginx หรือเซิร์ฟเวอร์อื่นเป็นผู้ทำ TLS termination ด้วยตนเอง การใช้งานทั้งสองเครื่องมือกับ hostname เดียวกันจะทำให้โควตา rate limits ของ Let's Encrypt หมดลงโดยไม่จำเป็น

ทำไม container ของฉันถึงตอบกลับด้วย 404 ผ่าน Traefik?

สถานะ 404 ที่ส่งมาจาก Traefik หมายความว่าไม่มี router ใดตรงกับคำขอนั้นเลย ให้ตรวจสอบว่า container มีการระบุ traefik.enable=true (จำเป็นต้องมีเมื่อตั้งค่า exposedByDefault=false แล้ว) ตรวจสอบว่าค่า Host() ตรงกับชื่อที่คุณระบุ และชื่อของ router จะต้องเหมือนกันในทุก label ของแอปนั้นๆ สำหรับ Traefik v3 จำเป็นต้องใช้ backtick หรือเครื่องหมายคำพูดคู่ที่ใส่ escape character ไว้ภายในตัวจับคู่ (matcher) โดยระบบจะไม่ยอมรับเครื่องหมายคำพูดเดี่ยว

ความแตกต่างระหว่าง 404 และ 502 ในกรณีนี้คืออะไร?

สถานะ 404 หมายความว่าการกำหนดเส้นทางไม่เกิดขึ้น ส่วน 502 หมายความว่า router พบเส้นทางที่ตรงกันแล้ว แต่ backend ปฏิเสธการเชื่อมต่อ สาเหตุที่พบบ่อยของ 502 คือ container ไม่ได้เชื่อมต่อกับเครือข่าย proxy หรือการตั้งค่า loadbalancer.server.port ชี้ไปยังพอร์ตที่เผยแพร่ออกมา (published port) แทนที่จะเป็นพอร์ตที่แอปใช้งานจริงภายใน container โดย access log จะระบุที่อยู่ปลายทางที่ Traefik พยายามเชื่อมต่อไว้อย่างชัดเจน

การ mount Docker socket แบบอ่านอย่างเดียว (read-only) เพียงพอหรือไม่?

flag :ro ทำให้ไฟล์ socket เป็นแบบอ่านอย่างเดียว แต่ไม่ได้จำกัดการเข้าถึง API ที่อยู่เบื้องหลัง คำขอ POST ยังคงส่งผ่านช่องทางนี้ได้ และการเข้าถึง Docker API มีสิทธิ์เทียบเท่ากับ root บนโฮสต์ วิธีการที่ปลอดภัยกว่าคือการใช้ container docker-socket-proxy ตามที่แสดงไว้ข้างต้น ซึ่งจะเปิดเผยเฉพาะการอ่านข้อมูล container และเครือข่ายให้กับ Traefik โดยจะบล็อกการเขียนข้อมูลทั้งหมดโดยตรง

Traefik สามารถออกใบรับรองแบบ wildcard ได้หรือไม่?

ทำได้เฉพาะผ่านการตรวจสอบแบบ DNS-01 โดยต้องใช้ API token ของผู้ให้บริการ DNS ของคุณเท่านั้น ส่วน TLS-ALPN-01 และ HTTP-01 จะตรวจสอบได้ทีละ hostname และไม่สามารถออกใบรับรองแบบ wildcard ได้ นอกจากนี้ DNS-01 ยังเป็นทางออกในกรณีที่มี CDN เช่น Cloudflare ทำหน้าที่เป็น TLS termination อยู่หน้า VPS ของคุณ ซึ่งทำให้การตรวจสอบอีกสองวิธีไม่สามารถดำเนินการจนเสร็จสิ้นได้