6.6 KiB
Deploy: Self-host (Coolify + MongoDB Atlas)
Run miti99bot as a long-lived container on Coolify with
MongoDB Atlas (free M0) for storage.
Architecture
Telegram <── long poll (getUpdates) ── container (outbound only)
in-process scheduler ───────────────────> module crons
MongoDB Atlas (db / one collection per module + system metadata)
Coolify env vars (plain secrets)
NO public ingress (polling = outbound only; no domain, no /webhook, no TLS in)
- Storage —
mongodbauto-selected whenMONGO_URLis set (noKV_PROVIDER). - Cron — an in-process scheduler (
internal/cron) runs unconditionally and fires each module cron on itsSchedule(UTC). - Transport — long polling (
b.Start) is the only transport. The bot opens an outbound connection to Telegram and pulls updates, so there is no public domain, no/webhook, and no webhook secret. The container clears any leftover webhook on startup (deleteWebhook) before polling.
Required environment
Copy .env.example → .env (gitignored) and fill in.
| Var | Required | Notes |
|---|---|---|
TELEGRAM_BOT_TOKEN |
✅ | from @BotFather |
MONGO_URL |
✅ | Atlas SRV string incl. credentials — secret, never logged |
MONGO_DATABASE |
✅ | e.g. miti99bot |
MODULES |
optional | CSV; empty = all modules |
OWNER_ID |
optional | owner-only commands (renamed from BOT_OWNER_ID) |
ADMIN_IDS |
optional | CSV of admin ids (renamed from ADMIN_USER_IDS) |
WC_FOOTBALL_DATA_TOKEN |
optional | football-data.org token for the wc module |
Leave UNSET on self-host: KV_PROVIDER, PORT,
TELEGRAM_WEBHOOK_SECRET, and GOLD_VNAPP_API_KEY. Stock, coin, and gold URL
overrides are not supported in runtime env; modules use coded defaults.
Cron runs in-process (
internal/cron) — there is no/cronHTTP route and noCRON_SHARED_SECRET. The scheduler is the sole trigger; nothing inbound.
1. MongoDB Atlas (M0)
-
Create a free M0 cluster (512 MB — ample for the tiny paper-trading KV).
-
Database user (least privilege): create a user with role
readWriteon the single app database only (e.g.miti99bot) — never Atlas admin or cluster-wide. Use a strong unique password. -
Network access: add
0.0.0.0/0.Accepted trade-off (validated decision). The Coolify host has no stable egress IP, so the Atlas IP allow-list is open to the internet. This widens the database surface. The mandatory compensating controls are: (1) strong unique password, (2) least-privilege
readWrite-on-one-db user, (3) the connection string is a secret and is never logged (the bot logs only the database name on startup). -
Copy the
mongodb+srv://…connection string intoMONGO_URLand put the db name inMONGO_DATABASE.
Storage layout: one collection per module plus a shared
systemcollection for startup metadata such as one-time migrations. Each document is a flattened native document —{ _id: <user key>, ...payload fields, version, updatedAt }with novalueenvelope. Payload fields are hoisted to the document root so they expand and are queryable in Compass. The two non-object values are wrapped in a named field:lolschedule subscribers undersubscribers(array) and the daily push date underdate. Concurrency uses theversionfield (optimistic lock);updatedAtis a BSON Date.The
lolmodule uses thelolcollection. First startup after the rename copies documents from the legacylolschedulecollection, drops that legacy collection, and records completion insystem.The
statscollection uses queryable aggregate documents for command/user counts and creates indexes on startup. First startup after the schema change migrates legacycount:,user:, andpair:stats keys into the new shape, deletes the legacy keys, and records completion insystem.
2. Coolify
- New resource → from this Git repo (Docker Compose), or a prebuilt image.
The committed
compose.ymldefines the singlebotservice. - Set the env vars above in Coolify.
- No public domain / port is needed — polling is outbound-only. Do not
publish a port or attach a domain.
expose: 8080keeps the health endpoint reachable only inside Coolify's network. - Exactly one replica. Telegram permits only one
getUpdatesconsumer per bot token; a second poller gets HTTP 409, and a second in-process scheduler double-fires crons. Prefer stop-first redeploys so two containers never overlap near a cron time. - deploynotify commit SHA:
SOURCE_COMMITis a Coolify predefined variable. The bot reads it at startup and DMs the owner on every boot; outside Coolify (localdocker compose up) it is unset and the DM showsunknown. Keep "Include Source Commit in Build" disabled: that setting affects build args only, is not needed for this runtime path, and would invalidate Docker cache on every commit. Do not addSOURCE_COMMITtocompose.yml; an interpolated empty value can override Coolify's runtime env-file value. - Health check: use Coolify's HTTP monitor against
GET /(returnstext/plainmiti99bot ok). Do not use a composehealthcheck— the distroless image has no shell/curl andcmd/serverhas no-healthcheckflag. Note:/reports healthy even if Mongo is unreachable (the driver auto-reconnects on the next op); a DB outage will not auto-restart the container — accepted trade-off.
3. Command menu
The bot registers its Telegram command menu from loaded public modules on startup. The manual target remains useful for repairs or local experiments:
TELEGRAM_BOT_TOKEN=… make telegram-commands
Operations
The live deployment is the Coolify container and MongoDB is the sole system of record. Keep exactly one replica running. To confirm Telegram is in polling mode:
TELEGRAM_BOT_TOKEN=… make telegram-webhook-info
url should be empty. If needed, clear the webhook explicitly:
TELEGRAM_BOT_TOKEN=… make telegram-deletewebhook
Local smoke test
cp .env.example .env # fill TELEGRAM_BOT_TOKEN, MONGO_URL, MONGO_DATABASE
docker compose up --build
Boot logs should show storage backend backend=mongodb database=… (no
connection string), cron scheduler started, and telegram long polling started. curl localhost:8080/ returns miti99bot ok. The bot's webhook must
be unset (the container clears it on startup) or getUpdates 409s.