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.
set_real_ip_fromserver_name (10 domain) · upstream least_conn · passive health check (max_fails=3 / 30s)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
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.
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;
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)…
}
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
worker_processes auto;
worker_connections 1024;
gzip on; gzip_comp_level 6;
include conf.d/*.conf;
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.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)
set_real_ip_from ở cổng 80, xem rủi ro #13–14 mục 13.nginx-lb chạy trên V1-Kiosk (178.156.194.24) — vừa reverse proxy vừa load balancer, và là điểm lỗi đơn (SPOF): V1 chết là cả hệ mất, dù các server khác vẫn sống.X-Forwarded-Proto do Cloudflare gắn.least_conn.unhealthy do healthcheck của image sai — không phải sự cố, chỉ cần sửa/tắt healthcheck (rủi ro #11).ubuntu-8gb-ash-1 khi SSH — chỉ IP mới phân biệt chắc chắn.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* · Disk | IPv4 public | IPv6 | IP nội bộ | Vai trò | Service (port) |
|---|---|---|---|---|---|---|
| Server 1 — V1-Kiosk | CCX23 · 4 vCPU / 16GB · 80GB | 178.156.194.24 | 2a01:4ff:f0:f237::/64 | 10.0.0.5 | Gateway + App | nginx-lb :80 · Backend API prod :3001–3004 · API dev :4001 · Dozzle :8080 |
| Server 2 — V2-Kiosk | CCX13 · 2 vCPU / 8GB · 80GB | 178.156.166.189 | 2a01:4ff:f0:afd6::/64 | 10.0.0.4 | App server | Backend API prod :3001–3004 · API dev :4001 · Dozzle :8080 |
| Server 3 — V3-Kiosk | CCX13 · 2 vCPU / 8GB · 80GB | 178.156.211.239 | 2a01:4ff:f0:5bf6::/64 | 10.0.0.2 | Data server | Redis master :6379 · Postgres 16 master :5432 · Dozzle :8080 |
| Server 4 — V4-Kiosk | CCX13 · 2 vCPU / 8GB · 80GB | 178.156.211.230 | 2a01:4ff:f0:5bf9::/64 | 10.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-engine | CCX23 · 4 vCPU / 16GB · 160GB | 178.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.
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.
Toàn bộ domain *.kioskservice.club proxy qua Cloudflare với SSL mode Flexible:
Client ──HTTPS :443──▶ Cloudflare ──HTTP :80──▶ nginx-lb (origin)
listen 443 nào trong conf.d/.X-Forwarded-Proto: https khi client dùng HTTPS; nginx forward nguyên giá trị này về backend qua proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto để app biết phiên gốc là HTTPS (cookie Secure, redirect…). Nguồn: nginx/conf.d/default.conf:46-54map fallback: nếu header rỗng thì ép thành https để tránh 403 sau login do cookie Secure. Nguồn: nginx/conf.d/omnidb.conf:6-10
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_from và
real_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;
| Header | Ý nghĩa | nginx forward bằng |
|---|---|---|
CF-Connecting-IP | IP thực của client | proxy_set_header CF-Connecting-IP $http_cf_connecting_ip |
CF-Ray | ID request trên mạng Cloudflare (debug/trace với CF support) | proxy_set_header CF-Ray $http_cf_ray |
CF-Visitor | JSON {"scheme":"https"} — scheme phía client | proxy_set_header CF-Visitor $http_cf_visitor |
X-Forwarded-Proto | Scheme gốc (https) | $http_x_forwarded_proto (ưu tiên giá trị CF gắn) |
X-Real-IP / X-Forwarded-For | IP client sau khi realip đã khôi phục | $remote_addr / $proxy_add_x_forwarded_for |
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.
*.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.
nginx:alpinenginx-lb, restart: alwaysnetwork_mode: host — để gọi thẳng các IP nội bộ 10.0.0.x không qua NAT/bridge./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.
nginx/nginx.conf)| Tham số | Giá trị | Ghi chú |
|---|---|---|
worker_processes | auto | = số CPU core |
worker_connections | 1024 | Trần kết nối đồng thời ≈ workers × 1024 (mỗi request proxy tốn 2 kết nối) |
keepalive_timeout | 65s | Phía client (Cloudflare) |
sendfile / tcp_nopush / tcp_nodelay | on | |
gzip | on, level 6 | text/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.
nginx/logs/ trên host, mỗi domain một cặp file riêng (dev-api-access.log, api-gamify-pro-access.log…).nginx -s reopen vào container. Cài qua setup-nginx-logrotate.sh → /etc/logrotate.d/nginx-kiosk. Nguồn: nginx/logrotate.conf# 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
| Domain (*.kioskservice.club) | File conf | Đích / Upstream | LB | Đặc thù |
|---|---|---|---|---|
| api | default.conf | upstream backend: 10.0.0.5:3001–3004 + 10.0.0.4:3001–3004 (8 instance) | least_conn | API production chính; buffering on 4k/8×4k; timeout 60s |
| dev-api | dev-api.conf | upstream dev-api-backend: 10.0.0.5:4001 + 10.0.0.4:4001 | least_conn | API môi trường dev |
| api-gamify-pro | api-gamify-pro.conf | upstream: 10.0.0.6:7003–7006 (4 instance) | round-robin | Gamification production (compose scale=4) |
| api-gamify-dev | api-gamify-dev.conf | 10.0.0.6:4003 | — | 1 instance dev |
| app-logs | app-logs.conf | 10.0.0.6:5601 (OpenSearch Dashboards) | — | Basic auth .htpasswd-app-logs; WebSocket; timeout send/read 300s; buffering off |
| log-server1…4 | dozzle.conf | 10.0.0.5 / 10.0.0.4 / 10.0.0.2 / 10.0.0.3 — :8080 | — | Dozzle per-server; WebSocket; không basic auth ở nginx |
| redisinsight | redisinsight.conf | 10.0.0.3:5540 | — | Basic auth .htpasswd-redisinsight; WebSocket; buffering off |
| pgadmin | omnidb.conf | 10.0.0.3:8000 (OmniDB) | — | WebSocket; forward Cookie/Referer; map fallback X-Forwarded-Proto=https; buffering off |
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;
}
least_conn phù hợp API có thời gian xử lý không đều — request mới dồn về instance đang rảnh nhất, thay vì xoay vòng mù như round-robin.max_fails=3) và bị loại khỏi pool trong 30 giây (fail_timeout=30s), sau đó được thử lại. Không có active health check (cần nginx Plus hoặc module ngoài — đã ghi chú trong file). Nguồn: nginx/conf.d/default.conf:1-16, README-NGINX.mdleast_conn). api-gamify-pro.conf:1-6| Nhóm | Giá trị |
|---|---|
| Timeout | proxy_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) |
| WebSocket | Dozzle, RedisInsight, OmniDB, Dashboards: proxy_http_version 1.1 + Upgrade $http_upgrade + Connection "upgrade" |
| Basic auth | app-logs, redisinsight — tạo bằng htpasswd -c ./nginx/.htpasswd-<tên> admin, mount qua nginx.yml |
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 -t → nginx -s reload, và thêm DNS record (proxied) trên Cloudflare.
Mỗi service một file compose ở root repo, chạy độc lập: docker compose -f <file>.yml up -d.
| redis-master | redis-slave | redis-dev | |
|---|---|---|---|
| File | redis-master.yml | redis-slave.yml | redis-dev.yml |
| Host | 10.0.0.2 (Server 3) | 10.0.0.3 (Server 4) | 10.0.0.3 (Server 4) |
| Port (host) | 6379 | 6379 | 6380 → 6379 |
| Replication | — | --replicaof 10.0.0.2 6379 | — |
| maxmemory | 1536mb | 1536mb | 512mb |
| Eviction | allkeys-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) | ||
| Persistence | AOF (appendonly yes) + RDB snapshot (save 900 1 / 300 10 / 60 10000) | ||
| Bảo mật | protected-mode no, bind 0.0.0.0, không password — chỉ an toàn nhờ firewall mạng nội bộ | ||
| Khác | memory limit container 1536M; healthcheck redis-cli ping/30s; log json-file 10m×3 | limit 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.
postgres-master.yml): db kiosk, user postgres (mật khẩu plaintext trong yml — không nêu ở đây), network_mode: bridge, volume postgres-master-data, init script setup-master.sh mount vào docker-entrypoint-initdb.d.archive_mode=on, copy vào /var/lib/postgresql/archive/ trong container) — hỗ trợ PITR, nhưng archive nằm cùng máy nên không chống được mất cả server.<host-slave>:5433. hot_standby_feedback=on giảm lỗi query bị hủy trên slave, đổi lại có thể giữ bloat trên master.rabbitmq.yml — image rabbitmq:3.13-management, host 10.0.0.3 (Server 4)kiosk, vhost / (mật khẩu plaintext trong yml)rabbitmq-diagnostics ping/10s; volume rabbitmq-datadocker.sock read-only, chỉ hiện container đang chạy, tail 1000 dòng) — truy cập qua log-server1…4.kioskservice.club.app-logs.kioskservice.club (basic auth ở nginx).
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:
docker.kiosk-gaming-backend.dev → chuẩn hoá lowercase, .→-, bỏ tiền tố docker-, thêm hậu tố -logs ⇒ index kiosk-gaming-backend-dev-logs-YYYY.MM.DD.var.lib.docker.containers hoặc /) → gộp chung docker-tail-logs-* (tránh mỗi container ID thành một index riêng).misc-logs. Prefix cắt tối đa 120 ký tự.docker-daemon.json (áp bằng setup-docker-logs.sh): mặc định toàn daemon json-file, max-size 10m, max-file 3, compress — mỗi container tối đa ~30MB log trên đĩa.check-logs.sh, check-dozzle.sh, check-redisinsight.sh. README khuyến nghị đặt cron 0 2 * * * chạy check-logs.sh, log giám sát ghi ra /var/log/kiosk-logs-check.log.README-LOG-MANAGEMENT.md): mỗi container ~30MB (10m×3), log nginx ~70–100MB/7 ngày — tổng ~100–150MB/server, tự xoá. Lưu ý: đổi daemon.json phải restart Docker và restart từng container mới có hiệu lực.vm.max_map_count=262144 (sysctl, persist qua /etc/sysctl.d/99-opensearch.conf) — thiếu cái này OpenSearch không lên. README-OPENSEARCH-SETUP.md §1kiosk-logging + khai báo logging driver trong compose:
networks:
kiosk-logging: { external: true, name: kiosk-logging }
services:
ten-service:
networks: [default, kiosk-logging]
logging:
driver: fluentd
options:
fluentd-address: tcp://fluent-bit:24224 # cùng host
tag: docker.ten-service.env # tag → tên index
Service ở máy khác thì trỏ tcp://10.0.0.6:24224 (cần firewall nội bộ cho phép; tuyệt đối không mở 24224 ra Internet). README-OPENSEARCH-SETUP.md §5kiosk-*-logs-*), field thời gian @timestamp.add_log_tag.lua bị Docker mount nhầm thành thư mục trên host → Fluent Bit lỗi "Is a directory"; đã đổi tên thành tag_prefix.lua. Nếu gặp lại: rm -rf fluent-bit/add_log_tag.lua rồi git pull. README-OPENSEARCH-SETUP.md §8README-OPENSEARCH-PRODUCTION.md): bật security plugin + đặt admin password qua env file không commit; retention bằng ISM hoặc cron xoá index theo ngày (hiện chưa có — disk Server 5 sẽ đầy dần); backup index bằng Snapshot API; heap giữ 512m–1g khi host còn chạy service khác, chỉ tăng 2–4g nếu OpenSearch là workload chính.curl localhost:9200/_cluster/health + _cat/indices + localhost:2020/api/v1/metrics chạy trực tiếp trên 10.0.0.6):
yellow — bình thường, không phải sự cố mới: 77 primary shard đều active, nhưng 70 unassigned vì phần lớn index khai báo number_of_replicas: 1 trong khi cluster chỉ có 1 node — OpenSearch không bao giờ đặt replica cùng node với primary, nên các shard replica này vĩnh viễn không gán được. Không mất dữ liệu, nhưng cũng không có redundancy thật; cluster sẽ không bao giờ lên green cho tới khi thêm node thứ 2 hoặc đổi index template về number_of_replicas: 0 (khuyến nghị, vì single-node thì replica hiện tại chỉ tốn disk/CPU mà không tăng độ an toàn).docker-tail-logs-2026.04.01 và kiosk-gaming-backend-dev-logs-2026.04.01 vẫn còn (hơn 3 tháng dữ liệu), khớp với cảnh báo ở trên — nên ưu tiên làm ISM/cron xoá index cũ.output.es.0 — errors:0, retries:0, retries_failed:0, dropped_records:0 trên hơn 25.8 triệu record đã xử lý — không mất log.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).
| Dev | Production | |
|---|---|---|
| Script | gamify-engine/dev/scrip-deploy-dev | gamify-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 |
| Compose | docker-compose-dev.app.yml | docker-compose.app.yml với --scale app=4 (⇒ 4 instance :7003–7006 khớp upstream nginx) |
| Quy trình | git pull → nạp lại .env (mở nano dán thủ công từ file env-*) → down → up -d --build → chạy npm run migration:run (pro thêm npm run seed) | |
| Endpoint public | api-gamify-dev.kioskservice.club → :4003 | api-gamify-pro.kioskservice.club → :7003–7006 |
down → up --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.
| Port | Service | Host | Expose public? |
|---|---|---|---|
| 80 | nginx-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–3004 | Backend API production (×4/server) | 10.0.0.5, 10.0.0.4 | Không (chỉ qua nginx) |
| 4001 | Backend API dev | 10.0.0.5, 10.0.0.4 | Không (qua dev-api) |
| 7003–7006 | Gamify production (scale 4) | 10.0.0.6 | Không (qua api-gamify-pro) |
| 4003 | Gamify dev | 10.0.0.6 | Không (qua api-gamify-dev) |
| 6379 | Redis master / slave | 10.0.0.2 / 10.0.0.3 | Không — nhưng không có auth, dựa hoàn toàn firewall |
| 6380 | Redis dev | 10.0.0.3 | Không |
| 5432 | Postgres master | 10.0.0.2 | Không |
| 5433 | Postgres slave (→5432 container) | 10.0.0.3 | Không |
| 5672 / 15672 | RabbitMQ AMQP / Management UI | 10.0.0.3 | Không (UI không qua nginx) |
| 9200 / 9600 | OpenSearch API / metrics | 10.0.0.6 | Không — security plugin tắt, dựa firewall |
| 5601 | OpenSearch Dashboards | 10.0.0.6 | Qua app-logs + basic auth |
| 24224 / 2020 | Fluent Bit forward / HTTP monitor | 10.0.0.6 | Không |
| 8080 | Dozzle (mỗi server) | 10.0.0.5/.4/.2/.3 | Qua log-server1…4 (không basic auth ở nginx) |
| 5540 | RedisInsight | 10.0.0.3 | Qua redisinsight + basic auth |
| 8000 | OmniDB | 10.0.0.3 | Qua pgadmin (auth riêng của OmniDB) |
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.
| Server | Vai trò | SSH vào | Lệnh kiểm tra chính |
|---|---|---|---|
| V1-Kiosk 10.0.0.5 | Gateway + App gw app | ssh 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 app | ssh root@178.156.166.189 |
docker ps (app :3001-3004, :4001, dozzle) · docker stats --no-stream |
| V3-Kiosk 10.0.0.2 | Data data | ssh root@178.156.211.239 |
redis-cli ping · pg_isready · xem 11.3 |
| V4-Kiosk 10.0.0.3 | Tools obs | ssh root@178.156.211.230 |
docker ps (redisinsight, omnidb, dozzle) · xem 11.4 |
| kiosk-gamify-engine 10.0.0.6 | Gamify + Log hub data obs | ssh root@178.156.139.212 |
curl -s localhost:9200/_cluster/health?pretty · xem 11.5 |
# 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}}"
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.
# 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.
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)
redisinsight.kioskservice.club, basic auth) — dùng để xem trực quan Redis master/slave đang sống, số key, memory realtime, thay cho gõ lệnh redis-cli thủ công.pgadmin.kioskservice.club) — xem Postgres master :5432 và slave :5433 qua UI, chạy query kiểm tra trực tiếp.redisinsight và dozzle 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).
# 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)
_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" là 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.
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.docker exec nginx-lb nginx -t && docker ps — gateway sống, cú pháp hợp lệ?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)?docker ps đủ 4 instance API + docker stats không có container OOM/restart loop.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ệ.| Việc | Lệnh / file |
|---|---|
| Kiểm tra cú pháp nginx | docker exec nginx-lb nginx -t |
| Reload nginx (không downtime) | docker exec nginx-lb nginx -s reload |
| Xem log nginx realtime | docker logs -f nginx-lb hoặc file trong nginx/logs/ |
| Khởi động một stack | docker compose -f <tên>.yml up -d |
| Xem log container per-server | Dozzle: log-server1…4.kioskservice.club; script ./check-logs.sh |
| Log tập trung | app-logs.kioskservice.club (basic auth) |
| Tạo basic auth mới | htpasswd -c ./nginx/.htpasswd-<tên> admin rồi mount qua nginx.yml |
| Logrotate nginx | setup-nginx-logrotate.sh + nginx/logrotate.conf |
| Giới hạn log Docker daemon | setup-docker-logs.sh + docker-daemon.json |
| Deploy gamify | gamify-engine/dev/scrip-deploy-dev · gamify-engine/production/scrip-deploy-pro |
| Kiểm tra Redis replication | docker exec redis-master redis-cli info replication |
| Kiểm tra Postgres replication | trên master: SELECT * FROM pg_stat_replication; |
| Test / ép chạy logrotate nginx | sudo 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 / OmniDB | tar 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ăng | docker system df → find ./nginx/logs/ -name "*.log.*" -mtime +7 -delete (xem README-LOG-MANAGEMENT.md trước khi prune) |
| # | Rủi ro | Mức | Khuyến nghị |
|---|---|---|---|
| 1 | Cloudflare 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ình | Cà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) |
| 2 | V1-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ống | Cao | Dự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 |
| 3 | OpenSearch tắt security plugin (không TLS/auth trên :9200); bảo vệ duy nhất là basic auth ở nginx cho Dashboards | Cao | Làm theo README-OPENSEARCH-PRODUCTION.md: bật security, đặt admin password, bỏ các biến DISABLE_* |
| 4 | Redis 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ụng | Cao | Thê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) |
| 5 | Secret plaintext trong repo: mật khẩu Postgres/RabbitMQ/replication trong yml, env thật, GitLab token trong script deploy | Cao | Thu 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 |
| 6 | Không auto-failover cho Redis lẫn Postgres (chỉ replication một chiều, promote thủ công) | Trung bình | Viết runbook promote slave; cân nhắc Sentinel (Redis) khi có ≥3 node |
| 7 | Dozzle không có basic auth ở nginx — log-server1…4 lộ log container (Dozzle mặc định cũng không auth) | Trung bình | Thêm auth_basic vào dozzle.conf giống app-logs, hoặc bật auth của Dozzle |
| 8 | Deploy gamify có downtime (down → build → up) và dán .env bằng nano thủ công, dễ sai | Trung bình | Build trước rồi up; hoặc rolling update từng instance sau lưng nginx |
| 9 | WAL archive nằm cùng máy master; backup off-site không thấy trong repo | Trung bình | Bổ sung pg_dump/basebackup định kỳ đẩy ra storage ngoài |
| 10 | Upstream chưa bật keepalive; passive health check có thể trả lỗi cho vài request trước khi loại instance hỏng | Thấp | Bật keepalive 32 + proxy_http_version 1.1 tới upstream; cân nhắc retry proxy_next_upstream |
| 11 | Healthcheck 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áo | Thấp | Sử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) |
| 12 | OpenSearch 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ật | Thấ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 |
| 13 | Rule 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 443 | Cao | Thê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 |
| 14 | SSH (: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 restriction | Trung bình | Giớ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 |
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
| README | Nội dung chính đáng nhớ |
|---|---|
README-NGINX.md | 4 phương thức LB, passive health check (max_fails/fail_timeout), mẫu thêm SSL :443, troubleshooting container/backend |
README-LOG-MANAGEMENT.md | 2 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.md | Setup 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.md | Checklist 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.md | Kiế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.md | Kết nối Redis master/slave, script setup-redisinsight-auth.sh, backup volume · ⚠ chứa credential plaintext |
README-OMNIDB.md | Bắ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.md | Connection 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.