Workload · Running your team's design tool

Penpot self hosted: your team's design tool on a server you control

You can run Penpot, the open-source design and prototyping tool, on your own server with the Docker Compose file the Penpot team publishes, and the hardware bar is modest: their guide says 16 GB of memory and 4 CPUs are enough for thousands of users. What you are really taking on is the care around it. Someone has to decide who may sign up, get email working so invitations arrive, put it behind HTTPS, and keep copies of the database and uploaded files somewhere else. This page walks you through each of those, in the order they matter.

The short version
  1. Hardware bar, per Penpot: 16 GB of memory, 4 CPUs
  2. Plan 50 to 100 GB of database space for a team of up to ten editors
  3. Real email is required for team invitations
  4. Back up two things: the database and the uploads
  5. Open source under the MPL-2.0 licence

Will one server carry your whole design team?

Yes, comfortably, for almost any team. Penpot does most of its drawing in each designer's browser, so the server mostly stores files and passes changes between people. That is why the Penpot team asks more of your laptops than of the machine: they suggest capable computers and the latest stable Chrome for everyone using it.

  • Processor and memory: 4 CPUs and 16 GB, which Penpot says supports thousands of users.
  • Database space: Penpot suggests 50 to 100 GB for a team of up to ten editors, on storage you can enlarge, plus roughly 5 GB per editor beyond ten.
  • Uploads: every image and SVG your team adds lands on disk as well, so leave space beyond the database.
  • Your own skills: Penpot expects comfort with Docker Compose, DNS records and a reverse proxy.

How fast the database grows depends on how complex your files are, so check the disk monthly at first.

What actually runs on the machine

The official compose file starts eight small services that talk to each other on a private Docker network. You will mostly deal with four of them:

  • The frontend, the part your browser loads. Penpot publishes it on port 9001, the address everything else hangs off.
  • The backend, which handles accounts, files and the live updates you see when a colleague moves something.
  • PostgreSQL 15, the database, and Valkey, an in-memory store Penpot uses to coordinate those live updates.
  • Mailpit, a test inbox that catches outgoing email until you connect a real mail service. Its web page is published on port 8025.

The rest run quietly in the background: an exporter that renders your exports, an admin console that arrived in version 2.18, and an MCP service.

Close the doors before you open the app

