Deep Dive Hạ Tầng Kioskgaming
Từ Cloudflare đến Nginx và toàn bộ backend services

Lập ngày 2026-07-03, cập nhật 2026-07-04 · Nguồn: repo hạ tầng nginx/ (cấu hình production đang chạy) + Hetzner Console (project "Kiosk VPS", Ashburn VA) + khảo sát trực tiếp 5 server · Phạm vi: Cloudflare edge → nginx gateway → app servers → data services → log pipeline → gamify engine.
Thông số trích từ file cấu hình thật, có dẫn nguồn file:dòng.
Bộ tài liệu: 📐 Sơ đồ hệ thống · ⚖️ So sánh & Kế hoạch chuyển đổi · 🎯 Hệ thống đích

1. Tổng quan kiến trúc

Toàn bộ traffic public của platform đi qua 3 tầng: Cloudflare (DNS + CDN + TLS termination) → nginx-lb (một container nginx làm reverse proxy / load balancer, định tuyến theo server_name) → app servers / data services / tools trên mạng nội bộ 10.0.0.0/24. Mọi thành phần đều chạy Docker, mỗi service một file docker-compose riêng ở root repo.

Người dùng / Client apps
Portals, Kiosk, Admin, Agent… — HTTPS :443
HTTPS — TLS kết thúc tại Cloudflare
Cloudflare — DNS · CDN · WAF (SSL mode: Flexible)
Tất cả domain *.kioskservice.club proxy qua Cloudflare · gắn CF-Connecting-IP / CF-Ray / CF-Visitor / X-Forwarded-Proto
15 dải IP Cloudflare được nginx tin cậy qua set_real_ip_from
HTTP :80 origin-pull — không TLS về origin
nginx-lb — nginx:alpine · network_mode: host
Định tuyến theo server_name (10 domain) · upstream least_conn · passive health check (max_fails=3 / 30s)
Chạy một instance duy nhất trên V1-Kiosk (178.156.194.24) — SPOF của toàn hệ
Firewall Hetzner (firewall-ssh, project chỉ có 1 firewall) chặn :80 trước cả nginx — nhưng chỉ whitelist 5/15 dải Cloudflare, xem card ⑦ bên dưới
định tuyến theo domain → 5 server nội bộ (10.0.0.0/24)

V1 — Gateway + App

178.156.194.24 · 10.0.0.5
  • ★ nginx-lb :80
  • API prod :3001–3004
  • API dev :4001
  • Dozzle :8080
CCX23

V2 — App

178.156.166.189 · 10.0.0.4
  • API prod :3001–3004
  • API dev :4001
  • Dozzle :8080
CCX13

V3 — Data master

178.156.211.239 · 10.0.0.2
  • Redis master :6379
  • Postgres 16 master :5432
  • Dozzle :8080
CCX13

V4 — Tools + Replica

178.156.211.230 · 10.0.0.3
  • Redis/Postgres slave, Redis dev
  • RabbitMQ, RedisInsight, OmniDB
  • Dozzle :8080
CCX13 · 7 container

V5 — Gamify + Log hub

178.156.139.212 · 10.0.0.6
  • Gamify pro ×4 + dev ×1
  • OpenSearch, Dashboards
  • Fluent Bit
CCX23 · 8 container
Hình 1 — Kiến trúc tổng thể: Cloudflare → nginx-lb → 5 server nội bộ. Redis/Postgres replicate 10.0.0.2 → 10.0.0.3; log container → Fluent Bit (10.0.0.6:24224) → OpenSearch, xem mục 7–8.

Cấu hình Nginx minh hoạ theo từng chặng

Các đoạn cấu hình thật tương ứng với từng mũi tên trong sơ đồ trên — chi tiết đầy đủ xem mục 3–5.

