fix: route compose db connections to postgres service

This commit is contained in:
Wiktor
2025-11-01 22:05:21 +01:00
parent c01f6aecf9
commit 63dc74e8a2
10 changed files with 191 additions and 95 deletions

12
.ENV.example Normal file
View 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=

View File

@@ -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
View File

@@ -2,4 +2,5 @@
__pycache__/
*.pyc
memes.db
.env
.env
.ENV

View File

@@ -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.

View File

@@ -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

View File

@@ -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"]

View File

@@ -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
View File

@@ -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>

View File

@@ -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"

View File

@@ -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: