Containers
Who is this for
Section titled “Who is this for”A developer or operator who needs to know what processes exist, what listens where, and in which direction traffic flows — for example before changing a firewall rule, moving the server or debugging “who is supposed to call whom”.
- The server runs two application processes:
sticker-api(uvicorn + FastAPI, loopback port 8000) andsticker-bot(one Python process that long-polls Telegram and also runs the queue worker, the delivery sweeper, the TTL cleanup and the leads loop). They share one SQLite file and one data directory. - Only Caddy listens on public ports (80 and 443). It terminates TLS for
stickers.eventtalks.uzand proxies everything tosticker-api. - The bot has no listening port. All Telegram and AI traffic is outbound. There is no webhook.
- The two processes never call each other. They coordinate only through the database: the API inserts rows, the bot picks them up by polling.
- The landing runs on Vercel and reaches the server with one call:
POST /leads. If that fails, it posts the request straight to Telegram. - The kiosk is a static page served by
sticker-api; it polls the API once per second while a session is open.
Container diagram
Section titled “Container diagram”flowchart LR
visitor["Site visitor browser"]
tablet["Kiosk page in the tablet browser"]
opbrowser["Operator browser"]
phone["Guest and admin Telegram apps"]
subgraph vercel["Vercel"]
landing["Landing - Next.js 16, server action"]
end
subgraph droplet["DigitalOcean droplet - shared with a second, unrelated project"]
caddy["Caddy - TLS and reverse proxy, ports 80 and 443"]
api["sticker-api - uvicorn and FastAPI, loopback port 8000"]
bot["sticker-bot - polling, worker, sweeper, cleanup, leads loop"]
db[("SQLite app.db in WAL mode")]
files[("File storage with TTL - sessions and debug_sheets")]
backup["sticker-backup timer - daily sqlite3 backup"]
end
tg["Telegram Bot API and t.me"]
ai["AI providers - Gemini primary, OpenAI fallback"]
visitor -- "HTTPS" --> landing
landing -- "HTTPS POST /leads with shared secret" --> caddy
landing -. "fallback sendMessage" .-> tg
tablet -- "HTTPS, polls every second" --> caddy
opbrowser -- "HTTPS /admin, Basic auth" --> caddy
caddy -- "HTTP reverse proxy" --> api
api -- "SQL" --> db
api -- "writes selfie" --> files
api -- "one-off sendMessage" --> tg
bot -- "SQL, polls the jobs queue and the leads outbox" --> db
bot -- "reads selfie, writes sheets and stickers, deletes by TTL" --> files
bot -- "long polling getUpdates, send, createNewStickerSet" --> tg
bot -- "HTTPS via provider SDKs" --> ai
backup -- "sqlite3 .backup" --> db
phone -- "Telegram protocol" --> tg
Arrows point from the side that opens the connection.
Containers
Section titled “Containers”| Container | Technology | Responsibility | Listens on | Source |
|---|---|---|---|---|
| Landing | Next.js 16 (App Router) on Vercel, React 19, zod |
Marketing page in ru / uz / en; request form; server action that validates, rate-limits and delivers the request |
HTTPS on Vercel’s edge (domain set in Vercel) | src/app/actions/submitRequest.ts |
| Kiosk | Static HTML + vanilla JavaScript, no build step, runs in the tablet browser in kiosk mode | Start screen, QR, outfit cards, camera capture, upload; resets itself on timeouts | — (a browser page) | bot/kiosk/ |
sticker-api |
Python, FastAPI under uvicorn, one process without --reload |
Kiosk endpoints (session, QR, templates, photo), the web admin panel, POST /leads, /health; serves the kiosk files |
TCP 8000 on the loopback interface only | bot/api.py, sticker-api.service |
sticker-bot |
Python, aiogram, asyncio; exactly one instance | Telegram updates (guest flow, admin commands, leads group), generation worker, delivery sweeper, TTL cleanup, leads outbox and housekeeping | Nothing — outbound connections only | bot/main.py, sticker-bot.service |
| SQLite database | One file app.db, journal mode WAL, busy_timeout 5000 ms per connection |
Users, sessions, the jobs queue, AI usage records, audit log, settings, leads | — (a file opened by both processes) | bot/db/db.py, bot/db/models.py |
| File storage with TTL | Plain directories under DATA_DIR (sessions/, debug_sheets/) |
Selfie, generated sheets, sticker PNGs, debug copies; every file is removed once it is older than PHOTO_TTL_HOURS |
— (a directory) | bot/services/storage.py |
| Caddy | Caddy web server from the official package | TLS for stickers.eventtalks.uz (automatic Let’s Encrypt), security headers, 20 MB request body cap, reverse proxy to sticker-api. One Caddy serves both projects on the droplet; this project owns only its own site file |
TCP 80 and 443 on all interfaces | deploy/caddy/stickers.caddy, deploy/caddy.sh |
| Backup timer | systemd timer + oneshot service running a shell script as root | Daily consistent copy of app.db (sqlite3 .backup), integrity check, gzip, keep the last 14 |
— | deploy/backup.sh, sticker-backup.timer |
| Telegram Bot API (external) | HTTPS API | Updates, messages, sticker sets | — | — |
| AI providers (external) | Gemini via google-genai, OpenAI via openai SDK |
Image generation and JSON-returning vision calls | — | bot/services/ai_pipeline.py |
The firewall (ufw, configured by deploy/setup.sh) allows inbound TCP 22, 80 and 443 only. Port 8000 is not
reachable from outside: uvicorn binds to the loopback interface and trusts forwarded headers only from it
(--proxy-headers --forwarded-allow-ips set to the loopback address in the unit file).
Who initiates each connection
Section titled “Who initiates each connection”| # | From | To | Protocol | When | Verified in |
|---|---|---|---|---|---|
| 1 | Tablet browser | Caddy → sticker-api |
HTTPS, then HTTP on loopback | Page load; POST /session on Start; GET /session/{token} every 1000 ms while a session is open; template choice; photo upload; POST /session/{token}/expire on any reset |
CFG.pollMs in kiosk.js |
| 2 | Guest’s phone | Telegram | Telegram client protocol | Scanning the QR opens https://t.me/<bot_username>?start=<session_token> |
_deep_link in api.py |
| 3 | sticker-bot |
Telegram Bot API | HTTPS long polling | Always. dp.start_polling(...); no webhook is registered anywhere |
main.py |
| 4 | sticker-bot |
Telegram Bot API | HTTPS | Replies to guests, alerts to ADMIN_CHAT_ID, createNewStickerSet, getStickerSet, lead cards and pings to LEADS_CHAT_ID |
worker.py, delivery.py, leads_bot.py |
| 5 | sticker-bot |
t.me |
HTTPS GET | Pack readiness check: the page https://t.me/addstickers/<pack_name> must say “the set exists” twice in a row |
telegram_stickers.py |
| 6 | sticker-bot |
AI providers | HTTPS through the SDKs, in daemon threads | Every generation and vision call of a job | ai_pipeline.py |
| 7 | sticker-api |
Telegram Bot API | HTTPS, one-off sendMessage |
After a photo is accepted (Генерирую… — “Generating…”); when the pack limit blocks a session; a flood alert from POST /leads. The API never calls getUpdates, so it does not conflict with the bot’s polling |
get_bot, notify_generating, notify_pack_limit in api.py |
| 8 | Landing server action (Vercel) | Caddy → sticker-api |
HTTPS POST /leads, header X-Stickers-Leads-Secret, 5 s timeout |
Each valid form submission, when LEADS_ENDPOINT and LEADS_SECRET are set |
deliverRequest.ts, leads_api.py |
| 9 | Landing server action (Vercel) | Telegram Bot API | HTTPS sendMessage |
Fallback when call 8 times out, fails on the network, or returns 5xx / 401 / 404; also the only route when the leads variables are not set | deliverRequest.ts |
| 10 | Operator browser | Caddy → sticker-api |
HTTPS, HTTP Basic | /admin page and /admin/api/*; the page refreshes itself every 10 s while visible |
admin_panel.py, admin/index.html |
| 11 | sticker-api and sticker-bot |
SQLite file | File I/O | A new connection per database call | _connect in db.py |
| 12 | Backup timer | SQLite file | sqlite3 .backup |
Daily at 03:30 UTC | sticker-backup.timer |
| 13 | Developer workstation | Droplet | SSH / SCP on port 22 | Deploy: upload an archive of the committed tree, then run the update script | deploy/push.ps1, deploy/update.sh |
| 14 | Caddy | Let’s Encrypt | ACME | Certificate issue and renewal, automatic | stickers.caddy |
How the two processes cooperate without talking
Section titled “How the two processes cooperate without talking”There is no queue broker and no RPC between sticker-api and sticker-bot. The database is the only channel:
| What | Written by | Picked up by | How |
|---|---|---|---|
| A generation job | sticker-api inserts a jobs row with status queued in the same transaction that moves the session to photo_taken |
A worker slot in sticker-bot |
Each slot runs claim_next_job() (one conditional UPDATE … RETURNING); an empty queue is re-checked every second |
| A session status | sticker-bot (link, consent) and sticker-api (template, photo, expire) |
The kiosk | The kiosk polls GET /session/{token} |
| A lead | sticker-api inserts a leads row with published_at empty |
The leads loop in sticker-bot |
Outbox polling every 2 s |
| Maintenance pause | Either process writes the maintenance key in settings |
Both | Read before every guarded action; no restart needed |
Why this shape: both processes start and restart independently, and a lead or a photo is never lost when the bot is
down — it waits in the database. Every status change is a conditional UPDATE … WHERE status = …, and multi-step
changes run inside BEGIN IMMEDIATE, so a double tap on the tablet or two worker slots cannot create a duplicate.
Resource limits on the shared droplet
Section titled “Resource limits on the shared droplet”Both units carry the same hardening and CPU weight; memory limits differ because the bot holds the image work.
| Unit | MemoryHigh |
MemoryMax |
CPUWeight |
Stop behaviour |
|---|---|---|---|---|
sticker-api |
1.2G | 1.6G | 50 | TimeoutStopSec=15 |
sticker-bot |
1.45G | 1.6G | 50 | KillSignal=SIGINT, TimeoutStopSec=30 |
Source: the unit files sticker-api.service and sticker-bot.service.
The comments there give the reason: the droplet has 2 vCPU and 4 GB and is shared, so above MemoryHigh the kernel
slows the process and reclaims its memory instead of letting it starve the neighbour; CPUWeight=50 against the
default 100 gives the neighbour twice the CPU share under contention.
Where to go next
Section titled “Where to go next”- Component: sticker-api — what is inside the API process.
- Component: sticker-bot — the five long-running tasks of the bot process.
- Component: storage and infrastructure — database, files, Caddy, systemd, backups in detail.
- Security — trust boundaries between these containers.
- Environment variables — every setting named on this page.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.