① Cloudflare trust — realipdefault.conf:28–43
set_real_ip_from 173.245.48.0/20;
… (15 dải CIDR của Cloudflare) …
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
Khôi phục IP thật của client vì mọi request tới origin đều đến từ IP Cloudflare.
② Upstream API productiondefault.conf:1–16
upstream backend {
    least_conn;
    server 10.0.0.5:3001 max_fails=3 fail_timeout=30s;
    …(8 instance: 10.0.0.5 + 10.0.0.4, :3001–3004)…
}
8 instance: 4 trên V1 + 4 trên V2.
③ WebSocket (Dozzle/RedisInsight/Dashboards)dozzle.conf, redisinsight.conf
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
3 service stream dữ liệu realtime qua WS — thiếu 3 dòng này thì UI đứng im dù nginx vẫn 200 OK.
④ Core nginx.confnginx/nginx.conf:1–37
worker_processes auto;
worker_connections 1024;
gzip on; gzip_comp_level 6;
include conf.d/*.conf;
Trần kết nối ≈ số core × 1024. V1 (CCX23) = 4 core; V2/V4 (CCX13) = 2 core.
⑤ Container nginx-lbnginx.yml:1–15
image: nginx:alpine
restart: always
network_mode: host
network_mode: host cho phép gọi thẳng 10.0.0.x, không cần network Docker riêng.
⑥ Hetzner Cloud FirewallConsole, áp cho 5/5 server
22/tcp  ← Any IPv4/IPv6 (không giới hạn)
80/tcp  ← 5/15 dải Cloudflare (thiếu 10 dải!)
443/tcp ← 15/15 dải (nginx không listen 443)
Tầng lọc network TRƯỚC nginx — lệch với set_real_ip_from ở cổng 80, xem rủi ro #13–14 mục 13.

Điểm mấu chốt cần nắm trong 60 giây

2. Danh sách VPS

Nguồn: iplist, comment trong nginx/conf.d/*.conf, các file compose; tên server/plan/IP server 5: Hetzner Console (project "Kiosk VPS", region Ashburn VA / us-east, screenshot 2026-07-03). Vai trò từng server suy từ IP nội bộ mà nginx trỏ tới và comment trong conf.

Server (tên Hetzner)Plan · vCPU/RAM* · DiskIPv4 publicIPv6IP nội bộVai tròService (port)
Server 1 — V1-KioskCCX23 · 4 vCPU / 16GB · 80GB178.156.194.242a01:4ff:f0:f237::/6410.0.0.5 Gateway + App nginx-lb :80 · Backend API prod :3001–3004 · API dev :4001 · Dozzle :8080
Server 2 — V2-KioskCCX13 · 2 vCPU / 8GB · 80GB178.156.166.1892a01:4ff:f0:afd6::/6410.0.0.4 App server Backend API prod :3001–3004 · API dev :4001 · Dozzle :8080
Server 3 — V3-KioskCCX13 · 2 vCPU / 8GB · 80GB178.156.211.2392a01:4ff:f0:5bf6::/6410.0.0.2 Data server Redis master :6379 · Postgres 16 master :5432 · Dozzle :8080
Server 4 — V4-KioskCCX13 · 2 vCPU / 8GB · 80GB178.156.211.2302a01:4ff:f0:5bf9::/6410.0.0.3 Tools / DB admin / Data replica / Queue Redis slave :6379 · Redis dev :6380 · Postgres slave :5433→5432 · RabbitMQ :5672/:15672 · RedisInsight :5540 · OmniDB :8000 · Dozzle :8080
Server 5 — kiosk-gamify-engineCCX23 · 4 vCPU / 16GB · 160GB178.156.139.212— (không có trong iplist)10.0.0.6 Gamification + Log hub Gamify pro :7003–7006 · Gamify dev :4003 · OpenSearch :9200/:9600 · Dashboards :5601 · Fluent Bit :24224/:2020

* vCPU/RAM theo catalog plan CCX (dedicated vCPU) của Hetzner; disk theo giá trị hiển thị trong console. Server tạo cách đây 4–9 tháng (V1 lâu nhất). iplist trong repo thiếu Server 5 — nên bổ sung.

Đối chiếu spec ↔ cấu hình trong repo: memory limit các service khớp với RAM 8GB của V3 (CCX13): Redis limit 1536MB + Postgres shared_buffers=1GB / effective_cache_size=3GB + hệ điều hành ≈ vừa khít — nghĩa là không còn nhiều headroom trên server data; nếu thêm service vào V3 phải tính lại. OpenSearch heap 1g + gamify 4 instance nằm thoải mái trong 16GB của Server 5. V4 (cũng CCX13, 8GB) cõng 7 container cùng lúc — Redis slave, Redis dev, Postgres slave, RabbitMQ, RedisInsight, OmniDB, Dozzle — dày đặc hơn hẳn so với vai trò "Tools" ban đầu tưởng nhẹ; nên là ứng viên tiếp theo cần soát memory limit tổng, tương tự V3.

3. Tầng Cloudflare (edge) edge

3.1. SSL mode: Flexible

Toàn bộ domain *.kioskservice.club proxy qua Cloudflare với SSL mode Flexible:

Client ──HTTPS :443──▶ Cloudflare ──HTTP :80──▶ nginx-lb (origin)

3.2. Khôi phục IP thực của client

Vì mọi request tới origin đều xuất phát từ IP của Cloudflare, nginx dùng module realip: mỗi server block khai báo 15 dải IP Cloudflare qua set_real_ip_fromreal_ip_header CF-Connecting-IP — nghĩa là: nếu request đến từ dải IP Cloudflare, lấy giá trị header CF-Connecting-IP làm $remote_addr. Nhờ đó log và rate-limit phía sau thấy đúng IP người dùng. Nguồn: nginx/conf.d/default.conf:28-43 (lặp lại trong mọi file conf)

set_real_ip_from 173.245.48.0/20;   set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;   set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;   set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;   set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;  set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;    set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;     set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;

3.3. Header Cloudflare được chuyển tiếp về backend

HeaderÝ nghĩanginx forward bằng
CF-Connecting-IPIP thực của clientproxy_set_header CF-Connecting-IP $http_cf_connecting_ip
CF-RayID request trên mạng Cloudflare (debug/trace với CF support)proxy_set_header CF-Ray $http_cf_ray
CF-VisitorJSON {"scheme":"https"} — scheme phía clientproxy_set_header CF-Visitor $http_cf_visitor
X-Forwarded-ProtoScheme gốc (https)$http_x_forwarded_proto (ưu tiên giá trị CF gắn)
X-Real-IP / X-Forwarded-ForIP client sau khi realip đã khôi phục$remote_addr / $proxy_add_x_forwarded_for

3.4. Hệ quả kiến trúc của Flexible mode

3.5. Mô hình DNS record (từ README trong repo)

Các README hướng dẫn map DNS theo nguyên tắc: mỗi domain là một A record (Proxied) trỏ thẳng vào IP public của server đích — ví dụ log-server1 → IP public Server 1, log-server2 → IP public Server 2, redisinsight/pgadmin → IP public Server 4. Nguồn: README-DOZZLE.md (mục "Map Domain trong Cloudflare"), README-REDISINSIGHT.md, README-OMNIDB.md.

Thực tế triển khai: nginx-lb chạy một instance duy nhất trên V1-Kiosk (178.156.194.24). Hướng dẫn "Map Domain" per-server trong các README là lỗi thời. Hệ quả: (1) mọi DNS record proxied của *.kioskservice.club phải trỏ về 178.156.194.24; (2) V1 là điểm hội tụ của toàn bộ traffic: gateway + 4 instance API prod + API dev trên cùng một máy CCX23; (3) tầng chặn :80 từ IP ngoài Cloudflare là Hetzner Firewall firewall-ssh (áp cho cả 5 server) — rule hiện thiếu 10/15 dải Cloudflare so với danh sách nginx tin tưởng, xem rủi ro #13 mục 13.

4. Nginx gateway (nginx-lb) gateway

4.1. Triển khai container

Image
nginx:alpine
Container
nginx-lb, restart: always
Network
network_mode: host — để gọi thẳng các IP nội bộ 10.0.0.x không qua NAT/bridge
Mount
./nginx/nginx.conf → /etc/nginx/nginx.conf (ro)
./nginx/conf.d → /etc/nginx/conf.d (ro)
./nginx/logs → /var/log/nginx
./nginx/.htpasswd-redisinsight, ./nginx/.htpasswd-app-logs (ro)

Nguồn: nginx.yml:1-15. Repo gốc trên GitLab tên kiosk-deployments; thư mục deploy trên server là kiosk_deploy/kiosk_deployments (suy từ prefix tên volume trong các lệnh backup của README). Host: V1-Kiosk (178.156.194.24 / 10.0.0.5); xem mục 3.5.

4.2. Cấu hình lõi (nginx/nginx.conf)

Tham sốGiá trịGhi chú
worker_processesauto= số CPU core
worker_connections1024Trần kết nối đồng thời ≈ workers × 1024 (mỗi request proxy tốn 2 kết nối)
keepalive_timeout65sPhía client (Cloudflare)
sendfile / tcp_nopush / tcp_nodelayon
gzipon, level 6text/css/js/json/xml/svg/font; gzip_vary on, gzip_proxied any
log_format main$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"

Nguồn: nginx/nginx.conf:1-37. Lưu ý: chưa bật keepalive tới upstream (dòng keepalive 32 đang comment trong default.conf:15) — mỗi request tạo kết nối TCP mới tới backend.

4.3. Log & logrotate

4.4. Quy trình thay đổi cấu hình (bắt buộc)

# 1. Sửa file trong nginx/conf.d/ (repo local chỉ là nơi soạn — phải đồng bộ lên server)
# 2. Kiểm tra cú pháp TRƯỚC khi reload:
docker exec nginx-lb nginx -t
# 3. Reload không downtime:
docker exec nginx-lb nginx -s reload

5. Domain routing & upstream

5.1. Bảng định tuyến đầy đủ

Domain (*.kioskservice.club)File confĐích / UpstreamLBĐặc thù
apidefault.confupstream backend: 10.0.0.5:3001–3004 + 10.0.0.4:3001–3004 (8 instance)least_connAPI production chính; buffering on 4k/8×4k; timeout 60s
dev-apidev-api.confupstream dev-api-backend: 10.0.0.5:4001 + 10.0.0.4:4001least_connAPI môi trường dev
api-gamify-proapi-gamify-pro.confupstream: 10.0.0.6:7003–7006 (4 instance)round-robinGamification production (compose scale=4)
api-gamify-devapi-gamify-dev.conf10.0.0.6:40031 instance dev
app-logsapp-logs.conf10.0.0.6:5601 (OpenSearch Dashboards)Basic auth .htpasswd-app-logs; WebSocket; timeout send/read 300s; buffering off
log-server1…4dozzle.conf10.0.0.5 / 10.0.0.4 / 10.0.0.2 / 10.0.0.3 — :8080Dozzle per-server; WebSocket; không basic auth ở nginx
redisinsightredisinsight.conf10.0.0.3:5540Basic auth .htpasswd-redisinsight; WebSocket; buffering off
pgadminomnidb.conf10.0.0.3:8000 (OmniDB)WebSocket; forward Cookie/Referer; map fallback X-Forwarded-Proto=https; buffering off

5.2. Upstream API production — phân tích

upstream backend {
    least_conn;                                    # chọn server ít kết nối nhất
    server 10.0.0.5:3001 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.5:3002 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.5:3003 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.5:3004 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.4:3001 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.4:3002 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.4:3003 weight=1 max_fails=3 fail_timeout=30s;
    server 10.0.0.4:3004 weight=1 max_fails=3 fail_timeout=30s;
}

5.3. Tham số proxy chung của mọi server block

NhómGiá trị
Timeoutproxy_connect/send/read_timeout 60s (riêng app-logs: send/read 300s, connect 75s — query Dashboards chạy lâu)
Buffer (API)proxy_buffering on, proxy_buffer_size 4k, proxy_buffers 8 4k, proxy_busy_buffers_size 8k
Buffer (UI tools)app-logs / redisinsight / omnidb: proxy_buffering off + proxy_request_buffering off (streaming UI)
WebSocketDozzle, RedisInsight, OmniDB, Dashboards: proxy_http_version 1.1 + Upgrade $http_upgrade + Connection "upgrade"
Basic authapp-logs, redisinsight — tạo bằng htpasswd -c ./nginx/.htpasswd-<tên> admin, mount qua nginx.yml
Mẫu thêm domain mới: copy dev-api.conf (file đơn giản nhất) → đổi server_name, upstream, log path. Bắt buộc giữ: khối 15 dải set_real_ip_from + real_ip_header CF-Connecting-IP, bộ proxy_set_header chuẩn (đặc biệt X-Forwarded-Proto $http_x_forwarded_proto vì Flexible mode). Service có WebSocket thì thêm 3 dòng upgrade. Sau đó nginx -tnginx -s reload, và thêm DNS record (proxied) trên Cloudflare.

6. Luồng request mẫu (API production)

Client Cloudflare nginx-lb :80 Backend (vd 10.0.0.5:3002) 1. HTTPS GET api.kioskservice.club/v1/... TLS handshake với cert Cloudflare 2. HTTP :80 (origin pull) + CF-Connecting-IP, CF-Ray, CF-Visitor + X-Forwarded-Proto: https 3. proxy_pass http://backend realip: $remote_addr = CF-Connecting-IP least_conn chọn 1/8 instance Host, X-Real-IP, X-Forwarded-For/Proto 4. Response 5. gzip (level 6), buffer 4k/8×4k 6. HTTPS + cache/CDN Cloudflare (nếu áp dụng)
Hình 2 — Vòng đời một request API production. Nếu instance lỗi 3 lần trong 30s, nginx tự loại khỏi pool (passive health check).

7. Data services data

Mỗi service một file compose ở root repo, chạy độc lập: docker compose -f <file>.yml up -d.

7.1. Redis 7.2 — master / slave / dev

redis-masterredis-slaveredis-dev
Fileredis-master.ymlredis-slave.ymlredis-dev.yml
Host10.0.0.2 (Server 3)10.0.0.3 (Server 4)10.0.0.3 (Server 4)
Port (host)637963796380 → 6379
Replication--replicaof 10.0.0.2 6379
maxmemory1536mb1536mb512mb
Evictionallkeys-lru — đầy RAM thì đuổi key ít dùng nhất (chấp nhận mất key ⇒ Redis đóng vai cache/session, không phải nguồn dữ liệu chính)
PersistenceAOF (appendonly yes) + RDB snapshot (save 900 1 / 300 10 / 60 10000)
Bảo mậtprotected-mode no, bind 0.0.0.0, không password — chỉ an toàn nhờ firewall mạng nội bộ
Khácmemory limit container 1536M; healthcheck redis-cli ping/30s; log json-file 10m×3limit 512M; volume redis-dev-data

Nguồn: redis-master.yml, redis-slave.yml, redis-dev.yml; host slave xác nhận qua redis-cli info replication (slave0:ip=10.0.0.3,state=online,lag=0). Replication một chiều master→slave; repo không có Sentinel/Cluster ⇒ không tự động failover — slave chỉ dùng đọc/dự phòng thủ công.

7.2. PostgreSQL 16 — streaming replication

postgres-master — 10.0.0.2:5432 postgres:16 · db "kiosk" · Server 3 wal_level = replica · max_wal_senders = 10 max_replication_slots = 10 · archive_mode = on archive → /var/lib/postgresql/archive/ shared_buffers 1GB · effective_cache_size 3GB work_mem 32MB · maintenance 256MB max_connections 100 · max_locks/txn 256 init: setup-master.sh (tạo user replicator) WAL streaming user: replicator postgres-slave — 10.0.0.3 (Server 4) postgres:16 · publish 5433 → 5432 container hot_standby = on (cho phép query đọc) primary_conninfo: host=10.0.0.2 port=5432 max_standby_streaming_delay = 30s wal_receiver_status_interval = 10s hot_standby_feedback = on tài nguyên: giống master (buffers/cache/mem) application_name = postgres-slave
Hình 3 — Postgres 16 streaming replication (async). Slave phục vụ đọc (hot standby); failover là thao tác thủ công.

7.3. RabbitMQ 3.13

File
rabbitmq.yml — image rabbitmq:3.13-management, host 10.0.0.3 (Server 4)
Port
5672 (AMQP) · 15672 (Management UI — không thấy expose qua nginx, truy cập trực tiếp/SSH tunnel)
Tài khoản
user kiosk, vhost / (mật khẩu plaintext trong yml)
Giới hạn
memory 1024M; healthcheck rabbitmq-diagnostics ping/10s; volume rabbitmq-data

8. Log & Observability observability

8.1. Kiến trúc 2 tầng

  1. Xem nhanh per-server: mỗi server chạy 1 container Dozzle (:8080, host network, đọc docker.sock read-only, chỉ hiện container đang chạy, tail 1000 dòng) — truy cập qua log-server1…4.kioskservice.club.
  2. Tập trung: Fluent Bit trên 10.0.0.6 gom log → OpenSearch → xem bằng OpenSearch Dashboards tại app-logs.kioskservice.club (basic auth ở nginx).
Container server khác logging driver: fluentd → gửi Forward protocol tới :24224 Container trên 10.0.0.6 json-file của Docker Engine /var/lib/docker/containers/*/*-json.log INPUT forward :24224 INPUT tail (parser docker, Mem_Buf 50MB, skip long lines) Fluent Bit 3.0.7 10.0.0.6 · network_mode: host FILTER lua tag_prefix.lua: tag → logstash_prefix OUTPUT es → 127.0.0.1:9200 Flush 5s · HTTP monitor :2020 bulk index OpenSearch 2.17.1 :9200 single-node "kiosk-logs" · heap 1g Index: <prefix>-YYYY.MM.DD vd: kiosk-gaming-backend-dev-logs-… ⚠ security plugin TẮT (không TLS/auth) volume opensearch-data OpenSearch Dashboards :5601 UI tìm kiếm / dashboard log nginx-lb + basic auth app-logs.kioskservice.club Dev / Ops qua Cloudflare HTTPS Song song: Dozzle :8080 trên từng server (log-server1…4.kioskservice.club) để tail log container trực tiếp, không qua pipeline này.
Hình 4 — Log pipeline tập trung: 2 nguồn vào Fluent Bit → OpenSearch → Dashboards, bảo vệ bởi basic auth tại nginx.

