# Penpot

Browser-based design and prototyping platform for product teams.

- Replaces: [Figma](https://openlogs.fyi/alternatives/figma)
- Summary: Paid interface design and prototyping services, such as Figma.
- Stack: Clojure (language), PostgreSQL (database), Valkey (database)
- Deployment: docker_compose
- License: MPL-2.0 (osi) [source](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/LICENSE)
- Business model: unknown. The README at the pinned commit does not state a business model, so it stays unknown. ([source](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/README.md), read 2026-10-22)
- Difficulty: hard. 8 services (2 database or broker: postgres, valkey), 2 volume mounts, 2 env values to set, no elevated host access. Heuristic score 7 of 11: hard.
- Requirements: not stated
- Repository: [penpot/penpot](https://github.com/penpot/penpot)

## Install

1. Docker with the Compose plugin ([source](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml))

   ```text
   A host with Docker and the Docker Compose plugin. This entry runs from a compose file.
   ```

2. Save the compose file as docker-compose.yaml ([source](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml))

   ```yaml
   ## Common flags:
   # demo-users
   # email-verification
   # log-emails
   # log-invitation-tokens
   # login-with-github
   # login-with-gitlab
   # login-with-google
   # login-with-ldap
   # login-with-oidc
   # login-with-password
   # prepl-server
   # registration
   # secure-session-cookies
   # smtp
   # smtp-debug
   # telemetry
   # webhooks
   ##
   ## You can read more about all available flags and other
   ## environment variables here:
   ## https://help.penpot.app/technical-guide/configuration/#penpot-configuration
   #
   # WARNING: if you're exposing Penpot to the internet, you should remove the flags
   # 'disable-secure-session-cookies' and 'disable-email-verification'
   x-flags: &penpot-flags
     PENPOT_FLAGS: disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies enable-mcp enable-admin-console
   
   ## Public URI where Penpot is reached. To serve Penpot from a subpath, include
   ## it with a trailing slash, e.g. https://example.com/penpot/. A reverse proxy in
   ## front must forward the subpath unchanged, without rewriting it.
   x-uri: &penpot-public-uri
     PENPOT_PUBLIC_URI: http://localhost:9001
   
   x-body-size: &penpot-http-body-size
     # Max body size
     PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
     # Deprecation warning: this variable is deprecated. Use PENPOT_HTTP_SERVER_MAX_BODY (defaults to 367001600)
     PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
   
   ## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems
   ## (eg http sessions, or invitations) are derived.
   ##
   ## We recommend to use a truly randomly generated
   ## 512 bits base64 encoded string here. You can generate one with:
   ##
   ## python3 -c "import secrets; print(secrets.token_urlsafe(64))"
   x-secret-key: &penpot-secret-key
     PENPOT_SECRET_KEY: change-this-insecure-key
   
   ## Single database configuration block, shared by backend and admin-console.
   ## Don't touch them unless you are using custom postgresql connection parameters.
   x-database: &penpot-database
     PENPOT_DATABASE_URI: postgresql://penpot-postgres/penpot
     PENPOT_DATABASE_USERNAME: penpot
     PENPOT_DATABASE_PASSWORD: penpot
   
   networks:
     penpot:
   
   volumes:
     penpot_postgres_v15:
     penpot_assets:
     # penpot_mailpit_data:
     # penpot_traefik:
   
   services:
     ## Traefik service declaration example. Consider using it if you are going to expose
     ## penpot to the internet, or a different host than `localhost`.
   
     # traefik:
     #   image: traefik:v3.3
     #   networks:
     #     - penpot
     #   command:
     #     - "--api.insecure=true"
     #     - "--entryPoints.web.address=:80"
     #     - "--providers.docker=true"
     #     - "--providers.docker.exposedbydefault=false"
     #     - "--entryPoints.websecure.address=:443"
     #     - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
     #     - "--certificatesresolvers.letsencrypt.acme.email=<EMAIL_ADDRESS>"
     #     - "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json"
     #   volumes:
     #     - "penpot_traefik:/traefik"
     #     - "/var/run/docker.sock:/var/run/docker.sock"
     #   ports:
     #     - "80:80"
     #     - "443:443"
   
     penpot-frontend:
       image: "penpotapp/frontend:${PENPOT_VERSION:-2.18}"
       restart: always
       ports:
         - 9001:8080
   
       volumes:
         - penpot_assets:/opt/data/assets
   
       depends_on:
         - penpot-backend
         - penpot-admin-console
         - penpot-mcp
         - penpot-exporter
   
       networks:
         - penpot
   
       # labels:
         # - "traefik.enable=true"
   
         # ## HTTPS: example of labels for the case where penpot will be exposed to the
         # ## internet with HTTPS using traefik.
   
         # - "traefik.http.routers.penpot-https.rule=Host(`<DOMAIN_NAME>`)"
         # ## When Penpot is served from a subpath, match it too:
         # - "traefik.http.routers.penpot-https.rule=Host(`<DOMAIN_NAME>`) && PathPrefix(`/<SUBPATH>`)"
         # - "traefik.http.routers.penpot-https.entrypoints=websecure"
         # - "traefik.http.routers.penpot-https.tls.certresolver=letsencrypt"
         # - "traefik.http.routers.penpot-https.tls=true"
   
       environment:
         << : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri]
   
         # Don't touch it; this uses an internal docker network to
         # communicate with the admin-console.
         PENPOT_ADMIN_CONSOLE_URI: http://penpot-admin-console:3000
   
         # Set to "true" on hosts where IPv6 is disabled at kernel boot level.
         # PENPOT_DISABLE_IPV6_LISTEN: "true"
   
     penpot-backend:
       image: "penpotapp/backend:${PENPOT_VERSION:-2.18}"
       restart: always
   
       volumes:
         - penpot_assets:/opt/data/assets
   
       depends_on:
         penpot-postgres:
           condition: service_healthy
         penpot-valkey:
           condition: service_healthy
   
       networks:
         - penpot
   
       ## Configuration environment variables for the backend container.
   
   
       environment:
         << : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key, *penpot-database]
   
         # Don't touch it; this uses an internal docker network to
         # communicate with the admin-console.
         PENPOT_ADMIN_CONSOLE_URI: http://penpot-admin-console:3000
   
         ## Valkey (or previously redis) is used for the websockets notifications. Don't touch
         ## unless the valkey container has different parameters or different name.
         PENPOT_REDIS_URI: redis://penpot-valkey/0
   
         ## Default configuration for assets storage: using filesystem based with all files
         ## stored in a docker volume.
         PENPOT_OBJECTS_STORAGE_BACKEND: fs
         PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/assets
   
         ## Also can be configured to to use a S3 compatible storage.
         # AWS_ACCESS_KEY_ID: <KEY_ID>
         # AWS_SECRET_ACCESS_KEY: <ACCESS_KEY>
         # PENPOT_OBJECTS_STORAGE_BACKEND: s3
         # PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <ENDPOINT>
         # PENPOT_OBJECTS_STORAGE_S3_BUCKET: <BUKET_NAME>
   
         ## Telemetry. When enabled, a periodical process will send anonymous data about this
         ## instance. Telemetry data will enable us to learn how the application is used,
         ## based on real scenarios. If you want to help us, please leave it enabled. You can
         ## audit what data we send with the code available on github.
         PENPOT_TELEMETRY_ENABLED: "true"
         PENPOT_TELEMETRY_REFERER: compose
   
         ## Example SMTP/Email configuration. By default, emails are sent to the Mailpit
         ## service, but for production usage it is recommended to setup a real SMTP
         ## provider. Emails are used to confirm user registrations & invitations. Look below
         ## how the Mailpit service is configured.
         PENPOT_SMTP_DEFAULT_FROM: no-reply@example.com
         PENPOT_SMTP_DEFAULT_REPLY_TO: no-reply@example.com
         PENPOT_SMTP_HOST: penpot-mailpit
         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:-2.18}"
       restart: always
   
       depends_on:
         penpot-postgres:
           condition: service_healthy
   
       networks:
         - penpot
   
       environment:
         << : [*penpot-public-uri, *penpot-secret-key, *penpot-database]
   
         # Don't touch it; this uses an internal docker network to
         # communicate with the frontend.
         PENPOT_INTERNAL_URI: http://penpot-frontend:8080
   
     penpot-mcp:
       image: "penpotapp/mcp:${PENPOT_VERSION:-2.18}"
       restart: always
       networks:
         - penpot
   
     penpot-exporter:
       image: "penpotapp/exporter:${PENPOT_VERSION:-2.18}"
       restart: always
   
       depends_on:
         penpot-valkey:
           condition: service_healthy
   
       networks:
         - penpot
   
       environment:
         << : [*penpot-flags, *penpot-secret-key, *penpot-public-uri]
   
         # Don't touch it; this uses an internal docker network to
         # communicate with the frontend.
         PENPOT_INTERNAL_URI: http://penpot-frontend:8080
   
         ## Valkey (or previously Redis) is used for the websockets notifications
         ## and for storing the state export jobs
         PENPOT_REDIS_URI: redis://penpot-valkey/0
   
         # PENPOT_EXPORTER_MAX_CONCURRENT_JOBS: 4
         # PENPOT_EXPORTER_MAX_JOBS_PER_PROFILE: 2
         # PENPOT_EXPORTER_QUEUE_MAX: 64
         # PENPOT_EXPORTER_JOB_TTL: 3600
         # PENPOT_WASM_WORKER_POOL_MAX: 2
         # PENPOT_WASM_WORKER_POOL_MIN: 1
         # PENPOT_WASM_WORKER_IDLE_TIMEOUT: 300
         # PENPOT_WASM_WORKER_IMAGE_CACHE_SIZE: 134217728
   
     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
   
     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:
         # You can increase the max memory size if you have sufficient resources,
         # although this should not be necessary.
         - VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu
   
     ## A Mailpit service, used as temporal SMTP server. You can access via HTTP to the
     ## port 8025 to read all emails the penpot platform has sent. Should be only used as a
     ## temporal solution while no real SMTP provider is configured. Messages are kept in
     ## memory only and are lost when the container is recreated; uncomment the volume
     ## below (and the one in the top-level volumes: section) to persist them instead.
   
     penpot-mailpit:
       image: axllent/mailpit:v1
       restart: always
       # volumes:
       #   - penpot_mailpit_data:/data
       # environment:
       #   - MP_DATABASE=/data/mailpit.db
       expose:
         - '1025'
       ports:
         - "8025:8025"
       networks:
         - penpot
   ```

3. Set the values the compose file needs ([source](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml))

   ```text
   These variables have no default in the compose file: PENPOT_SMTP_PASSWORD, PENPOT_SMTP_USERNAME. Compose reads them from the shell or from a .env file next to the compose file.
   ```

4. Start it ([source](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml))

   ```bash
   docker compose up -d
   ```

5. Open the app ([source](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml))

   ```text
   Open http://localhost:9001. The compose file publishes host port 9001 for the penpot-frontend service.
   ```

Compose file check: config_ok on 2026-10-22.

## Gotchas and maintenance

### Gotchas

- The compose file publishes host ports: Host ports: 9001 (penpot-frontend), 8025 (penpot-mailpit). A port without a host address listens on all host network interfaces. ([source](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml), checked 2026-10-22)

### Signals

Computed from a GitHub snapshot taken 2026-10-22.

- Open issues and pull requests: 881
- Stars: 61480, forks: 4258
- Last push: 2026-10-22 (0 days ago)
- Latest release: 2.18.3 on 2026-10-06 (15 days ago)

## Sources

- [Repository: https://github.com/penpot/penpot](https://github.com/penpot/penpot)
- [License: https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/LICENSE](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/LICENSE)
- [Business model: https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/README.md](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/README.md)
- [Compose file: https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml](https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml)

---
Canonical: https://openlogs.fyi/tools/penpot  
Last updated: 2026-10-22
