# penpot-nix Nix packages and a NixOS module for self-hosting [Penpot](https://penpot.app) (pinned to release **2.18.3**). Every component is built from upstream source: the Clojure API backend, the static frontend (served by nginx), the headless-Chromium exporter, the MCP server and the Rust/WASM renderer. One module wires the stack together with PostgreSQL and Valkey, either provisioned locally or pointed at infrastructure you already run. Derived from [debling/nix-penpot](https://github.com/debling/nix-penpot) (MIT, see [LICENSE](LICENSE)). `x86_64-linux` only. ## Packages | Attribute | Output | |-----------------------------|------------------------------------------------------------------------| | `penpot-backend` | `bin/penpot-backend` (JVM wrapper), `bin/penpot-manage` | | `penpot-frontend` | `dist/` static bundle (+ `plugins/`, `plugins/mcp/`), `config.js` | | `penpot-exporter` | `bin/penpot-exporter` (node + nixpkgs chromium) | | `penpot-mcp` | `bin/penpot-mcp` | | `penpot-render-wasm` | `render-wasm.{js,wasm}`, `shared.js`, `worker/render.js` (frontend) | | `penpot-render-wasm-export` | export flavor of the renderer (unused on 2.18.x) | The same packages are exposed as an overlay (`overlays.default`: `pkgs.penpot-backend`, `pkgs.penpot-frontend`, `pkgs.penpot-exporter`, `pkgs.penpot-mcp`). The module's package defaults resolve through the overlay, so it is required. ## Deploying on NixOS ```nix # flake.nix { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; penpot-nix.url = "git+https://"; }; outputs = { self, nixpkgs, penpot-nix }: { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ ./configuration.nix { nixpkgs.overlays = [ penpot-nix.overlays.default ]; } penpot-nix.nixosModules.default ]; }; }; } ``` ```nix # configuration.nix { services.penpot = { enable = true; publicUri = "https://penpot.example.com"; secretKeyFile = "/run/secrets/penpot-secret-key"; }; networking.firewall.allowedTCPPorts = [ 80 443 ]; } ``` That is a complete deployment: PostgreSQL and Valkey are provisioned locally, the nginx vhost is generated, exporter and MCP run behind it. Secrets are files loaded through systemd credentials; generate the master key once (`openssl rand -base64 64 | tr -d '\n'`) and manage it with agenix/sops-nix. Commonly tuned options (all documented inline in [`modules/penpot.nix`](modules/penpot.nix)): | Option | Default | What it does | |----------------------------------|-----------------------------|-------------------------------------------------------------| | `publicUri` | – (required) | Public URL of the instance | | `secretKeyFile` | – (required) | Master secret key file | | `flags` | `[ "enable-prepl-server" ]` | Upstream feature flags | | `admins` | `[ ]` | Emails granted admin on login | | `database.createLocally` | `true` | Local PostgreSQL, or set `database.uri` for an external one | | `redis.createLocally` | `true` | Local Valkey, or set `redis.uri` | | `storage.backend` | `"fs"` | `"fs"` or `"s3"` (+ endpoint/bucket/keys) | | `smtp.*` / `mailpit.enable` | off | Outgoing email, or a local throwaway SMTP sink | | `exporter.enable` / `mcp.enable` | `true` | Toggle the auxiliary services | | `nginx.forceHttps` | `false` | Force HTTPS (configure TLS yourself) | ### Users and admins No default credentials. Register through the UI, or on the backend host: ```bash penpot-manage create-profile -n "Jane Doe" -e jane@example.org -p 'secret' ``` Admin rights come from `services.penpot.admins = [ "jane@example.org" ];`. ## Trying it in a VM ```bash nix build .#nixosConfigurations.penpot-vm.config.system.build.vm ./result/bin/run-penpot-vm-vm ``` Host port 8080 → nginx, 8025 → mailpit UI. `curl http://127.0.0.1:8080/readyz` once the backend is up (~1–2 min). Needs ~3 GB RAM. ## Development ```bash direnv allow # or: nix develop — devShell ships the afk harness, jj, git nix flake check --no-build nix build .#checks.x86_64-linux.penpot-vm-test # end-to-end VM test nix fmt ``` Development runs under [afk](https://github.com/DavHau/afk) (`afk` is on PATH in the devShell). ## Updating Penpot 1. Bump `penpot.url` **and** `penpotVersion` in `flake.nix` (upstream's `package.json` version is stale, so it is pinned by hand). 2. Regenerate `deps-lock.json` with clj-nix from a writable copy of the penpot source with the `RELEASE` pins from `packages/frontend.nix` (`pinDepsEdn`) applied: ```bash nix run github:jlesquembre/clj-nix/#deps-lock -- \ --deps-include backend/deps.edn --deps-include common/deps.edn \ --deps-include frontend/deps.edn --deps-include exporter/deps.edn \ --alias-exclude outdated --alias-exclude jvm-repl ``` 3. Rehash the fixed-output derivations (set to `lib.fakeHash`, build, copy `got:`): `penpot-frontend.{frontendPnpmDeps,pluginsPnpmDeps,mcpPnpmDeps}`, `penpot-exporter.pnpmDeps`, `penpot-mcp.pnpmDeps`, `penpot-backend.builtinTemplates` (after `nix flake update penpot-files`), and `cargoDeps` / Rust / Skia hashes in `packages/render-wasm.nix` if `render-wasm/Cargo.lock`, the Rust version or the Skia URL changed. 4. Rebase `packages/patches/*` if `frontend/src/app/plugins/library.cljs` moved. ## License Nix code: MIT ([LICENSE](LICENSE)). Penpot: MPL-2.0. The remaining stack (PostgreSQL, Valkey, Chromium, …) ships under its own licenses.