8.2. Quy tắc đặt tên index (Lua filter)

fluent-bit/tag_prefix.lua (hàm append_fluentbit_tag) gán 2 field cho mỗi record: docker_log_tag (tag gốc) và logstash_prefix. Quy tắc chuyển tag → prefix:

8.3. Chống phình log ở tầng Docker

8.4. Chi tiết setup & giới hạn (đúc kết từ các README)

Trạng thái thực tế (curl localhost:9200/_cluster/health + _cat/indices + localhost:2020/api/v1/metrics chạy trực tiếp trên 10.0.0.6):

9. Gamification engine

Thư mục gamify-engine/ chứa script + env deploy backend gamification trên server 10.0.0.6 (code nằm ở repo GitLab riêng kiosk_gamification_engine, không phải trong repo nginx).

DevProduction
Scriptgamify-engine/dev/scrip-deploy-devgamify-engine/production/scrip-deploy-pro
Thư mục trên server/var/www/dev/gamify-be/backend/deployments/var/www/production/kiosk_gamification_engine/backend/deployments
Composedocker-compose-dev.app.ymldocker-compose.app.yml với --scale app=4 (⇒ 4 instance :7003–7006 khớp upstream nginx)
Quy trìnhgit pull → nạp lại .env (mở nano dán thủ công từ file env-*) → downup -d --build → chạy npm run migration:run (pro thêm npm run seed)
Endpoint publicapi-gamify-dev.kioskservice.club → :4003api-gamify-pro.kioskservice.club → :7003–7006
⚠ Deploy có downtime và chứa secret: quy trình downup --build làm dừng toàn bộ instance trong lúc build (không rolling). File env-dev/env-production là env thật, và scrip-deploy-pro chứa cả GitLab access token nhúng trong URL git — token này nên được thu hồi/xoay và chuyển sang deploy key; tuyệt đối không copy các file này ra ngoài.

