penguin is distributed as a Docker image through GitHub Container Registry.
Pull the latest release and supply the Discord credentials through environment variables:
docker pull ghcr.io/mangoplex/penguin:latest
docker run --rm \
-e DISCORD_TOKEN='your-discord-token' \
-e GUILD_ID='your-discord-guild-id' \
ghcr.io/mangoplex/penguin:latestUse a version tag such as v1.2.7 instead of latest to pin a release.
The container runs as an unprivileged user and does not store credentials.
Features that aren't security-sensitive (e.g. which channel the Vietnam price
tracker posts to) are controlled by a config.toml file rather than
environment variables. Copy config.sample.toml to config.toml, edit it,
and mount it into the container:
docker run --rm \
-e DISCORD_TOKEN='your-discord-token' \
-e GUILD_ID='your-discord-guild-id' \
-v "$(pwd)/config.toml:/config.toml:ro" \
ghcr.io/mangoplex/penguin:latestThe bot polls config.toml for changes every 10 seconds and reloads it
automatically — no restart needed. The bot reads config.toml from its working
directory, which is / in the container.
Set fuel_channel_id and/or electricity_channel_id under [price_tracker]
in config.toml to Discord channel IDs. Leave a key unset to disable that
tracker.
- Fuel (via giaxanghomnay.com) and electricity (via quanlydien.com, which tracks official Ministry of Industry and Trade decisions) are both checked once a week, Thursday 15:05 local time (UTC+7), right after the fixed weekly adjustment slot.
Fuel posts a weekly embed every Thursday, including each item's change since the prior published snapshot, even if prices are unchanged. Electricity stores its first successful response as a baseline, then posts only when a tariff changes.
Failed fetches retry after 1, 5, 15, and 60 minutes, then hourly for up to 24 hours from the original check. Each attempt has a 30-second timeout. Disabling a tracker cancels its pending fetch/retries. Config reloads do not start overlapping retry chains. Discord send failures are logged without automatic resend because a failed response can still mean a delivered message.
Set channel_id under [birthday] to the Discord channel for announcements,
and list people in people. Leave channel_id unset to disable the feature.
[birthday]
channel_id = 123456789012345678
people = [
{ name = "Alice", month = 1, day = 15 },
# user_id is optional and mentions them instead of printing the name.
{ name = "Bob", month = 3, day = 8, user_id = 123456789012345678 },
]An announcement is posted every day at announce_hour (0-23, default 8) local
time (UTC+7) for everyone whose birthday it is. February 29 birthdays are
celebrated on February 28 in years without a leap day. Entries with an invalid
date, or an announce_hour outside 0-23, are ignored with a warning. Edits
apply on the next config reload; a restart or reload never re-announces, so a
restart after the announce hour skips that day's post.
Set VALKEY_URL (e.g. redis://localhost:6379) to have the price/tariff
trackers persist their last-seen snapshot in Valkey (or any Redis-compatible
store) instead of only in memory. This survives bot restarts, so a restart
around the weekly fuel check doesn't lose the baseline needed to detect a
change. Each snapshot is stored with a last_updated timestamp.
Fuel stores its published Thursday period with the snapshot. Startup and config
reloads recover a missing weekly post only through Friday 15:05 UTC+7; already
published periods are skipped. Older cache entries infer their period from
last_updated until the next successful post writes an explicit period.
Electricity uses its first successful snapshot as a silent baseline and posts
only later changes. Leave VALKEY_URL unset to keep the snapshots in memory;
restarting then creates a new baseline. Duplicate suppression across restarts
requires working Valkey persistence and a single bot instance. A crash after
Discord accepts a message but before the cache is saved can still cause a
duplicate after restart; exactly-once delivery is not guaranteed.
docker build -t penguin .Pushing a tag named v<version> starts the GitHub Actions workflow. The tag
must match the package version in Cargo.toml. A successful build publishes
ghcr.io/mangoplex/penguin:<tag> and updates
ghcr.io/mangoplex/penguin:latest.