Configuration¶
All configuration is done via environment variables (or a .env file at the project root).
Variables¶
| Variable | Default | Description |
|---|---|---|
STORAGE_BACKEND |
sqlite |
sqlite or redis |
SQLITE_PATH |
./ghostbit.db |
Path to the SQLite database file (Docker overrides to /data/ghostbit.db) |
SQLITE_POOL_SIZE |
5 |
Number of pooled SQLite connections. WAL enables parallel readers; raise if ghostbit_sqlite_pool_wait_seconds shows contention. |
REDIS_URL |
redis://localhost:6379 |
Redis connection URL |
REDIS_PASSWORD |
— | Redis password. Injected into REDIS_URL automatically. Ignored if the URL already contains credentials. |
MAX_PASTE_SIZE |
524288 |
Maximum paste size in bytes (default: 512 KB) |
PORT |
8000 |
HTTP port the server listens on |
WEBHOOK_SECRET |
— | If set, signs webhook deliveries with HMAC-SHA256 (X-Ghostbit-Signature) |
RATE_LIMIT_CREATE |
30/minute |
Rate limit for paste creation per IP (POST /api/v1/pastes) |
RATE_LIMIT_VIEW |
120/minute |
Rate limit for paste reads per IP (GET /api/v1/pastes/{id}) |
TRUST_PROXY_HEADERS |
false |
Read client IP from X-Forwarded-For for rate-limiting. See the Reverse proxy note below before enabling. |
BASE_URL |
— | Public base URL (e.g. https://paste.example.com). Builds the absolute URLs in social-preview meta tags (og:image, og:url). Derived from the request when unset — see Reverse proxy. |
No server-side encryption key
All encryption is performed client-side (AES-256-GCM in the browser or CLI). The server never sees plaintext — no ENCRYPTION_KEY is needed.
Storage backends¶
Default backend, no extra dependencies. An hourly cleanup task removes expired pastes.
Better for high-traffic instances. TTL is handled natively by Redis EXPIRE.
Start with the Redis profile:
With a password — two equivalent approaches:
When REDIS_PASSWORD is set, the managed Redis container is started with --requirepass automatically.
Rate limiting¶
Rate limits are applied per client IP using slowapi.
The format is "N/period" where period is second, minute, or hour.
When a limit is exceeded the API returns 429 Too Many Requests.
Reverse proxy¶
When Ghostbit sits behind a reverse proxy (Nginx, Caddy, Traefik, Cloudflare…),
the direct peer address is the proxy's, not the client's — so per-IP rate limits
would apply globally. Enable TRUST_PROXY_HEADERS=true to key limits on
X-Forwarded-For instead.
The rightmost XFF entry is used: that's the IP appended by the hop nearest
to Ghostbit, which is the only entry you can trust. Leftmost entries are
client-controlled (a malicious client could forge X-Forwarded-For: 1.2.3.4 to
impersonate another IP).
When TRUST_PROXY_HEADERS=true, the Docker image also starts uvicorn with
--proxy-headers so the real client IP is substituted for the proxy's address
in the access log and in request.client.host. Otherwise the uvicorn logs
would show only the reverse proxy's internal IP, which is not useful for
incident triage. If you run the server outside of Docker, pass those flags
yourself (uvicorn app.main:app --proxy-headers --forwarded-allow-ips="*").
Absolute URLs for link previews¶
Ghostbit puts absolute URLs in its social-preview <meta> tags (og:image,
og:url) so link-unfurl bots — iMessage, Slack, Discord… — can fetch the
preview banner. They are derived from the incoming request by default, which
is correct for direct exposure and for proxies that forward the scheme and
Host header. If a TLS-terminating proxy would otherwise make the app emit
http:// URLs, pin the public origin explicitly:
A malformed value (missing http:// / https:// scheme) fails fast at startup.
Multi-hop setups (CDN → LB → app)
If more than one trusted proxy sits between the client and Ghostbit, the
rightmost entry will be the nearest proxy (not the client), and rate
limits will apply to that proxy globally. Collapse the chain at the
nearest proxy (Nginx: set_real_ip_from <CDN-range>; real_ip_header
X-Forwarded-For;) so only one hop contributes by the time Ghostbit
reads the header.
Observability¶
Ghostbit exposes three unauthenticated endpoints for operators:
| Endpoint | Format | Purpose |
|---|---|---|
/healthz |
JSON ({"status": "ok"}) |
Liveness probe — always 200 while the process is alive. Does not touch the storage backend, so a Redis outage will not trigger a container restart. |
/readyz |
JSON ({"status": "ok"} or "error") |
Readiness probe — pings the storage backend. Returns 503 if it doesn't answer, so the ingress / load balancer drains traffic until the dependency recovers. |
/metrics |
Prometheus text | Scrape target for Prometheus/Grafana/Alertmanager. |
The /metrics endpoint has no sensitive data (only aggregate counters and a
latency histogram) but you can still restrict it to your scraper's IP at the
reverse proxy if you prefer.
Exposed metrics:
| Metric | Type | Labels |
|---|---|---|
ghostbit_pastes_created_total |
counter | has_password |
ghostbit_pastes_viewed_total |
counter | burned |
ghostbit_pastes_deleted_total |
counter | — |
ghostbit_webhook_deliveries_total |
counter | outcome (ok | timeout | error | ssrf_blocked) |
ghostbit_http_request_duration_seconds |
histogram | method, path, status |
ghostbit_sqlite_pool_wait_seconds |
histogram | — |
/healthz, /readyz and /metrics are excluded from the HTTP latency histogram so probe
traffic doesn't skew P99.