10. Bảng port tổng hợp

PortServiceHostExpose public?
80nginx-lb (mọi domain)10.0.0.5 (V1-Kiosk)Có — qua Cloudflare; IP ngoài dải Cloudflare bị Hetzner Firewall chặn
3001–3004Backend API production (×4/server)10.0.0.5, 10.0.0.4Không (chỉ qua nginx)
4001Backend API dev10.0.0.5, 10.0.0.4Không (qua dev-api)
7003–7006Gamify production (scale 4)10.0.0.6Không (qua api-gamify-pro)
4003Gamify dev10.0.0.6Không (qua api-gamify-dev)
6379Redis master / slave10.0.0.2 / 10.0.0.3Không — nhưng không có auth, dựa hoàn toàn firewall
6380Redis dev10.0.0.3Không
5432Postgres master10.0.0.2Không
5433Postgres slave (→5432 container)10.0.0.3Không
5672 / 15672RabbitMQ AMQP / Management UI10.0.0.3Không (UI không qua nginx)
9200 / 9600OpenSearch API / metrics10.0.0.6Không — security plugin tắt, dựa firewall
5601OpenSearch Dashboards10.0.0.6Qua app-logs + basic auth
24224 / 2020Fluent Bit forward / HTTP monitor10.0.0.6Không
8080Dozzle (mỗi server)10.0.0.5/.4/.2/.3Qua log-server1…4 (không basic auth ở nginx)
5540RedisInsight10.0.0.3Qua redisinsight + basic auth
8000OmniDB10.0.0.3Qua pgadmin (auth riêng của OmniDB)

