PrivateBin mit Docker Compose: verschlüsselter Pastebin für Zugangsdaten
PrivateBin verschlüsselt Texte im Browser, der Server speichert nur Chiffretext. Die Anleitung zeigt Docker Compose mit dem offiziellen Image 2.1.0, schreibgeschütztem Dateisystem, conf.php, HTTPS-Pflicht, Backup, Restore und typischen Fehlern.
Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Zugangsdaten in E-Mails, Chats und Tickets bleiben dauerhaft in Postfächern und Verläufen. PrivateBin verschlüsselt im Browser, der Server speichert nur Chiffretext, der Schlüssel steht im Link hinter dem #. Mit Ablaufzeit und Löschung nach dem ersten Lesen verschwindet das Geheimnis.
Grundlage ist das offizielle Image privatebin/nginx-fpm-alpine mit Docker Compose. Beschrieben ist Version 2.1.0 vom 18.10.2026.
Nutzen und Grenzen von PrivateBin
PrivateBin ist ein Pastebin nach dem Zero-Knowledge-Prinzip: Der Browser verschlüsselt Text, Anhänge und Kommentare laut Projektdokumentation mit AES-256-GCM. Der Schlüssel (32 Zufallsbytes, Base58) steht im URL-Fragment hinter dem #, das Browser nicht senden.
Einsatzfelder:
- Erstpasswörter für neue Mitarbeitende (Ablauf nach einem Tag, Löschung nach dem Lesen)
- WLAN-Schlüssel, Recovery-Codes oder API-Tokens für Dienstleister
- Optional ein Passwort über einen zweiten Kanal
Grenzen laut Projekt (What it doesn't provide):
- Vertrauen in den Server bleibt nötig. Ein kompromittierter Server kann manipuliertes JavaScript ausliefern, gespeicherte Dokumente bleiben verschlüsselt, neue Eingaben nicht.
- Der Link ist der Schlüssel. Links in Tickets oder Chats sind so sensibel wie das Geheimnis.
- Metadaten sind unverschlüsselt: Zeiten, Format, Burn- und Diskussions-Flag, Kommentar-Icon. Zugriffe stehen in den Webserver-Logs.
- HTTPS ist Pflicht. Ohne TLS lässt sich das JavaScript manipulieren, Chromium stellt die Web Crypto API über HTTP nicht bereit.
PrivateBin dient der kurzlebigen Übergabe, dauerhafte Geheimnisse gehören in einen Tresor wie Vaultwarden oder Passbolt. Lizenz: Zlib/libpng (Bibliotheken GPLv2, BSD, MIT, Apache, CC-BY).
Voraussetzungen und Ressourcen
- Linux-Host (x86-64 oder ARM) mit Docker Engine und Compose v2, 1 CPU-Kern, 256 MB RAM.
- Subdomain wie
paste.example.demit DNS-Eintrag. - Reverse Proxy mit TLS (Caddy, Traefik, Nginx).
- Im Test belegte der Container 28 MiB RAM, das Image ist etwa 40 MB groß.
Das Image enthält Nginx und PHP-FPM (s6) auf Port 8080 mit HTTP. Speicher ist das Dateisystem unter /srv/data, eine Datenbank ist nicht nötig.
Plattformen, Architekturen und getestete Version
privatebin/nginx-fpm-alpine:2.1.0 gibt es für amd64, 386, arm/v6, arm/v7, arm64, ppc64le und s390x (Alpine 3.24, PHP 8.5). Schlankere Varianten: privatebin/fs, pdo, gcs, s3.
| Tag | Bedeutung laut Image-Doku | Empfehlung |
|---|---|---|
2.1.0 | PrivateBin 2.1.0, Rebuild bei Sicherheitsfixes | Produktion |
2.1.0-alpine3.24 | Unveränderliches Image | Strikte Freigabe |
stable | Neuestes Release | Automatische Updates |
latest / nightly | Neuestes Release, häufige Rebuilds | Nur mit Monitoring |
edge | Vorschau auf kommendes Alpine | Nicht für Produktion |
Getestet: 2.0.6 und 2.1.0 auf amd64 mit Docker 29.1.3 und Compose 2.40.3.
Vollständige compose.yaml und .env
Die Datei folgt der Image-Dokumentation: schreibgeschütztes Root-Dateisystem, tmpfs, Volume für /srv/data, conf.php schreibgeschützt, keine Capabilities, Speicherlimit.
name: privatebin
services:
privatebin:
image: privatebin/nginx-fpm-alpine:${PRIVATEBIN_TAG:-2.1.0}
container_name: privatebin
restart: unless-stopped
read_only: true
user: "65534:82"
ports:
- "${PRIVATEBIN_BIND:-127.0.0.1:8080}:8080"
environment:
TZ: ${TZ:-Europe/Berlin}
PHP_TZ: ${TZ:-Europe/Berlin}
volumes:
- privatebin-data:/srv/data
- ./conf.php:/srv/cfg/conf.php:ro
tmpfs:
- /tmp:nodev,noexec,mode=1777
- /run:nodev,exec,mode=1777
- /var/lib/nginx/tmp:nodev,noexec,mode=1777
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
mem_limit: 128m
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
volumes:
privatebin-data:# /opt/privatebin/.env
# Image-Version fest pinnen, Updates bewusst per Tag-Wechsel
PRIVATEBIN_TAG=2.1.0
# Nur lokal binden, der Reverse Proxy spricht mit diesem Port
PRIVATEBIN_BIND=127.0.0.1:8080
TZ=Europe/Berlin| Parameter | Wirkung |
|---|---|
read_only: true | Root-Dateisystem schreibgeschützt, beschreibbar nur /srv/data und tmpfs. |
user: "65534:82" | User des Images (nobody, www-data), explizit gesetzt gegen stilles Root. |
tmpfs | /run braucht exec für s6, /tmp und Nginx-Puffer nicht. |
cap_drop: ALL | Der Dienst bindet Port 8080 und braucht keine Sonderrechte. |
PHP_TZ | PHP-Zeitzone, später nicht ändern (Ablaufzeiten). |
./conf.php:/srv/cfg/conf.php:ro | Eigene Konfiguration, sonst Standardwerte. |
healthcheck | BusyBox-wget: Status in docker compose ps. |
docker compose config zeigt read_only: true: Image-Tag 2.0.6 und die Portbindung an 127.0.0.1.
conf.php: die wichtigsten Einstellungen
INI-Datei mit PHP-Endung, Basis ist die conf.sample.php des Release-Tags. Auszug mit den geänderten Werten:
[main]
name = "PrivateBin intern"
basepath = "https://paste.example.de/"
discussion = true
opendiscussion = false
password = true
fileupload = false
burnafterreadingselected = true
defaultformatter = "plaintext"
sizelimit = 10000000
qrcode = true
[expire]
default = "1day"
[traffic]
limit = 10
header = "X_FORWARDED_FOR"
[purge]
limit = 300
batchsize = 10
[model]
class = Filesystem
[model_options]
dir = PATH "data"| Option | Wirkung |
|---|---|
basepath | Öffentliche URL mit abschließendem Schrägstrich (Vorschaulinks). |
burnafterreadingselected | Löschen nach dem ersten Lesen als Vorauswahl, für Zugangsdaten sinnvoll. |
[expire] default | Vorausgewählte Ablaufzeit (5 Minuten bis 1 Jahr oder never, in [expire_options]). |
fileupload | Dateianhänge (Standard aus), Grenzen sizelimit 10 MB, Nginx 15M. |
[traffic] limit | Mindestabstand in Sekunden je IP. |
[traffic] header | Header mit Client-IP hinter dem Proxy. |
creators | Optional: Anlegen nur für bestimmte Netze. Außerdem discussion und [purge] nach Bedarf. |
ls -l conf.php zeigt -rw-r--r--; nach dem Start trägt die Seite den Namen aus name und nicht mehr „PrivateBin“.
Installation und Start
$ sudo mkdir -p /opt/privatebin
$ cd /opt/privatebin# conf.php aus dem Release-Tag$ sudo curl -fsSL -o conf.php https://raw.githubusercontent.com/PrivateBin/PrivateBin/2.1.0/cfg/conf.sample.php# lesbar für UID 65534$ sudo chmod 644 conf.php# compose.yaml und .env wie oben, dann starten$ sudo docker compose up -d
$ sudo docker compose psNach wenigen Sekunden zeigt docker compose ps healthy. Beim ersten Speichern legt PrivateBin salt.php in /srv/data an.
docker compose ps zeigt (healthy), das Log enthält ready to handle connections.
Funktionsprüfung und Healthcheck
# Status der Startseite$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/# Sicherheitsheader$ curl -sI http://127.0.0.1:8080/ | grep -i "content-security-policy\|x-frame-options\|referrer-policy"# Prozess-User und Rechte$ docker exec privatebin sh -c 'id; ls -la /srv/data'# Statistik$ docker exec -t privatebin administration --statisticsIm Test lieferte die Startseite 200 mit strenger Content-Security-Policy, X-Frame-Options: deny und Referrer-Policy: no-referrer: PHP lief als nobody:www-data und Schreibversuche in /var/www scheiterten.
Die JSON-API zeigt, dass der Server nichts Lesbares speichert (Dokumente im Test per Skript nach der Formatspezifikation angelegt):
# Abruf eines Dokuments über die JSON-API (liefert nur Chiffretext)
curl -s -H "X-Requested-With: JSONHttpRequest" "http://127.0.0.1:8080/?dae1fd2ec834b764"
# Antwort (gekürzt)
{"status":0,"id":"dae1fd2ec834b764","adata":[["BusBQkxRL5GtRw64NnDsVQ==","WbGiM6axdtQ=",100000,256,128,"aes","gcm","zlib"],"plaintext",0,0],"meta":{"time_to_live":86395},"v":2,"ct":"oI7lE0rjS4bZhvrK5ISyqNHKZjWtI614..."}Mit richtigem Schlüssel ließ sich der Text entschlüsseln, mit falschem oder ohne Passwort schlug die GCM-Authentifizierung fehl.
Die Startseite liefert HTTP 200 mit Content-Security-Policy, id im Container meldet uid=65534(nobody) gid=82(www-data).
Ablaufzeit, Burn after reading und Diskussionen
Abgelaufene Dokumente liefert PrivateBin nicht mehr aus und löscht sie beim Purge-Lauf. Mit Burn after reading entfällt das Dokument nach dem ersten Abruf.
Link-Scanner in Mail- und Chat-Lösungen können ein Dokument per JavaScript öffnen und verbrennen: Links mit Bindestrich nach dem # (?id#-schlüssel) verlangen eine Bestätigung.
Der Ersteller erhält einen Löschtoken (falsch: Wrong deletion token. Document was not deleted.).
Ein Burn-Dokument zeigt beim ersten Abruf den Inhalt und beim zweiten Document does not exist, has expired or has been deleted.
Persistente Daten und Rechte
Alles Bleibende liegt in /srv/data (Dokumente, salt.php: Limiter-Dateien), im Test drwx------ und -rw-r----- mit Eigentümer 65534:82.
Das Verzeichnis muss 65534:82 gehören. Named Volumes übernehmen die Rechte aus dem Image, bei Bind-Mounts wie ./data:/srv/data setzen Sie sie selbst (mit userns-remap den subuid-Bereich addieren).
# Bind-Mount: Eigentümer setzen$ sudo chown -R 65534:82 /opt/privatebin/data# Ursache im Log$ docker compose logs --no-log-prefix | grep -i "permission denied\|failed to store"Zur Pflege dient das Verwaltungsskript im Image:
# Abgelaufene Dokumente entfernen (z. B. nächtlich per Cron)$ docker exec privatebin administration --purge# Leere Verzeichnisse aufräumen$ docker exec privatebin administration --empty-dirs# Einzelnes Dokument löschen$ docker exec privatebin administration --delete dae1fd2ec834b764docker exec privatebin ls -ln /srv/data zeigt salt.php mit Besitzer 65534 82, administration --purge läuft ohne Fehler.
Netzwerkfreigabe, Reverse Proxy und TLS
Der Container bindet nur an 127.0.0.1:8080, nach außen spricht der Reverse Proxy mit gültigem Zertifikat. Das Projekt schreibt HTTPS vor (empfohlen: HSTS): Über HTTP warnt PrivateBin, und Chromium deaktiviert die Web Crypto API, ohne die weder Ver- noch Entschlüsselung geht. Lassen Sie httpwarning an.
Ein Minimalbeispiel mit Caddy, das Zertifikate automatisch bezieht:
# /etc/caddy/Caddyfile
paste.example.de {
encode gzip
header Strict-Transport-Security "max-age=31536000"
reverse_proxy 127.0.0.1:8080
}Caddy setzt X-Forwarded-For selbst (passend zu header = "X_FORWARDED_FOR"), bei Nginx gehört proxy_set_header X-Forwarded-For $remote_addr; in den Location-Block. Ein CDN mit eigenen Skripten bricht die Content-Security-Policy (siehe FAQ). Für interne Instanzen beschränkt creators das Anlegen auf Firmen- und VPN-Netze.
curl -sI https://paste.example.de/ liefert HTTP 200 mit Strict-Transport-Security, und ein Aufruf über die Server-IP auf Port 8080 von außen scheitert.
Backup und Restore
Sichern Sie /srv/data vollständig, auch salt.php (ohne ihn funktionieren Löschtokens nicht). Die Schlüssel liegen nie auf dem Server: Das Backup enthält nur Chiffretext.
$ cd /opt/privatebin# Anhalten$ docker compose stop# Volume als tar.gz auf den Host$ docker run --rm -v privatebin_privatebin-data:/srv/data:ro --entrypoint tar privatebin/nginx-fpm-alpine:2.1.0 -C /srv/data -czf - . > privatebin-data-$(date +%F).tar.gz
$ docker compose start# Inhalt prüfen$ tar -tzf privatebin-data-$(date +%F).tar.gz | headDer Restore spielt das Archiv in ein frisches Volume zurück. docker compose create legt es mit Eigentümer 65534:82 an, das Entpacken als derselbe User braucht keine Nacharbeit.
$ cd /opt/privatebin# ACHTUNG: löscht das aktuelle Volume mit allen Dokumenten$ docker compose down -v# Container und leeres Volume anlegen$ docker compose create# Archiv als 65534:82 zurückspielen$ docker run --rm -i --user 65534:82 -v privatebin_privatebin-data:/srv/data --entrypoint tar privatebin/nginx-fpm-alpine:2.1.0 -C /srv/data -xzf - < privatebin-data-2026-09-26.tar.gz
$ docker compose startIm Test (Backup, down -v: Restore) blieb salt.php identisch und ein Dokument mit seinem Link-Schlüssel entschlüsselbar. Eine tägliche Sicherung mit kurzer Aufbewahrung genügt, längere konserviert Chiffretexte, die gelöscht sein sollten.
tar -tzf listet ./salt.php, nach dem Restore öffnet ein vorher angelegter Link das Dokument wieder.
Updates und Rollback-Grenzen
Updates erfolgen über einen neuen Image-Tag. Sichern Sie vorher das Volume und vergleichen Sie die conf.sample.php des neuen Releases mit Ihrer conf.php, sonst bleiben neue Optionen auf Standardwerten.
$ cd /opt/privatebin# Nach Backup Tag anheben (2.1.1 ist ein Platzhalter)$ sed -i 's/^PRIVATEBIN_TAG=.*/PRIVATEBIN_TAG=2.1.1/' .env
$ docker compose pull
$ docker compose up -d
$ docker compose ps# Rollback$ sed -i 's/^PRIVATEBIN_TAG=.*/PRIVATEBIN_TAG=2.1.0/' .env
$ docker compose up -dIm Test lief der Wechsel von 2.0.6 auf 2.0.5 und zurück problemlos, aber nur innerhalb derselben Hauptversion. Größere Downgrades sind nicht unterstützt, sicher ist der Restore des Backups. Lesen Sie vor jedem Hauptversionswechsel die Versionshinweise.
Von 2.0 auf 2.1
PrivateBin 2.1.0 ersetzt laut Release Notes jQuery im Frontend durch reines JavaScript und entfernt dabei das Template bootstrap (Bootstrap 3) samt seinen Varianten. Haben Sie in Ihrer conf.php im Abschnitt [main] template = "bootstrap" oder eine Variante gesetzt, ändern Sie den Wert vor dem Update auf bootstrap5 oder kommentieren die Zeile aus. Die Liste availabletemplates[] enthält in der neuen conf.sample.php nur noch bootstrap5. Sicherheitsseitig verhindert 2.1.0 MIME-Raten bei JSON-Antworten (X-Content-Type-Options: nosniff), entfernt CSS-Klassen und Inline-Styles aus Markdown-HTML und behebt Probleme bei der Verarbeitung von Kommentardateien.
Im Test lief das Update von 2.0.6 auf 2.1.0 mit der compose.yaml und .env aus dieser Anleitung: Der Container meldete (healthy), ein unter 2.0.6 gespeichertes Dokument lieferte über die JSON-API denselben Chiffretext, neue Dokumente ließen sich speichern, und die Sicherheitsheader blieben unverändert. Mit template = "bootstrap" antwortete 2.1.0 dagegen mit HTTP 500, im Log stand eine Exception aus /srv/lib/View.php. Der Rollback auf 2.0.6 innerhalb derselben Hauptversion öffnete das Dokument ebenfalls, ebenso der Restore des unter 2.1.0 erstellten Backups nach down -v.
docker compose ps zeigt den neuen Tag und (healthy), ein vorher angelegtes Dokument ist weiter lesbar.
Typische Fehler mit Diagnose und Lösung
| Symptom | Ursache | Lösung |
|---|---|---|
Startseite lädt, Speichern meldet Error saving document. Sorry., im Log mkdir(): Permission denied und failed to store the server salt | Datenverzeichnis gehört nicht 65534:82 (Bind-Mounts), Healthcheck bleibt grün | chown -R 65534:82 auf das Verzeichnis, danach Neustart |
Eigene Einstellungen wirken nicht, Seite heißt wieder PrivateBin | conf.php für UID 65534 nicht lesbar (etwa chmod 600), PrivateBin nutzt still Standardwerte | chmod 644 conf.php: Container neu starten |
Please wait 10 seconds between each post. | Traffic-Limiter, ohne IP-Header teilen sich alle Nutzer ein Limit | header = "X_FORWARDED_FOR" setzen, bei Bedarf exempted für Netze |
Document does not exist, has expired or has been deleted. | Dokument abgelaufen, per Burn after reading bereits gelesen oder gelöscht | Neu anlegen, bei Mail-Links #- nutzen |
| Warnung insecure connection oder Meldung PrivateBin requires a modern browser to work | Zugriff über HTTP, Web Crypto API oder WebAssembly nicht verfügbar | Nur über HTTPS anbieten, Browser aktualisieren |
| Text lässt sich trotz Link nicht entschlüsseln | Link gekürzt (Fragment fehlt), falsches oder fehlendes Passwort | Vollständigen Link inklusive #-Teil übergeben, Passwort prüfen |
Nach dem Update auf 2.1 liefert die Startseite HTTP 500, im Log eine Exception aus /srv/lib/View.php | In der conf.php steht noch template = "bootstrap" oder eine Variante, die 2.1.0 entfernt hat | Wert auf bootstrap5 setzen oder Zeile auskommentieren, Container neu starten |
administration --statistics zeigt PHP Deprecated-Zeilen | PHP-8.5-Hinweis, ohne Einfluss | Ignorieren |
Saubere Deinstallation
Warnung: Die Befehle löschen alle Dokumente endgültig, verteilte Links werden ungültig.
$ cd /opt/privatebin# Container, Netzwerk UND Volume entfernen$ docker compose down -v --remove-orphans# Image namentlich entfernen$ docker rmi privatebin/nginx-fpm-alpine:2.1.0# Projektverzeichnis löschen$ cd / && sudo rm -rf /opt/privatebinDanach sind docker ps -a --filter name=privatebin und docker volume ls --filter name=privatebin leer. Entfernen Sie Proxy- und DNS-Eintrag.
Passende Anleitungen auf S-EDV
- Caddy als Reverse Proxy mit automatischem HTTPS
- Docker Compose absichern: Secrets, Healthchecks, Non-Root
- Vaultwarden härten, sichern und mit Fail2ban schützen
Quellen
- PrivateBin: Projekt-Repository und README (What it doesn't provide)
- PrivateBin: offizielles Docker-Image, Dockerfile und Dokumentation
- Docker Hub: privatebin/nginx-fpm-alpine, Tags und Architekturen
- PrivateBin: conf.sample.php mit allen Optionen
- PrivateBin Wiki: FAQ zu HTTPS, Web Crypto API und Metadaten
- PrivateBin Wiki: Verschlüsselungsformat und URL-Aufbau
- PrivateBin Wiki: JSON-API
- PrivateBin: Release 2.1.0
Grundlagen dazu: Docker und Docker Compose auf Linux installieren, Linux-Server absichern: UFW und Fail2ban.


