- Just 85.5%
- Dockerfile 14.5%
| nginx | ||
| .env | ||
| .env.example | ||
| compose.yaml | ||
| Containerfile | ||
| Justfile | ||
| penpot.service | ||
| README.org | ||
Self-Hosted Penpot
- Overview
- Quick Start
- Configuration
- Lifecycle Commands
- Systemd Service
- Backups
- Upgrades
- Troubleshooting
- File Layout
- References
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
podman5.xpodman-compose1.xjustcommand 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:
- Uncomment the HTTPS server block in
nginx/penpot.conf - Uncomment the
443:443port mapping incompose.yaml - Place your TLS certificate and key inside the container
- Set
PENPOT_PUBLIC_URIto yourhttps://address - Remove
disable-secure-session-cookiesfromPENPOT_FLAGS - 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