11. Playbook kiểm tra hạ tầng (health-check)

Mục này gộp lại cách xác minh từng tầng đang sống hay chết, đi từ ngoài (Cloudflare) vào trong (data server) — dùng khi nghi ngờ sự cố, trước/sau khi reload nginx, hoặc kiểm tra định kỳ. Nguyên tắc: luôn kiểm tra từ layer gần user nhất trước (domain public) rồi mới đi sâu vào server nội bộ, để khoanh vùng nhanh layer nào đang lỗi.

11.1. Bảng tra nhanh theo server

ServerVai tròSSH vàoLệnh kiểm tra chính
V1-Kiosk
10.0.0.5
Gateway + App gw appssh root@178.156.194.24 docker exec nginx-lb nginx -t · docker ps (nginx-lb, app :3001-3004, :4001, dozzle)
V2-Kiosk
10.0.0.4
App appssh root@178.156.166.189 docker ps (app :3001-3004, :4001, dozzle) · docker stats --no-stream
V3-Kiosk
10.0.0.2
Data datassh root@178.156.211.239 redis-cli ping · pg_isready · xem 11.3
V4-Kiosk
10.0.0.3
Tools obsssh root@178.156.211.230 docker ps (redisinsight, omnidb, dozzle) · xem 11.4
kiosk-gamify-engine
10.0.0.6
Gamify + Log hub data obsssh root@178.156.139.212 curl -s localhost:9200/_cluster/health?pretty · xem 11.5

