I self-hosted Penpot so client mockups stay on my VPS

I self-hosted Penpot so client mockups stay on my VPS

I wanted a Figma-class editor without sending NDA mockups to a SaaS. Penpot's official Compose stack on Ubuntu 24.04 came up, then the browser showed a blank page because PENPOT_PUBLIC_URI did not match HTTPS. Here's that wall and the Caddy proxy that fixed websockets.

· Updated · 7 min read #self-hosted #open-source #penpot #design #deployment #docker #vps

 Penpot open-source design platform

Caption: Penpot on my VPS — design files stay in my Docker volumes, not in a vendor cloud.

 Penpot self-hosted stack overview

Caption: Compose runs Penpot with PostgreSQL and Redis; Caddy terminates HTTPS in front.

Introduction

I wanted a browser design tool for client work under NDA — not another Figma seat that stores boards on someone else's disk. Penpot is SVG-native, real-time, and the official path is Docker Compose with PostgreSQL and Redis.

On a fresh Ubuntu 24.04 box this stack is famous for a blank page and websocket errors when PENPOT_PUBLIC_URI is not exactly the HTTPS URL people type. I set http:// in .env while Caddy served https://, and the editor never fully connected. Matching scheme + host (no surprise trailing slash) and proxying to port 9001 fixed it. A 502 after that is usually the frontend container not healthy yet, or Caddy pointing at the wrong port.

I lock registration after the first team accounts exist.

Why I picked Penpot

  • MPL 2.0 — I can run it on my terms.
  • Live cursors in the same file, which is what I actually needed for handoff.
  • SVG stays closer to what Laravel/Vue frontends consume.
  • Tokens and components so the design system does not live in a chat thread.
  • Official images and Compose files I can pin and upgrade.

I outgrew desktop-only tools. I did not want a per-seat SaaS bill for every freelancer on a project.

Prerequisites

Hardware:

  • 2+ CPU cores (4 if several people edit at once)
  • 4 GB RAM minimum (8 GB for a team)
  • 30 GB free disk (more for asset libraries)
  • Ubuntu 24.04 LTS with sudo

Software & accounts:

  • Docker Engine 24+ and Compose plugin
  • Domain such as penpot.example.com
  • DNS A at the VPS
  • SMTP (optional on day one; I want it before invites)

Security notes:

  • SSH keys; password login off.
  • TLS at the reverse proxy before I invite anyone.
  • Disable open registration after admins exist.
  • Back up PostgreSQL and asset volumes before upgrades.
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl git ufw
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker

Firewall:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Installation Guide

I use Penpot's maintained Compose files from GitHub, not a homemade container.

1. Download the Official Compose Stack

sudo mkdir -p /opt/penpot
sudo chown "$USER":"$USER" /opt/penpot
cd /opt/penpot
curl -LO https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
curl -LO https://raw.githubusercontent.com/penpot/penpot/main/docker/images/env.sample
cp env.sample .env

2. Configure Environment Variables

Edit .env with the public URL and mail:

PENPOT_PUBLIC_URI=https://penpot.example.com

# Example SMTP (adjust for your provider)
PENPOT_SMTP_ENABLED=true
PENPOT_SMTP_DEFAULT_FROM=noreply@example.com
PENPOT_SMTP_DEFAULT_REPLY_TO=support@example.com
PENPOT_SMTP_HOST=smtp.example.com
PENPOT_SMTP_PORT=587
PENPOT_SMTP_USERNAME=your-user
PENPOT_SMTP_PASSWORD=your-password
PENPOT_SMTP_TLS=true
PENPOT_SMTP_SSL=false

# Feature flags (adjust after first login)
PENPOT_FLAGS=enable-registration enable-login-with-password enable-smtp

PENPOT_PUBLIC_URI must be the exact HTTPS URL. Mismatch here is the websocket wall I hit.

3. Start Penpot

cd /opt/penpot
docker compose pull
docker compose up -d
docker compose ps

