mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-07-23 22:21:22 +00:00
195 lines
8.9 KiB
Markdown
195 lines
8.9 KiB
Markdown
# Deploy: Self-host (Coolify + MongoDB Atlas)
|
|
|
|
Run `miti99bot` as a long-lived container on [Coolify](https://coolify.io) with
|
|
[MongoDB Atlas](https://www.mongodb.com/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** — `mongodb` 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.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`) |
|
|
| `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`](https://github.com/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`:
|
|
|
|
```env
|
|
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:
|
|
|
|
```env
|
|
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. 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. Stock stores cash as `vnd` and embeds positions as
|
|
> `assets.<symbol>.{quantity,base,dividendCheckedAt}`. Coin stores cash as `usd`
|
|
> and embeds positions as `assets.<symbol>.{quantity,base}`. Completed migration
|
|
> records remain in `system` as audit history; the completed one-time migration
|
|
> code no longer runs at startup.
|
|
|
|
## 2. Coolify
|
|
|
|
1. New resource → from this Git repo (Docker Compose), or a prebuilt image.
|
|
The committed [`compose.yml`](../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
|
|
every startup. The Go module registry is the single source of truth; no separate
|
|
command-menu file or manual registration step is required. A command's
|
|
description plus optional `Parameters` metadata feed both surfaces. Telegram
|
|
renders the command name separately and accepts only a single-line plain-text
|
|
description. Both the native menu and `/help` show syntax plus the summary and
|
|
omit example invocations.
|
|
|
|
## 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:
|
|
|
|
```sh
|
|
# POSIX shells (Linux/macOS)
|
|
curl "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo"
|
|
```
|
|
|
|
```powershell
|
|
# PowerShell
|
|
Invoke-RestMethod "https://api.telegram.org/bot$env:TELEGRAM_BOT_TOKEN/getWebhookInfo"
|
|
```
|
|
|
|
`url` should be empty. If needed, clear the webhook explicitly:
|
|
|
|
```sh
|
|
# POSIX shells (Linux/macOS)
|
|
curl -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/deleteWebhook" \
|
|
--data "drop_pending_updates=false"
|
|
```
|
|
|
|
```powershell
|
|
# PowerShell
|
|
Invoke-RestMethod -Method Post -Uri "https://api.telegram.org/bot$env:TELEGRAM_BOT_TOKEN/deleteWebhook" -Body @{ drop_pending_updates = "false" }
|
|
```
|
|
|
|
## Local smoke test
|
|
|
|
```powershell
|
|
# PowerShell
|
|
Copy-Item .env.example .env # fill TELEGRAM_BOT_TOKEN, MONGO_URL, MONGO_DATABASE
|
|
docker compose up --build
|
|
```
|
|
|
|
```sh
|
|
# POSIX shells (Linux/macOS)
|
|
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`. A request to `http://localhost:8080/` returns `miti99bot ok` (use
|
|
`Invoke-WebRequest` in PowerShell or `curl` in a POSIX shell). The bot's webhook
|
|
must be unset (the container clears it on startup) or `getUpdates` 409s.
|