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 |
none | 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 |
none | 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 |
none | 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, so 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 request.client.host (and in the access log, if you enabled it, see
below). If you run the server outside of Docker, pass those flags yourself
(uvicorn app.main:app --proxy-headers --forwarded-allow-ips="*").
Privacy notice identity¶
The app serves a /privacy notice grounded in what the code actually does.
Three variables identify the controller on it: required for a public
GDPR/nLPD deployment, neutral fallback wording otherwise:
Access logging¶
The Docker image starts uvicorn with --no-access-log by default. An
access log line pairs a client IP with a paste ID and a timestamp: exactly
the correlation the rest of Ghostbit is designed to avoid, and the basis for
the "no IP addresses are ever logged" statement in the docs.
Set ACCESS_LOG=true to turn access logging back on for debugging. Be aware
of what that means: combined with TRUST_PROXY_HEADERS=true you are then
recording real client IPs next to capability URLs, which makes you a
processor of personal data under the GDPR/nLPD: put a retention on those
logs and say so in your privacy notice. Outside of Docker, uvicorn logs
accesses unless you pass --no-access-log yourself.
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 endpoints for operators: /healthz and /readyz
are always unauthenticated; /metrics is unauthenticated unless
METRICS_TOKEN is set:
| 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 carries no per-paste data (only aggregate counters
and a latency histogram, with route templates as labels, never real paste
IDs), but it does reveal usage rates and the exact Python version. On a
public deployment, set METRICS_TOKEN and give Prometheus the same value:
Requests without Authorization: Bearer <token> then get 401. Leaving the
variable empty keeps the endpoint open, which is fine when it is only
reachable from a private network, or restrict it at the reverse proxy.
Exposed metrics:
| Metric | Type | Labels |
|---|---|---|
ghostbit_pastes_created_total |
counter | has_password |
ghostbit_pastes_viewed_total |
counter | burned |
ghostbit_pastes_deleted_total |
counter | n/a |
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 | n/a |
/healthz, /readyz and /metrics are excluded from the HTTP latency histogram so probe
traffic doesn't skew P99.