Penpot mit Docker installieren: Open-Source-Designtool als Figma-Alternative selbst hosten
Penpot 2.18 mit Docker Compose selbst hosten: offizielles Compose-File mit festen Tags, Secret Key, erster Nutzer, gesperrte Registrierung, HTTPS über Reverse Proxy, Updates und Backups.
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

Penpot ist eine quelloffene Design- und Prototyping-Plattform auf SVG-Basis. Sie vereint UI/UX-Design, Prototyping und Code-Inspektion (CSS, HTML) und steht unter der Lizenz MPL-2.0. Diese Anleitung richtet Penpot 2.18 nach dem offiziellen Compose-File auf einem Linux-Server, einer VM oder einem NAS ein, mit festen Image-Tags und HTTPS über einen Reverse Proxy.
Voraussetzungen
- Docker Engine >= 20.10 und Docker Compose Plugin v2 (Befehl:
docker composeohne Bindestrich), siehe Docker und Docker Compose auf Linux installieren - Linux-Host, VM oder NAS mit Docker-Unterstützung (x86_64 oder arm64, die Penpot-Images gibt es für beide Architekturen)
- Mindestens 2 GB RAM, empfohlen 4 GB für den Betrieb mit mehreren Nutzern
- Mindestens 10 GB freier Festplattenplatz (Datenbank und Assets wachsen mit der Zeit)
- python3 auf dem Host (für die Secret-Key-Generierung)
- Für den Produktionsbetrieb: Domain mit DNS-Eintrag und ein Reverse Proxy für HTTPS, siehe Traefik als Docker-Reverse-Proxy mit automatischem HTTPS oder Caddy als Reverse Proxy
Penpot-Dienste und Eckdaten
| Dienst | Image | Funktion | Port (extern) |
|---|---|---|---|
| penpot-frontend | penpotapp/frontend:2.18 | Nginx + SPA (Web-UI) | 127.0.0.1:9001 → 8080 |
| penpot-backend | penpotapp/backend:2.18 | Clojure-App-Server | nur intern |
| penpot-admin-console | penpotapp/admin-console:2.18 | Verwaltungsoberfläche (neu ab 2.18) | nur intern |
| penpot-mcp | penpotapp/mcp:2.18 | MCP-Server für KI-Assistenten | nur intern |
| penpot-exporter | penpotapp/exporter:2.18 | Node.js Export-Service | nur intern |
| penpot-postgres | postgres:15 | Relationale Datenbank | nur intern |
| penpot-valkey | valkey/valkey:8.1 | In-Memory-Store (Redis-Fork) | nur intern |
| penpot-mailcatch | sj26/mailcatcher:v0.11.0 | SMTP-Falle für Tests | 127.0.0.1:1080 (Web-UI) |
| Volume | Inhalt | Backup-Priorität |
|---|---|---|
| penpot_assets | Hochgeladene Bilder, Fonts, Exporte | Hoch |
| penpot_postgres_v15 | Alle Designdaten (PostgreSQL) | Kritisch |
Schritt 1: Projektordner anlegen und Secret Key generieren
Legen Sie einen eigenen Ordner für den Penpot-Stack an. Der Ordnername penpot wird zum Compose-Projektnamen und damit zum Präfix der Container und Volumes:
$ mkdir -p /opt/penpot
$ cd /opt/penpotErzeugen Sie den PENPOT_SECRET_KEY. Aus ihm leitet Penpot die Schlüssel für Sitzungen und Einladungslinks ab. Ändert er sich, werden alle Sitzungen ungültig:
$ python3 -c "import secrets; print(secrets.token_urlsafe(64))"Die Ausgabe sieht etwa so aus (Ihr Wert ist anders):
xK8mZ2q...v9Lp4nR # 88 Zeichen, Base64-URL-sicherDen Wert tragen Sie im nächsten Schritt in die .env ein. Ein Datenbankpasswort erzeugen Sie mit openssl rand -hex 24.
ls -la /opt/penpot/ zeigt ein leeres Verzeichnis, der Schlüssel ist 86 Zeichen lang.
Schritt 2: .env-Datei mit Konfiguration anlegen
Die .env enthält alle anpassbaren Werte. PENPOT_PUBLIC_URI muss exakt der Adresse entsprechen, unter der Nutzer Penpot aufrufen (im Produktivbetrieb mit https://):
# Penpot – Konfiguration
# Generiert mit: python3 -c "import secrets; print(secrets.token_urlsafe(64))"
PENPOT_SECRET_KEY=HIER_DEN_ERZEUGTEN_SCHLUESSEL_EINTRAGEN
# Öffentliche URL – muss exakt der Adresse entsprechen, unter der Nutzer Penpot erreichen.
# Für lokale Tests: http://localhost:9001
# Für Produktion: https://penpot.example.de
PENPOT_PUBLIC_URI=http://localhost:9001
# Feste Penpot-Version für alle Penpot-Images
PENPOT_VERSION=2.18
# Datenbankpasswort (openssl rand -hex 24)
PENPOT_DATABASE_PASSWORD=HIER_PASSWORT_EINTRAGENBeschränken Sie die Leserechte auf root:
$ chmod 600 /opt/penpot/.envstat -c %a /opt/penpot/.env zeigt 600, und grep HIER_ /opt/penpot/.env liefert keine Zeile mehr.
Schritt 3: compose.yaml anlegen
Die folgende compose.yaml entspricht dem offiziellen Compose-File von Penpot 2.18 mit vier Anpassungen: Passwort, Schlüssel und Version kommen aus der .env, Frontend und Mailcatcher lauschen nur auf 127.0.0.1, der Mailcatcher hat ein festes Tag, und die Telemetrie ist abgeschaltet (PENPOT_TELEMETRY_ENABLED: "true" schaltet sie wieder ein). Die YAML-Anker (&penpot-flags usw.) vermeiden doppelte Einträge:
x-flags: &penpot-flags
PENPOT_FLAGS: disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies enable-mcp enable-admin-console
x-uri: &penpot-public-uri
PENPOT_PUBLIC_URI: ${PENPOT_PUBLIC_URI}
x-body-size: &penpot-http-body-size
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
x-secret-key: &penpot-secret-key
PENPOT_SECRET_KEY: ${PENPOT_SECRET_KEY}
x-database: &penpot-database
PENPOT_DATABASE_URI: postgresql://penpot-postgres/penpot
PENPOT_DATABASE_USERNAME: penpot
PENPOT_DATABASE_PASSWORD: ${PENPOT_DATABASE_PASSWORD}
networks:
penpot:
volumes:
penpot_postgres_v15:
penpot_assets:
services:
penpot-frontend:
image: "penpotapp/frontend:${PENPOT_VERSION}"
restart: always
ports:
- "127.0.0.1:9001:8080"
volumes:
- penpot_assets:/opt/data/assets
depends_on:
- penpot-backend
- penpot-admin-console
- penpot-mcp
- penpot-exporter
networks:
- penpot
environment:
<<: [*penpot-flags, *penpot-http-body-size, *penpot-public-uri]
PENPOT_ADMIN_CONSOLE_URI: http://penpot-admin-console:3000
penpot-backend:
image: "penpotapp/backend:${PENPOT_VERSION}"
restart: always
volumes:
- penpot_assets:/opt/data/assets
depends_on:
penpot-postgres:
condition: service_healthy
penpot-valkey:
condition: service_healthy
networks:
- penpot
environment:
<<: [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key, *penpot-database]
PENPOT_ADMIN_CONSOLE_URI: http://penpot-admin-console:3000
PENPOT_REDIS_URI: redis://penpot-valkey/0
PENPOT_OBJECTS_STORAGE_BACKEND: fs
PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/assets
PENPOT_TELEMETRY_ENABLED: "false"
PENPOT_SMTP_DEFAULT_FROM: no-reply@example.com
PENPOT_SMTP_DEFAULT_REPLY_TO: no-reply@example.com
PENPOT_SMTP_HOST: penpot-mailcatch
PENPOT_SMTP_PORT: 1025
PENPOT_SMTP_USERNAME:
PENPOT_SMTP_PASSWORD:
PENPOT_SMTP_TLS: "false"
PENPOT_SMTP_SSL: "false"
penpot-admin-console:
image: "penpotapp/admin-console:${PENPOT_VERSION}"
restart: always
depends_on:
penpot-postgres:
condition: service_healthy
networks:
- penpot
environment:
<<: [*penpot-public-uri, *penpot-secret-key, *penpot-database]
PENPOT_INTERNAL_URI: http://penpot-frontend:8080
penpot-mcp:
image: "penpotapp/mcp:${PENPOT_VERSION}"
restart: always
networks:
- penpot
penpot-exporter:
image: "penpotapp/exporter:${PENPOT_VERSION}"
restart: always
depends_on:
penpot-valkey:
condition: service_healthy
networks:
- penpot
environment:
<<: [*penpot-flags, *penpot-secret-key, *penpot-public-uri]
PENPOT_INTERNAL_URI: http://penpot-frontend:8080
PENPOT_REDIS_URI: redis://penpot-valkey/0
penpot-postgres:
image: "postgres:15"
restart: always
stop_signal: SIGINT
healthcheck:
test: ["CMD-SHELL", "pg_isready -U penpot"]
interval: 2s
timeout: 10s
retries: 5
start_period: 2s
volumes:
- penpot_postgres_v15:/var/lib/postgresql/data
networks:
- penpot
environment:
- POSTGRES_INITDB_ARGS=--data-checksums
- POSTGRES_DB=penpot
- POSTGRES_USER=penpot
- POSTGRES_PASSWORD=${PENPOT_DATABASE_PASSWORD}
penpot-valkey:
image: valkey/valkey:8.1
restart: always
healthcheck:
test: ["CMD-SHELL", "valkey-cli ping | grep PONG"]
interval: 1s
timeout: 3s
retries: 5
start_period: 3s
networks:
- penpot
environment:
- VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu
penpot-mailcatch:
image: sj26/mailcatcher:v0.11.0
restart: always
expose:
- "1025"
ports:
- "127.0.0.1:1080:1080"
networks:
- penpotDie Flags disable-email-verification und disable-secure-session-cookies sind nur für den ersten Test gedacht; Schritt 5 und 6 entfernen sie.
Prüfen Sie Syntax und Variablen vor dem ersten Start:
$ docker compose -f /opt/penpot/compose.yaml config --quiet && echo "YAML OK"Erwartete Ausgabe: YAML OK ohne Warnungen wie „variable is not set“.
Schritt 4: Stack starten und Gesundheit prüfen
Starten Sie alle Dienste aus dem Projektordner:
$ cd /opt/penpot
$ docker compose up -dBackend, Admin Console und Exporter warten per Healthcheck, bis PostgreSQL und Valkey bereit sind.
Status aller Dienste prüfen:
$ docker compose psErwartete Ausgabe nach etwa 30 bis 60 Sekunden:
NAME STATUS PORTS
penpot-penpot-admin-console-1 Up
penpot-penpot-backend-1 Up
penpot-penpot-exporter-1 Up
penpot-penpot-frontend-1 Up 127.0.0.1:9001->8080/tcp
penpot-penpot-mailcatch-1 Up 127.0.0.1:1080->1080/tcp
penpot-penpot-mcp-1 Up
penpot-penpot-postgres-1 Up (healthy)
penpot-penpot-valkey-1 Up (healthy)Prüfen Sie die Backend-Logs auf Startfehler:
$ docker compose logs penpot-backend --tail 30Die Ausgabe enthält keine Zeilen mit ERROR oder Datenbank-Verbindungsfehlern.
$ curl -I http://localhost:9001Erwartete Antwort: HTTP/1.1 200 OK oder HTTP/1.1 302 Found. Bei „Connection refused“ oder 502 ist das Frontend noch nicht bereit; wiederholen Sie den Aufruf nach 30 Sekunden.
Schritt 5: Ersten Nutzer registrieren und Login testen
Öffnen Sie Penpot unter der PENPOT_PUBLIC_URI. Läuft der Server entfernt und ist noch kein Proxy eingerichtet, erreichen Sie ihn per SSH-Tunnel: ssh -L 9001:127.0.0.1:9001 -L 1080:127.0.0.1:1080 benutzer@server, dann http://localhost:9001. Legen Sie auf der Registrierungsseite den ersten Nutzer an.
Da disable-email-verification aktiv ist, entfällt die E-Mail-Bestätigung.
Sperren Sie danach die offene Registrierung: Ersetzen Sie in PENPOT_FLAGS disable-email-verification durch disable-registration und erstellen Sie die Dienste neu:
$ docker compose up -dWeitere Nutzer legen Sie per CLI an (setzt das Flag enable-prepl-server voraus) oder laden sie im Team per E-Mail ein:
$ docker compose exec penpot-backend python3 manage.py create-profileNach dem Login erscheint das Penpot-Dashboard. Legen Sie ein Testprojekt an und öffnen Sie eine leere Designdatei; der Editor lädt ohne Fehlermeldung. Ein Aufruf der Registrierungsseite im privaten Browserfenster zeigt nach der Sperre keine Registrierung mehr. Versandte E-Mails sehen Sie im Mailcatcher unter http://localhost:1080.
Schritt 6: HTTPS und Reverse Proxy für den Produktionsbetrieb
Für den Einsatz im Team ist HTTPS Pflicht. Ohne HTTPS sperrt der Browser unter anderem die Zwischenablage (Kopieren und Einfügen von Design-Elementen). Das Flag disable-secure-session-cookies löst nur das Cookie-Problem.
Richten Sie einen Reverse Proxy auf demselben Host ein, der auf http://127.0.0.1:9001 weiterleitet und WebSockets durchreicht:
- Traefik: im offiziellen Compose-File als auskommentierter Block enthalten. Anleitung: Traefik als Docker-Reverse-Proxy mit automatischem HTTPS
- Caddy: Caddy als Reverse Proxy einrichten
- Nginx Proxy Manager: grafische Oberfläche
Setzen Sie danach PENPOT_PUBLIC_URI in der .env auf die HTTPS-URL, entfernen Sie disable-secure-session-cookies aus PENPOT_FLAGS und erstellen Sie die Container neu:
# in /opt/penpot/.env
PENPOT_PUBLIC_URI=https://penpot.example.de
# danach
cd /opt/penpot && docker compose up -d$ curl -I https://penpot.example.deErwartete Antwort: HTTP/2 200 oder HTTP/2 302. Im Browser funktioniert Kopieren und Einfügen von Design-Elementen, und ss -tlnp | grep 9001 zeigt Port 9001 nur auf 127.0.0.1.
Schritt 7: Updates einspielen und Backups einrichten
Lesen Sie vor jedem Update die Upgrade-Hinweise in der offiziellen Dokumentation und sichern Sie beide Volumes. Passen Sie dann PENPOT_VERSION in der .env an:
$ cd /opt/penpot
$ docker compose pull
$ docker compose up -dAktualisieren Sie in kleinen Versionsschritten. Wer eine eigene compose.yaml von 2.17 oder älter pflegt, muss für 2.18 den Dienst penpot-admin-console, das Flag enable-admin-console und PENPOT_ADMIN_CONSOLE_URI ergänzen, wie in Schritt 3 gezeigt. Beobachten Sie die Logs:
$ docker compose logs -f penpot-backendPrüfen Sie bei einem neuen Compose-File mit docker volume ls, dass die Volume-Namen gleich bleiben; sonst startet Penpot mit einem leeren Volume.
Sichern Sie beide Volumes. Docker Compose stellt den Projektnamen voran, das Assets-Volume heißt daher penpot_penpot_assets:
# Assets-Volume sichern
docker run --rm \
-v penpot_penpot_assets:/data:ro \
-v /backup/penpot:/backup \
alpine:3.22 tar czf /backup/penpot_assets_$(date +%Y%m%d).tar.gz -C /data .
# PostgreSQL-Datenbank-Dump
mkdir -p /backup/penpot
docker compose exec -T penpot-postgres \
pg_dump -U penpot penpot | gzip > /backup/penpot_db_$(date +%Y%m%d).sql.gzFür automatisierte Backups siehe MySQL & PostgreSQL Backup automatisieren mit cron und rclone.
$ docker compose psNach dem Update zeigen alle acht Container den Status Up, die Datenbank-Container Up (healthy). Bestehende Projekte öffnen sich im Browser. ls -lh /backup/penpot/ zeigt beide Sicherungsdateien mit einer Größe über 0.
Troubleshooting / Typische Fehler
- Alle Nutzer werden nach Neustart ausgeloggt
PENPOT_SECRET_KEYist nicht gesetzt oder ändert sich bei jedem Start . Lösung: Schlüssel wie in Schritt 1 erzeugen, fest in die.enveintragen,docker compose up -d.- Einladungslinks und OAuth-Callbacks schlagen fehl
PENPOT_PUBLIC_URIstimmt nicht mit der tatsächlichen Zugriffs-URL überein. Wert in der.envanpassen,docker compose up -d.- Clipboard-API funktioniert nicht (Kopieren/Einfügen im Editor)
Penpot läuft ohne HTTPS. Richten Sie den Reverse Proxy aus Schritt 6 ein.
- Backend startet nicht, Logs zeigen „database does not exist“ oder „connection refused“
docker compose logs penpot-postgresprüfen. Häufige Ursache: Das Passwort in der.envwurde nach der Erstinitialisierung geändert.- Dienst fehlt oder Frontend startet nicht
Die
compose.yamlpasst nicht zur Version inPENPOT_VERSION, etwa fehltpenpot-admin-consolebei 2.18. Lösung: Compose-File zur Version verwenden (Schritt 3).manage.py create-profileschlägt fehlDas Flag
enable-prepl-serverfehlt inPENPOT_FLAGS. Ergänzen,docker compose up -d, Befehl wiederholen.- IPv6-Fehler beim Frontend-Start
Auf Hosts mit beim Booten deaktiviertem IPv6 kann Nginx im Frontend-Container nicht binden. Lösung:
PENPOT_DISABLE_IPV6_LISTEN: "true"in die Umgebung vonpenpot-frontend.- Projekte nach Update verschwunden
Das neue Compose-File nutzt einen anderen Volume-Namen; Docker hat ein leeres Volume angelegt. Volume-Namen in der
compose.yamlauf das alte Volume zurücksetzen (docker volume ls).
Häufige Fragen
Unter welcher Lizenz steht Penpot im Self-Hosting?
Unter der Mozilla Public License 2.0, ohne Nutzerlimit. Aufwand entsteht für Server, SMTP-Versand, Updates und Backups.
Läuft Penpot auf ARM-Servern oder Apple-Silicon-Macs?
Ja, die offiziellen Images gibt es für linux/arm64; Docker wählt die Architektur automatisch.
Wie lege ich weitere Nutzer an, wenn die Registrierung gesperrt ist?
Mit docker compose exec penpot-backend python3 manage.py create-profile (Flag enable-prepl-server nötig). Alternativ laden Sie Nutzer per E-Mail ins Team ein (echter SMTP-Server nötig).
Was ist der MCP-Server?
Der Dienst penpotapp/mcp implementiert das Model Context Protocol. KI-Assistenten greifen damit auf Penpot-Designs zu. Den Schlüssel erzeugt jeder Nutzer selbst unter Integrationen. Wer MCP nicht nutzt, entfernt enable-mcp aus PENPOT_FLAGS.
Kann ich Penpot mit LDAP oder OIDC verbinden?
Ja. LDAP wird über PENPOT_LDAP_*-Variablen und das Flag enable-login-with-ldap konfiguriert. OIDC (Keycloak, Azure AD, Authentik) über PENPOT_OIDC_*-Variablen. Google, GitHub und GitLab haben eigene enable-login-with-*-Flags. Details stehen in der Penpot-Konfigurationsreferenz.
Wie sichere ich meine Penpot-Daten?
Wie in Schritt 7; für eine Kopie außer Haus eignet sich rclone.
Fazit
Für den Produktivbetrieb entscheidend sind ein fester PENPOT_SECRET_KEY, eine korrekte PENPOT_PUBLIC_URI, HTTPS über einen Reverse Proxy, eine gesperrte Registrierung und regelmäßige Sicherungen beider Volumes.
Weiterführende Anleitungen und Quellen
- Docker und Docker Compose auf Linux installieren: die Self-Hosting-Grundlage
- Traefik als Docker-Reverse-Proxy mit automatischem HTTPS einrichten
- Docker Compose absichern: Secrets, Healthchecks und Non-Root für den Produktivbetrieb
- MySQL & PostgreSQL Backup automatisieren mit cron und rclone
Offizielle Quellen: Penpot Self-Hosting Dokumentation | Penpot Konfigurationsreferenz | Penpot GitHub Repository


