{
  "schema_version": 1,
  "generated_at": "2026-10-22T00:00:00.000Z",
  "canonical_url": "https://openlogs.fyi/tools/penpot",
  "slug": "penpot",
  "name": "Penpot",
  "tagline": "Browser-based design and prototyping platform for product teams.",
  "replaces": {
    "summary": "Paid interface design and prototyping services, such as Figma.",
    "products": [
      {
        "slug": "figma",
        "name": "Figma",
        "vendor": "Figma",
        "url": "https://openlogs.fyi/alternatives/figma"
      }
    ]
  },
  "stack": [
    {
      "kind": "language",
      "name": "Clojure"
    },
    {
      "kind": "database",
      "name": "PostgreSQL"
    },
    {
      "kind": "database",
      "name": "Valkey"
    }
  ],
  "deployment_methods": [
    "docker_compose"
  ],
  "requirements": {
    "min_ram_mb": null,
    "min_cpu_cores": null,
    "source_url": null
  },
  "license": {
    "spdx": "MPL-2.0",
    "class": "osi",
    "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/LICENSE"
  },
  "business_model": {
    "value": "unknown",
    "notes": "The README at the pinned commit does not state a business model, so it stays unknown.",
    "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/README.md",
    "checked_at": "2026-10-22"
  },
  "difficulty": {
    "value": "hard",
    "reason": "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."
  },
  "repository": {
    "owner": "penpot",
    "name": "penpot",
    "url": "https://github.com/penpot/penpot"
  },
  "compose_check": {
    "status": "config_ok",
    "validated_at": "2026-10-22",
    "summary": "config_ok on 2026-10-22"
  },
  "recipe": {
    "version": 1,
    "source_path": "docker/images/docker-compose.yaml",
    "source_repo": "penpot/penpot",
    "source_commit_sha": "347a52d51465b35d77b48b6d1a3fbd47c8b861f0",
    "source_kind": "app_repo",
    "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml"
  },
  "install_steps": [
    {
      "order": 1,
      "kind": "prerequisite",
      "language": "text",
      "label": "Docker with the Compose plugin",
      "body": "A host with Docker and the Docker Compose plugin. This entry runs from a compose file.",
      "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml",
      "validation_status": "unvalidated"
    },
    {
      "order": 2,
      "kind": "compose",
      "language": "yaml",
      "label": "Save the compose file as docker-compose.yaml",
      "body": "## Common flags:\n# demo-users\n# email-verification\n# log-emails\n# log-invitation-tokens\n# login-with-github\n# login-with-gitlab\n# login-with-google\n# login-with-ldap\n# login-with-oidc\n# login-with-password\n# prepl-server\n# registration\n# secure-session-cookies\n# smtp\n# smtp-debug\n# telemetry\n# webhooks\n##\n## You can read more about all available flags and other\n## environment variables here:\n## https://help.penpot.app/technical-guide/configuration/#penpot-configuration\n#\n# WARNING: if you're exposing Penpot to the internet, you should remove the flags\n# 'disable-secure-session-cookies' and 'disable-email-verification'\nx-flags: &penpot-flags\n  PENPOT_FLAGS: disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies enable-mcp enable-admin-console\n\n## Public URI where Penpot is reached. To serve Penpot from a subpath, include\n## it with a trailing slash, e.g. https://example.com/penpot/. A reverse proxy in\n## front must forward the subpath unchanged, without rewriting it.\nx-uri: &penpot-public-uri\n  PENPOT_PUBLIC_URI: http://localhost:9001\n\nx-body-size: &penpot-http-body-size\n  # Max body size\n  PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600\n  # Deprecation warning: this variable is deprecated. Use PENPOT_HTTP_SERVER_MAX_BODY (defaults to 367001600)\n  PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600\n\n## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems\n## (eg http sessions, or invitations) are derived.\n##\n## We recommend to use a truly randomly generated\n## 512 bits base64 encoded string here. You can generate one with:\n##\n## python3 -c \"import secrets; print(secrets.token_urlsafe(64))\"\nx-secret-key: &penpot-secret-key\n  PENPOT_SECRET_KEY: change-this-insecure-key\n\n## Single database configuration block, shared by backend and admin-console.\n## Don't touch them unless you are using custom postgresql connection parameters.\nx-database: &penpot-database\n  PENPOT_DATABASE_URI: postgresql://penpot-postgres/penpot\n  PENPOT_DATABASE_USERNAME: penpot\n  PENPOT_DATABASE_PASSWORD: penpot\n\nnetworks:\n  penpot:\n\nvolumes:\n  penpot_postgres_v15:\n  penpot_assets:\n  # penpot_mailpit_data:\n  # penpot_traefik:\n\nservices:\n  ## Traefik service declaration example. Consider using it if you are going to expose\n  ## penpot to the internet, or a different host than `localhost`.\n\n  # traefik:\n  #   image: traefik:v3.3\n  #   networks:\n  #     - penpot\n  #   command:\n  #     - \"--api.insecure=true\"\n  #     - \"--entryPoints.web.address=:80\"\n  #     - \"--providers.docker=true\"\n  #     - \"--providers.docker.exposedbydefault=false\"\n  #     - \"--entryPoints.websecure.address=:443\"\n  #     - \"--certificatesresolvers.letsencrypt.acme.tlschallenge=true\"\n  #     - \"--certificatesresolvers.letsencrypt.acme.email=<EMAIL_ADDRESS>\"\n  #     - \"--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json\"\n  #   volumes:\n  #     - \"penpot_traefik:/traefik\"\n  #     - \"/var/run/docker.sock:/var/run/docker.sock\"\n  #   ports:\n  #     - \"80:80\"\n  #     - \"443:443\"\n\n  penpot-frontend:\n    image: \"penpotapp/frontend:${PENPOT_VERSION:-2.18}\"\n    restart: always\n    ports:\n      - 9001:8080\n\n    volumes:\n      - penpot_assets:/opt/data/assets\n\n    depends_on:\n      - penpot-backend\n      - penpot-admin-console\n      - penpot-mcp\n      - penpot-exporter\n\n    networks:\n      - penpot\n\n    # labels:\n      # - \"traefik.enable=true\"\n\n      # ## HTTPS: example of labels for the case where penpot will be exposed to the\n      # ## internet with HTTPS using traefik.\n\n      # - \"traefik.http.routers.penpot-https.rule=Host(`<DOMAIN_NAME>`)\"\n      # ## When Penpot is served from a subpath, match it too:\n      # - \"traefik.http.routers.penpot-https.rule=Host(`<DOMAIN_NAME>`) && PathPrefix(`/<SUBPATH>`)\"\n      # - \"traefik.http.routers.penpot-https.entrypoints=websecure\"\n      # - \"traefik.http.routers.penpot-https.tls.certresolver=letsencrypt\"\n      # - \"traefik.http.routers.penpot-https.tls=true\"\n\n    environment:\n      << : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri]\n\n      # Don't touch it; this uses an internal docker network to\n      # communicate with the admin-console.\n      PENPOT_ADMIN_CONSOLE_URI: http://penpot-admin-console:3000\n\n      # Set to \"true\" on hosts where IPv6 is disabled at kernel boot level.\n      # PENPOT_DISABLE_IPV6_LISTEN: \"true\"\n\n  penpot-backend:\n    image: \"penpotapp/backend:${PENPOT_VERSION:-2.18}\"\n    restart: always\n\n    volumes:\n      - penpot_assets:/opt/data/assets\n\n    depends_on:\n      penpot-postgres:\n        condition: service_healthy\n      penpot-valkey:\n        condition: service_healthy\n\n    networks:\n      - penpot\n\n    ## Configuration environment variables for the backend container.\n\n\n    environment:\n      << : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key, *penpot-database]\n\n      # Don't touch it; this uses an internal docker network to\n      # communicate with the admin-console.\n      PENPOT_ADMIN_CONSOLE_URI: http://penpot-admin-console:3000\n\n      ## Valkey (or previously redis) is used for the websockets notifications. Don't touch\n      ## unless the valkey container has different parameters or different name.\n      PENPOT_REDIS_URI: redis://penpot-valkey/0\n\n      ## Default configuration for assets storage: using filesystem based with all files\n      ## stored in a docker volume.\n      PENPOT_OBJECTS_STORAGE_BACKEND: fs\n      PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/assets\n\n      ## Also can be configured to to use a S3 compatible storage.\n      # AWS_ACCESS_KEY_ID: <KEY_ID>\n      # AWS_SECRET_ACCESS_KEY: <ACCESS_KEY>\n      # PENPOT_OBJECTS_STORAGE_BACKEND: s3\n      # PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <ENDPOINT>\n      # PENPOT_OBJECTS_STORAGE_S3_BUCKET: <BUKET_NAME>\n\n      ## Telemetry. When enabled, a periodical process will send anonymous data about this\n      ## instance. Telemetry data will enable us to learn how the application is used,\n      ## based on real scenarios. If you want to help us, please leave it enabled. You can\n      ## audit what data we send with the code available on github.\n      PENPOT_TELEMETRY_ENABLED: \"true\"\n      PENPOT_TELEMETRY_REFERER: compose\n\n      ## Example SMTP/Email configuration. By default, emails are sent to the Mailpit\n      ## service, but for production usage it is recommended to setup a real SMTP\n      ## provider. Emails are used to confirm user registrations & invitations. Look below\n      ## how the Mailpit service is configured.\n      PENPOT_SMTP_DEFAULT_FROM: no-reply@example.com\n      PENPOT_SMTP_DEFAULT_REPLY_TO: no-reply@example.com\n      PENPOT_SMTP_HOST: penpot-mailpit\n      PENPOT_SMTP_PORT: 1025\n      PENPOT_SMTP_USERNAME:\n      PENPOT_SMTP_PASSWORD:\n      PENPOT_SMTP_TLS: \"false\"\n      PENPOT_SMTP_SSL: \"false\"\n\n  penpot-admin-console:\n    image: \"penpotapp/admin-console:${PENPOT_VERSION:-2.18}\"\n    restart: always\n\n    depends_on:\n      penpot-postgres:\n        condition: service_healthy\n\n    networks:\n      - penpot\n\n    environment:\n      << : [*penpot-public-uri, *penpot-secret-key, *penpot-database]\n\n      # Don't touch it; this uses an internal docker network to\n      # communicate with the frontend.\n      PENPOT_INTERNAL_URI: http://penpot-frontend:8080\n\n  penpot-mcp:\n    image: \"penpotapp/mcp:${PENPOT_VERSION:-2.18}\"\n    restart: always\n    networks:\n      - penpot\n\n  penpot-exporter:\n    image: \"penpotapp/exporter:${PENPOT_VERSION:-2.18}\"\n    restart: always\n\n    depends_on:\n      penpot-valkey:\n        condition: service_healthy\n\n    networks:\n      - penpot\n\n    environment:\n      << : [*penpot-flags, *penpot-secret-key, *penpot-public-uri]\n\n      # Don't touch it; this uses an internal docker network to\n      # communicate with the frontend.\n      PENPOT_INTERNAL_URI: http://penpot-frontend:8080\n\n      ## Valkey (or previously Redis) is used for the websockets notifications\n      ## and for storing the state export jobs\n      PENPOT_REDIS_URI: redis://penpot-valkey/0\n\n      # PENPOT_EXPORTER_MAX_CONCURRENT_JOBS: 4\n      # PENPOT_EXPORTER_MAX_JOBS_PER_PROFILE: 2\n      # PENPOT_EXPORTER_QUEUE_MAX: 64\n      # PENPOT_EXPORTER_JOB_TTL: 3600\n      # PENPOT_WASM_WORKER_POOL_MAX: 2\n      # PENPOT_WASM_WORKER_POOL_MIN: 1\n      # PENPOT_WASM_WORKER_IDLE_TIMEOUT: 300\n      # PENPOT_WASM_WORKER_IMAGE_CACHE_SIZE: 134217728\n\n  penpot-postgres:\n    image: \"postgres:15\"\n    restart: always\n    stop_signal: SIGINT\n\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U penpot\"]\n      interval: 2s\n      timeout: 10s\n      retries: 5\n      start_period: 2s\n\n    volumes:\n      - penpot_postgres_v15:/var/lib/postgresql/data\n\n    networks:\n      - penpot\n\n    environment:\n      - POSTGRES_INITDB_ARGS=--data-checksums\n      - POSTGRES_DB=penpot\n      - POSTGRES_USER=penpot\n      - POSTGRES_PASSWORD=penpot\n\n  penpot-valkey:\n    image: valkey/valkey:8.1\n    restart: always\n\n    healthcheck:\n      test: [\"CMD-SHELL\", \"valkey-cli ping | grep PONG\"]\n      interval: 1s\n      timeout: 3s\n      retries: 5\n      start_period: 3s\n\n    networks:\n      - penpot\n\n    environment:\n      # You can increase the max memory size if you have sufficient resources,\n      # although this should not be necessary.\n      - VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu\n\n  ## A Mailpit service, used as temporal SMTP server. You can access via HTTP to the\n  ## port 8025 to read all emails the penpot platform has sent. Should be only used as a\n  ## temporal solution while no real SMTP provider is configured. Messages are kept in\n  ## memory only and are lost when the container is recreated; uncomment the volume\n  ## below (and the one in the top-level volumes: section) to persist them instead.\n\n  penpot-mailpit:\n    image: axllent/mailpit:v1\n    restart: always\n    # volumes:\n    #   - penpot_mailpit_data:/data\n    # environment:\n    #   - MP_DATABASE=/data/mailpit.db\n    expose:\n      - '1025'\n    ports:\n      - \"8025:8025\"\n    networks:\n      - penpot\n",
      "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml",
      "validation_status": "config_ok"
    },
    {
      "order": 3,
      "kind": "env",
      "language": "text",
      "label": "Set the values the compose file needs",
      "body": "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.",
      "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml",
      "validation_status": "unvalidated"
    },
    {
      "order": 4,
      "kind": "shell",
      "language": "bash",
      "label": "Start it",
      "body": "docker compose up -d",
      "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml",
      "validation_status": "unvalidated"
    },
    {
      "order": 5,
      "kind": "verify",
      "language": "text",
      "label": "Open the app",
      "body": "Open http://localhost:9001. The compose file publishes host port 9001 for the penpot-frontend service.",
      "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml",
      "validation_status": "unvalidated"
    }
  ],
  "notes": [
    {
      "kind": "gotcha",
      "title": "The compose file publishes host ports",
      "body": "Host ports: 9001 (penpot-frontend), 8025 (penpot-mailpit). A port without a host address listens on all host network interfaces.",
      "source_url": "https://github.com/penpot/penpot/blob/347a52d51465b35d77b48b6d1a3fbd47c8b861f0/docker/images/docker-compose.yaml",
      "checked_at": "2026-10-22"
    }
  ],
  "signals": {
    "captured_at": "2026-10-22",
    "stars": 61480,
    "forks": 4258,
    "open_issues": 881,
    "last_push": "2026-10-22",
    "days_since_push": 0,
    "latest_release": "2.18.3",
    "latest_release_date": "2026-10-06",
    "days_since_release": 15
  },
  "links": {
    "html": "https://openlogs.fyi/tools/penpot",
    "markdown": "https://openlogs.fyi/tools/penpot.md",
    "json": "https://openlogs.fyi/tools/penpot.json"
  },
  "updated_at": "2026-10-22"
}
