Self-Hosting Penpot with Docker Compose
What Is Penpot?
Penpot is an open-source design and prototyping platform — the self-hosted alternative to Figma. It runs in the browser, supports real-time collaboration, uses SVG as its native format (no proprietary files), and includes components, design systems, interactive prototypes, and CSS-ready inspect mode. Licensed under MPL-2.0 and backed by the Kaleidos team. Official site.
Prerequisites
- A Linux server with 4 GB RAM minimum (8 GB recommended)
- Docker and Docker Compose installed (guide)
- 20 GB of free disk space
- A domain name (recommended for team collaboration)
Docker Compose Configuration
Penpot runs as four services: frontend, backend, exporter, plus PostgreSQL and Valkey (Redis-compatible cache). Create a project directory:
mkdir penpot && cd penpot
Create a .env file:
PENPOT_VERSION=2.13.3
Create docker-compose.yml:
services:
penpot-frontend:
image: penpotapp/frontend:${PENPOT_VERSION:-2.13.3}
container_name: penpot-frontend
restart: unless-stopped
ports:
- "9001:8080"
volumes:
- penpot_assets:/opt/data/assets # Shared with backend
depends_on:
- penpot-backend
- penpot-exporter
environment:
- PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies
- PENPOT_BACKEND_URI=http://penpot-backend:6060
- PENPOT_EXPORTER_URI=http://penpot-exporter:6061
- PENPOT_HTTP_SERVER_MAX_BODY_SIZE=31457280
- PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=367001600
networks:
- penpot
penpot-backend:
image: penpotapp/backend:${PENPOT_VERSION:-2.13.3}
container_name: penpot-backend
restart: unless-stopped
volumes:
- penpot_assets:/opt/data/assets # Shared with frontend
depends_on:
penpot-postgres:
condition: service_healthy
penpot-valkey:
condition: service_healthy
environment:
- PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies
- PENPOT_SECRET_KEY=CHANGE_THIS_generate_a_64_char_random_string
- PENPOT_PUBLIC_URI=http://localhost:9001
- PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot
- PENPOT_DATABASE_USERNAME=penpot
- PENPOT_DATABASE_PASSWORD=penpot_db_password
- PENPOT_REDIS_URI=redis://penpot-valkey/0
- PENPOT_OBJECTS_STORAGE_BACKEND=fs
- PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets
- PENPOT_TELEMETRY_ENABLED=false
# SMTP — configure for password resets and invitations
- [email protected]
- [email protected]
- PENPOT_SMTP_HOST=smtp.example.com
- PENPOT_SMTP_PORT=587
- PENPOT_SMTP_USERNAME=
- PENPOT_SMTP_PASSWORD=
- PENPOT_SMTP_TLS=true
- PENPOT_SMTP_SSL=false
networks:
- penpot
penpot-exporter:
image: penpotapp/exporter:${PENPOT_VERSION:-2.13.3}
container_name: penpot-exporter
restart: unless-stopped
depends_on:
penpot-valkey:
condition: service_healthy
environment:
- PENPOT_SECRET_KEY=CHANGE_THIS_generate_a_64_char_random_string # Must match backend
- PENPOT_PUBLIC_URI=http://penpot-frontend:8080 # Internal Docker address
- PENPOT_REDIS_URI=redis://penpot-valkey/0
networks:
- penpot
penpot-postgres:
image: postgres:15
container_name: penpot-postgres
restart: unless-stopped
volumes:
- penpot_postgres:/var/lib/postgresql/data
environment:
- POSTGRES_DB=penpot
- POSTGRES_USER=penpot
- POSTGRES_PASSWORD=penpot_db_password # Must match backend config
- POSTGRES_INITDB_ARGS=--data-checksums
healthcheck:
test: ["CMD-SHELL", "pg_isready -U penpot"]
interval: 10s
timeout: 5s
retries: 5
networks:
- penpot
penpot-valkey:
image: valkey/valkey:8.1
container_name: penpot-valkey
restart: unless-stopped
command: valkey-server --maxmemory 128mb --maxmemory-policy volatile-lfu
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- penpot
volumes:
penpot_assets:
penpot_postgres:
networks:
penpot:
Generate a secret key before starting:
python3 -c "import secrets; print(secrets.token_urlsafe(64))"
Replace CHANGE_THIS_generate_a_64_char_random_string in both the backend and exporter with the generated value. They must match.
Start the stack:
docker compose up -d
Initial Setup
Access Penpot at http://your-server:9001.
With the default flags (disable-email-verification), registration is open. Click Create an account and register.
To create an admin account via CLI instead:
docker exec -it penpot-backend python3 manage.py create-profile
Production Checklist
Before exposing Penpot to a team:
| Task | How |
|---|---|
Set a real PENPOT_PUBLIC_URI | Change from http://localhost:9001 to your domain |
Generate a unique PENPOT_SECRET_KEY | Random 64+ character string |
| Enable email verification | Remove disable-email-verification from PENPOT_FLAGS |
| Enable secure cookies | Remove disable-secure-session-cookies (requires HTTPS) |
| Change database password | Update both POSTGRES_PASSWORD and PENPOT_DATABASE_PASSWORD |
| Configure SMTP | Set real SMTP credentials for invitations and password resets |
| Disable telemetry | Set PENPOT_TELEMETRY_ENABLED=false |
Configuration
Authentication Options
Penpot supports multiple SSO providers via PENPOT_FLAGS:
OpenID Connect (Keycloak, Authentik):
Add enable-login-with-oidc to PENPOT_FLAGS and set:
- PENPOT_OIDC_CLIENT_ID=your-client-id
- PENPOT_OIDC_CLIENT_SECRET=your-client-secret
- PENPOT_OIDC_BASE_URI=https://your-idp.example.com/realms/your-realm
Google, GitHub, GitLab OAuth are also supported — add the respective enable-login-with-* flag and client credentials.
S3 Storage
For large teams, offload file storage to S3-compatible storage:
- PENPOT_OBJECTS_STORAGE_BACKEND=s3
- AWS_ACCESS_KEY_ID=your-access-key
- AWS_SECRET_ACCESS_KEY=your-secret-key
- PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=https://s3.amazonaws.com
- PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot-assets
- PENPOT_OBJECTS_STORAGE_S3_REGION=us-east-1
Registration Control
| Goal | Flag |
|---|---|
| Disable registration entirely | Add disable-registration to PENPOT_FLAGS |
| Restrict to email domain | Add enable-email-whitelist + set PENPOT_REGISTRATION_DOMAIN_WHITELIST=yourdomain.com |
| Disable password login (SSO only) | Add disable-login-with-password |
Air-Gapped Mode
For fully isolated deployments with no outbound network access:
Add enable-air-gapped-conf to PENPOT_FLAGS. This disables Google Fonts and GitHub font library loading.
Reverse Proxy
Only port 9001 needs to be exposed. The frontend proxies requests to the backend and exporter internally.
location / {
proxy_pass http://127.0.0.1:9001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 350M; # Match PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE
}
See Reverse Proxy Setup.
Backup
Back up two volumes: assets and the PostgreSQL database.
# Stop services for consistent backup
docker compose stop
# Back up PostgreSQL
docker exec penpot-postgres pg_dump -U penpot penpot > penpot-db-$(date +%Y%m%d).sql
# Back up assets volume
docker run --rm -v penpot_assets:/data -v $(pwd):/backup alpine \
tar czf /backup/penpot-assets-$(date +%Y%m%d).tar.gz /data
docker compose start
See Backup Strategy.
Troubleshooting
Exports fail or produce blank files
Symptom: PDF, SVG, or PNG exports are empty or fail silently.
Fix: The exporter’s PENPOT_PUBLIC_URI must point to the frontend’s internal Docker address (http://penpot-frontend:8080), not the external URL. The exporter renders designs by loading them from the frontend. If it can’t reach the frontend, exports fail.
Login works but sessions don’t persist
Symptom: You can log in but get redirected to the login page on the next request.
Fix: If using HTTPS, remove disable-secure-session-cookies from PENPOT_FLAGS. If NOT using HTTPS, keep the flag — secure cookies can’t be set over plain HTTP.
”change-this-insecure-key” warning
Symptom: Penpot starts but security is compromised.
Fix: Generate a real secret key with python3 -c "import secrets; print(secrets.token_urlsafe(64))". Set it in BOTH the backend and exporter. If you change the key after users have registered, existing sessions and invitation links break.
Assets display as broken images
Symptom: Uploaded images and design files show broken image icons.
Fix: Both the frontend and backend must mount the same penpot_assets volume. If they use separate volumes, uploaded assets won’t be accessible to the frontend.
High memory usage
Symptom: Backend container consumes 2-4 GB RAM.
Fix: The backend runs on the JVM, which is memory-hungry. This is normal for Penpot. Minimum 4 GB system RAM recommended. Set Valkey’s --maxmemory 128mb to cap cache memory.
Resource Requirements
- RAM: 4 GB minimum, 8 GB recommended (JVM backend is the primary consumer)
- CPU: 2-4 vCPUs recommended (client-side rendering minimizes server CPU needs)
- Disk: 20 GB for Docker images and base data. Assets storage grows with usage.
- Database: PostgreSQL 13+ (15 recommended)
Verdict
Penpot is the only serious open-source alternative to Figma. Real-time collaboration, components, design systems, interactive prototypes, and CSS inspect mode — it covers the core design workflow. The SVG-native format means no vendor lock-in, and the self-hosted deployment gives you full data control.
The trade-off is resource usage — 4 GB minimum RAM puts it among the heavier self-hosted apps. Setup is more complex than most (5 services). And while it covers the fundamentals well, power users coming from Figma will notice missing features around plugin ecosystems and advanced prototyping. For teams that need a collaborative design tool without a Figma subscription, Penpot is the clear choice.
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments