Skip to content

Self-hosting

Docker images

Registry Image
Docker Hub stackopshq/ghostbit
GHCR ghcr.io/stackopshq/ghostbit

Available tags: sha-<short-commit> (amd64), one per commit on main: pin by digest for deployments. The moving tags (latest, edge, semver) are frozen at the last multi-arch build and will move again once the build farm regains arm64 support; do not use them for new installs.


SQLite

1
2
3
4
git clone https://github.com/stackopshq/ghostbit
cd ghostbit
cp .env.example .env
docker compose up -d

Redis

STORAGE_BACKEND=redis docker compose --profile redis up -d

With a password:

STORAGE_BACKEND=redis REDIS_PASSWORD=mysecret docker compose --profile redis up -d

The REDIS_PASSWORD variable is forwarded to both the Redis container (--requirepass) and the app (injected into the connection URL). If you connect to an external Redis that already has credentials in REDIS_URL, leave REDIS_PASSWORD unset.

Redis data is persisted via AOF + RDB snapshots on a named Docker volume.


HTTPS and Web Crypto

Ghostbit uses the Web Crypto API for all client-side encryption. Browsers only expose this API in a Secure Context, which means:

  • https://: always secure ✓
  • http://localhost or http://127.0.0.1: granted by the browser for local development ✓
  • http:// on any other host: not a Secure Context, encryption is blocked ✗

Reverse proxy with HTTPS termination

The most common production setup (HTTPS on the proxy, plain HTTP internally) works perfectly:

Browser ──HTTPS──▶ Nginx / Caddy ──HTTP──▶ Ghostbit :8000

The browser sees https://paste.example.com → Secure Context → Web Crypto works. The internal HTTP leg between proxy and app is invisible to the browser.

Never expose the app directly over HTTP in production

If users can reach Ghostbit via http:// (bypassing the proxy), the browser will refuse to encrypt and the app will show an error. Always ensure HTTPS is enforced at the proxy level.


Nginx reverse proxy

server {
    listen 443 ssl;
    server_name paste.example.com;

    ssl_certificate     /etc/letsencrypt/live/paste.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/paste.example.com/privkey.pem;

    client_max_body_size 10M;

    location / {
        proxy_pass         http://127.0.0.1:8000;
        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;
    }
}

# Redirect HTTP → HTTPS
server {
    listen 80;
    server_name paste.example.com;
    return 301 https://$host$request_uri;
}

HTTPS is required in production

crypto.subtle (Web Crypto API) requires a Secure Context. Without HTTPS, the browser will refuse to encrypt pastes.


Caddy reverse proxy

1
2
3
paste.example.com {
    reverse_proxy localhost:8000
}

Caddy handles HTTPS automatically via Let's Encrypt.


Data persistence

Backend Persistence
SQLite Named Docker volume ghostbit_data mounted at /data
Redis Named Docker volume redis_data, AOF + RDB snapshots

Backup SQLite

docker compose exec ghostbit sqlite3 /data/ghostbit.db ".backup /data/ghostbit.db.bak"
docker compose cp ghostbit:/data/ghostbit.db.bak ./backup.db

Backup Redis

1
2
3
4
5
# Without password
docker compose exec redis redis-cli BGSAVE
# With password
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" BGSAVE
docker compose cp redis:/data/dump.rdb ./dump.rdb

Updating

git pull
docker compose up -d --build

Privacy

  • No IP addresses or User-Agent strings are logged by default: the Docker image starts uvicorn with --no-access-log. Setting ACCESS_LOG=true (or running uvicorn yourself without --no-access-log) re-enables access lines that pair client IPs with paste IDs, see Configuration → Access logging
  • Paste IDs are secrets.token_urlsafe(6): random, non-sequential
  • Burn-after-read fires only on API reads, not on HTML page loads
  • The server never sees plaintext or passwords
  • Backups (scripts/backup.sh) are age-encrypted and pruned after BACKUP_RETENTION_DAYS (30 by default)
  • The in-app /privacy notice names your instance's controller once you set PRIVACY_OPERATOR, PRIVACY_CONTACT_URL and PRIVACY_AUTHORITY: required for a public GDPR/nLPD-compliant deployment. The full checklist lives in the README under "Compliant deployments (GDPR / nLPD)"