No description
  • Just 85.5%
  • Dockerfile 14.5%
Find a file
2026-06-04 09:09:12 +02:00
nginx Init commit 2026-06-04 09:09:12 +02:00
.env Init commit 2026-06-04 09:09:12 +02:00
.env.example Init commit 2026-06-04 09:09:12 +02:00
compose.yaml Init commit 2026-06-04 09:09:12 +02:00
Containerfile Init commit 2026-06-04 09:09:12 +02:00
Justfile Init commit 2026-06-04 09:09:12 +02:00
penpot.service Init commit 2026-06-04 09:09:12 +02:00
README.org Init commit 2026-06-04 09:09:12 +02:00

Self-Hosted Penpot

Overview

This project deploys Penpot 2.15.4 as a self-hosted design platform using Podman and podman-compose. An nginx reverse proxy (Fedora 42 container) fronts the Penpot frontend and terminates HTTP on port 8080.

Architecture

  Browser :8080
      |
      v
  nginx-proxy (Fedora 42)
      |
      v
  penpot-frontend (:8080)
      |
      +-- penpot-backend (:6060)
      |       |
      |       +-- penpot-postgres (:5432)
      |       +-- penpot-valkey (:6379)
      |
      +-- penpot-exporter (:6061)
      +-- penpot-mcp
      +-- penpot-mailcatch (:1080)

Services

Service Image Purpose
nginx-proxy Fedora 42 + nginx (local build) Reverse proxy on port 8080
penpot-frontend penpotapp/frontend:2.15.4 Serves the Penpot web UI
penpot-backend penpotapp/backend:2.15.4 API and business logic
penpot-exporter penpotapp/exporter:2.15.4 Renders designs to PNG/SVG/PDF
penpot-mcp penpotapp/mcp:2.15.4 MCP server for AI integrations
penpot-postgres postgres:15 PostgreSQL database
penpot-valkey valkey/valkey:8.1 WebSocket notifications cache
penpot-mailcatch sj26/mailcatcher:latest Catch-all SMTP for development

Ports

Port Service Protcol
8080 nginx-proxy (HTTP) TCP
1080 mailcatch web UI TCP

Quick Start

Prerequisites

  • podman 5.x
  • podman-compose 1.x
  • just command runner

Installation

just init          # Create .env from template

Edit ~/penpot/.env and change at least:

PENPOT_SECRET_KEY
Generate with python3 -c "import secrets; print(secrets.token_urlsafe(64))"
PENPOT_PUBLIC_URI
Set to your public URL (e.g. https://penpot.example.com)

Then build and start:

just build
just up

Penpot is available at http://localhost:8080.

Configuration

All configuration lives in .env.

Flags

The PENPOT_FLAGS variable controls which features are enabled. The current defaults are:

disable-email-verification  :: Skip email verification for new accounts
disable-registration         :: Prevent new account sign-ups from the login page
enable-smtp                  :: Enable email sending (via mailcatch in dev)
enable-prepl-server          :: Enable the internal REPL for =manage.py=
enable-mcp                   :: Enable the MCP AI integration server

Common flags

enable-login-with-password    :: Allow password-based login (default: on)
enable-login-with-oidc        :: Enable OpenID Connect SSO
enable-login-with-github      :: Enable GitHub OAuth
enable-login-with-gitlab       :: Enable GitLab OAuth
enable-login-with-google       :: Enable Google OAuth
enable-login-with-ldap        :: Enable LDAP authentication
enable-secure-session-cookies :: Require HTTPS for session cookies
enable-telemetries            :: Send anonymous usage data to Penpot
enable-webhooks               :: Enable outgoing webhooks
enable-access-tokens          :: Enable personal access tokens for the API
enable-auto-file-snapshot     :: Keep automatic version history
enable-air-gapped-conf        :: Disable all external network requests
disable-onboarding            :: Skip the onboarding wizard for new users
disable-google-fonts-provider :: Remove Google Fonts from the font picker
disable-dashboard-templates-section :: Hide the templates section

Adding Users

Since disable-registration is active, new accounts can only be created via the CLI:

just create-user user@example.com

Or skip the onboarding tutorial:

just create-user-skip-onboarding user@example.com

SMTP Configuration

By default, mailcatch captures all outgoing email. View its web UI at http://localhost:1080.

To switch to a real SMTP provider, edit these variables in .env:

PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com
PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com
PENPOT_SMTP_HOST=smtp.your-provider.com
PENPOT_SMTP_PORT=587
PENPOT_SMTP_USERNAME=your-user
PENPOT_SMTP_PASSWORD=your-password
PENPOT_SMTP_TLS=true
PENPOT_SMTP_SSL=false

Then also update PENPOT_FLAGS:

PENPOT_FLAGS="disable-registration enable-smtp enable-secure-session-cookies enable-prepl-server enable-mcp"

HTTPS

The nginx/penpot.conf file contains a commented-out HTTPS server block. To enable it:

  1. Uncomment the HTTPS server block in nginx/penpot.conf
  2. Uncomment the 443:443 port mapping in compose.yaml
  3. Place your TLS certificate and key inside the container
  4. Set PENPOT_PUBLIC_URI to your https:// address
  5. Remove disable-secure-session-cookies from PENPOT_FLAGS
  6. Rebuild and restart: just build && just restart

Lifecycle Commands

Command Description
just init Create .env from template
just build Build the nginx proxy image
just up Start all containers
just down Stop all containers
just restart Stop and start all containers
just pull Pull latest Penpot images
just logs Follow all container logs
just logs-backend Follow backend logs only
just check Show container status
just create-user EMAIL Create a user account
just create-user-skip-onboarding EMAIL Create a user without tutorial
just backup-pg Dump the PostgreSQL database to a timestamped SQL file
just restore-pg DUMPFILE Restore the database from a SQL dump
just service-enable Enable and start the systemd service
just service-disable Stop and disable the systemd service
just service-status Show the systemd service status

Systemd Service

A user-level systemd service automatically starts Penpot after a reboot.

just service-enable   # Enable and start
just service-status    # Check status
just service-disable   # Stop and disable

Linger is enabled for the marcus user, so the service runs at boot even when no one is logged in.

The service file lives at: ~/.config/systemd/user/penpot.service

Backups

Dump the database

just backup-pg

Creates a file like penpot_backup_20260604_090000.sql in the project directory.

Restore the database

just restore-pg penpot_backup_20260604_090000.sql

Restoring on a fresh machine

just init
just build
just up
# Wait for postgres to be healthy, then:
just down
just up -d penpot-postgres
sleep 3
podman exec -i penpot-penpot-postgres-1 psql -U penpot penpot < penpot_backup.sql
just up -d

The penpot_assets volume holds uploaded images and SVGs. Back it up separately with:

podman volume inspect penpot_penpot_assets
# Then tar the directory shown in Mountpoint

Upgrades

# Edit PENPOT_VERSION in .env, then:
just pull
just restart

The official Penpot documentation recommends upgrading in small incremental version steps rather than jumping across multiple major versions.

Troubleshooting

Browser shows old login page after changing flags

Hard-refresh the page: Ctrl+Shift+R. The frontend caches config.js aggressively.

502 Bad Gateway errors

The backend takes a few seconds to start. If errors persist, check the logs:

just logs-backend

Database connection errors

The PENPOT_DATABASE_URI must use the host-only format: postgresql://penpot-postgres/penpot. Credentials are passed separately via PENPOT_DATABASE_USERNAME and PENPOT_DATABASE_PASSWORD. Do not embed user:pass@ in the URI; the JDBC driver cannot parse it.

Container fails to bind port 80

Rootless Podman cannot bind ports below 1024. This project uses port 8080 internally mapped to port 80 inside the nginx container.

File Layout

~/penpot/
├── Containerfile              # Fedora 42 nginx reverse proxy
├── compose.yaml               # podman-compose orchestration
├── Justfile                   # Task automation
├── .env                       # Secrets and configuration (do not commit)
├── .env.example               # Template for .env
└── nginx/
    ├── nginx.conf             # Main nginx configuration
    └── penpot.conf            # Reverse proxy and HTTPS server blocks