fix: route compose db connections to postgres service
This commit is contained in:
12
.ENV.example
Normal file
12
.ENV.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# Sample environment for Meme Wrangler Docker stack.
|
||||
# Copy to `.ENV` and adjust secrets before deploying.
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
OWNER_ID=0
|
||||
CHANNEL_ID=
|
||||
POSTGRES_DB=meme_wrangler
|
||||
POSTGRES_USER=meme
|
||||
POSTGRES_PASSWORD=meme
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
MEMEBOT_BACKUP_DIR=/app/backups
|
||||
# MEMEBOT_BACKUP_PASSWORD_HASH=
|
||||
@@ -1,32 +1,8 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.db-journal
|
||||
|
||||
# Environment variables
|
||||
.git
|
||||
.gitignore
|
||||
__pycache__
|
||||
*.pyc
|
||||
backups
|
||||
.env
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Docker
|
||||
data/
|
||||
.env.*
|
||||
.ENV
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
memes.db
|
||||
.env
|
||||
.env
|
||||
.ENV
|
||||
|
||||
@@ -29,6 +29,6 @@
|
||||
- Link issues or TODOs, flag breaking changes in bold, and include screenshots only when UI-facing Telegram copy changes.
|
||||
|
||||
## Deployment & Secret Tips
|
||||
- Keep `.env` out of version control; copy from `.env.example` and set `TELEGRAM_BOT_TOKEN`, `OWNER_ID`, `CHANNEL_ID`, `POSTGRES_*` credentials, and `DATABASE_URL` for local runs.
|
||||
- Keep `.ENV` (or `.env`) out of version control; copy from `.ENV.example` and set `TELEGRAM_BOT_TOKEN`, `OWNER_ID`, `CHANNEL_ID`, `POSTGRES_*` credentials, and `DATABASE_URL` for local runs.
|
||||
- Only override `MEMEBOT_BACKUP_PASSWORD_HASH` if you plan to replace the baked-in SHA-256 hash for backup commands.
|
||||
- Docker workflows mount backups in `./backups/` and keep the PostgreSQL cluster in the `pgdata` volume; prune carefully when resetting schedules. New memes automatically generate a fresh backup file in that directory.
|
||||
|
||||
@@ -37,8 +37,8 @@ sudo usermod -aG docker $USER
|
||||
Copy the example file and edit it:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # or use any text editor
|
||||
cp .ENV.example .ENV
|
||||
nano .ENV # or use any text editor
|
||||
```
|
||||
|
||||
Fill in your actual values:
|
||||
@@ -49,16 +49,22 @@ CHANNEL_ID=@yourchannel
|
||||
POSTGRES_DB=meme_wrangler
|
||||
POSTGRES_USER=meme
|
||||
POSTGRES_PASSWORD=meme
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
# Optional: adjust which env file Compose should read (defaults to .ENV)
|
||||
# COMPOSE_ENV_FILE=staging.env
|
||||
# Optional: hash (SHA-256) for replacing the baked-in backup secret
|
||||
# MEMEBOT_BACKUP_PASSWORD_HASH=<your_sha256_hash>
|
||||
# Optional: adjust DATABASE_URL for non-compose workflows
|
||||
# DATABASE_URL=postgresql://meme:meme@postgres:5432/meme_wrangler
|
||||
```
|
||||
|
||||
Leave `POSTGRES_HOST=postgres` when running through Docker Compose or Portainer; it's the internal service name that the bot rewrites into any localhost-style URLs so the container connects to the bundled PostgreSQL instance. Override it only if your database lives elsewhere.
|
||||
|
||||
### 2. Build and Run with Docker Compose (Easiest)
|
||||
|
||||
```bash
|
||||
# Build and start in background
|
||||
# Build and start in background (stack reads .ENV automatically)
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
@@ -73,6 +79,10 @@ docker-compose restart
|
||||
|
||||
That's it! Your bot is now running in Docker! 🎉
|
||||
|
||||
#### Deploying with Portainer Stacks
|
||||
|
||||
When launching the stack from Portainer, upload your `.ENV` file through the **Environment variables** tab—Portainer saves it beside the stack as `/data/compose/<stack-id>/.ENV`, which matches the default expected by `docker-compose.yml`. Only set `COMPOSE_ENV_FILE` if you deliberately use a different name.
|
||||
|
||||
## Alternative: Using Docker Commands Directly
|
||||
|
||||
### Build the Image
|
||||
@@ -152,9 +162,9 @@ ssh -i /path/to/ssh_key username@server_ip
|
||||
# Navigate to bot directory
|
||||
cd ~/meme-wrangler
|
||||
|
||||
# Create .env file
|
||||
cp .env.example .env
|
||||
nano .env # Fill in your credentials
|
||||
# Create .ENV file
|
||||
cp .ENV.example .ENV
|
||||
nano .ENV # Fill in your credentials
|
||||
|
||||
# Build and run
|
||||
docker-compose up -d
|
||||
@@ -308,8 +318,8 @@ scp -i /path/to/key -r \
|
||||
ssh -i /path/to/key username@server_ip
|
||||
cd ~/memebot
|
||||
|
||||
# Create .env file
|
||||
nano .env # Add your credentials
|
||||
# Create .ENV file
|
||||
nano .ENV # Add your credentials
|
||||
|
||||
# Run with Docker Compose
|
||||
docker-compose up -d
|
||||
|
||||
32
Dockerfile
32
Dockerfile
@@ -1,32 +1,38 @@
|
||||
# Use Python 3.12 slim image
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Configure Python environment
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies (if needed)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
# Install system dependencies required for asyncpg build
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first (for better caching)
|
||||
COPY requirements.txt .
|
||||
COPY requirements.txt ./
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir python-telegram-bot==20.7
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the bot code
|
||||
COPY bot.py .
|
||||
# Copy application source
|
||||
COPY . .
|
||||
|
||||
# Create directory for backups
|
||||
# Ensure backups directory exists inside the container
|
||||
RUN mkdir -p /app/backups
|
||||
|
||||
# Set environment variables (will be overridden by docker-compose or run command)
|
||||
ENV TELEGRAM_BOT_TOKEN=""
|
||||
ENV OWNER_ID=""
|
||||
ENV CHANNEL_ID=""
|
||||
ENV DATABASE_URL=""
|
||||
ENV MEMEBOT_BACKUP_DIR="/app/backups"
|
||||
ENV TELEGRAM_BOT_TOKEN="" \
|
||||
OWNER_ID="" \
|
||||
CHANNEL_ID="" \
|
||||
DATABASE_URL="" \
|
||||
MEMEBOT_BACKUP_DIR="/app/backups"
|
||||
|
||||
# Run the bot
|
||||
CMD ["python", "bot.py"]
|
||||
|
||||
11
README.md
11
README.md
@@ -10,10 +10,10 @@ The easiest way to run the bot is using Docker:
|
||||
|
||||
1. **Set up environment variables:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your bot credentials
|
||||
cp .ENV.example .ENV
|
||||
nano .ENV # Edit with your bot credentials
|
||||
```
|
||||
The compose stack expects PostgreSQL credentials, so populate `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD` (defaults provided). Backups are protected by a built-in SHA-256 hash; optionally define `MEMEBOT_BACKUP_PASSWORD_HASH` to replace it.
|
||||
The compose file now looks for `.ENV` by default so Portainer and other orchestrators can supply secrets without additional flags. Copy `.ENV.example` to `.ENV`, then populate `TELEGRAM_BOT_TOKEN`, `OWNER_ID`, `CHANNEL_ID`, and the Postgres settings. Leave `POSTGRES_HOST=postgres` (and `POSTGRES_PORT=5432` unless your database listens elsewhere) when you run inside Docker. The bot rewrites any localhost URLs with that host so the container connects to the bundled PostgreSQL service instead of looping back on itself. You can still point the stack at a different file by exporting `COMPOSE_ENV_FILE` (e.g. `COMPOSE_ENV_FILE=staging.env docker compose up -d`). Backups are protected by a built-in SHA-256 hash; optionally define `MEMEBOT_BACKUP_PASSWORD_HASH` to replace it.
|
||||
|
||||
2. **Run with Docker Compose:**
|
||||
```bash
|
||||
@@ -25,6 +25,8 @@ The easiest way to run the bot is using Docker:
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
Deploying via Portainer? Upload your `.ENV` file under **Environment variables**—Portainer stores it beside the stack so the compose file picks it up automatically. Override `COMPOSE_ENV_FILE` only when you use a differently named file.
|
||||
|
||||
4. **Stop the bot:**
|
||||
```bash
|
||||
docker-compose down
|
||||
@@ -51,6 +53,9 @@ export TELEGRAM_BOT_TOKEN=123:ABC
|
||||
export OWNER_ID=123456789
|
||||
export CHANNEL_ID=@yourchannel # or -1001234567890
|
||||
export DATABASE_URL=postgresql://meme:meme@localhost:5432/meme_wrangler
|
||||
# Optional pieces that mirror the Docker variables
|
||||
# export POSTGRES_HOST=localhost
|
||||
# export POSTGRES_PORT=5432
|
||||
# Optional: where JSON backups are written
|
||||
# export MEMEBOT_BACKUP_DIR=/path/to/backups
|
||||
# export MEMEBOT_BACKUP_PASSWORD_HASH=<sha256 hash of your backup secret>
|
||||
|
||||
104
bot.py
104
bot.py
@@ -10,6 +10,7 @@ import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from datetime import datetime, time, timedelta
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
@@ -30,31 +31,41 @@ if TYPE_CHECKING:
|
||||
from telegram import Update, Message, InputFile
|
||||
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters
|
||||
else:
|
||||
Update = Message = InputFile = Any # type: ignore
|
||||
ContextTypes = SimpleNamespace(DEFAULT_TYPE=Any) # type: ignore
|
||||
try:
|
||||
from telegram import Update, Message, InputFile # type: ignore
|
||||
from telegram.ext import ( # type: ignore
|
||||
ApplicationBuilder,
|
||||
ContextTypes,
|
||||
CommandHandler,
|
||||
MessageHandler,
|
||||
filters,
|
||||
)
|
||||
except ModuleNotFoundError:
|
||||
Update = Message = InputFile = Any # type: ignore
|
||||
ContextTypes = SimpleNamespace(DEFAULT_TYPE=Any) # type: ignore
|
||||
|
||||
class _MissingTelegramModule:
|
||||
"""Lazily raise when Telegram features are used without dependency."""
|
||||
class _MissingTelegramModule:
|
||||
"""Lazily raise when Telegram features are used without dependency."""
|
||||
|
||||
def __getattr__(self, item):
|
||||
raise RuntimeError(
|
||||
"python-telegram-bot must be installed to use the Meme Wrangler bot (missing telegram module)."
|
||||
)
|
||||
def __getattr__(self, item):
|
||||
raise RuntimeError(
|
||||
"python-telegram-bot must be installed to use the Meme Wrangler bot (missing telegram module)."
|
||||
)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"python-telegram-bot must be installed to use the Meme Wrangler bot (missing telegram module)."
|
||||
)
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"python-telegram-bot must be installed to use the Meme Wrangler bot (missing telegram module)."
|
||||
)
|
||||
|
||||
ApplicationBuilder = CommandHandler = MessageHandler = _MissingTelegramModule() # type: ignore
|
||||
ApplicationBuilder = CommandHandler = MessageHandler = _MissingTelegramModule() # type: ignore
|
||||
|
||||
class _MissingFilters(SimpleNamespace):
|
||||
def __getattr__(self, item):
|
||||
raise RuntimeError(
|
||||
"python-telegram-bot must be installed to use the Meme Wrangler bot (missing telegram filters)."
|
||||
)
|
||||
class _MissingFilters(SimpleNamespace):
|
||||
def __getattr__(self, item):
|
||||
raise RuntimeError(
|
||||
"python-telegram-bot must be installed to use the Meme Wrangler bot (missing telegram filters)."
|
||||
)
|
||||
|
||||
filters = _MissingFilters() # type: ignore
|
||||
filters = _MissingFilters() # type: ignore
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -79,7 +90,60 @@ def _ensure_ist(dt: datetime) -> datetime:
|
||||
return _ist_localize(dt)
|
||||
return dt.astimezone(IST)
|
||||
|
||||
DATABASE_URL = os.environ.get("DATABASE_URL") or os.environ.get("MEMEBOT_DB")
|
||||
def _build_database_url() -> Optional[str]:
|
||||
"""Derive the database URL from explicit env vars or component pieces."""
|
||||
|
||||
raw_url = os.environ.get("DATABASE_URL") or os.environ.get("MEMEBOT_DB")
|
||||
if not raw_url:
|
||||
user = os.environ.get("POSTGRES_USER")
|
||||
password = os.environ.get("POSTGRES_PASSWORD")
|
||||
db_name = os.environ.get("POSTGRES_DB")
|
||||
host = os.environ.get("POSTGRES_HOST", "localhost")
|
||||
port = os.environ.get("POSTGRES_PORT", "5432")
|
||||
if user and password and db_name:
|
||||
raw_url = f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
|
||||
|
||||
if raw_url:
|
||||
return _normalize_database_url(raw_url)
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_database_url(url: str) -> str:
|
||||
"""Replace localhost hosts with the configured Postgres host for container runs."""
|
||||
|
||||
host_override = os.environ.get("POSTGRES_HOST")
|
||||
if not host_override:
|
||||
return url
|
||||
|
||||
host_override = host_override.strip()
|
||||
if not host_override or host_override in {"localhost", "127.0.0.1", "::1"}:
|
||||
return url
|
||||
|
||||
parsed = urlparse(url)
|
||||
if parsed.hostname not in {"localhost", "127.0.0.1", "::1"}:
|
||||
return url
|
||||
|
||||
username = parsed.username or ""
|
||||
password = parsed.password
|
||||
auth = ""
|
||||
if username:
|
||||
auth = username
|
||||
if password is not None:
|
||||
auth += f":{password}"
|
||||
auth += "@"
|
||||
|
||||
if parsed.port is not None:
|
||||
port_fragment = f":{parsed.port}"
|
||||
else:
|
||||
port_env = os.environ.get("POSTGRES_PORT")
|
||||
port_fragment = f":{port_env}" if port_env else ""
|
||||
|
||||
new_netloc = f"{auth}{host_override}{port_fragment}"
|
||||
rebuilt = parsed._replace(netloc=new_netloc)
|
||||
return urlunparse(rebuilt)
|
||||
|
||||
|
||||
DATABASE_URL = _build_database_url()
|
||||
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
|
||||
OWNER_ID = int(os.environ.get("OWNER_ID", "0"))
|
||||
CHANNEL_ID = os.environ.get("CHANNEL_ID") # @channelusername or -100<id>
|
||||
|
||||
22
deploy.sh
22
deploy.sh
@@ -26,16 +26,20 @@ if [ -z "$SERVER" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 1: Checking if .env file exists..."
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "Error: .env file not found!"
|
||||
echo "Please create .env file with your credentials:"
|
||||
echo " cp .env.example .env"
|
||||
echo " nano .env"
|
||||
ENV_FILE=".ENV"
|
||||
echo "Step 1: Checking for environment file (.ENV or .env)..."
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "✓ .ENV file found"
|
||||
elif [ -f ".env" ]; then
|
||||
ENV_FILE=".env"
|
||||
echo "✓ .env file found"
|
||||
else
|
||||
echo "Error: No environment file found!"
|
||||
echo "Please create one with your credentials:"
|
||||
echo " cp .ENV.example .ENV"
|
||||
echo " nano .ENV"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ .env file found"
|
||||
echo ""
|
||||
|
||||
echo "Step 2: Uploading files to server..."
|
||||
@@ -45,7 +49,7 @@ scp -i "$SSH_KEY" \
|
||||
docker-compose.yml \
|
||||
bot.py \
|
||||
requirements.txt \
|
||||
.env \
|
||||
"$ENV_FILE" \
|
||||
"$SERVER":~/meme-wrangler/
|
||||
|
||||
echo "✓ Files uploaded"
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
x-env-file: &compose_env_file ${COMPOSE_ENV_FILE:-.ENV}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: meme-wrangler-db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- *compose_env_file
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-meme_wrangler}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-meme}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-meme}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-meme} -d ${POSTGRES_DB:-meme_wrangler}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
|
||||
meme-wrangler:
|
||||
build: .
|
||||
container_name: meme-wrangler
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- *compose_env_file
|
||||
environment:
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
- OWNER_ID=${OWNER_ID}
|
||||
- CHANNEL_ID=${CHANNEL_ID}
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
||||
- MEMEBOT_BACKUP_DIR=/app/backups
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
||||
- OWNER_ID=${OWNER_ID:-}
|
||||
- CHANNEL_ID=${CHANNEL_ID:-}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-meme_wrangler}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-meme}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-meme}
|
||||
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
||||
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-meme}:${POSTGRES_PASSWORD:-meme}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-meme_wrangler}}
|
||||
- MEMEBOT_BACKUP_DIR=${MEMEBOT_BACKUP_DIR:-/app/backups}
|
||||
volumes:
|
||||
- ./backups:/app/backups
|
||||
logging:
|
||||
|
||||
Reference in New Issue
Block a user