Running in production¶
What to get right before it runs for real, then setups for systemd, Docker and Kubernetes.
Before it runs for real¶
The token and the passphrase come from files or a secrets manager, not from the code. Use
PGPCHAT_BOT_TOKEN_FILEandPGPCHAT_BOT_KEY_PASSPHRASE_FILE.SQLiteStorageon a local disk, in a folder only the bot’s user can read.One process per bot (or several webhook workers on the same database file).
Logs at
INFO. Your handlers don’t log message text from encrypted chats.The bot’s private key is backed up, offline, with its passphrase stored separately.
The server’s clock is synchronised (NTP): signatures carry times.
For a webhook: HTTPS in front, a secret set,
SQLiteStorage.You know how to
revoke the tokenand rotate the key if they leak.You checked the Security page.
systemd¶
/etc/systemd/system/weatherbot.service:
[Unit]
Description=Weather bot for PGP Chat
After=network-online.target
Wants=network-online.target
[Service]
User=weatherbot
WorkingDirectory=/opt/weatherbot
ExecStart=/opt/weatherbot/.venv/bin/python bot.py
Environment=PGPCHAT_BOT_TOKEN_FILE=%d/token
Environment=PGPCHAT_BOT_KEY_FILE=/opt/weatherbot/bot-key.asc
Environment=PGPCHAT_BOT_KEY_PASSPHRASE_FILE=%d/passphrase
LoadCredential=token:/etc/weatherbot/token
LoadCredential=passphrase:/etc/weatherbot/passphrase
StateDirectory=weatherbot
Restart=on-failure
RestartSec=5
TimeoutStopSec=45
# hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/weatherbot
LimitCORE=0
[Install]
WantedBy=multi-user.target
In bot.py, use SQLiteStorage("/var/lib/weatherbot/bot.db"). LoadCredential gives the
secrets to the service only (%d is the folder systemd puts them in), and LimitCORE=0
keeps the unlocked key out of core dumps. TimeoutStopSec should be longer than the bot’s
shutdown_timeout (30 seconds), so handlers can finish when systemd stops the bot with
SIGTERM.
sudo systemctl enable --now weatherbot
journalctl -u weatherbot -f
Docker¶
FROM python:3.14-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --uid 10001 bot
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py .
USER bot
VOLUME /data
CMD ["python", "bot.py"]
requirements.txt pins the minor version, PGPCbot>=0.1,<0.2. Install ca-certificates: httpx2 uses the system’s
certificate store.
# compose.yaml
services:
weatherbot:
build: .
restart: unless-stopped
stop_grace_period: 45s
environment:
PGPCHAT_BOT_TOKEN_FILE: /run/secrets/bot_token
PGPCHAT_BOT_KEY_FILE: /run/secrets/bot_key
PGPCHAT_BOT_KEY_PASSPHRASE_FILE: /run/secrets/bot_key_passphrase
secrets: [bot_token, bot_key, bot_key_passphrase]
volumes:
- botdata:/data
secrets:
bot_token:
file: ./secrets/token
bot_key:
file: ./secrets/bot-key.asc
bot_key_passphrase:
file: ./secrets/passphrase
volumes:
botdata:
docker stop sends SIGTERM, which the bot handles; stop_grace_period gives it time to
finish. Keep the database on a named volume (/data/bot.db).
Kubernetes¶
Run one replica for a polling bot (
replicas: 1,strategy: Recreate): two pods polling the same bot take turns and get nothing done. WithSQLiteStorageon a volume, the second pod would also be refused by the lock.Put the token, the key and the passphrase in a
Secret, mounted as files, and point the_FILEvariables at them.terminationGracePeriodSeconds: 45.The database needs a
ReadWriteOncevolume.
A webhook bot should also run in one pod. Inside it, several web workers can share one database file. Several pods with separate databases would each see only part of the updates: the order inside a chat and the removal of repeated deliveries would no longer hold.
Webhook behind a reverse proxy¶
See Webhooks. The short version: HTTPS at the
proxy, proxy_read_timeout above 10 seconds, no redirects, and uvicorn listening on
127.0.0.1 only.
Logs and monitoring¶
The library logs under
pgpcbot.*. Useful lines atINFO: start-up (running as @...), webhook registration, published keys, stops. AtWARNING: retries, a paused bot, members left out of an encrypted message, failed signature checks with a reason.pgpcbot whoamiandpgpcbot webhook infoshow the queue on the server.await bot.failed_updates()lists what failed; alert on it if your bot matters.A
/healthroute in your web app can check that the bot started:bot.me is not None.
Backups¶
The private key: a copy offline, with the passphrase stored somewhere else.
The database:
sqlite3 bot.db ".backup copy.db"while running. It holds pending and failed updates and the pinned keys. Losing it costs the pins and the failed list; updates that were pending are lost only if they were already confirmed to the server.
Upgrading¶
Read the changelog and the versioning rules.
Stop the bot, upgrade (
pip install -U PGPCbot), start it. Pending updates inSQLiteStoragerun after the restart.