Turn on the firewall first, allowing only SSH, and make sure it covers IPv6 before you add a single rule. There is one trap worth knowing about. The stock file lists its ports without an address (9001:8080), and Docker then opens them on every IPv4 and IPv6 address of the server. Connections to such a port bypass ufw's rules, certainly over IPv4, so ufw cannot shut it. What does work is publishing Penpot on 127.0.0.1 alone and letting a web server on the same machine bring visitors in. Use Docker Engine 28 or newer for this: on older versions, other machines sharing the server's local network could still get through to a 127.0.0.1 port (Docker issue moby#45610).

Mailpit's page shows every email Penpot sends, including sign-up confirmations and invitation links. Never leave port 8025 reachable from outside; the steps below bind it to 127.0.0.1 as well.

Up and running in an evening

  1. Get Docker running first: Docker Engine 28 or newer plus its Compose plugin, installed the way Docker's documentation describes. sudo docker version tells you which engine you have.
  2. Run the first block: firewall on, Penpot's compose file downloaded, and your secrets in a .env file only root can read (mode 600), the key made with Penpot's recommended generator.
  3. Run the second block: it changes the published ports to 127.0.0.1, swaps the placeholder secrets for references to .env, and removes disable-secure-session-cookies and disable-email-verification, the two flags the file itself says to drop on an internet-facing install.
  4. Penpot is now running, but nobody outside the server can reach it yet.
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable

sudo mkdir -p /srv/penpot
cd /srv/penpot
sudo curl -o docker-compose.yaml https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
sudo install -m 600 /dev/null .env
{
  echo "PENPOT_VERSION=2.18"
  echo "PENPOT_PUBLIC_URI=https://penpot.example.org"
  echo "PENPOT_SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_urlsafe(64))')"
  echo "PENPOT_DB_PASSWORD=$(openssl rand -hex 24)"
} | sudo tee .env >/dev/null
cd /srv/penpot
sudo sed -i \
  -e 's|- 9001:8080|- "127.0.0.1:9001:8080"|' \
  -e 's|"8025:8025"|"127.0.0.1:8025:8025"|' \
  -e 's|PUBLIC_URI: http://localhost:9001|PUBLIC_URI: ${PENPOT_PUBLIC_URI}|' \
  -e 's|SECRET_KEY: change-this-insecure-key|SECRET_KEY: ${PENPOT_SECRET_KEY}|' \
  -e 's|DATABASE_PASSWORD: penpot|DATABASE_PASSWORD: ${PENPOT_DB_PASSWORD}|' \
  -e 's|POSTGRES_PASSWORD=penpot|POSTGRES_PASSWORD=${PENPOT_DB_PASSWORD}|' \
  -e 's| disable-email-verification||; s| disable-secure-session-cookies||' \
  docker-compose.yaml
sudo docker compose -p penpot -f docker-compose.yaml up -d
sudo docker compose -p penpot ps

PENPOT_PUBLIC_URI must be the exact address your team will type; the configuration guide wants it pointing at the address users open. The database password has to be in place before the very first start, because the PostgreSQL container only applies it when it creates a new, empty database. Pinning PENPOT_VERSION is what Penpot recommends over always pulling the latest image. Anonymous telemetry to Penpot is on in the file; set PENPOT_TELEMETRY_ENABLED to "false" to stop it.

Choosing who gets an account

Out of the box, anyone who can open your Penpot address can make an account. For production, Penpot's advice is to keep that, with email verification and working mail, rather than switch sign-up off. Your options for a private team:

  • Limit sign-up to your company's email domain with PENPOT_REGISTRATION_DOMAIN_WHITELIST in the backend's settings. Verification then proves the address is real.
  • Use the login your team already has. Add enable-login-with-oidc and the base URI, client ID and client secret your provider gives you; the provider needs the callback https://penpot.example.org/api/auth/oidc/callback. Add enable-oidc-registration so new colleagues get an account on first sign-in, and disable-login-with-password if you want the password form gone. Our authentik guide sets up such a provider.
  • Close sign-up completely with disable-registration. Penpot lists this for demo setups, and once it is on, the command line is the only way to add people.

Flags go on the PENPOT_FLAGS line, one of the first lines in the compose file; secrets such as the OIDC client secret go in .env. Run the same up -d command after each change. Then make your own account from the command line while the site is still unreachable from outside: cd /srv/penpot && sudo docker compose -p penpot exec penpot-backend python3 manage.py create-profile, the Compose form of the docker exec command in Penpot's guide. It asks for your name, email and password. Do not use its -p option, which would leave the password in your shell history and in the process list.

Email that actually arrives

Penpot cannot add a colleague to a team without sending them an invitation they accept by email, so mail is not optional for a shared install. Until you configure a provider, everything lands in Mailpit, which forgets it all when the container is recreated. To peek at it meanwhile, open an SSH tunnel as yourself, ssh -N -L 8025:127.0.0.1:8025 you@your-server, then open localhost:8025 in your browser.

When you are ready, open the backend's mail block and fill it in: sender and reply addresses on your own domain (PENPOT_SMTP_DEFAULT_REPLY_TO, PENPOT_SMTP_DEFAULT_FROM), your provider's server in PENPOT_SMTP_HOST on port 587, its login name in PENPOT_SMTP_USERNAME, and PENPOT_SMTP_TLS set to true. The password itself goes in .env, referenced from the compose file as ${PENPOT_SMTP_PASSWORD}.

Putting it on your own web address

Penpot strongly recommends HTTPS; over plain HTTP some browser features, the clipboard for one, stop working. Create a DNS record for your Penpot hostname, then put a web server in front of port 9001. Penpot's docs show nginx, Caddy and Traefik. Caddy is the shortest: a Caddyfile site block for penpot.example.org holding the single line reverse_proxy 127.0.0.1:9001. By default Caddy fetches and renews a certificate by itself, and its reverse proxy passes WebSocket connections through, which matters because Penpot's live collaboration uses a WebSocket connection on the /ws/notifications path.

With nginx instead, copy what Penpot's example adds: locations for /ws/notifications and /mcp/ws that forward the Upgrade and Connection headers, and client_max_body_size 367001600 to match Penpot's upload limit.

Once your account exists and the sign-up rules are settled, let visitors in: sudo ufw allow 443/tcp for HTTPS, plus sudo ufw allow 80/tcp for the certificate check and redirects. IPv6 was switched on before the first rule, so both cover IPv6 visitors too.

Keeping your designs safe, and current

Everything your team makes lives in two Docker volumes: the database and the uploaded assets. Penpot warns against copying the volume folders straight off the disk and points to Docker's method instead: a throwaway container mounts the volume read-only and packs it into a tar file. The script below does that for the uploads, takes a pg_dump of the database, and keeps two weeks. Create its folder, readable by root alone (sudo install -d -m 700 /srv/penpot-backups), then schedule it from root's crontab.

# save as /root/penpot-backup.sh
set -eu
umask 077
OUT=/srv/penpot-backups
DAY=$(date +%F)
cd /srv/penpot
docker compose -p penpot exec -T penpot-postgres pg_dump -U penpot -Fc penpot > "$OUT/penpot-db-$DAY.dump"
docker run --rm -v penpot_penpot_assets:/assets:ro -v "$OUT":/out ubuntu tar czf "/out/penpot-assets-$DAY.tar.gz" -C /assets .
find "$OUT" -type f -mtime +14 -delete

# root crontab line (sudo crontab -e):
15 3 * * * umask 077; bash /root/penpot-backup.sh > /srv/penpot-backups/log-$(date +\%F).txt 2>&1

The volume names assume the project is called penpot; docker volume ls confirms them. Copy the backup folder to a different machine regularly; copies on the server's own disk vanish with it. You manage the server; we keep the hardware and the network running.

Updating takes three moves: run the backup, raise PENPOT_VERSION in .env, then, from /srv/penpot, sudo docker compose -p penpot pull and sudo docker compose -p penpot up -d. Penpot asks you to update in small increments rather than leaping across many versions, and to read the upgrade notes, since some releases change the compose file; version 2.18, for instance, introduced the admin console service. As your copy is customised, download the new official file, apply the same edits, and compare before switching.

The server that fits

Penpot's figure of 4 CPUs and 16 GB lines up with the smaller machines on our dedicated servers page. A cartridge built on an Intel Xeon E3-1585L v5 has 4 cores and 16 to 64 GB of memory on NVMe storage, with a choice of disks on its order page; the Xeon D-1587 gives you 16 cores and a 1.92 TB NVMe disk if you want the database and uploads to have room for years. If the same box will also run your team's code hosting or wiki, step up to an everyday single-processor server.

  • The whole machine is yours, with root access, so a busy database never waits on someone else's workload.
  • The port is unmetered, so heavy exports and uploads never become a bandwidth bill.
  • Every rack server and blade has a console (screen plus power button), as do two cartridge models, m510 and m710x, so a firewall mistake does not lock you out for good.
  • A ready server is handed over within 30 minutes of payment approval, and you can reinstall the system yourself from the control panel.
  • No contract: it runs month to month, and you can cancel when any month ends.

Questions people ask

Is Penpot free to run on my own server?
Penpot's code is published under the Mozilla Public License 2.0, and the self-hosting guide describes the same experience as Penpot's cloud. Your costs are the server and your own time for updates and backups. Self-hosted images arrive shortly after each cloud release rather than on the same day.
How do I create the first account?
A new install has no accounts. Create yours with manage.py create-profile in the backend container before the site is public, and set your sign-up rules at the same time, so nobody else can sign up while you set things up.
Can my team sign in with our existing company login?
Yes, if your identity provider speaks OpenID Connect. Turn on enable-login-with-oidc, give Penpot the base URI plus the client ID and secret, and register the /api/auth/oidc/callback address with the provider. Add enable-oidc-registration if new colleagues should get an account on their first sign-in.
Do I need a separate mail server?
You need a mail service Penpot can send through, not necessarily your own. Team invitations only work by email, and verification emails protect open sign-up. Any SMTP provider you already use will do: set its host, port, username and password in the backend settings.

Give your design team a home of its own

Pick a machine sized for your editors and their files, then run Penpot on it behind HTTPS.

Get a server for your Penpot install

Read next