Self-hosting
Run Pinwren on your own server — one container plus Postgres, with your data on your infrastructure.
Pinwren is built to be self-hosted. The whole product — dashboard, API, widget bundles, background jobs and database migrations — ships as one Docker image. Postgres is the only other service it needs: it holds the data, the sessions, the rate-limit counters and the job queue. There is no Redis, message broker or object store to run.
Going live on a server?
Deploy on your server takes a fresh VPS to HTTPS, the first account, email and the widget step by step, with Docker Compose or a Portainer stack.
Requirements
| Resource | Minimum |
|---|---|
| Docker | Docker Engine with the Compose plugin (or any container runtime that runs Compose files) |
| CPU, RAM | 1–2 vCPU and 1 GB RAM handle about 70 requests/s — see Scaling |
| Database | Postgres 17 (included in the compose file), or your own Postgres |
| Domain | A hostname with HTTPS in front, e.g. feedback.example.com — see Reverse proxy |
The image
Releases are published to ghcr.io/krithik-m-codes/pinwren for linux/amd64 and linux/arm64, with an SBOM
and build provenance attached. Each release is tagged three ways:
| Tag | Example | Moves when |
|---|---|---|
| version | 0.1.0 | never — pin this in production |
| minor | 0.1 | a new patch release is out |
latest | latest | any new release |
The image is based on node:22-alpine, runs as the unprivileged node user, listens on port 3000, keeps
uploaded files in /data and has a HEALTHCHECK on /api/health. It starts PINWREN_WORKERS Node processes
(default 2) that share the port.
The compose file
docker/compose.yml runs the app and
Postgres:
services:
pinwren:
image: ${PINWREN_IMAGE:-pinwren:latest}
build:
context: ..
dockerfile: docker/Dockerfile
restart: unless-stopped
# Every variable in docker/.env reaches the app; environment: below wins for the same name.
env_file:
- path: .env
required: false
ports:
- "${PINWREN_BIND:-127.0.0.1}:${PINWREN_PORT:-3000}:3000"
environment:
PINWREN_URL: ${PINWREN_URL:?Set PINWREN_URL to the public URL, e.g. https://feedback.example.com}
PINWREN_SECRET: ${PINWREN_SECRET:?Set PINWREN_SECRET (openssl rand -base64 48)}
DATABASE_URL: postgres://pinwren:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@db:5432/pinwren
PINWREN_TRUST_PROXY: ${PINWREN_TRUST_PROXY:-1}
# Unset: first account + invitations only. false: open sign-up.
PINWREN_DISABLE_SIGNUP: ${PINWREN_DISABLE_SIGNUP:-}
PINWREN_ADMIN_EMAILS: ${PINWREN_ADMIN_EMAILS:-}
PINWREN_OUTBOUND_ALLOWLIST: ${PINWREN_OUTBOUND_ALLOWLIST:-}
PINWREN_WORKERS: ${PINWREN_WORKERS:-2}
PINWREN_LICENSE_FILE: ${PINWREN_LICENSE_FILE:-}
AI_PROVIDER: ${AI_PROVIDER:-}
AI_MODEL: ${AI_MODEL:-}
AI_API_KEY: ${AI_API_KEY:-}
AI_BASE_URL: ${AI_BASE_URL:-}
PINWREN_EU_PRIVACY: ${PINWREN_EU_PRIVACY:-}
volumes:
- pinwren-data:/data
depends_on:
db:
condition: service_healthy
db:
image: postgres:17-alpine
# … user, password and database "pinwren", a pg_isready health check
volumes:
- pinwren-db:/var/lib/postgresql/data
volumes:
pinwren-data:
pinwren-db:- Port. The app is published on
127.0.0.1:3000only, so it is reachable through a reverse proxy on the same host but not from the internet. ChangePINWREN_BIND(e.g.0.0.0.0) andPINWREN_PORTindocker/.envif you need to. - Volumes.
pinwren-dbholds Postgres;pinwren-dataholds uploaded screenshots, attachments and session replays. Back up both — see Backups. - Image or build. Without
PINWREN_IMAGEthe file builds the image from your checkout (--build). SetPINWREN_IMAGE=ghcr.io/krithik-m-codes/pinwren:<version>to use a published release instead.
Passing more settings
Every variable you put in docker/.env reaches the app: the compose file loads it with env_file (the
.env next to compose.yml, optional; this needs Docker Compose v2.24 or later). So SMTP, S3 storage or
rate limits are just more lines in that file:
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=…
EMAIL_FROM="Pinwren <feedback@example.com>"
STORAGE_DRIVER=s3
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.comRestart with the same up -d command after a change. Every variable is listed in the
environment reference, and docker/.env.example
has commented blocks for email, storage and HTTPS. You rarely need them, though: SMTP, security and the licence
can all be set in the dashboard.
First run
The short version is below; Getting started walks through every screen, from install to the first emailed report.
- Start the stack:
docker compose -f docker/compose.yml --env-file docker/.env up -d. Migrations run on start. - Open
PINWREN_URLand create the first account. The first account on a new server is its instance admin: it can set up email, security and the licence straight away, with nothing to copy from the logs. Keep the server private until you have done this — the compose file binds it to127.0.0.1, so only your reverse proxy reaches it. After the first account, sign-up is closed except for invited people (setPINWREN_DISABLE_SIGNUP=falsefor open sign-up).PINWREN_ADMIN_EMAILSmakes more people admins. - Name your workspace, create the first project, enter your site's domain and say what the site is for: a public website (bug reports stay private) or a staging / client review site (reviewers see each other's pins).
- Settings › Email: enter your SMTP server and send a test email, so new reports reach the team.
- Copy the snippet from the project's Install tab into your site. The tab shows Connected once the widget has loaded, and lists any site it refused with an Allow button.
- Add everyone else from Settings › Users: create their account directly, or invite them by email.
Before you go live
Reverse proxy and HTTPS
Caddy, nginx and Traefik: forwarded headers, upload sizes, brotli and the live stream.
Storage
Keep files on the volume or move them to S3, R2 or MinIO.
Backups
Dump Postgres and copy the files volume — and test a restore.
Security checklist
Secrets, allowed sites, sign-up, outbound requests and privacy mode.
Running without Docker
You can run the app straight from a checkout with Node 22.12+ and pnpm 12, against any Postgres:
corepack enable && pnpm install
cp apps/web/.env.example apps/web/.env.local # set DATABASE_URL, PINWREN_URL, PINWREN_SECRET
pnpm --filter @pinwren/widget build # widget.js and its chunks into apps/web/public
pnpm --filter @pinwren/web build
pnpm --filter @pinwren/web start -- -p 3000 # migrations run on startThis runs a single Node process. The Docker image adds the multi-process cluster (docker/cluster.cjs) and
sets PINWREN_DATA_DIR=/data; run the cluster entry yourself or put a process manager in front if you need
more than one core.