Files
miti99bot/docs/deploy-coolify-selfhosted.md
T

7.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)
  • Storagemongodb auto-selected when MONGO_URL is set (no KV_PROVIDER).
  • Cron — an in-process scheduler (internal/cron) runs unconditionally and fires each module cron on its Schedule (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
WHEELOFNAMES_API_URL optional full /api/gif endpoint for remote /wheelofnames GIF rendering
WHEELOFNAMES_API_TOKEN optional bearer token matching the wheelofnames service API_TOKEN

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 /cron HTTP route and no CRON_SHARED_SECRET. The scheduler is the sole trigger; nothing inbound.

Optional wheelofnames renderer

/wheelofnames uses a remote GIF renderer when WHEELOFNAMES_API_URL is set. The standard renderer is a deployment of tiennm99/wheelofnames, but any service that implements the same /api/gif contract can be used. Set the URL to the full GIF endpoint and set the token to the same value as the service API_TOKEN:

WHEELOFNAMES_API_URL=http://wheelofnames:3000/api/gif
WHEELOFNAMES_API_TOKEN=<same value as wheelofnames API_TOKEN>

Use a public HTTPS URL instead when the bot cannot reach the service on a private Coolify/Docker network:

WHEELOFNAMES_API_URL=https://wheelofnames.example.com/api/gif
WHEELOFNAMES_API_TOKEN=<same value as wheelofnames API_TOKEN>

The bot sends outbound HTTP only; no public bot ingress is required. Remote renders use 512px, 20fps, and 7 seconds total by default. If the remote service is unset, unavailable, unauthorized, or returns a non-GIF response, /wheelofnames falls back to the same plain text winner reply as /random. Successful GIF replies include the result behind Telegram spoiler formatting.

1. MongoDB Atlas (M0)

  1. Create a free M0 cluster (512 MB — ample for the tiny paper-trading KV).

  2. Database user (least privilege): create a user with role readWrite on the single app database only (e.g. miti99bot) — never Atlas admin or cluster-wide. Use a strong unique password.

  3. 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).

  4. Copy the mongodb+srv://… connection string into MONGO_URL and put the db name in MONGO_DATABASE.

Storage layout: one collection per module. Each document is a flattened native document — { _id: <user key>, ...payload fields, version, updatedAt } with no value envelope. 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: lol schedule subscribers under subscribers (array) and the daily push date under date. Concurrency uses the version field (optimistic lock); updatedAt is a BSON Date.

The stats collection uses queryable aggregate documents for command/user counts and creates indexes on startup. Renamed command rows are merged into the current command name. Deleted legacy command rows are retained with deleted: true; /stats queries filter those rows from visible results. A historical system collection may remain in MongoDB with completed migration records and can be reused if a future one-time startup migration is needed.

2. Coolify

  1. New resource → from this Git repo (Docker Compose), or a prebuilt image. The committed compose.yml defines the single bot service.
  2. Set the env vars above in Coolify.
  3. No public domain / port is needed — polling is outbound-only. Do not publish a port or attach a domain. expose: 8080 keeps the health endpoint reachable only inside Coolify's network.
  4. Exactly one replica. Telegram permits only one getUpdates consumer 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.
  5. deploynotify commit SHA: SOURCE_COMMIT is a Coolify predefined variable. The bot reads it at startup and DMs the owner on every boot; outside Coolify (local docker compose up) it is unset and the DM shows unknown. 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 add SOURCE_COMMIT to compose.yml; an interpolated empty value can override Coolify's runtime env-file value.
  6. Health check: use Coolify's HTTP monitor against GET / (returns text/plain miti99bot ok). Do not use a compose healthcheck — the distroless image has no shell/curl and cmd/server has no -healthcheck flag. 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.