Frontend, backend, PostgreSQL, Redis, exporter. First boot can take a few minutes while databases initialize.

4. Put HTTPS in Front with Caddy

Official Compose exposes the app on localhost port 9001 by default — I verify with docker compose port penpot-frontend 80 if the file differs:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy

/etc/caddy/Caddyfile:

penpot.example.com {
    reverse_proxy localhost:9001
}
sudo systemctl reload caddy

 Penpot collaborative workspace

Caption: Artboards and layers in the browser — this only works if the public URI and websockets agree.

Configuration

First Admin Account

  1. Visit https://penpot.example.com.
  2. Register the first user (instance owner on a fresh install).
  3. Create a team and invite people if SMTP is configured.

Lock Down Registration

After the team is in, I drop public signup in .env:

PENPOT_FLAGS=enable-login-with-password enable-smtp disable-registration
cd /opt/penpot
docker compose up -d

Backups

docker compose down
sudo tar -czf penpot-backup-$(date +%F).tar.gz /opt/penpot/.env /var/lib/docker/volumes
docker compose up -d

I record volume names with docker volume ls | grep penpot — they follow the Compose project name.

Updates

cd /opt/penpot
docker compose pull
docker compose up -d

I read Penpot release notes before upgrading anything a designer is using.

Usage

Getting started:

  1. Log in and create a Team.
  2. Create a Project and a File.
  3. Frames, components, color styles; share the file link.
  4. Inspect for CSS/SVG for developers.
  5. Export PNG/SVG when marketing needs a still.

How I collaborate:

  • File permissions per role.
  • Shared libraries for buttons and type.
  • Prototype flows before I hand a Laravel/Vue build a pile of screenshots.

Where it broke

On a lab Ubuntu 24.04 box this is the failure Penpot is famous for once Compose says "healthy."

1. Blank page / websocket errors

The UI loaded a shell and then sat there. Browser console talked about websockets. Official troubleshooting: PENPOT_PUBLIC_URI must match the HTTPS domain exactly — scheme + host, no games with http:// behind Caddy or a trailing slash that does not match what I typed.

Fix:

PENPOT_PUBLIC_URI=https://penpot.example.com

Then:

cd /opt/penpot
docker compose up -d

2. 502 from the reverse proxy

Caddy returned 502. docker compose logs -f showed the frontend still coming up, or the proxy port did not match Compose. I wait for healthy containers, then:

docker compose port penpot-frontend 80

I point Caddy at that host port (9001 in the official file). If I guessed 80 on the host, that is the 502.

Email not sending is enable-smtp missing from PENPOT_FLAGS, or SMTP credentials that fail outside Penpot too. I test SMTP with a one-off before I blame the flags.

Troubleshooting

  • Slow editor: more RAM; assets on SSD volumes.
  • Cannot register after disable-registration: CLI/admin flows in Penpot help, or temporarily re-enable registration.
  • Upgrade failures: stop stack, backup volumes, pull, docker compose up -d, read logs.

Scaling, Securing, and Next Steps

  • S3-compatible storage for media when libraries get large (Penpot admin docs).
  • SSO/OIDC if the team already has an IdP.
  • Uptime Kuma on /api/health.
  • Off-site copies of volume archives.
  • Point the component library at the Tailwind/Laravel set we actually ship.

Conclusion

Penpot is running under /opt/penpot on my Ubuntu 24.04 VPS: official Compose, Caddy on penpot.example.com, registration closed after the first team. Client boards stay in my volumes.

Next I would restore that backup tarball once, turn on SMTP for real invites, and only then give designers the URL. The blank-page wall was PENPOT_PUBLIC_URI, not "Docker is hard."

Did you hit the same wall?

I got stuck on a blank editor because PENPOT_PUBLIC_URI did not match HTTPS (websockets never connected). Did you hit the same thing, or a different one — Caddy 502, SMTP flags, registration locked too early? Tell me in the comments. I read them.

Need this done on your server?

I deploy and harden Laravel/CodeCanyon apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.