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
- Get Docker running first: Docker Engine 28 or newer plus its Compose plugin, installed the way Docker's documentation describes.
sudo docker versiontells you which engine you have. - Run the first block: firewall on, Penpot's compose file downloaded, and your secrets in a
.envfile only root can read (mode 600), the key made with Penpot's recommended generator. - Run the second block: it changes the published ports to 127.0.0.1, swaps the placeholder secrets for references to
.env, and removesdisable-secure-session-cookiesanddisable-email-verification, the two flags the file itself says to drop on an internet-facing install. - 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_WHITELISTin the backend's settings. Verification then proves the address is real. - Use the login your team already has. Add
enable-login-with-oidcand the base URI, client ID and client secret your provider gives you; the provider needs the callbackhttps://penpot.example.org/api/auth/oidc/callback. Addenable-oidc-registrationso new colleagues get an account on first sign-in, anddisable-login-with-passwordif 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-profilein 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/callbackaddress with the provider. Addenable-oidc-registrationif 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.