Deep Dive Kioskgaming Infrastructure
From Cloudflare to Nginx and all the backend services

Created on 2026-07-03, updated 2026-07-04 Β· Source: infrastructure reponginx/(production configuration running) + Hetzner Console (project "Kiosk VPS", Ashburn VA) + live survey of 5 servers Β· Scope: Cloudflare edge β†’ nginx gateway β†’ app servers β†’ data services β†’ log pipeline β†’ gamify engine.
Parameters extracted from real configuration file, with source referencefile:line.
Document set:πŸ“ System diagram Β· βš–οΈ Compare & Conversion plan Β· 🎯 Target system

1. Architectural overview

All public traffic of the platform passes through3 floors: Cloudflare (DNS + CDN + TLS termination) β†’ nginx-lb(an nginx container as reverse proxy / load balancer, routing accordinglyserver_name) β†’ app servers / data services / toolson the internal network10.0.0.0/24. All components run Docker, each service has a filedocker-composeonly in the root repo.

User / Client apps
Portals, Kiosk, Admin, Agent… β€” HTTPS :443
HTTPS β€” TLS ends at Cloudflare
Cloudflare β€” DNS Β· CDN Β· WAF (SSL mode: Flexible)
All domains *.kioskservice.club proxy via Cloudflare Β· attach CF-Connecting-IP / CF-Ray / CF-Visitor / X-Forwarded-Proto
15 Cloudflare IP ranges trusted by nginxset_real_ip_from
HTTP :80 origin-pull β€” no TLS to origin
nginx-lb β€” nginx:alpine Β· network_mode: host
Routing byserver_name (10 domain) Β· upstream least_conn Β· passive health check (max_fails=3 / 30s)
Runa single instance on V1-Kiosk (178.156.194.24)β€” SPOF of the entire system
Firewall Hetzner (firewall-ssh, project only has 1 firewall) blocks :80 before nginx β€” but only whitelists 5/15 Cloudflare bands, see card ⑦ below
Routing by domain β†’ 5 internal servers (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
Figure 1 β€” Overall architecture: Cloudflare β†’ nginx-lb β†’ 5 internal servers. Redis/Postgres replicate 10.0.0.2 β†’ 10.0.0.3; log container β†’ Fluent Bit (10.0.0.6:24224) β†’ OpenSearch, see sections 7–8.

Nginx configuration illustrated step by step

The actual configuration segments correspond to each arrow in the diagram above β€” for complete details see sections 3–5.

β‘  Cloudflare trust β€” realipdefault.conf:28–43
set_real_ip_from 173.245.48.0/20;
… (15 Cloudflare CIDR bands) …
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
Restore the client's real IP because all requests to the origin come from Cloudflare IP.
β‘‘ 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 instances: 4 on V1 + 4 on 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 services stream realtime data via WS β€” without these 3 lines, the UI freezes even though nginx is still 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;
Connection ceiling β‰ˆ number of cores Γ— 1024. V1 (CCX23) = 4 cores; V2/V4 (CCX13) = 2 cores.
β‘€ Container nginx-lbnginx.yml:1–15
image: nginx:alpine
restart: always
network_mode: host
network_mode: hostAllows calling 10.0.0.x directly, without needing a separate Docker network.
β‘₯ Hetzner Cloud FirewallConsole, applies to 5/5 servers
22/tcp ← Any IPv4/IPv6 (unlimited)
80/tcp ← 5/15 Cloudflare strips (10 strips missing!)
443/tcp ← 15/15 ranges (nginx does not listen to 443)
Network filtering layer BEFORE nginx β€” different fromset_real_ip_fromat port 80, see risks #13–14 section 13.

The key point to grasp in 60 seconds

2. List of VPS

Source:iplist, comment in nginx/conf.d/*.conf, compose files; server name/plan/IP server 5:Hetzner Console(project "Kiosk VPS", region Ashburn VA / us-east, screenshot 2026-07-03). The role of each server is derived from the internal IP that nginx points to and commented in the conf.

Server (name Hetzner)Plan Β· vCPU/RAM* Β· DiskIPv4 publicIPv6Internal IPRoleService (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β€” (not in 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 according to Hetzner's CCX (dedicated vCPU) plan catalog; disk according to the value displayed in the console. Server created 4–9 months ago (V1 the longest).iplistServer 5 is missing in the repo β€” should be added.

Compare spec ↔ configuration in repo:memory limit of services matches V3's 8GB RAM (CCX13): Redis limit 1536MB + Postgresshared_buffers=1GB / effective_cache_size=3GB+ operating system β‰ˆ fit β€” ieThere isn't much headroom left on the data server; If you add a service to V3, you have to recalculate. OpenSearch heap 1g + gamify 4 instances sit comfortably in 16GB of Server 5. V4 (also CCX13, 8GB) piggybacks7 containerat the same time β€” Redis slave, Redis dev, Postgres slave, RabbitMQ, RedisInsight, OmniDB, Dozzle β€” much denser than the "Tools" role initially thought to be light; should be the next candidate to control the total memory limit, similar to V3.

3. Cloudflare tier (edge)edge

3.1. SSL mode: Flexible

Entire domain*.kioskservice.clubproxy via Cloudflare with SSL modeFlexible:

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

3.2. Restore the client's real IP

Because every request to the origin comes from Cloudflare's IP, nginx uses modulerealip: each server block declared15 Cloudflare IP ranges via set_real_ip_fromand real_ip_header CF-Connecting-IPβ€” meaning: if request comes from Cloudflare IP range, get header value CF-Connecting-IPdo$remote_addr. Thanks to that, the log and rate-limit behind see the correct user IP. Source:nginx/conf.d/default.conf:28-43(repeat in every conf file)

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. The Cloudflare header is forwarded to the backend

HeaderMeaningnginx forward equals
CF-Connecting-IPReal IP of the clientproxy_set_header CF-Connecting-IP $http_cf_connecting_ip
CF-RayRequest ID on Cloudflare network (debug/trace with CF support)proxy_set_header CF-Ray $http_cf_ray
CF-VisitorJSON {"scheme":"https"}β€” client-side schemeproxy_set_header CF-Visitor $http_cf_visitor
X-Forwarded-ProtoOriginal scheme (https)$http_x_forwarded_proto(priority attached CF value)
X-Real-IP / X-Forwarded-ForClient IP after realip has been restored$remote_addr / $proxy_add_x_forwarded_for

3.4. Architectural consequences of Flexible mode

3.5. DNS record model (from README in repo)

The README guides DNS mapping according to the following principles:Each domain has an A record (Proxied) pointing directly to the public IP of the target server- For examplelog-server1 β†’ IP public Server 1, log-server2 β†’ IP public Server 2, redisinsight/pgadmin β†’ IP public Server 4. Source:README-DOZZLE.md(section "Map Domain in Cloudflare"),README-REDISINSIGHT.md, README-OMNIDB.md.

Actual implementation:nginx-lb runsa single instance on V1-Kiosk (178.156.194.24). The per-server "Map Domain" instructions in the README areoutdated. Consequences: (1) all proxy's DNS records*.kioskservice.clubmust point back178.156.194.24; (2) V1 is the convergence point of all traffic: gateway + 4 API prod instances + API dev on the same CCX23 machine; (3) blocking layer :80 from IP outside Cloudflare is Hetzner Firewallfirewall-ssh(applies to all 5 servers) β€” rule appearsMissing 10/15 Cloudflare bandsvs nginx trust list, see risk #13 section 13.

4. Nginx gateway (nginx-lb) gateway

4.1. Deploy containers

Image
nginx:alpine
Container
nginx-lb, restart: always
Network
network_mode: hostβ€” to call internal IPs directly10.0.0.xnot through 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)

Source:nginx.yml:1-15. Original repo on GitLab namekiosk-deployments; The deploy directory on the server iskiosk_deploy/kiosk_deployments(inferred from the volume name prefix in the backup commands of the README).Host: V1-Kiosk (178.156.194.24 / 10.0.0.5); see section 3.5.

4.2. Core configuration (nginx/nginx.conf)

ParameterValueNote
worker_processesauto= number of CPU cores
worker_connections1024Concurrent connection ceiling β‰ˆ workers Γ— 1024 (each proxy request costs 2 connections)
keepalive_timeout65sClient side (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"

Source:nginx/nginx.conf:1-37. Note: not enabled yetkeepaliveto upstream (linekeepalive 32commenting indefault.conf:15) β€” each request creates a new TCP connection to the backend.

4.3. Log & logrotate

4.4. Configuration change process (required)

# 1. Edit the file in nginx/conf.d/ (local repo is just a place to edit β€” must be synchronized to the server)
#2. Check syntax BEFORE reloading:
docker exec nginx-lb nginx -t
#3. Reload without downtime:
docker exec nginx-lb nginx -s reload

5. Domain routing & upstream

5.1. Full routing table

Domain (*.kioskservice.club)File confDestination/UpstreamLBSpecific
apidefault.confupstream backend: 10.0.0.5:3001–3004 + 10.0.0.4:3001–3004 (8 instance)least_connMain production API; buffering on 4k/8Γ—4k; timeout 60s
dev-apidev-api.confupstream dev-api-backend: 10.0.0.5:4001 + 10.0.0.4:4001least_conndev environment API
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:4003β€”1 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 β€” :8080β€”Dozzle per-server; WebSocket; no basic auth in nginx
redisinsightredisinsight.conf10.0.0.3:5540β€”Basic 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 β€” analysis

upstream backend {
least_conn;                                    # choose the server with the least connection
    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. Common proxy parameters of all server blocks

GroupValue
Timeoutproxy_connect/send/read_timeout 60s(app-logs only: send/read 300s, connect 75s β€” query Dashboards take a long time)
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 β€” create withhtpasswd -c ./nginx/.htpasswd-<name> admin. admin, mount via nginx.yml
Sample to add new domain: copy dev-api.conf(simplest file) β†’ changeserver_name, upstream, log path. Required to keep: block of 15 stripsset_real_ip_from + real_ip_header CF-Connecting-IP, setproxy_set_headerstandard (espX-Forwarded-Proto $http_x_forwarded_protobecause of Flexible mode). If a service has WebSocket, there are 3 additional upgrade lines. Afterwardnginx -t β†’ nginx -s reload, and add DNS record (proxied) on Cloudflare.

6. Sample request flow (API production)

Client Cloudflare nginx-lb :80 Backend (vd 10.0.0.5:3002) 1. HTTPS GET api.kioskservice.club/v1/... TLS handshake with Cloudflare cert 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 selects 1/8 instances Host, X-Real-IP, X-Forwarded-For/Proto 4. Response 5. gzip (level 6), buffer 4k/8Γ—4k 6. HTTPS + cache/CDN Cloudflare (if applicable)
Figure 2 β€” Lifecycle of a production API request. If the instance fails 3 times in 30 seconds, nginx automatically removes itself from the pool (passive health check).

7. Data services data

Each service has a compose file in the root repo, running independently: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β€” If RAM is full, expel the least used key (accept key loss β‡’ Redis acts as a cache/session, not the main data source)
PersistenceAOF (appendonly yes) + RDB snapshot (save 900 1 / 300 10 / 60 10000)
Securityprotected-mode no, bind 0.0.0.0, no passwordβ€” only safe thanks to the internal network firewall
Othermemory limit container 1536M; healthcheck redis-cli ping/30s; log json-file 10mΓ—3limit 512M; volume redis-dev-data

Source:redis-master.yml, redis-slave.yml, redis-dev.yml; Host slave confirms passredis-cli info replication (slave0:ip=10.0.0.3,state=online,lag=0). One-way replication master→slave; repo without Sentinel/Cluster ⇒No automatic failover— slave is for manual read/standby only.

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 (create user replicator) WAL streaming user: replicator postgres-slave β€” 10.0.0.3 (Server 4) postgres:16 Β· publish 5433 β†’ 5432 container hot_standby = on (allow query to read) primary_conninfo: host=10.0.0.2 port=5432 max_standby_streaming_delay = 30s wal_receiver_status_interval = 10s hot_standby_feedback = on resources: same as master (buffers/cache/mem) application_name = postgres-slave
Figure 3 β€” Postgres 16 streaming replication (async). Slave for reading (hot standby); failover is a manual operation.

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 β€” exposed via nginx not visible, direct access/SSH tunnel)
Account
user kiosk, vhost /(plaintext password in yml)
Limit
memory 1024M; healthcheck rabbitmq-diagnostics ping/10s; volume rabbitmq-data

8. Log & Observability observability

8.1. 2-storey architecture

  1. Xem nhanh per-server:Each server runs 1 containerDozzle(:8080, host network, readdocker.sockread-only, only currently running container, tail 1000 lines) β€” accessed vialog-server1…4.kioskservice.club.
  2. Concentrate:Fluent Bit on 10.0.0.6 log collection β†’ OpenSearch β†’ view with OpenSearch Dashboards atapp-logs.kioskservice.club(basic auth in nginx).
Other container servers logging driver: fluentd β†’ send Forward protocol to :24224 Containers on 10.0.0.6 json-file of 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 OFF (no TLS/auth) volume opensearch-data OpenSearch Dashboards :5601 Search UI / dashboard log nginx-lb + basic auth app-logs.kioskservice.club Dev / Ops via Cloudflare HTTPS Parallel: Dozzle :8080 on each server (log-server1…4.kioskservice.club) to tail log container directly, without going through this pipeline.
Figure 4 β€” Centralized log pipeline: 2 Fluent Bit inputs β†’ OpenSearch β†’ Dashboards, protected by basic auth at nginx.

8.2. Index naming rules (Lua filter)

fluent-bit/tag_prefix.lua(jawappend_fluentbit_tag) assign 2 fields to each record:docker_log_tag(original tag) andlogstash_prefix. Rules for converting tag β†’ prefix:

8.3. Prevent log bloat at the Docker layer

8.4. Details of setup & Limitations (summarized from READMEs)

Actual state (curl localhost:9200/_cluster/health + _cat/indices + localhost:2020/api/v1/metricsrun directly on 10.0.0.6):

9. Gamification engine

Directorygamify-engine/contains script + env deploy gamification backend on server 10.0.0.6 (code is in separate GitLab repokiosk_gamification_engine, not in the nginx repo).

DevProduction
Scriptgamify-engine/dev/scrip-deploy-devgamify-engine/production/scrip-deploy-pro
Directory on the server/var/www/dev/gamify-be/backend/deployments/var/www/production/kiosk_gamification_engine/backend/deployments
Composedocker-compose-dev.app.ymldocker-compose.app.ymlwith--scale app=4(β‡’ 4 instances :7003–7006 match upstream nginx)
Proceduregit pull → reload.env(open nano and paste it manually from env-* file) →down → up -d --build→ runnpm run migration:run(more pronpm run seed)
Endpoint publicapi-gamify-dev.kioskservice.club β†’ :4003api-gamify-pro.kioskservice.club β†’ :7003–7006
⚠ Deploy has downtime and contains secrets:proceduredown β†’ up --buildStops the entire instance during build (no rolling). Fileenv-dev/env-productionis the real env, andscrip-deploy-procontainGitLab access token embedded in the git URLβ€” this token should be revoked/rotated and converted to deploy key; Absolutely do not copy these files out.

10. General port table

PortServiceHostExpose public?
80nginx-lb (all domains)10.0.0.5 (V1-Kiosk)Yes β€” via Cloudflare; IPs outside the Cloudflare range are blocked by Hetzner Firewall
3001–3004Backend API production (Γ—4/server)10.0.0.5, 10.0.0.4No (only via nginx)
4001Backend API dev10.0.0.5, 10.0.0.4No (via dev-api)
7003–7006Gamify production (scale 4)10.0.0.6No (via api-gamify-pro)
4003Gamify dev10.0.0.6No (via api-gamify-dev)
6379Redis master / slave10.0.0.2 / 10.0.0.3No β€” but no auth, completely firewall based
6380Redis dev10.0.0.3Are not
5432Postgres master10.0.0.2Are not
5433Postgres slave (β†’5432 container)10.0.0.3Are not
5672 / 15672RabbitMQ AMQP / Management UI10.0.0.3No (UI not via nginx)
9200 / 9600OpenSearch API / metrics10.0.0.6No β€” security plugin disabled, firewall based
5601OpenSearch Dashboards10.0.0.6Via app-logs + basic auth
24224 / 2020Fluent Bit forward / HTTP monitor10.0.0.6Are not
8080Dozzle (per server)10.0.0.5/.4/.2/.3Via log-server1…4 (no basic auth in nginx)
5540RedisInsight10.0.0.3Via redisinsight + basic auth
8000OmniDB10.0.0.3Via pgadmin (OmniDB's own auth)

11. Infrastructure check playbook (health-check)

This section is combinedHow to verify whether each floor is alive or dead, going from outside (Cloudflare) to inside (data server) β€” Use when suspecting a problem, before/after reloading nginx, or periodically checking. Rule: always checkfrom the layer closest to the user first (public domain) and then go deeper into the internal server, to quickly localize which layer is failing.

11.1. Quick lookup table by server

ServerRoleSSH inMain test command
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 Β· see 11.3
V4-Kiosk
10.0.0.3
Tools obsssh root@178.156.211.230 docker ps (redisinsight, omnidb, dozzle) Β· see 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 Β· see 11.5

11.2. Layer 1 β€” Edge & gateway (from any machine, no SSH required)

# Does the domain respond via Cloudflare (note the cf-ray header = via Cloudflare)
curl -sI https://api.kioskservice.club | grep -i "cf-ray\|http"

# Is the current nginx syntax valid (running ON V1, before reload)
docker exec nginx-lb nginx -t

# Is nginx-lb running, which containers have restarted recently (sign of crash loop)
docker ps --filter name=nginx-lb --format "table {{.Names}}\t{{.Status}}"
Read the results:has headerscf-ray⇒ the request went through Cloudflare to the origin and got a response — the error (if any) is in nginx/backend, not DNS/Cloudflare. Do not havecf-rayor timeout ⇒ check DNS record / Cloudflare status before suspecting the server.

Latest run results on V1 and V2: bothnginx -tOK (V1, gateway); 4/4 API prod instances (:3001–3004) + 1 dev (:4001) allhealthyon each server; Low CPU/RAM, lots of headroom on both (~90-100MB/container / 8GB RAM). Privatedozzle unhealthyon both β€” see 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 matches slave number pointing to 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;"
# each return line = 1 slave replicating; "streaming" state is fine, lack of stream β‡’ slave lost connection

Actual container name dependscontainer_namedeclared inredis-master.yml / postgres-master.yml- usedocker psto confirm if the above command says "No such container".

Latest run result: RedisPONG, replica online lag=0; Postgres pg_isreadyOK, replica streaming. The server runs exactly 3 containers (postgres-master, redis-master, dozzle). Privatedozzle unhealthy β€” see 11.4.

11.4. Layer 2 β€” Tools server (V4-Kiosk, 10.0.0.3)

V4 runs7 container: postgres-slave, redis-slave, redis-dev, redisinsight, omnidb, dozzle, rabbitmq.

docker ps --format "table {{.Names}}\t{{.Status}}"
# expect all 7 containers "Up"; Notice the STATUS column has "(healthy)" / "(unhealthy)" / nothing (container does not declare healthcheck)
redisinsightanddozzleon V4 newspaperunhealthybut the service still works normally β€” the image's default healthcheck is wrong (all 4 Dozzle V1–V4 containers have the same symptoms). Cleaning: fix or turn off healthcheck to eliminate false alarms (risk #11, item 13).

11.5. Layer 2 β€” Gamify + Log hub (10.0.0.6)

# OpenSearch cluster (runs on the 10.0.0.6 server itself, or via SSH tunnel)
curl -s localhost:9200/_cluster/health?pretty          # status: green/yellow/red
curl -s localhost:9200/_cat/indices?v # index list, detect abnormal index swelling

# Fluent Bit β€” is receiving/pushing logs
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}}" # enough 4 instances :7003-7006 (pro) / 1 instance :4003 (dev)
OpenSearch status "red"or_cat/indicesshows that the index is continuously increasing without cleaning up β‡’ correct risk #3 noted in item 13 (no retention/ISM) β€” Server 5 disk will gradually fill up, not a new configuration error. Status"yellow"To bestable state of this cluster, not a crashβ€” detailed explanation in section 8.4 (single-node + replica=1 can never be fully assigned).

Latest run results: V5 runs exactly 8 containers β€” gamifydeployments_app_1..4 (prod) + deployments_app-dev_1(dev, enough 5healthy, match scale compose item 9),opensearch (healthy), opensearch-dashboards, fluent-bit. For details on OpenSearch/Fluent Bit, see section 8.4.

11.6. End-to-end testing (quick checklist when a problem is suspected)

  1. curl -sI https://api.kioskservice.club- Havecf-ray+ status 2xx/3xx? None β‡’ stops at DNS/Cloudflare, no need to SSH to any server.
  2. SSH V1 β†’ docker exec nginx-lb nginx -t && docker psβ€” gateway alive, valid syntax?
  3. docker logs --tail 100 nginx-lbβ€” at faultconnect() failedto the backend (points to which app server is dead)?
  4. SSH into the suspected app server (V1/V2) β†’docker psenough 4 API instances + docker stats without OOM container/restart loop.
  5. If API returns error regarding data β†’ SSH V3, run 11.3 (Redis ping, Postgres pg_isready).
  6. Need detailed log for further debugging β†’ Dozzle (log-server1…4) for instant logs, or Dashboards (app-logs) to look up by time/keyword throughout the system.

12. Common operations

JobCommand/files
Check nginx syntaxdocker exec nginx-lb nginx -t
Reload nginx (no downtime)docker exec nginx-lb nginx -s reload
Xem log nginx realtimedocker logs -f nginx-lbor files innginx/logs/
Start a stackdocker compose -f <name>.yml up -d
Xem log container per-serverDozzle: log-server1…4.kioskservice.club; script ./check-logs.sh
Centralized loggingapp-logs.kioskservice.club (basic auth)
Create new basic authhtpasswd -c ./nginx/.htpasswd-<name> admin. adminthen mount itnginx.yml
Logrotate nginxsetup-nginx-logrotate.sh + nginx/logrotate.conf
Docker daemon log limitsetup-docker-logs.sh + docker-daemon.json
Deploy gamifygamify-engine/dev/scrip-deploy-dev Β· gamify-engine/production/scrip-deploy-pro
Check Redis replicationdocker exec redis-master redis-cli info replication
Check Postgres replicationon master:SELECT * FROM pg_stat_replication;
Test / force run logrotate nginxsudo logrotate -d /etc/logrotate.d/nginx-kiosk (dry-run) Β· -f(force running)
Create OmniDB admin (first time / forgot password)docker exec -it omnidb python omnidb-server.py --createsuperuser=admin '<password>'β€” Passwords with special characters must be enclosed in single quotes
Backup settings RedisInsight / OmniDBtar volume redisinsight-data / omnidb-datavia alpine container (sample commands in README of each tool)
Manually clear logs when disk is stresseddocker system df β†’ find ./nginx/logs/ -name "*.log.*" -mtime +7 -delete (see README-LOG-MANAGEMENT.mdbefore prune)

13. Risks & recommended

#RiskLevelRecommended
1Cloudflare Flexible:CFβ†’origin leg goes plain HTTP over the Internet β€” data (including tokens/credentials) is not encrypted between the Cloudflare PoP and the server. Hetzner Firewall has limited the input source to :80 so it is difficult to bypass the origin, but cannot change the encryptionMediumInstall Cloudflare Origin CA cert + nginx listen 443 β†’ switch to Full (strict) to encrypt the origin leg (Edge L0–L1 category of DDoS Resilience plan)
2V1-Kiosk is the heaviest SPOF system:single-handedly carrying nginx-lb (gateway for 10 domains) + 4 API prod instances + API dev. V1 death = ENTIRE system loss even though V2–V5 are still aliveHighBuild second nginx-lb (switch plan chooses V4 because failure-domain is better β€” seeconversion planstep 1A) + Cloudflare 2nd LB/A record to failover origin
3OpenSearch disables security plugin(no TLS/auth above :9200); The only protection is basic auth in nginx for DashboardsHighFollowREADME-OPENSEARCH-PRODUCTION.md: enable security, set admin password, remove DISABLE_* variables
4Redis without password, protected-mode no, bind 0.0.0.0 β€” completely firewall dependent security (confirmedfirewall-sshis the only firewall in the project, there is no open rule :6379 β‡’ default-deny from the Internet) but no real authentication at the application layerHighMorerequirepass/masterauthto not be 100% dependent on a single firewall layer (in case the rule is mistakenly edited later)
5Secret plaintext in repo:Postgres/RabbitMQ/replication password in yml, real env, GitLab token in deploy scriptHighRevoke/rotate GitLab tokens immediately; move secret to .env without commit / secret manager; Minimum private repo guaranteed
6No auto-failoverfor Redis and Postgres (only one-way replication, manual promotion)MediumWrite runbook promote slave; Consider Sentinel (Redis) when there are β‰₯3 nodes
7Dozzle does not have basic auth in nginxβ€” log-server1…4 exposes log container (Dozzle does not auth by default)MediumMoreauth_basicenterdozzle.conflike app-logs, or enable Dozzle auth
8Deploy gamify has downtime(down β†’ build β†’ up) and paste .env with nano manually, easy to make mistakesMediumBuild first then upgrade; or rolling update each instance behind nginx
9WAL archive is located on the same master machine; Off-site backup not found in repoMediumAdd pg_dump/basebackup to periodically push to external storage
10Upstream has not enabled keepalive; Passive health checks can return errors for several requests before eliminating a failed instanceShortTurn onkeepalive 32 + proxy_http_version 1.1to upstream; Consider retryingproxy_next_upstream
11Fake healthcheck on monitoring container:all 4dozzle(V1–V4) andredisinsight(V4) newspaperunhealthyeven though the service works normally β€” the image's default healthcheck is wrong; Consequently, STATUS column ofdocker psLoss of warning valueShortEdit or disable healthcheck indozzle.yml/redisinsight.ymltounhealthyFrom now on it means real crash (no need to restart).
12OpenSearch cluster permanentlyyellow(70/147 unassigned shards) because the index is usednumber_of_replicas: 1There is only 1 node on the cluster β€” replicas can never be assigned, it only consumes more disk/CPU without real redundancyShortChange the default index template backnumber_of_replicas: 0for single-node (nothing lost because the replica is currently useless); only return to rep=1 if a 2nd OpenSearch node is added
13Rule TCP :80 on Hetzner Firewall (firewall-ssh, applies to all 5 servers) only whitelist 5/15 Cloudflare bandsβ€” missing 10 bands that nginx still trustsset_real_ip_from. Real requests from Cloudflare PoP that are within 10 missing ranges will be DROP by Hetzner Firewall before reaching nginx, causing intermittent connection loss that is difficult to reproduce (depending on Cloudflare routing). Rule :443 is enough to get 15/15 even if nginx doesn'tlisten 443HighAdd all 15 Cloudflare bands to the :80 rule in Hetzner Console, compare exactly withset_real_ip_from in default.conf
14SSH (:22) is open to the entire Internet(Any IPv4 + Any IPv6, no source limit) on the same firewall for all 5 servers β€” security based only on key-based auth, no network-level restriction layerMediumLimit rule :22 source to specific admin/VPN IP range in Hetzner Console if available; If you don't have a fixed IP, consider your own bastion/VPN

14. Appendix

14.1. Repo structurenginx/

nginx/ # infrastructure repo (infra/ops), does NOT contain app code
                                # original GitLab name: kiosk-deployments (dustin.le)
β”œβ”€β”€ nginx.yml                   # compose container nginx-lb
β”œβ”€β”€ nginx/
β”‚ β”œβ”€β”€ nginx.conf # core configuration
β”‚ β”œβ”€β”€ conf.d/ # 8 conf files ↔ 10 domains (section 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 # operation script
β”œβ”€β”€ docker-daemon.json # limit Docker logs daemon-wide
β”œβ”€β”€ iplist                      # IP public 4 server
β”œβ”€β”€ env, env-dev # ⚠ real env contains secret
└── README-*.md # documents each component

14.2. References in the repo

READMEMemorable key content
README-NGINX.md4 LB methods, passive health check (max_fails/fail_timeout), additional SSL form :443, troubleshooting container/backend
README-LOG-MANAGEMENT.md2 rotation layers (daemon.json + logrotate), expected capacity ~100–150MB/server, cron monitoring at 2am, manual cleanup command; To change daemon.json, you must restart Docker + container
README-OPENSEARCH-SETUP.mdSetup copy-paste: sysctl vm.max_map_count, network kiosk-logging, other service ways to send logs (fluentd driver + tag), common problem table (including Lua "Is a directory" service)
README-OPENSEARCH-PRODUCTION.mdChecklist go-live: enable security plugin, heap by host RAM, backup Snapshot API, retention ISM β€”These items are NOT currently available in production
README-DOZZLE.mdDozzle per-server architecture; Instructions for mapping DNS domain β†’ destination public server IP (based on section 3.5); The README itself also recommends adding basic auth (risk #7)
README-REDISINSIGHT.mdConnect Redis master/slave, scriptsetup-redisinsight-auth.sh, backup volume · ⚠ contains credential plaintext
README-OMNIDB.mdRequired to create superuser for the first time (--createsuperuser, password wrapped in single quote), connection PG master :5432 / slave :5433, backup volume · ⚠ contains credential plaintext
README-RABBITMQ.mdConnection string AMQP + Management UI · ⚠ contains credential plaintext; the GitLab boilerplate reveals the original repo namekiosk-deployments

Documents created from repo/Users/nhim/dev/2026/kiosk/nginx+ Direct survey of 5 servers, updated 2026-07-04. Passwords/tokens in configuration files are intentionally omitted. Blueprint diagram:system-diagram.htmlΒ· Risk handling plan section 13:comparison-and-migration-plan.htmlΒ· target architecture:target-system.html.