Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Cloud / Hosting 26.06.2026 · 10 min Lesezeit Geprüft 30.09.2026

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

Titelbild: Penpot mit Docker installieren, Design, Prototypen, Team

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 compose ohne 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

DienstImageFunktionPort (extern)
penpot-frontendpenpotapp/frontend:2.18Nginx + SPA (Web-UI)127.0.0.1:9001 → 8080
penpot-backendpenpotapp/backend:2.18Clojure-App-Servernur intern
penpot-admin-consolepenpotapp/admin-console:2.18Verwaltungsoberfläche (neu ab 2.18)nur intern
penpot-mcppenpotapp/mcp:2.18MCP-Server für KI-Assistentennur intern
penpot-exporterpenpotapp/exporter:2.18Node.js Export-Servicenur intern
penpot-postgrespostgres:15Relationale Datenbanknur intern
penpot-valkeyvalkey/valkey:8.1In-Memory-Store (Redis-Fork)nur intern
penpot-mailcatchsj26/mailcatcher:v0.11.0SMTP-Falle für Tests127.0.0.1:1080 (Web-UI)
VolumeInhaltBackup-Priorität
penpot_assetsHochgeladene Bilder, Fonts, ExporteHoch
penpot_postgres_v15Alle 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/penpot

Erzeugen 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-sicher

Den 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_EINTRAGEN

Beschränken Sie die Leserechte auf root:

$ chmod 600 /opt/penpot/.env

stat -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:
      - penpot

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

Backend, Admin Console und Exporter warten per Healthcheck, bis PostgreSQL und Valkey bereit sind.

Status aller Dienste prüfen:

$ docker compose ps

Erwartete 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 30

Die Ausgabe enthält keine Zeilen mit ERROR oder Datenbank-Verbindungsfehlern.

$ curl -I http://localhost:9001

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

Weitere 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-profile

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

  1. Traefik: im offiziellen Compose-File als auskommentierter Block enthalten. Anleitung: Traefik als Docker-Reverse-Proxy mit automatischem HTTPS
  2. Caddy: Caddy als Reverse Proxy einrichten
  3. 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.de

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

Aktualisieren 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-backend

Prü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.gz

Für automatisierte Backups siehe MySQL & PostgreSQL Backup automatisieren mit cron und rclone.

$ docker compose ps

Nach 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_KEY ist nicht gesetzt oder ändert sich bei jedem Start . Lösung: Schlüssel wie in Schritt 1 erzeugen, fest in die .env eintragen, docker compose up -d.

Einladungslinks und OAuth-Callbacks schlagen fehl

PENPOT_PUBLIC_URI stimmt nicht mit der tatsächlichen Zugriffs-URL überein. Wert in der .env anpassen, 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-postgres prüfen. Häufige Ursache: Das Passwort in der .env wurde nach der Erstinitialisierung geändert.

Dienst fehlt oder Frontend startet nicht

Die compose.yaml passt nicht zur Version in PENPOT_VERSION, etwa fehlt penpot-admin-console bei 2.18. Lösung: Compose-File zur Version verwenden (Schritt 3).

manage.py create-profile schlägt fehl

Das Flag enable-prepl-server fehlt in PENPOT_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 von penpot-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.yaml auf 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

  1. Docker und Docker Compose auf Linux installieren: die Self-Hosting-Grundlage
  2. Traefik als Docker-Reverse-Proxy mit automatischem HTTPS einrichten
  3. Docker Compose absichern: Secrets, Healthchecks und Non-Root für den Produktivbetrieb
  4. MySQL & PostgreSQL Backup automatisieren mit cron und rclone

Offizielle Quellen: Penpot Self-Hosting Dokumentation | Penpot Konfigurationsreferenz | Penpot GitHub Repository

Passende Anleitungen auf S-EDV

  1. PostgreSQL schließt elf Sicherheitslücken in den Versionen 14 bis 18
  2. Prometheus node_exporter nativ installieren und als systemd-Dienst betreiben
  3. netcup Local Block Storage bestellen, einrichten und unter Linux einbinden