GICT knowledge base
How to Self-Host Penpot with Docker on Ubuntu 24.04
Published
Penpot gives you a browser-based workspace for interface design without putting every design file in a hosted service. Installing it involves more than starting the editor: the backend must save files, and the exporter must turn your design into a usable image.
This guide helps you install Penpot 2.17.2 with Docker on Ubuntu 24.04. You will create a local account, draw a small design, reopen it, and export it. Access stays private through an SSH tunnel, so you do not need a domain to complete the installation.
Before you start
Use an Ubuntu 24.04 VPS with 4 vCPUs, 6 GB RAM, sufficient free disk space for the images and your designs, and a sudo-capable SSH account. You also need a current desktop browser and SSH client on your computer. Replace ubuntu and SERVER_IP in the tunnel command with your own login and server address.
This is a fresh, single-server installation. It uses Penpot’s frontend, backend, and exporter images, with PostgreSQL and Valkey. The database and design assets have persistent Docker volumes. It is not a high-availability setup, and a hosted design tool is simpler if you do not want to manage a server.
Install Docker and prepare the files
On a fresh Ubuntu server, install Docker and the Compose plugin:
$ sudo apt-get update
$ sudo apt-get install -y docker.io docker-compose-v2 curl
$ sudo systemctl enable --now docker
$ sudo docker version
$ sudo docker compose version
If Docker is already installed from another supported source, keep that installation and check that docker compose version works. Do not mix package sources just to repeat this block.
Create the project directory and a private environment file:
$ mkdir -p ~/penpot
$ cd ~/penpot
$ touch .env
$ chmod 600 .env
Create two different random secrets in your password manager: a 128-character hexadecimal string for Penpot and a 64-character hexadecimal string for PostgreSQL. Edit ~/penpot/.env with these two lines, replacing both placeholders with the generated values:
PENPOT_SECRET_KEY=REPLACE_WITH_128_RANDOM_HEX_CHARACTERS
POSTGRES_PASSWORD=REPLACE_WITH_64_RANDOM_HEX_CHARACTERS
Do not use the placeholder text as a secret. Keep this file private and retain it with the installation: the backend and exporter need the same Penpot key, and the database password must remain consistent.
Create ~/penpot/compose.yaml with these contents:
x-common: &common
PENPOT_PUBLIC_URI: http://localhost:18094
PENPOT_FLAGS: >-
disable-registration disable-email-verification
disable-secure-session-cookies disable-telemetry
enable-prepl-server disable-onboarding
services:
penpot-frontend:
image: penpotapp/frontend:2.17.2
restart: unless-stopped
ports:
- "127.0.0.1:9001:8080"
volumes:
- penpot_assets:/opt/data/assets
environment: *common
depends_on:
- penpot-backend
- penpot-exporter
penpot-backend:
image: penpotapp/backend:2.17.2
restart: unless-stopped
volumes:
- penpot_assets:/opt/data/assets
environment:
<<: *common
PENPOT_SECRET_KEY: ${PENPOT_SECRET_KEY:?Set PENPOT_SECRET_KEY}
PENPOT_DATABASE_URI: postgresql://penpot-postgres/penpot
PENPOT_DATABASE_USERNAME: penpot
PENPOT_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
PENPOT_REDIS_URI: redis://penpot-valkey/0
PENPOT_OBJECTS_STORAGE_BACKEND: fs
PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/assets
depends_on:
penpot-postgres:
condition: service_healthy
penpot-valkey:
condition: service_healthy
penpot-exporter:
image: penpotapp/exporter:2.17.2
restart: unless-stopped
environment:
<<: *common
PENPOT_SECRET_KEY: ${PENPOT_SECRET_KEY:?Set PENPOT_SECRET_KEY}
PENPOT_INTERNAL_URI: http://penpot-frontend:8080
PENPOT_REDIS_URI: redis://penpot-valkey/0
depends_on:
penpot-valkey:
condition: service_healthy
penpot-postgres:
image: postgres:15
restart: unless-stopped
environment:
POSTGRES_DB: penpot
POSTGRES_USER: penpot
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
volumes:
- penpot_postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U penpot"]
interval: 5s
timeout: 5s
retries: 10
penpot-valkey:
image: valkey/valkey:8.1
restart: unless-stopped
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
volumes:
penpot_postgres:
penpot_assets:
The shared x-common settings keep the browser address consistent across services. The flags disable the optional onboarding survey and enable the container-local management server needed for account creation. PENPOT_PUBLIC_URI is the URL you will open on your computer; its name does not make the service publicly accessible. Only the frontend publishes a port, and that port listens on the VPS loopback address.
Keep PENPOT_INTERNAL_URI pointing to the frontend’s Docker service name. The exporter runs inside Docker and cannot use your computer’s localhost tunnel to reach the editor.
Start the services and create your account
Validate the configuration, download the images, and start the stack:
$ sudo docker compose config --quiet
$ sudo docker compose pull
$ sudo docker compose up -d
$ sudo docker compose ps
The first start initializes PostgreSQL and runs backend migrations. Wait for the database and Valkey health checks to pass, and check that the other services remain running. Read the backend logs if initialization is still in progress:
$ sudo docker compose logs --tail=50 penpot-backend
$ curl -I http://127.0.0.1:9001
Expect HTTP 200 from the frontend. That response is only the first check; it does not prove that file saving or export works. Create your first account through Penpot’s supported management command:
$ sudo docker compose exec penpot-backend python3 manage.py create-profile
Enter the email address, name, and password when prompted. The password prompt does not echo what you type. There is no default account to leave active. Browser registration stays disabled in this setup.
Open Penpot and save a small design
On your own computer, start the tunnel in a separate terminal:
$ ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:18094:127.0.0.1:9001 ubuntu@SERVER_IP
Leave that terminal open and visit http://localhost:18094 in your browser. Log in with the account you just created. Do not browse to the VPS’s public port 9001; it is intentionally unavailable directly.
In Drafts, choose + New File. Double-click the file name at the top left and rename it Installation check. Select Board (B) and drag on the canvas to make a board. Use Rectangle (R) to draw inside it, then Text (T) to add Penpot works. Give the rectangle a visible fill so the exported result is easy to recognize. This small fixture is enough to test the installation; you do not need to build a full interface.
Wait for the saving indicator beside the file name to finish before leaving the file. Click Drafts to return to the dashboard, reopen Installation check, and confirm that the board, rectangle, and text remain. Reload once more if necessary to distinguish saved content from what is still in the browser’s memory.
Export and verify the result
Select the board in Layers, scroll to Export in the right-hand panel, and click Add export (the plus button). Leave PNG at 1x, choose Export 1 element, and open the downloaded file. Check that it contains the expected rectangle and text, not a blank canvas or error page. To check vector export as well, change the format dropdown from PNG to SVG and export again.
Our TeamCompute test exported a 600 × 360 board as a 5,322-byte PNG in 4.60 seconds, measured from clicking Export until the browser download was saved. The image contained the colored rectangle and text. The SVG also had a 600 × 360 viewBox and included the expected text. These are functional observations, not a rendering-speed benchmark.
These checks exercise different parts of the stack. Reopening the file checks saved design data; a PNG export also exercises the exporter service. A working login screen alone cannot confirm either result.
Keep the file private while testing. Open its ordinary editor URL in a separate browser session that is not logged in but still has access through the tunnel. It should ask for a login rather than reveal the design. Sharing a prototype is a separate action, not part of this installation check.
If something does not work
- Login does not appear or keeps failing: check that the account-creation command completed, that the backend is running, and that the browser URL exactly matches
PENPOT_PUBLIC_URI. - The browser cannot connect: keep the SSH terminal open and ensure local port 18094 is free. If you change that port, update
PENPOT_PUBLIC_URIand apply the configuration again. - The editor works but PNG export fails: inspect the exporter logs with
sudo docker compose logs --tail=50 penpot-exporter. Keep its internal URI on the Docker frontend address, and check that all Penpot images use the same version. - Services restart or initialization stalls: inspect memory, disk space, and service logs. Do not delete the named volumes as a troubleshooting shortcut; they contain the database and design assets.
You now have a private Penpot installation with a saved design and a checked export. Closing the SSH tunnel disconnects your browser, but it does not stop the services on the VPS.
Related guides
How to Self-Host DocuSeal for Document Signing
Self-host DocuSeal with private administrator setup, PostgreSQL and HTTPS. Test signing, understand email data flows and create consistent backups.
Self HostedHow to Self-Host Docmost with Docker on Ubuntu 24.04
Install Docmost with Docker, PostgreSQL and Redis. Set up a private workspace and verify editing, live updates and attachment retrieval.
Self HostedHow to Remove Image Backgrounds Locally with rembg
Remove image backgrounds locally with rembg. Preserve originals, check PNG transparency and inspect cutout quality on both light and dark backgrounds.
Self HostedHow to Translate Text Offline with Argos Translate
Translate plain text locally with Argos Translate. Install a language pair, prepare caches, verify offline operation and review translation quality.
