Zalo-TG Dev & Release Runbook¶
When to use: Develop new features, test on DEV (LucNguyen account), promote to PROD (CaoNguyen account on unraid), or triage bridge issues.
Estimated time: 2 min for DEV iteration, 5–10 min for PROD release (mostly Komodo Build time).
Source of truth: stacks/applications/zalo-tg/ in this repo (monorepo pattern, like openclaw, chat-adapter, nanobot).
Architecture recap¶
graph TD
SRC["Source (this repo)
stacks/applications/zalo-tg/src/ ← TypeScript
stacks/applications/zalo-tg/Dockerfile ← Multi-stage Node 20"]
SRC -->|git push origin main| BUILD["Komodo Build (auto-trigger on push)
linked_repo=docker-compose
Tags: :latest + :version-N + :commit-sha
→ Gitea: 100.68.251.84:3005/lucndm/zalo-tg"]
BUILD --> DEV["DEV (local dev machine)
Account: LucNguyen · Container: zalo-tg
TG group: -1003869533735
Compose: docker-compose.dev.yml
Secrets: .env (gitignored) · manual build"]
BUILD --> PROD["PROD (unraid via Komodo)
Account: CaoNguyen · Container: zalo-caonguyen
Compose: compose.yaml
Stack TOML: komodo/stacks/applications/zalo-tg.toml
auto_update=true"]
Two environments¶
| DEV (local) | PROD (unraid) | |
|---|---|---|
| Container name | zalo-tg |
zalo-caonguyen |
| Zalo account | LucNguyen | CaoNguyen |
| Compose file | docker-compose.dev.yml |
compose.yaml |
| Secrets | .env (gitignored) |
Komodo secrets (encrypted) |
| Data volume | zalo-tg-data named volume |
/mnt/user/appdata/zalo-tg/caonguyen bind |
| Deploy trigger | Manual (./docker-local.sh up) |
Auto on push (Komodo Stack auto_update=true) |
| Build source | Local docker build . |
Komodo Build (linked_repo=docker-compose) |
Scenario 1 — Iterate on DEV (LucNguyen, local)¶
When: Editing code, want fast feedback loop. No push needed.
cd /workspaces/github.com/minhluc-info/docker-compose/stacks/applications/zalo-tg
# Edit src/*.ts...
# Typecheck
docker run --rm -v "$PWD":/app -w /app node:20-alpine npm run build
# Build + recreate DEV container
./docker-local.sh up
# Watch boot (expect <2s, zero code 221)
docker logs -f zalo-tg
Verify: Send a test message from Zalo LucNguyen → check it appears in DEV Telegram group (-1003869533735).
Useful commands:
./docker-local.sh logs # follow logs
./docker-local.sh restart # restart container
./docker-local.sh down # stop
./docker-local.sh shell # exec sh into container
Scenario 2 — Promote to PROD (CaoNguyen, unraid)¶
When: DEV verified, ready to ship. Single push triggers full pipeline.
cd /workspaces/github.com/minhluc-info/docker-compose
# Stage + commit changes
git add stacks/applications/zalo-tg/
git commit -m "fix(zalo-tg): <description>"
git push origin main
What happens next (automatic):
- Komodo Sync detects push → updates repo on builder
- Komodo Build
zalo-tgtriggers → builds image → pushes:latest+ version tags - Komodo Stack
zalo-tg(auto_update=true) detects new digest → pulls on unraid → recreates container
Verify PROD:
# Watch build progress
km list actions -n zalo-tg
# Watch prod boot
ssh root@100.68.251.84 'docker logs -f --since 2m zalo-caonguyen'
Expect: [Boot] Zalo listener started ✓ within 2s of container start, zero code 221 rate-limit lines.
Pre-prod checklist (self-verify before push):
-
npm run buildpasses with 0 TypeScript errors - DEV container ran new code ≥30 min without errors
- No
Unhandled rejectionin DEV logs - Bridge 2-way works (Zalo→TG + TG→Zalo) on DEV
- All commands respond (
/login,/topic,/stopfollow,/follow)
Scenario 3 — Rollback PROD¶
When: Prod broken after release. Need to revert fast.
Method 1: Komodo UI (preferred)¶
- Open http://100.126.172.96:9120/builds → select
zalo-tg - Find last known-good Build version (check
Createdcolumn) - Note the
:version-Nor:commit-shatag
Method 2: CLI (re-tag previous version)¶
# List recent image tags on Gitea registry
TOKEN="$GITEA_ACCESS_TOKEN"
curl -s -H "Authorization: token $TOKEN" \
"http://100.68.251.84:3005/v2/lucndm/zalo-tg/tags/list" \
| python3 -m json.tool
# Re-tag previous version as :latest, push, redeploy
docker pull 100.68.251.84:3005/lucndm/zalo-tg:<previous-version>
docker tag 100.68.251.84:3005/lucndm/zalo-tg:<previous-version> \
100.68.251.84:3005/lucndm/zalo-tg:latest
docker push 100.68.251.84:3005/lucndm/zalo-tg:latest
# Force Komodo Stack to pull + recreate
km x -y stack zalo-tg
Method 3: Git revert + push¶
git revert <bad-commit-sha>
git push origin main
# Komodo Build auto-triggers on revert commit → new image
Scenario 4 — DEV container won't boot¶
Symptom: ./docker-local.sh up succeeds but container exits/crashes.
Triage¶
| Boot log line | Likely cause | Fix |
|---|---|---|
Missing required environment variable: TG_TOKEN |
.env missing or empty |
cp .env.example .env, fill TG_TOKEN + TG_GROUP_ID |
Chưa có file credentials.json |
LucNguyen not logged in | Send /login in DEV Telegram group, scan QR with LucNguyen Zalo app |
| TypeScript compile error | Bad code | docker run --rm -v "$PWD":/app -w /app node:20-alpine npm run build to see errors |
EACCES on /app/data/* |
Volume permission mismatch | docker volume rm zalo-tg_zalo-tg-data (destroys data!) |
| Container starts, immediate exit | Check docker logs zalo-tg for stack trace |
Patch + rebuild |
Reset DEV completely (lose history!)¶
./docker-local.sh down
docker volume rm zalo-tg_zalo-tg-data
./docker-local.sh up
# Then /login in DEV TG group to re-auth LucNguyen
Scenario 5 — PROD silent (no messages arriving)¶
Symptom: PROD Telegram group hasn't received Zalo forwards in >10 min, but Zalo app on phone shows new messages.
Triage in 4 steps¶
# Step 1: Container alive?
ssh root@100.68.251.84 'docker ps --filter name=zalo-caonguyen'
# Step 2: Recent logs
ssh root@100.68.251.84 'docker logs --since 5m zalo-caonguyen'
# Step 3: Session conflict? (multiple bridges on same account)
ssh root@100.68.251.84 'docker inspect zalo-caonguyen --format "{{.State.StartedAt}}"'
# Compare against local LucNguyen container — different Zalo accounts, should NOT conflict
# Step 4: Zalo API healthy? Try login flow
# Send /login in PROD TG group → bot should reply with QR image
Common causes¶
| Symptom | Cause | Fix |
|---|---|---|
Boot log shows Zalo listener started ✓ but no [Zalo→TG] lines |
Zalo server-side throttle (from past rate-limit storm) | Wait 1-2h, or /login to refresh session |
[Boot] Zalo bị ngắt do đăng nhập trùng phiên |
Same CaoNguyen logged in elsewhere | Close Zalo Web/PC on other devices, then /login |
[Boot] Zalo đã ngắt phiên bridge (kick connection) |
Zalo kicked the bridge session | /login in PROD TG group |
code 221 storm in logs |
Zalo API rate-limit | Auto-handled by rate-limiter + negative cache (wait) |
| Image pull fails on unraid | Gitea registry unreachable | km x -y restart-stack gitea first, then km x -y stack zalo-tg |
Scenario 6 — Sync upstream williamcachamwri/zalo-tg¶
When: Original project published new features/fixes you want to port.
Old archived Gitea repo (lucndm/zalo-tg on git.minhluc.info) serves as upstream tracker:
cd /workspaces/github.com/minhluc-info/zalo-tg # old archived workspace
git fetch upstream # williamcachamwri/zalo-tg
git log upstream/main --oneline -20 # see what changed
# For each meaningful commit:
git show <sha> # read diff
# Manually port to docker-compose/stacks/applications/zalo-tg/src/
# Test on DEV (Scenario 1), then promote (Scenario 2)
Upstream changes are usually small (project is mature). Manual porting keeps git history clean and avoids merge conflicts with local modifications (rate-limit fix, /stopfollow command, SQLite cache).
Daily operations¶
View PROD logs (last 5 min)¶
Force /login on PROD (credentials expired)¶
- Send
/logincommand in PROD Telegram group - Bot replies with QR image
- Open CaoNguyen Zalo app on phone → Settings → QR Login → Scan
- New
credentials.jsonsaved to/mnt/user/appdata/zalo-tg/caonguyen/credentials.json
Restart PROD without code change¶
Check Komodo Build status¶
Trigger Komodo Build manually (skip auto-trigger)¶
List image tags on Gitea registry¶
TOKEN="$GITEA_ACCESS_TOKEN"
curl -s -H "Authorization: token $TOKEN" \
"http://100.68.251.84:3005/v2/lucndm/zalo-tg/tags/list"
Image tag strategy¶
| Tag | Purpose | Lifetime |
|---|---|---|
:latest |
Auto-update target for PROD Komodo Stack | Rolling (overwritten each build) |
:version-N |
Sequential build version (auto-increment) | Permanent |
:commit-sha |
Git commit hash at build time | Permanent |
Use :version-N or :commit-sha for rollback pinning. Never tag a rollback image as a new :version-N.
Key files reference¶
| File | Role |
|---|---|
stacks/applications/zalo-tg/src/ |
TypeScript source (24 files) |
stacks/applications/zalo-tg/Dockerfile |
Multi-stage Node 20 alpine build |
stacks/applications/zalo-tg/compose.yaml |
PROD compose (CaoNguyen) |
stacks/applications/zalo-tg/docker-compose.dev.yml |
DEV compose (LucNguyen, local-only) |
stacks/applications/zalo-tg/.env |
DEV secrets — gitignored at repo root |
stacks/applications/zalo-tg/.env.example |
DEV secrets template (committed) |
stacks/applications/zalo-tg/docker-local.sh |
DEV helper script (build/up/down/logs) |
komodo/builds/zalo-tg.toml |
Komodo Build resource config |
komodo/stacks/applications/zalo-tg.toml |
Komodo Stack resource config (unraid deploy) |
Related docs¶
- Original VPN workflow runbook — style reference for triage pattern
- Unraid memory pressure runbook — when prod host OOMs
- Old Gitea repo (archived): http://git.minhluc.info/lucndm/zalo-tg — kept as upstream tracker