11.2. Layer 1 — Edge & gateway (từ máy bất kỳ, không cần SSH)

# Domain có trả lời qua Cloudflare không (chú ý header cf-ray = đã qua Cloudflare)
curl -sI https://api.kioskservice.club | grep -i "cf-ray\|http"

# Cú pháp nginx hiện tại có hợp lệ không (chạy TRÊN V1, trước khi reload)
docker exec nginx-lb nginx -t

# nginx-lb có đang chạy, container nào restart gần đây (dấu hiệu crash loop)
docker ps --filter name=nginx-lb --format "table {{.Names}}\t{{.Status}}"
Đọc kết quả: có header cf-ray ⇒ request đã đi qua Cloudflare tới origin và có phản hồi — lỗi (nếu có) nằm ở nginx/backend, không phải DNS/Cloudflare. Không có cf-ray hoặc timeout ⇒ kiểm tra DNS record / trạng thái Cloudflare trước khi nghi ngờ server.

Kết quả lần chạy gần nhất trên V1 và V2: cả hai nginx -t OK (V1, gateway); 4/4 instance API prod (:3001–3004) + 1 dev (:4001) đều healthy trên mỗi server; CPU/RAM thấp, còn nhiều headroom trên cả hai (~90-100MB/container / 8GB RAM). Riêng dozzle unhealthy trên cả hai — xem 11.4.

11.3. Layer 2 — Data server (V3-Kiosk, 10.0.0.2)

# Redis master
docker exec -it redis-master redis-cli ping                     # → PONG
docker exec -it redis-master redis-cli info replication          # connected_slaves khớp số slave đang trỏ về 10.0.0.2
docker exec -it redis-master redis-cli info memory | grep used_memory_human

# Postgres master
docker exec -it postgres-master pg_isready
docker exec -it postgres-master psql -U postgres -d kiosk -c "SELECT * FROM pg_stat_replication;"
# mỗi dòng trả về = 1 slave đang replicate; state "streaming" là khỏe, thiếu dòng ⇒ slave rớt kết nối

Tên container thực tế tuỳ container_name khai báo trong redis-master.yml / postgres-master.yml — dùng docker ps để xác nhận nếu lệnh trên báo "No such container".

Kết quả lần chạy gần nhất: Redis PONG, replica online lag=0; Postgres pg_isready OK, replica streaming. Server chạy đúng 3 container (postgres-master, redis-master, dozzle). Riêng dozzle unhealthy — xem 11.4.

11.4. Layer 2 — Tools server (V4-Kiosk, 10.0.0.3)

V4 chạy 7 container: postgres-slave, redis-slave, redis-dev, redisinsight, omnidb, dozzle, rabbitmq.

