Penpot is an open-source design and prototyping platform for designers, developers, and product teams, running entirely in the browser and storing files in open web standards like SVG so design and code stay in sync. This guide deploys Penpot on a Linux server using Docker Compose with PostgreSQL for data storage, Valkey for websocket notifications, and Traefik handling TLS termination, then walks through account creation and a full design-to-prototype workflow. By the end, you'll have a self-hosted Penpot instance running securely over HTTPS with a working team, project, and prototype.
Set Up the Directory Structure, Configuration, and Environment Variables
Penpot separates its runtime into a frontend, a backend, an exporter, a PostgreSQL database, and a Valkey cache. PostgreSQL and the uploaded assets require persistent storage, while Valkey holds only transient data and needs no volume.
1. Create the project directory and a subdirectory for TLS certificates, then move into it:
$ mkdir -p ~/penpot/letsencrypt
$ cd ~/penpot
2. Generate a random secret key for the application:
$ python3 -c "import secrets; print(secrets.token_urlsafe(64))"
Copy the output for use in the environment file in the next step.
3. Create the environment file:
$ nano .env
Add the following configuration. Replace penpot.example.com with your domain name, admin@example.com with your email address, GENERATED_SECRET_KEY with the value from the previous step, and STRONG_DATABASE_PASSWORD with a secure password.
# Penpot version
PENPOT_VERSION=2.17.0
# Domain and TLS
DOMAIN=penpot.example.com
LETSENCRYPT_EMAIL=admin@example.com
# Public URI (must match your domain over HTTPS)
PENPOT_PUBLIC_URI=https://penpot.example.com
# Feature flags
PENPOT_FLAGS=enable-registration enable-login-with-password disable-email-verification enable-prepl-server
# Master secret key
PENPOT_SECRET_KEY=GENERATED_SECRET_KEY
# Request body size limits (350 MiB)
PENPOT_HTTP_SERVER_MAX_BODY_SIZE=367001600
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=367001600
# PostgreSQL configuration
POSTGRES_DB=penpot
POSTGRES_USER=penpot
POSTGRES_PASSWORD=STRONG_DATABASE_PASSWORD
PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot
PENPOT_DATABASE_USERNAME=penpot
PENPOT_DATABASE_PASSWORD=STRONG_DATABASE_PASSWORD
# Valkey (Redis-compatible) for websocket notifications
PENPOT_REDIS_URI=redis://penpot-valkey/0
# Exporter internal connection
PENPOT_INTERNAL_URI=http://penpot-frontend:8080
# Asset storage
PENPOT_OBJECTS_STORAGE_BACKEND=fs
PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets
# SMTP configuration (Optional)
# PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com
# PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com
# PENPOT_SMTP_HOST=smtp.example.com
# PENPOT_SMTP_PORT=587
# PENPOT_SMTP_USERNAME=SMTP_USERNAME
# PENPOT_SMTP_PASSWORD=SMTP_PASSWORD
# PENPOT_SMTP_TLS=true
# PENPOT_SMTP_SSL=false
Save and close the file. PENPOT_SECRET_KEY is the master key from which Penpot derives other subsystem keys, such as those for HTTP sessions and invitations. PENPOT_FLAGS controls platform features: the flags above enable self-service registration and password login, disable email verification so you can create the first account without an external mail server, and enable the PREPL server used for command-line administration.
Note: The
disable-email-verificationflag lets any visitor register without confirming an email address. For an internet-facing production instance, configure a real SMTP provider, removedisable-email-verification, and addenable-smtptoPENPOT_FLAGSso new accounts and invitations must be verified by email.
Deploy with Docker Compose
The stack runs Penpot's frontend, backend, and exporter containers alongside PostgreSQL, Valkey, and Traefik as the reverse proxy that terminates TLS and routes the configured domain to the frontend. This configuration is based on the official Penpot Docker Compose setup.
1. Create the Docker Compose file:
$ nano docker-compose.yaml
services:
traefik:
image: traefik:v3.7.9
container_name: penpot-traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
- penpot
penpot-frontend:
image: "penpotapp/frontend:${PENPOT_VERSION:-2.17.0}"
container_name: penpot-frontend
restart: unless-stopped
volumes:
- penpot_assets:/opt/data/assets
depends_on:
- penpot-backend
- penpot-exporter
networks:
- penpot
env_file: .env
labels:
- "traefik.enable=true"
- "traefik.http.routers.penpot.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.penpot.entrypoints=websecure"
- "traefik.http.routers.penpot.tls.certresolver=letsencrypt"
- "traefik.http.services.penpot.loadbalancer.server.port=8080"
penpot-backend:
image: "penpotapp/backend:${PENPOT_VERSION:-2.17.0}"
container_name: penpot-backend
restart: unless-stopped
volumes:
- penpot_assets:/opt/data/assets
depends_on:
penpot-postgres:
condition: service_healthy
penpot-valkey:
condition: service_healthy
networks:
- penpot
env_file: .env
penpot-exporter:
image: "penpotapp/exporter:${PENPOT_VERSION:-2.17.0}"
container_name: penpot-exporter
restart: unless-stopped
depends_on:
penpot-valkey:
condition: service_healthy
networks:
- penpot
env_file: .env
penpot-postgres:
image: postgres:15
container_name: penpot-postgres
restart: unless-stopped
stop_signal: SIGINT
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 2s
timeout: 10s
retries: 5
start_period: 2s
volumes:
- penpot_postgres:/var/lib/postgresql/data
networks:
- penpot
environment:
POSTGRES_INITDB_ARGS: "--data-checksums"
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
penpot-valkey:
image: valkey/valkey:8.1
container_name: penpot-valkey
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "valkey-cli ping | grep PONG"]
interval: 1s
timeout: 3s
retries: 5
start_period: 3s
networks:
- penpot
networks:
penpot:
volumes:
penpot_postgres:
penpot_assets:
Save and close the file. traefik handles reverse proxying and TLS termination, provisioning Let's Encrypt certificates over ports 80 and 443. penpot-frontend serves the web interface on internal port 8080, routed by the Traefik labels. penpot-backend runs the application logic and shares the penpot_assets volume with the frontend. penpot-exporter renders boards and design files to PNG, SVG, and PDF. penpot-postgres runs PostgreSQL 15 with a health check that gates backend startup, and penpot-valkey runs Valkey 8.1 for websocket notifications and transient state.
2. Start all services in detached mode:
$ docker compose up -d
3. Verify that all containers are running:
$ docker compose ps -a
The output lists all six containers as Up, with penpot-postgres and penpot-valkey also showing (healthy).
4. View the backend logs to confirm that Penpot started successfully:
$ docker compose logs penpot-backend
The output shows the backend applying database migrations and reporting that the HTTP server is listening. Traefik requests the Let's Encrypt certificate on the first HTTPS access, which can take up to a minute to complete.
Access and Configure Penpot
After deployment, access Penpot through your domain, verify the HTTPS certificate, and create the first account. Because email verification is disabled in this configuration, the first account is active immediately after registration.
- Replace
penpot.example.comwith your configured domain and openhttps://penpot.example.comin a web browser. - On the login screen, click Create an account.
- Enter your FULL NAME, WORK EMAIL, and a PASSWORD, then click CREATE AN ACCOUNT.
- Complete the onboarding questions to reach the Penpot dashboard.
- Review the workspace layout. The left sidebar lists your teams and projects, the Drafts area holds files that are not assigned to a project, and the account menu in the upper-right corner opens profile and password settings.
Demonstrate an End-to-End Design Workflow in Penpot
Penpot organizes work into teams, projects, and files. This section walks through creating a team and project, designing a screen with boards and components, linking screens with a prototype interaction, and sharing the result.
Create a Team and Project
- In the left sidebar, click Your Penpot at the top. From the dropdown, click Create new team.
- Enter a team name such as
Product Design, then click CREATE NEW TEAM. - Inside the new team, click + NEW PROJECT at the top right, enter a name such as
Mobile App, and press Enter.
Create a Design File
- Open the project and click + NEW FILE to open the file in the design workspace.
- Rename the file by double-clicking its name in the top toolbar and entering
Login Flow.
Add Boards, Shapes, Text, and Components
- Select the Board tool (keyboard shortcut
B) and choose a preset size such as a Phone from the Size presets in the right panel, then double-click on the board to create a screen. - Use the Rectangle tool (
R) and the Ellipse tool (E) to draw UI elements such as buttons and input fields on the board. - Select the Text tool (
T), click the board, and type a label such asSign in. - Style the selected elements using the Design panel on the right to set fill colors, borders, and corner radius.
- To create a reusable component, select one or more elements, right-click the selection, and choose Create component (
K). The component appears in the ASSETS tab for reuse across files.
Create a Prototype Interaction
- Create a second board to act as the destination screen, for example a
Homescreen. - Switch to the PROTOTYPE tab in the upper-right panel.
- Select the button on the first board. A connection handle appears on the edge of the selection. Drag it to the second board to create a flow.
- In the INTERACTION settings, set the trigger to On click and the action to Navigate to the destination board.
- Click the Play icon in the upper-right corner to preview the prototype and test the click interaction between screens.
Share the Design
- Click the Share button in the upper-right corner, enter the Email addresses of people you would like to add as a member to the team, and assign a role such as Editor, Viewer, or Admin.
- Click SEND INVITATION to confirm collaboration on the shared team.
Note: Team invitations are delivered by email and require the SMTP configuration. Without SMTP, invitation emails are not sent.
Next Steps
Penpot is running with a full design-to-prototype workflow validated end to end. From here you can:
- Configure a real SMTP provider and switch on email verification for production use
- Import existing designs from Figma or Sketch to migrate a team's workflow
- Build a shared component library and design tokens for consistent UI across projects
For the full guide with additional tips, visit the original article on Vultr Docs.
Top comments (0)