6.8 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. Startup also migrates renamed command stats to the current command names and marks removed command rows withdeleted: true;/statsqueries filter those retained legacy rows.
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.