13.4 docker-compose topology
บทนี้เจาะจง docker-compose ของทั้งระบบ ซึ่งเป็น orchestration หลักของ production — ไม่ใช่เครื่องมือพัฒนาเท่านั้น
สรุปโครงสร้าง docker-compose
| Repo | ไฟล์ | จำนวน service | network mode | replica หลัก |
|---|---|---|---|---|
| vtrc-api | docker-compose.yml | 7 (api ×4, web-proxy, redis, pma) | nginx เท่านั้น host; api/redis/pma = bridge + ports: | 4 (APP_NO=1,2,3,5) |
| vtrc-api | docker-compose.dev.yml | (dev variant) | — | — |
| cu-central-api | docker-compose.yml | 5 (api ×3, web-proxy, redis) | nginx เท่านั้น host; api/redis = bridge + ports: | 3 (APP_NO=1,2,3) |
| cu-central-api | docker-compose-dev.yml | (dev variant) | — | — |
| centralize-api | docker-compose.yml | 2 (dev, prod) | bridge + external network | 1 |
| vtrc-rc-backoffice | docker-compose.yml | 1 (nginx) | bridge + external network | 1 |
vtrc-api — docker-compose หลัก
vtrc-api/docker-compose.yml:1-100
โครงสร้าง service:
vtrc-redis-db (Redis 6379) — build from docker-redis/
vtrc-web-proxy (nginx) — host network, port 80/443
vtrc-pma (phpMyAdmin) — port 8093 (มีใน compose แต่ nginx block ไว้)
vtrc-api (Node, APP_NO=1) — port 8081:8081
vtrc-api-2 (Node, APP_NO=2) — port 8082:8081
vtrc-api-3 (Node, APP_NO=3) — port 8083:8081
vtrc-api-5 (Node, APP_NO=5) — port 8085:8081 (ข้าม APP_NO=4)จุดสังเกตสำคัญ:
network_mode: hostของ nginx (docker-compose.yml:34) — container ใช้ network ของ host โดยตรง ไม่มี port isolation เลย ทำให้ nginx สามารถเรียกlocalhost:8081,localhost:8082เพื่อไปยัง Node replica ได้ — เป็นรูปแบบที่ตรงข้ามกับ best practice ของ DockerNode replica ทั้งหมดใช้ image เดียวกัน —
image: vtrc-api_vtrc-api:latest(docker-compose.yml:48,63,76,89) ต่างกันเพียงที่APP_NOที่ส่งเข้าไปไม่มี environment variable สำหรับ DB credentials — docker-compose ไม่กำหนด
DB_HOST,DB_PASSWORDหรือJWT_SECRETในส่วนenvironment:หมายความว่า config เหล่านี้ต้องฝังใน image เอง (ผ่านไฟล์.envที่ COPY ใน Docker build) หรืออยู่ในconfig.jsที่ hard-codedVolume mount ระหว่าง replica — ทั้งสี่ replica ใช้ volume
./api-storageและ./api-logsร่วมกัน (docker-compose.yml:58-59,71-72,84-85,97-98) — ทำให้การเขียนไฟล์ (เช่น PDF payslip) มองเห็นได้ทุก replica แต่ก็มีความเสี่ยงเรื่อง race condition เมื่อหลาย replica เขียนไฟล์พร้อมกันTZ=Asia/Bangkokถูกตั้งในทุก replica — สำคัญเพราะ Node 12 ใช้ system timezone โดย default
vtrc-api — phpMyAdmin ที่ expose สาธารณะ
vtrc-api/docker-compose.yml:35-46:
vtrc-pma:
image: phpmyadmin/phpmyadmin
container_name: vtrc-phpMyAdmin
restart: always
ports:
- "8093:80"
environment:
PMA_ABSOLUTE_URI: http://vtrcapi.redcross.or.th/db-management/
PMA_HOST: vtrc-database
UPLOAD_LIMIT: 100MphpMyAdmin รันอยู่ใน compose ที่ expose port 8093 สาธารณะ — แม้ว่า nginx จะ comment block /db-management/ ไว้ (vtrc-api/nginx/default.conf:186-193) แต่ port 8093 เองยังเปิดอยู่
ปัญหานี้ถูกรายงานไว้ใน docs/audit/05-tech-debt-register.md (PERF-7) ว่าเป็น "Critical" — admin database surface อยู่ใน production network โดยไม่จำกัด IP
cu-central-api — docker-compose หลัก
cu-central-api/docker-compose.yml:1-93
โครงสร้าง service:
redis-db (Redis 6379) — build from Dockerfile-redis
rc-web-proxy (nginx) — host network, port 443 + 25563
cu-cen-api (Node, APP_NO=1) — port 8000:8000
cu-cen-api_2 (Node, APP_NO=2) — port 8002:8000
cu-cen-api_3 (Node, APP_NO=3) — port 8003:8000จุดสังเกต:
image: gcr.io/vtrc-nonprod/cu-centralize-api:latest— image ดึงจาก GCR ของ projectvtrc-nonprodทั้ง ๆ ที่ compose นี้ใช้ใน production (บ่งชี้ว่าใช้ GCR project เดียวกันทั้ง prod และ UAT)extra_hostsสำหรับ LDAP —docker-compose.yml:54-55,73-74,90-91:yamlextra_hosts: - "sso.redcross.or.th ssoapi.redcross.or.th:10.188.128.213"บอก container ให้ resolve ชื่อ
sso.redcross.or.thไปที่ IP on-prem โดยตรง — เป็นวิธีที่ใช้เชื่อม container ใน GCP เข้ากับ LDAP ของ on-premNODE_ENV=development— ส่งค่า environment เป็นdevelopmentแม้ใน compose production (docker-compose.yml:45,64,81) ทำให้ code ที่ตรวจสอบIS_PRODUCTIONทำงานผิดพลาด — เช่น GraphQL playground จะเปิดทำงานPort 25563 —
docker-compose.yml:31เปิด port 25563 ของ nginx เพิ่มเติมจาก 443 บ่งชี้ว่ามี endpoint ภายในหรือการเข้าถึงพิเศษผ่าน port นี้ (ดูcu-central-api/nginx/default.conf:65-107)
centralize-api — ใช้ external network
centralize-api/docker-compose.yml:1-32:
version: "3.8"
services:
dev:
build:
dockerfile: Dockerfile
target: develop
ports:
- "3000:3000"
command: yarn start:dev
networks:
- cu-centralize-api_default
prod:
build:
dockerfile: Dockerfile
target: production
ports:
- "3000:3000"
command: yarn start:prod
networks:
- cu-centralize-api_default
networks:
cu-centralize-api_default:
external: trueจุดสังเกต:
- External network
cu-centralize-api_default— centralize-api เชื่อมต่อเข้ากับ network ที่สร้างโดย docker-compose ของ cu-central-api เพื่อให้ container สองระบบสามารถเรียกกันได้ devและprodอยู่ในไฟล์เดียวกัน — ทีมต้องระบุdocker-compose up devหรือdocker-compose up prodจึงจะรู้ว่าจะรัน service ไหน — โอกาสสับสนสูง- ไม่มี
restart: always— ถ้า container crash จะไม่ restart อัตโนมัติ
vtrc-rc-backoffice — nginx เท่านั้น
vtrc-rc-backoffice/docker-compose.yml:1-23:
version: '3.8'
services:
web-server:
image: nginx:latest
container_name: nginx-web-server
restart: always
environment:
TZ: Asia/Bangkok
volumes:
- ./build:/usr/share/nginx/html
- ./meeting:/usr/share/nginx/meeting
- ./nginx:/etc/nginx/conf.d
- ./web-logs:/var/log/nginx
- ./cert:/etc/sslcert
ports:
- 80:80
- 443:443
networks:
- uat-vtrc-backoffice-network
networks:
uat-vtrc-backoffice-network:
external: trueจุดสังเกต:
./build:/usr/share/nginx/html— mount directory build ที่ build ด้วย CRA ไปที่ nginx ทำให้ไม่ต้อง build Docker image ใหม่เพื่อ deploy แค่ copy build ใหม่มาทับแล้ว reload nginx./cert:/etc/sslcert— mount certificate SSL เข้า container ทำให้ต้องจัดการ renewal certificate บน host โดยตรง (อาจผ่าน certbot)- Network name
uat-vtrc-backoffice-network— ชื่อ network บ่งชี้ว่า compose นี้ใช้กับ UAT แต่ก็น่าจะใช้กับ production ด้วยเนื่องจากไม่มี compose แยก
network mode: host vs bridge — สรุปผลกระทบ
ความจริงจาก compose: network_mode: host ถูกตั้ง เฉพาะ service nginx (vtrc-web-proxy บรรทัด 34, rc-web-proxy บรรทัด 35) — Node API replica และ Redis ใช้ bridge ปกติแล้ว map port ออก host (8081:8081, 8000:8000, ฯลฯ) ดังนั้น nginx บน host network จึงเรียก localhost:8081 / localhost:8000 ได้
| มิติ | host network (nginx เท่านั้น) | bridge + ports (API, Redis, PMA, NestJS, backoffice) |
|---|---|---|
| การเข้าถึง service อื่น | localhost:PORT โดยตรง (เห็น port ที่ map บน host) | ต้องผ่านชื่อ container หรือ IP / หรือผ่าน port map ของ host |
| ความปลอดภัย | ต่ำ — nginx container เห็น network ของ host | สูงกว่า — isolation โดย default |
| การจัดการ port | ใช้ port ของ host ตรง ๆ | map ผ่าน ports: |
| ความสามารถในการ scale | จำกัด — ทุก replica ต้องใช้ host port ต่างกัน | ดี — แต่ละ container มี IP ของตัวเอง |
การใช้ network_mode: host ของ nginx เป็นเหตุผลหลักที่ upstream localhost:8081 / localhost:8000 ทำงานได้ — แต่ก็ทำให้การย้ายไป Kubernetes หรือ orchestration อื่นยากขึ้น
ปัญหาเชิงปฏิบัติการที่พบ
| ปัญหา | ที่มา | ผลกระทบ |
|---|---|---|
ไม่มี depends_on หรือ healthcheck | ทุก compose | redis อาจยังไม่พร้อมเมื่อ Node เริ่ม start → retry ด้วยตนเอง |
NODE_ENV=development ใน cu-central-api production | cu-central-api/docker-compose.yml:45,64,81 | code ที่ตรวจ IS_PRODUCTION ทำงานผิด |
image: latest ทุก service | docker-compose ทุกไฟล์ | ไม่สามารถ rollback ได้ด้วย image tag — ต้องดู tag ใน GCR |
ไม่มี resource limit (mem_limit, cpus) | ทุก compose | replica หนึ่งที่ใช้ CPU/Memory สูงจะดึงทรัพยากรจาก host จนกระทบ replica อื่น |
network_mode: host ใน nginx | vtrc-api, cu-central-api | ลด isolation ทำให้ย้ายไป orchestration อื่นยาก |
ข้อแนะนำเพื่อปรับปรุง
- ใช้ bridge network แทน host — กำหนด
ports:อย่างชัดเจน - กำหนด
depends_on+ healthcheck — ให้ Node รอจนกว่า redis จะพร้อม - ตั้ง
NODE_ENV=productionใน compose ของ cu-central-api - ใช้ image tag ที่กำหนดเวอร์ชัน แทน
latest— เช่นgcr.io/.../vtrc-api:2026-07-11-abc1234 - กำหนด resource limit — ป้องกัน noisy neighbor problem ระหว่าง replica