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:

TaskHow
Set a real PENPOT_PUBLIC_URIChange from http://localhost:9001 to your domain
Generate a unique PENPOT_SECRET_KEYRandom 64+ character string
Enable email verificationRemove disable-email-verification from PENPOT_FLAGS
Enable secure cookiesRemove disable-secure-session-cookies (requires HTTPS)
Change database passwordUpdate both POSTGRES_PASSWORD and PENPOT_DATABASE_PASSWORD
Configure SMTPSet real SMTP credentials for invitations and password resets
Disable telemetrySet 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

GoalFlag
Disable registration entirelyAdd disable-registration to PENPOT_FLAGS
Restrict to email domainAdd 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.

Comments