# Penpot — VPS (ROOTLESS PODMAN). Ported from the home server 2026-08-12. # # ⚠️ NOT STARTED. No systemd unit is enabled. It has never run on either machine, so # there is no data to migrate. Bring it up with: # systemctl --user enable --now podman-penpot.service # # BEFORE FIRST START, create its role and database on the SHARED postgres (this stack # no longer ships its own): # podman exec -i postgresql psql -U npm <<'SQL' # CREATE ROLE penpot LOGIN PASSWORD ''; # CREATE DATABASE penpot OWNER penpot; # SQL # Then add the same lines to PostgreSQL/initdb/00-roles-and-databases.sql. # # NOTE: the home version used postgres:15-alpine with --data-checksums. The shared # instance is postgres 17 without checksums. Penpot does not care, and since there is # no existing data this is not a migration — but if you ever DO need checksums, they # can only be set at initdb time for the whole cluster. # # Redis stays local to this stack: Penpot uses it for transient session and # notification state only, so it is not worth centralising. # # Rootless adaptations from the home version: # - Dropped its private postgres:15-alpine; uses the shared instance (AGENTS.md §2). # - PENPOT_DATABASE_URI repointed from penpot-db to the shared postgresql. # - :Z on the assets bind mount. It is shared between frontend and backend, so it # uses LOWERCASE :z -- uppercase would give each container a private MCS category # and the second to start would lock the first out (AGENTS.md §6). # - /etc/localtime mounts removed in favour of TZ (AGENTS.md §8). # # Registration is disabled by default (PENPOT_FLAGS disable-registration), so create # the first account from the backend container before you can log in. # # NPM: proxy penpot.gbrown.org -> http://penpot-frontend:8080 networks: nginx-bridge: external: true services: penpot-frontend: image: docker.io/penpotapp/frontend:latest container_name: penpot-frontend hostname: penpot-frontend domainname: penpot.gbrown.org networks: ['nginx-bridge'] environment: - TZ=America/New_York - PENPOT_FLAGS=disable-registration enable-login-with-password disable-smtp labels: com.centurylinklabs.watchtower.enable: "true" volumes: - ./volumes/assets:/opt/data/assets:z depends_on: - penpot-backend - penpot-exporter tty: true stdin_open: true restart: unless-stopped penpot-backend: image: docker.io/penpotapp/backend:latest container_name: penpot-backend hostname: penpot-backend domainname: penpot.gbrown.org networks: ['nginx-bridge'] environment: - TZ=America/New_York - PENPOT_FLAGS=disable-registration enable-login-with-password disable-smtp enable-prepl-server - PENPOT_PUBLIC_URI=https://penpot.gbrown.org - PENPOT_SECRET_KEY=${PENPOT_SECRET_KEY} # Shared postgres, not a private container - PENPOT_DATABASE_URI=postgresql://postgresql/penpot - PENPOT_DATABASE_USERNAME=penpot - PENPOT_DATABASE_PASSWORD=${PENPOT_DATABASE_PASSWORD} - PENPOT_REDIS_URI=redis://penpot-redis/0 - PENPOT_ASSETS_STORAGE_BACKEND=assets-fs - PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets - PENPOT_TELEMETRY_ENABLED=false labels: com.centurylinklabs.watchtower.enable: "true" volumes: - ./volumes/assets:/opt/data/assets:z depends_on: - penpot-redis tty: true stdin_open: true restart: unless-stopped penpot-exporter: image: docker.io/penpotapp/exporter:latest container_name: penpot-exporter hostname: penpot-exporter domainname: penpot.gbrown.org networks: ['nginx-bridge'] environment: - TZ=America/New_York - PENPOT_PUBLIC_URI=http://penpot-frontend:8080 - PENPOT_REDIS_URI=redis://penpot-redis/0 labels: com.centurylinklabs.watchtower.enable: "true" depends_on: - penpot-redis tty: true stdin_open: true restart: unless-stopped penpot-redis: image: docker.io/library/redis:7-alpine container_name: penpot-redis hostname: penpot-redis domainname: penpot.gbrown.org networks: ['nginx-bridge'] environment: - TZ=America/New_York labels: com.centurylinklabs.watchtower.enable: "true" tty: true stdin_open: true restart: unless-stopped