docker ps --format "table {{.Names}}\t{{.Status}}"
# kỳ vọng cả 7 container "Up"; chú ý cột STATUS có "(healthy)" / "(unhealthy)" / không có gì (container không khai báo healthcheck)
redisinsightdozzle trên V4 báo unhealthy nhưng dịch vụ vẫn hoạt động bình thường — healthcheck mặc định của image sai (cả 4 container Dozzle V1–V4 cùng triệu chứng). Việc vệ sinh: sửa hoặc tắt healthcheck để hết báo động giả (rủi ro #11, mục 13).

11.5. Layer 2 — Gamify + Log hub (10.0.0.6)

# OpenSearch cluster (chạy trên chính server 10.0.0.6, hoặc qua SSH tunnel)
curl -s localhost:9200/_cluster/health?pretty          # status: green/yellow/red
curl -s localhost:9200/_cat/indices?v                  # danh sách index, phát hiện index phình bất thường

# Fluent Bit — có đang nhận/đẩy log không
curl -s localhost:2020/api/v1/metrics | head -50        # HTTP monitor plugin, :2020

# Gamify backend
docker ps --filter name=app --format "table {{.Names}}\t{{.Status}}"   # đủ 4 instance :7003-7006 (pro) / 1 instance :4003 (dev)
OpenSearch status "red" hoặc _cat/indices cho thấy index tăng liên tục không dọn ⇒ đúng rủi ro #3 đã ghi ở mục 13 (chưa có retention/ISM) — disk Server 5 sẽ đầy dần, không phải lỗi cấu hình mới. Status "yellow"trạng thái ổn định của cluster này, không phải sự cố — giải thích chi tiết ở mục 8.4 (single-node + replica=1 không bao giờ gán hết được).

Kết quả lần chạy gần nhất: V5 chạy đúng 8 container — gamify deployments_app_1..4 (prod) + deployments_app-dev_1 (dev, đủ 5 healthy, khớp scale compose mục 9), opensearch (healthy), opensearch-dashboards, fluent-bit. Chi tiết OpenSearch/Fluent Bit xem mục 8.4.

11.6. Kiểm tra end-to-end (checklist nhanh khi nghi có sự cố)

  1. curl -sI https://api.kioskservice.club — có cf-ray + status 2xx/3xx? Không có ⇒ dừng ở DNS/Cloudflare, chưa cần SSH vào server nào.
  2. SSH V1 → docker exec nginx-lb nginx -t && docker ps — gateway sống, cú pháp hợp lệ?
  3. docker logs --tail 100 nginx-lb — có lỗi connect() failed tới backend không (chỉ điểm app server nào chết)?
  4. SSH vào app server bị nghi (V1/V2) → docker ps đủ 4 instance API + docker stats không có container OOM/restart loop.
  5. Nếu API trả lỗi liên quan dữ liệu → SSH V3, chạy 11.3 (Redis ping, Postgres pg_isready).
  6. Cần log chi tiết để debug tiếp → Dozzle (log-server1…4) cho log tức thời, hoặc Dashboards (app-logs) để tra theo thời gian/từ khoá trên toàn hệ.

12. Vận hành thường gặp

ViệcLệnh / file
Kiểm tra cú pháp nginxdocker exec nginx-lb nginx -t
Reload nginx (không downtime)docker exec nginx-lb nginx -s reload
Xem log nginx realtimedocker logs -f nginx-lb hoặc file trong nginx/logs/
Khởi động một stackdocker compose -f <tên>.yml up -d
Xem log container per-serverDozzle: log-server1…4.kioskservice.club; script ./check-logs.sh
Log tập trungapp-logs.kioskservice.club (basic auth)
Tạo basic auth mớihtpasswd -c ./nginx/.htpasswd-<tên> admin rồi mount qua nginx.yml
Logrotate nginxsetup-nginx-logrotate.sh + nginx/logrotate.conf
Giới hạn log Docker daemonsetup-docker-logs.sh + docker-daemon.json
Deploy gamifygamify-engine/dev/scrip-deploy-dev · gamify-engine/production/scrip-deploy-pro
Kiểm tra Redis replicationdocker exec redis-master redis-cli info replication
Kiểm tra Postgres replicationtrên master: SELECT * FROM pg_stat_replication;
Test / ép chạy logrotate nginxsudo logrotate -d /etc/logrotate.d/nginx-kiosk (dry-run) · -f (ép chạy)
Tạo admin OmniDB (lần đầu / quên pass)docker exec -it omnidb python omnidb-server.py --createsuperuser=admin '<mật-khẩu>' — password có ký tự đặc biệt phải bọc nháy đơn
Backup settings RedisInsight / OmniDBtar volume redisinsight-data / omnidb-data qua container alpine (lệnh mẫu trong README từng tool)
Dọn log thủ công khi disk căngdocker system dffind ./nginx/logs/ -name "*.log.*" -mtime +7 -delete (xem README-LOG-MANAGEMENT.md trước khi prune)

13. Rủi ro & khuyến nghị

#Rủi roMứcKhuyến nghị
1Cloudflare Flexible: chặng CF→origin đi HTTP thuần trên Internet — dữ liệu (kể cả token/credentials) không mã hoá giữa PoP Cloudflare và server. Hetzner Firewall đã giới hạn nguồn vào :80 nên khó bypass origin, nhưng không thay được mã hoáTrung bìnhCài Cloudflare Origin CA cert + nginx listen 443 → chuyển Full (strict) để mã hoá nốt chặng origin (hạng mục Edge L0–L1 của kế hoạch DDoS Resilience)
2V1-Kiosk là SPOF nặng nhất hệ thống: một mình gánh nginx-lb (cửa ngõ 10 domain) + 4 instance API prod + API dev. V1 chết = mất TOÀN BỘ hệ thống dù V2–V5 vẫn sốngCaoDựng nginx-lb thứ 2 (kế hoạch chuyển đổi chọn V4 vì failure-domain tốt hơn — xem kế hoạch chuyển đổi bước 1A) + Cloudflare LB/A record thứ 2 để failover origin
3OpenSearch tắt security plugin (không TLS/auth trên :9200); bảo vệ duy nhất là basic auth ở nginx cho DashboardsCaoLàm theo README-OPENSEARCH-PRODUCTION.md: bật security, đặt admin password, bỏ các biến DISABLE_*
4Redis không password, protected-mode no, bind 0.0.0.0 — an toàn phụ thuộc hoàn toàn firewall (đã xác nhận firewall-ssh là firewall duy nhất trong project, không có rule mở :6379 ⇒ default-deny từ Internet) chứ không có auth thật ở tầng ứng dụngCaoThêm requirepass/masterauth để không phụ thuộc 100% vào 1 lớp firewall duy nhất (phòng trường hợp rule bị sửa nhầm sau này)
5Secret plaintext trong repo: mật khẩu Postgres/RabbitMQ/replication trong yml, env thật, GitLab token trong script deployCaoThu hồi/xoay token GitLab ngay; chuyển secret sang .env không commit / secret manager; đảm bảo repo private tối thiểu
6Không auto-failover cho Redis lẫn Postgres (chỉ replication một chiều, promote thủ công)Trung bìnhViết runbook promote slave; cân nhắc Sentinel (Redis) khi có ≥3 node
7Dozzle không có basic auth ở nginx — log-server1…4 lộ log container (Dozzle mặc định cũng không auth)Trung bìnhThêm auth_basic vào dozzle.conf giống app-logs, hoặc bật auth của Dozzle
8Deploy gamify có downtime (down → build → up) và dán .env bằng nano thủ công, dễ saiTrung bìnhBuild trước rồi up; hoặc rolling update từng instance sau lưng nginx
9WAL archive nằm cùng máy master; backup off-site không thấy trong repoTrung bìnhBổ sung pg_dump/basebackup định kỳ đẩy ra storage ngoài
10Upstream chưa bật keepalive; passive health check có thể trả lỗi cho vài request trước khi loại instance hỏngThấpBật keepalive 32 + proxy_http_version 1.1 tới upstream; cân nhắc retry proxy_next_upstream
11Healthcheck giả trên container giám sát: cả 4 dozzle (V1–V4) và redisinsight (V4) báo unhealthy dù dịch vụ hoạt động bình thường — healthcheck mặc định của image sai; hệ quả là cột STATUS của docker ps mất giá trị cảnh báoThấpSửa hoặc tắt healthcheck trong dozzle.yml/redisinsight.yml để unhealthy từ nay đồng nghĩa sự cố thật (không cần restart)
12OpenSearch cluster vĩnh viễn yellow (70/147 shard unassigned) vì index dùng number_of_replicas: 1 trên cluster chỉ 1 node — replica không bao giờ gán được, chỉ tốn thêm disk/CPU mà không có redundancy thậtThấpĐổi index template mặc định về number_of_replicas: 0 cho single-node (không mất gì vì replica hiện tại vô dụng); chỉ quay lại rep=1 nếu thêm node OpenSearch thứ 2
13Rule TCP :80 trên Hetzner Firewall (firewall-ssh, áp cho cả 5 server) chỉ whitelist 5/15 dải Cloudflare — thiếu 10 dải mà nginx vẫn tin tưởng qua set_real_ip_from. Request thật từ Cloudflare PoP nằm trong 10 dải thiếu sẽ bị Hetzner Firewall DROP trước khi tới nginx, gây mất kết nối ngắt quãng khó tái hiện (tuỳ định tuyến Cloudflare). Rule :443 lại có đủ 15/15 dù nginx không listen 443CaoThêm đủ 15 dải Cloudflare vào rule :80 trong Hetzner Console, đối chiếu chính xác với set_real_ip_from trong default.conf
14SSH (:22) mở cho toàn Internet (Any IPv4 + Any IPv6, không giới hạn nguồn) trên cùng firewall áp cho cả 5 server — an toàn chỉ dựa vào key-based auth, không có lớp network-level restrictionTrung bìnhGiới hạn nguồn rule :22 về dải IP admin/VPN cụ thể trong Hetzner Console nếu có; nếu không có IP cố định thì cân nhắc bastion/VPN riêng

14. Phụ lục

14.1. Cấu trúc repo nginx/

nginx/                          # repo hạ tầng (infra/ops), KHÔNG chứa code app
                                # tên gốc GitLab: kiosk-deployments (dustin.le)
├── nginx.yml                   # compose container nginx-lb
├── nginx/
│   ├── nginx.conf              # cấu hình lõi
│   ├── conf.d/                 # 8 file conf ↔ 10 domain (mục 5)
│   └── logrotate.conf
├── redis-master.yml / redis-slave.yml / redis-dev.yml
├── postgres-master.yml / postgres-slave.yml
├── rabbitmq.yml
├── opensearch.yml              # OpenSearch + Dashboards + Fluent Bit
├── fluent-bit/                 # fluent-bit.conf + tag_prefix.lua
├── dozzle.yml / redisinsight.yml / omnidb.yml
├── gamify-engine/              # script + env deploy (dev/, production/)
├── setup-*.sh, check-*.sh      # script vận hành
├── docker-daemon.json          # giới hạn log Docker toàn daemon
├── iplist                      # IP public 4 server
├── env, env-dev                # ⚠ env thật chứa secret
└── README-*.md                 # tài liệu từng thành phần

14.2. Tài liệu tham chiếu trong repo

READMENội dung chính đáng nhớ
README-NGINX.md4 phương thức LB, passive health check (max_fails/fail_timeout), mẫu thêm SSL :443, troubleshooting container/backend
README-LOG-MANAGEMENT.md2 tầng rotation (daemon.json + logrotate), dung lượng dự kiến ~100–150MB/server, cron giám sát 2h sáng, lệnh dọn thủ công; đổi daemon.json phải restart Docker + container
README-OPENSEARCH-SETUP.mdSetup copy-paste: sysctl vm.max_map_count, network kiosk-logging, cách service khác gửi log (fluentd driver + tag), bảng sự cố thường gặp (gồm vụ Lua "Is a directory")
README-OPENSEARCH-PRODUCTION.mdChecklist go-live: bật security plugin, heap theo RAM host, backup Snapshot API, retention ISM — các mục này hiện CHƯA làm trên production
README-DOZZLE.mdKiến trúc Dozzle per-server; hướng dẫn map DNS domain → IP public server đích (căn cứ của mục 3.5); chính README cũng khuyến nghị thêm basic auth (rủi ro #7)
README-REDISINSIGHT.mdKết nối Redis master/slave, script setup-redisinsight-auth.sh, backup volume · ⚠ chứa credential plaintext
README-OMNIDB.mdBắt buộc tạo superuser lần đầu (--createsuperuser, password bọc nháy đơn), kết nối PG master :5432 / slave :5433, backup volume · ⚠ chứa credential plaintext
README-RABBITMQ.mdConnection string AMQP + Management UI · ⚠ chứa credential plaintext; phần boilerplate GitLab lộ tên repo gốc kiosk-deployments

Tài liệu lập từ repo /Users/nhim/dev/2026/kiosk/nginx + khảo sát trực tiếp 5 server, cập nhật 2026-07-04. Mật khẩu/token trong các file cấu hình được cố ý lược bỏ. Sơ đồ blueprint: So-Do-He-Thong.html · kế hoạch xử lý các rủi ro mục 13: so-sanh-va-ke-hoach-chuyen-doi.html · kiến trúc đích: he-thong-dich.html.