Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Docker 26.09.2026 · 10 min Lesezeit Geprüft 20.10.2026

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

Titelbild: PrivateBin mit Docker Compose, Verschlüsselt, Einmal lesen, Teilen

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.de mit 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.

TagBedeutung laut Image-DokuEmpfehlung
2.1.0PrivateBin 2.1.0, Rebuild bei SicherheitsfixesProduktion
2.1.0-alpine3.24Unveränderliches ImageStrikte Freigabe
stableNeuestes ReleaseAutomatische Updates
latest / nightlyNeuestes Release, häufige RebuildsNur mit Monitoring
edgeVorschau auf kommendes AlpineNicht 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
ParameterWirkung
read_only: trueRoot-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: ALLDer Dienst bindet Port 8080 und braucht keine Sonderrechte.
PHP_TZPHP-Zeitzone, später nicht ändern (Ablaufzeiten).
./conf.php:/srv/cfg/conf.php:roEigene Konfiguration, sonst Standardwerte.
healthcheckBusyBox-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"
OptionWirkung
basepathÖffentliche URL mit abschließendem Schrägstrich (Vorschaulinks).
burnafterreadingselectedLöschen nach dem ersten Lesen als Vorauswahl, für Zugangsdaten sinnvoll.
[expire] defaultVorausgewählte Ablaufzeit (5 Minuten bis 1 Jahr oder never, in [expire_options]).
fileuploadDateianhänge (Standard aus), Grenzen sizelimit 10 MB, Nginx 15M.
[traffic] limitMindestabstand in Sekunden je IP.
[traffic] headerHeader mit Client-IP hinter dem Proxy.
creatorsOptional: 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 ps

Nach 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 --statistics

Im 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 dae1fd2ec834b764

docker 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 | head

Der 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 start

Im 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 -d

Im 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

SymptomUrsacheLösung
Startseite lädt, Speichern meldet Error saving document. Sorry., im Log mkdir(): Permission denied und failed to store the server saltDatenverzeichnis gehört nicht 65534:82 (Bind-Mounts), Healthcheck bleibt grünchown -R 65534:82 auf das Verzeichnis, danach Neustart
Eigene Einstellungen wirken nicht, Seite heißt wieder PrivateBinconf.php für UID 65534 nicht lesbar (etwa chmod 600), PrivateBin nutzt still Standardwertechmod 644 conf.php: Container neu starten
Please wait 10 seconds between each post.Traffic-Limiter, ohne IP-Header teilen sich alle Nutzer ein Limitheader = "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öschtNeu anlegen, bei Mail-Links #- nutzen
Warnung insecure connection oder Meldung PrivateBin requires a modern browser to workZugriff über HTTP, Web Crypto API oder WebAssembly nicht verfügbarNur über HTTPS anbieten, Browser aktualisieren
Text lässt sich trotz Link nicht entschlüsselnLink gekürzt (Fragment fehlt), falsches oder fehlendes PasswortVollstä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.phpIn der conf.php steht noch template = "bootstrap" oder eine Variante, die 2.1.0 entfernt hatWert auf bootstrap5 setzen oder Zeile auskommentieren, Container neu starten
administration --statistics zeigt PHP Deprecated-ZeilenPHP-8.5-Hinweis, ohne EinflussIgnorieren

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/privatebin

Danach 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

Quellen

Grundlagen dazu: Docker und Docker Compose auf Linux installieren, Linux-Server absichern: UFW und Fail2ban.

PrivateBinDocker ComposeSelfhostingVerschlüsselungZugangsdatenPastebinSicherheit