A FastAPI-based web service that generates embeddable HTML for Minecraft server MOTDs (Message of the Day). Fetches live server status, parses Minecraft formatting codes, and generates styled HTML embeds.
Note
I did not write a single line of code in this project. I wanted to test out the capabilities of claude code myself and I needed an app like this.
- Live Server Status: Queries Minecraft servers in real-time
- MOTD Parsing: Converts Minecraft Β§ formatting codes to HTML/CSS
- Caching: 30-second cache to reduce server load
- Embeddable: Generate HTML embeds for use in websites
- Docker Support: Production-ready containerized deployment
| π¨ HTML embed | Self-contained <iframe>-ready document with Minecraft-style colours & formatting |
| πΌοΈ PNG image | 500Γ90 rasterised image with server icon, name, and MOTD |
| β‘ TTL cache | 30-second per-IP cache β one live query per server per period |
| π‘οΈ SSRF-safe | Private/loopback IPs and non-Minecraft ports are blocked |
| π Prometheus | /metrics exposes cache hits, misses, and request latency |
| π Security headers | CSP, HSTS, X-Frame-Options, X-Content-Type-Options on every response |
| π³ Docker-first | Multi-stage image (~200 MB), runs as non-root, read-only FS |
docker run -d \
--name motd-embed-api \
-p 8000:8000 \
ghcr.io/ajxd2/motd-embed-api:latestThen open:
http://localhost:8000/v1/server/mc.hypixel.net/embed
http://localhost:8000/v1/server/mc.hypixel.net/image
http://localhost:8000/health
http://localhost:8000/docs
# 1. Copy the example env file and edit it
cp .env.example .env
# 2. Start the service (pulls image from GHCR)
docker compose up -d
# 3. Tail logs
docker compose logs -f
# 4. Stop
docker compose downLocal build instead of pulling?
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
Interactive docs are available at /docs (Swagger UI) and /redoc.
Returns a self-contained HTML document rendering the server MOTD with Minecraft colour codes.
Suitable for use in an <iframe>.
curl http://localhost:8000/v1/server/mc.hypixel.net/embed
curl http://localhost:8000/v1/server/play.example.com:25565/embed| Status | Meaning |
|---|---|
200 |
HTML embed (server may be offline β that's still a 200) |
400 |
Invalid address, private IP, or blocked port |
429 |
Rate limit exceeded |
Returns a 500Γ90 PNG image of the MOTD embed.
curl -o motd.png http://localhost:8000/v1/server/mc.hypixel.net/image{"status": "ok"}Prometheus text-format scrape endpoint. Enabled by default β restrict access at your ingress/network layer in production (no built-in auth).
All settings are loaded from environment variables or a .env file.
cp .env.example .env
$EDITOR .env| Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
Bind address |
PORT |
8000 |
Listen port |
RELOAD |
false |
uvicorn auto-reload β dev only |
LOG_LEVEL |
info |
debug / info / warning / error / critical |
ALLOWED_ORIGINS |
* |
Comma-separated CORS origins. Set to your domain(s) in production |
CACHE_TTL_SECONDS |
30 |
How long server info is cached |
CACHE_MAXSIZE |
1000 |
Maximum number of cached entries (LRU eviction) |
SERVER_TIMEOUT |
5.0 |
Minecraft query timeout in seconds |
RATE_LIMIT_EMBED |
30/minute |
Per-IP rate limit for /embed |
RATE_LIMIT_IMAGE |
10/minute |
Per-IP rate limit for /image |
RATE_LIMIT_HEALTH |
60/minute |
Per-IP rate limit for /health |
MOTD_MAX_LENGTH |
2048 |
Maximum MOTD character length |
FAVICON_MAX_BYTES |
150000 |
Maximum favicon data URI size in bytes |
STATIC_DIR |
(auto) | Override path to static/ assets β set automatically to /app/static in Docker |
METRICS_ENABLED |
true |
Expose /metrics endpoint |
Rate limit strings must follow the format <count>/<period> where period is second, minute, hour, or day.
Invalid values cause the application to fail at startup.
- Set
ALLOWED_ORIGINSto your specific domain(s) - Set
RELOAD=false - Confirm
LOG_LEVEL=info(orwarning) - Restrict
/metricsat your reverse proxy / firewall - Put a TLS-terminating reverse proxy (nginx / Traefik / Caddy) in front
- Set up log aggregation (Loki, ELK, CloudWatchβ¦)
upstream motd_api {
server 127.0.0.1:8000;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.example.com.crt;
ssl_certificate_key /etc/ssl/private/api.example.com.key;
location / {
proxy_pass http://motd_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
}
# Cache static assets at the edge
location /static/ {
proxy_pass http://motd_api/static/;
add_header Cache-Control "public, max-age=86400";
}
# Block external access to metrics
location /metrics {
deny all;
}
}Add to the api service in docker-compose.yml:
labels:
- "traefik.enable=true"
- "traefik.http.routers.motd-api.rule=Host(`api.example.com`)"
- "traefik.http.routers.motd-api.entrypoints=websecure"
- "traefik.http.routers.motd-api.tls.certresolver=letsencrypt"
- "traefik.http.services.motd-api.loadbalancer.server.port=8000"
# Block /metrics from the outside
- "traefik.http.middlewares.strip-metrics.redirectregex.regex=^.*/metrics$$"
- "traefik.http.middlewares.strip-metrics.redirectregex.replacement=/"
- "traefik.http.routers.motd-api.middlewares=strip-metrics"# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install dependencies (including dev)
uv sync
# Run with auto-reload
RELOAD=true uv run motd-embed-api
# Run tests
uv run pytest
# Run tests with coverage
uv run pytest --cov=motd_embed_api --cov-report=term-missing
# Lint & format
uv run ruff check src/ tests/
uv run ruff format src/ tests/motd-embed-api/
βββ .github/
β βββ workflows/
β βββ ci.yml # Test β lint β build & push to GHCR
βββ src/
β βββ motd_embed_api/
β βββ config.py # Pydantic-settings (all env vars)
β βββ main.py # FastAPI app, lifespan, routes
β βββ server.py # Minecraft status queries (SSRF-safe)
β βββ motd_parser.py # Β§ code β HTML spans
β βββ html_generator.py # Self-contained HTML embed
β βββ image_generator.py # 500Γ90 PNG renderer (Pillow)
β βββ cache.py # Thread-safe TTL+LRU cache
β βββ metrics.py # Prometheus counters & histograms
β βββ middleware.py # RequestID, security headers, JSON logging
βββ static/
β βββ motd-embed.css
β βββ minecraft-background-dark-160x-K223BAAL.png
β βββ unknown_server.jpg
β βββ Minecraft.ttf # optional β place here for authentic font
βββ tests/ # 111 pytest tests
βββ Dockerfile # Multi-stage, non-root, read-only FS
βββ docker-compose.yml # Pulls from GHCR
βββ docker-compose.build.yml # Override for local builds
βββ pyproject.toml
βββ .env.example
The GitHub Actions workflow at .github/workflows/ci.yml runs on every push and pull request:
- Test β runs the full pytest suite on Python 3.12 and 3.13
- Lint β ruff check + ruff format
- Publish β builds a multi-arch (
linux/amd64,linux/arm64) image and pushes to GHCR
Image tags produced:
| Trigger | Tags |
|---|---|
Push to main |
latest, main, sha-<short> |
Tag v1.2.3 |
1.2.3, 1.2, sha-<short> |
Pull a specific version:
docker pull ghcr.io/ajxd2/motd-embed-api:1.2.3
docker pull ghcr.io/ajxd2/motd-embed-api:sha-a1b2c3dContainer won't start
docker compose logs api
# Common causes: port 8000 in use, invalid RATE_LIMIT_* format, missing static dirCORS errors in the browser
Make sure ALLOWED_ORIGINS matches your domain exactly β including the protocol (https://) and no trailing slash.
Slow responses
- Check if the target Minecraft server is reachable
- Increase
SERVER_TIMEOUTif the server is slow to respond - Cache hits should be near-instant; check
/metricsforcache_hits_totalvscache_misses_total
/metrics returns 404
Set METRICS_ENABLED=true in your environment (it is true by default).
MIT β see LICENSE.