[Solved] Public-uri stuck on localhost:9001 behind Caddy

I managed to get it working through a Docker bridge network (caddy_network shared with Caddy) instead of exposed ports, as promised.

The changes were straightforward: on the Penpot side, add the shared caddy_network to penpot-frontend’s networks list and remove its ports: mapping; then just update the reverse_proxy line in the Caddyfile to point at penpot-frontend:8080 (the Docker service name) instead of an IP:port.

One thing worth flagging: after switching to the bridge, I hit a new CrowdSec AppSec false positive that I hadn’t seen before — rule 943120 (“Possible Session Fixation Attack”) blocking Penpot’s WebSocket notifications endpoint (/ws/notifications?session-id=...), which legitimately lacks a Referer header during the handshake.

Note for CrowdSec’s users
I’m not fully sure whether this is specific to the bridge setup or something I just hadn’t triggered yet with exposed ports — but if you’re running CrowdSec AppSec with the CRS ruleset, watch out for it. The workaround (a scoped on_match exception) is in my previous post above, alongside the 920420 one.

Happy to share the full updated compose.yml + Caddyfile snippet if anyone wants it.

Here is the compose.yml

x-flags: &penpot-flags
  PENPOT_FLAGS: enable-smtp enable-prepl-server enable-mcp

x-uri: &penpot-public-uri
  PENPOT_PUBLIC_URI: https://penpot.mywebsite.com

x-body-size: &penpot-http-body-size
  PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
  PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600

x-secret-key: &penpot-secret-key
  PENPOT_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

networks:
  penpot:
  caddy_network:
    external: true

volumes:
  penpot_postgres_v15:
  penpot_assets:

services:

  penpot-frontend:
    image: "penpotapp/frontend:${PENPOT_VERSION:-2.16}"
    restart: always

    volumes:
      - penpot_assets:/opt/data/assets

    depends_on:
      - penpot-backend
      - penpot-exporter
      - penpot-mcp

    networks:
      - penpot
      - caddy_network

    environment:
      << : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri]

  penpot-backend:
    image: "penpotapp/backend:${PENPOT_VERSION:-2.16}"
    restart: always

    volumes:
      - penpot_assets:/opt/data/assets

    depends_on:
      penpot-postgres:
        condition: service_healthy
      penpot-valkey:
        condition: service_healthy

    networks:
      - penpot

    environment:
      << : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key]

      PENPOT_DATABASE_URI: postgresql://penpot-postgres/penpot
      PENPOT_DATABASE_USERNAME: penpot
      PENPOT_DATABASE_PASSWORD: penpot

      PENPOT_REDIS_URI: redis://penpot-valkey/0

      AWS_ACCESS_KEY_ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      AWS_SECRET_ACCESS_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      PENPOT_OBJECTS_STORAGE_BACKEND: s3
      PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: https://xxxxxxxxxxxxxxxx.r2.cloudflarestorage.com
      PENPOT_OBJECTS_STORAGE_S3_BUCKET: penpot-assets
      PENPOT_OBJECTS_STORAGE_S3_REGION: auto

      PENPOT_TELEMETRY_ENABLED: "true"
      PENPOT_TELEMETRY_REFERER: compose

      PENPOT_SMTP_DEFAULT_FROM: noreply@mywebsite.com
      PENPOT_SMTP_DEFAULT_REPLY_TO: noreply@mywebsite.com
      PENPOT_SMTP_HOST: mymxserver.mxrouting.net
      PENPOT_SMTP_PORT: 465
      PENPOT_SMTP_USERNAME: noreply@mywebsite.com
      PENPOT_SMTP_PASSWORD: xxxxxxxxxxxxxxxxx
      PENPOT_SMTP_TLS: "true"
      PENPOT_SMTP_SSL: "true"

  penpot-mcp:
    image: "penpotapp/mcp:${PENPOT_VERSION:-2.16}"
    restart: always
    networks:
      - penpot

  penpot-exporter:
    image: "penpotapp/exporter:${PENPOT_VERSION:-2.16}"
    restart: always

    depends_on:
      penpot-valkey:
        condition: service_healthy

    networks:
      - penpot

    environment:
      << : [*penpot-secret-key, *penpot-public-uri]
      PENPOT_INTERNAL_URI: http://penpot-frontend:8080
      PENPOT_REDIS_URI: redis://penpot-valkey/0

  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:
      - VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu

And the caddyfile entry

penpot.mywebsite.com {
        import crowdsec_bouncer
        reverse_proxy penpot-frontend:8080
}
1 Like