feat(selfhost): containerize for Coolify + decommission AWS deploy

Add multi-stage Dockerfile, docker-compose.yml for local dev, .env.example template,
and Makefile helpers. Disable GitHub Actions AWS deploy workflow. Supports Coolify
container orchestration for self-hosted deployments.
This commit is contained in:
2026-06-28 09:58:32 +07:00
parent 3f4802b24c
commit b0b4b4b4c0
5 changed files with 163 additions and 4 deletions
+47
View File
@@ -0,0 +1,47 @@
# miti99bot — self-host (Coolify + MongoDB Atlas) environment.
# Copy to .env and fill in. .env is gitignored — never commit real secrets.
# ============================ Required ============================
# Telegram bot token from @BotFather.
TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
# MongoDB Atlas connection. MONGO_URL is the full SRV string INCLUDING the
# db username + password — treat it as a secret (it is never logged).
# Create a least-privilege user: readWrite on this one database only.
MONGO_URL=mongodb+srv://botuser:STRONG_UNIQUE_PASSWORD@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority
MONGO_DATABASE=miti99bot
# ============================ Operational =========================
# Comma-separated module list. Empty = load every module.
MODULES=
# Telegram user id for owner-only commands (renamed from BOT_OWNER_ID).
OWNER_ID=
# Comma-separated admin Telegram user ids (renamed from ADMIN_USER_IDS).
ADMIN_IDS=
# ============================ Optional ============================
# Only the twentyq module needs this. Leave blank to disable that command.
GEMINI_API_KEY=
# GIT_SHA is injected at build time by Coolify for the deploynotify owner DM.
# Leave unset for local `docker compose up` — deploynotify just stays silent.
# GIT_SHA=
# ====================== Leave UNSET on self-host ==================
# These are AWS-only. cmd/server reads secrets directly from the plain env
# vars above; *_PARAMETER_NAME would force an SSM lookup that FAILS with no AWS
# credentials and bricks startup. Do NOT set any of them:
# TELEGRAM_BOT_TOKEN_PARAMETER_NAME
# TELEGRAM_WEBHOOK_SECRET_PARAMETER_NAME
# CRON_SHARED_SECRET_PARAMETER_NAME
# GEMINI_API_KEY_PARAMETER_NAME
# STOCK_INCOME_EVENTS_API_TOKEN_PARAMETER_NAME
# GOLD_VNAPP_API_KEY_PARAMETER_NAME
#
# Also leave unset (defaults are correct for self-host):
# KV_PROVIDER — auto-selects mongodb because MONGO_URL is set
# PORT — defaults to 8080 (internal health server)
# TELEGRAM_WEBHOOK_SECRET — long polling has no webhook
# CRON_SHARED_SECRET — unset → /cron route 404s; the in-process scheduler is the trigger
# GOLD_VNAPP_API_KEY — gold module auto-fetches + caches the key to Mongo
# STOCK/COIN/GOLD *_API_URL overrides — modules use their coded default providers
+7 -2
View File
@@ -1,8 +1,13 @@
name: deploy-aws
# RETIRED: miti99bot is self-hosted on Coolify + MongoDB Atlas (see
# docs/deploy-coolify-selfhosted.md). The AWS stack is decommissioned
# (docs/aws-decommission-runbook.md). The automatic push-to-main trigger is
# removed so a merge can never recreate the AWS stack. Kept as manual-only
# (workflow_dispatch) for reference; it requires the github-deploy-miti99bot
# OIDC role, which the decommission deletes — so a manual run fails until AWS
# is intentionally re-bootstrapped.
on:
push:
branches: [main]
workflow_dispatch:
permissions:
+8 -1
View File
@@ -5,8 +5,15 @@ COPY go.mod go.sum ./
RUN go mod download
COPY . .
# GIT_SHA is baked into the binary so internal/deploynotify can DM the owner
# once per new version (parity with the Makefile build). Coolify exposes the
# commit SHA as a build arg — pass it with
# --build-arg GIT_SHA=$(git rev-parse --short HEAD)
# When unset, deploynotify treats the empty SHA as "stay silent".
ARG GIT_SHA=""
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-ldflags="-s -w -X main.gitSHA=${GIT_SHA}" \
-o /out/server \
./cmd/server
+60 -1
View File
@@ -1,4 +1,4 @@
.PHONY: help test test-emulator test-dynamodb firestore-emulator dynamodb-local dynamodb-local-stop vet build build-lambda run sam-validate sam-build sam-deploy telegram-setup telegram-webhook telegram-webhook-info telegram-commands telegram-commands-info logs clean
.PHONY: help test test-emulator test-dynamodb test-mongo firestore-emulator dynamodb-local dynamodb-local-stop mongo-local mongo-local-stop vet build build-lambda run sam-validate sam-build sam-deploy telegram-setup telegram-webhook telegram-webhook-info telegram-commands telegram-commands-info telegram-commands-selfhost telegram-deletewebhook-selfhost telegram-webhook-info-selfhost migrate-dynamo-to-mongo migrate-verify logs clean
# Lambda target architecture. Match Globals.Architectures in template.yaml.
LAMBDA_GOOS ?= linux
@@ -48,6 +48,13 @@ test-dynamodb: dynamodb-local ## Run DynamoDB tests against DynamoDB Local
DYNAMODB_LOCAL_URL=http://localhost:$(DDB_PORT) LOG_LEVEL=error \
go test -race -count=1 ./internal/storage/...
# Run MongoDB integration tests against a local Mongo container.
# Override MONGO_PORT if 27017 is taken on your host.
MONGO_PORT ?= 27017
test-mongo: mongo-local ## Run MongoDB tests against a local Mongo container
MONGODB_TEST_URL=mongodb://127.0.0.1:$(MONGO_PORT) LOG_LEVEL=error \
go test -race -count=1 ./internal/storage/...
# ---- Lint / Vet -----------------------------------------------------------
vet: ## go vet
@@ -87,6 +94,34 @@ dynamodb-local: ## Start DynamoDB Local container on :$(DDB_PORT) (idempotent)
dynamodb-local-stop: ## Stop DynamoDB Local
-docker stop miti99bot-ddb
# ---- MongoDB local for tests ----------------------------------------------
mongo-local: ## Start MongoDB container on :$(MONGO_PORT) (idempotent)
@if ! docker ps --format '{{.Names}}' | grep -q '^miti99bot-mongo$$'; then \
docker run -d --rm --name miti99bot-mongo -p $(MONGO_PORT):27017 mongo:7; \
echo "MongoDB started on :$(MONGO_PORT)"; \
sleep 2; \
else \
echo "MongoDB already running"; \
fi
mongo-local-stop: ## Stop local MongoDB
-docker stop miti99bot-mongo
# ---- Data migration (DynamoDB → MongoDB Atlas) ----------------------------
# Requires MONGO_URL + MONGO_DATABASE in the environment and AWS credentials
# for a READ-ONLY profile with dynamodb:Scan on the table. Use DRY_RUN=1 first.
# make migrate-dynamo-to-mongo DRY_RUN=1 MONGO_URL=… MONGO_DATABASE=…
# make migrate-dynamo-to-mongo MONGO_URL=… MONGO_DATABASE=…
# make migrate-verify MONGO_URL=… MONGO_DATABASE=…
MIGRATE_TABLE ?= miti99bot-data
migrate-dynamo-to-mongo: ## Copy DynamoDB → Mongo (DRY_RUN=1 for a dry run)
go run ./cmd/migrate-dynamo-to-mongo --dynamodb-table $(MIGRATE_TABLE) $(if $(DRY_RUN),--dry-run,)
migrate-verify: ## Verify per-module counts DynamoDB vs Mongo (exit non-zero on mismatch)
go run ./cmd/migrate-dynamo-to-mongo --dynamodb-table $(MIGRATE_TABLE) --verify
# ---- SAM (require AWS CLI + SAM CLI installed locally) -------------------
sam-validate: ## Validate template.yaml without contacting AWS
@@ -154,6 +189,30 @@ telegram-commands-info: ## Show Telegram getMyCommands using token from SSM
curl -sS "https://api.telegram.org/bot$${TOKEN}/getMyCommands"; \
echo
# ---- Telegram (self-host: token from TELEGRAM_BOT_TOKEN env, no AWS/SSM) ---
telegram-commands-selfhost: ## Register command menu using TELEGRAM_BOT_TOKEN env
@set -eu; \
: "$${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN}"; \
echo "Registering Telegram commands from $(TELEGRAM_COMMANDS_FILE)"; \
curl -sS -X POST "https://api.telegram.org/bot$${TELEGRAM_BOT_TOKEN}/setMyCommands" \
-H 'Content-Type: application/json' \
--data-binary "@$(TELEGRAM_COMMANDS_FILE)"; \
echo
telegram-deletewebhook-selfhost: ## Cutover: delete the webhook so the poller can run (keeps buffered updates)
@set -eu; \
: "$${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN}"; \
curl -sS -X POST "https://api.telegram.org/bot$${TELEGRAM_BOT_TOKEN}/deleteWebhook" \
-d 'drop_pending_updates=false'; \
echo
telegram-webhook-info-selfhost: ## getWebhookInfo using TELEGRAM_BOT_TOKEN env (confirm url empty + pending draining)
@set -eu; \
: "$${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN}"; \
curl -sS "https://api.telegram.org/bot$${TELEGRAM_BOT_TOKEN}/getWebhookInfo"; \
echo
logs: ## Tail Lambda logs (last 5m). Override with SINCE=10m.
@sam logs --tail --stack-name miti99bot --start-time $${SINCE:-5m}ago
+41
View File
@@ -0,0 +1,41 @@
services:
bot:
build:
context: .
args:
# Coolify exposes the commit SHA; pass it so deploynotify DMs the owner
# on each new version. Optional — empty SHA just stays silent.
GIT_SHA: ${GIT_SHA:-}
# Or pin a prebuilt image instead of building:
# image: ghcr.io/tiennm99/miti99bot:latest
restart: unless-stopped
environment:
# --- Required ---
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
MONGO_URL: ${MONGO_URL} # Atlas SRV string incl. credentials — SECRET
MONGO_DATABASE: ${MONGO_DATABASE}
# --- Operational ---
MODULES: ${MODULES} # CSV; empty = all modules
OWNER_ID: ${OWNER_ID} # Telegram user id for owner-only commands
ADMIN_IDS: ${ADMIN_IDS} # CSV of admin Telegram user ids
# --- Optional ---
GEMINI_API_KEY: ${GEMINI_API_KEY} # only the twentyq module needs it
# Storage auto-selects mongodb because MONGO_URL is set — no KV_PROVIDER.
# The in-process cron scheduler runs by default — no CRON_MODE.
# PORT defaults to 8080 (internal health server) — omit unless overriding.
# Long polling = no TELEGRAM_WEBHOOK_SECRET, no /webhook, no public domain.
# Leave CRON_SHARED_SECRET unset → /cron route is 404 (scheduler is the trigger).
# Do NOT set any *_PARAMETER_NAME vars (those force an SSM/AWS lookup that
# fails with no AWS creds and bricks startup). See .env.example.
# No stock/coin/gold *_API_URL overrides — modules use their coded default
# providers (stock: SSI/VCI/KBS; coin: Binance→Coinbase→CoinGecko;
# gold: VNAppMob→spot). GOLD_VNAPP_API_KEY auto-fetches + caches to Mongo.
# Long polling is outbound-only: nothing inbound to route, so no published
# ports and no public domain. `expose` keeps :8080 reachable inside the
# Coolify network for the container health monitor against GET / only.
expose:
- "8080"
# No compose healthcheck: distroless has no shell/curl and cmd/server has no
# -healthcheck flag. Configure Coolify's HTTP monitor against GET / instead
# (returns text/plain "miti99bot ok"). Note: a plain / check does not verify
# Mongo connectivity — see docs/deploy-coolify-selfhosted.md.