A self-hosted workshop and virtual tabletop for solo print-and-play games.
- Add your PnP files — drop in the PDF, scans or photos you'd otherwise print.
- Cut out the pieces — lay a grid over the card sheets, say where the backs are, and pnpforge cuts every card, board and token at print quality.
- Play — on a table that behaves like cardboard: slide the top card off a deck, flip it, fan your hand, shuffle, roll dice. Undo anything. Your game saves itself.
Built for desktop (mouse and keyboard) and tablets (touch). Everything runs on your own machine; nothing is uploaded anywhere.
docker run -d --name pnpforge -p 3717:3717 -v pnpforge-data:/data ghcr.io/troglobitten/pnpforge:latestThen open http://localhost:3717. The server binds all interfaces, so a tablet on the same network
can open http://<your-computer's-ip>:3717.
Or with compose, which keeps your games in ./data next to the compose file:
docker compose up -dThe container runs as an unprivileged user, so the folder you mount at /data has to belong to a
user it can write as. A named volume (the docker run line above) handles this for you; a bind
mount like ./data does not, and you'll see EACCES: permission denied, mkdir '/data/games'.
Find your own ids on the host with id -u and id -g, then either give the folder to the user the
container runs as:
sudo chown -R 1000:100 ./data…or run the container as the folder's owner. The bundled docker-compose.yml already does this and
reads PUID/PGID, so a .env file beside it is enough:
PUID=1000
PGID=100On OpenMediaVault, Unraid and Synology these are usually your own account's ids rather than
1000:1000, so check rather than assume.
Image tags: latest (newest release), 0.1.0 (an exact version), 0.1 (newest patch of that
minor). Built for linux/amd64 and linux/arm64.
| Variable | Default | Meaning |
|---|---|---|
PNPFORGE_PORT |
3717 |
Port the app and API listen on |
PNPFORGE_HOST |
0.0.0.0 |
Bind address |
PNPFORGE_DATA |
./data (/data in Docker) |
Where games, images and saves are stored |
PNPFORGE_LOG |
warn |
Log level (fatal…trace) |
Everything lives in the data folder: one directory per game holding game.json, its images and its
play sessions. Back it up by copying that folder. You can also export a single game as a
.pnpforge file from the library (optionally with your saves) and import it on another install.
Deleted games move to data/trash rather than being erased.
A fresh install starts with an empty library. Drop any .pnpforge file into a samples/ folder
beside the server and it will be imported on first run.
Requires Node 20 or newer.
npm install
npm run devThe app is on http://localhost:5173 and the API on 3717. For a production build:
npm run build
npm startThat serves the built app and the API together on http://localhost:3717.
npm run typecheck # TypeScript, web and server
node scripts/smoke.mjs # API smoke test against a running server
npx tsx scripts/check-zones.mts # drop/stacking rules
npx tsx scripts/check-throws.mts # dice throwingThere is deliberately no browser-driving UI test suite — see CONTRIBUTING.md.
Table materials are CC0 photographs from Poly Haven; see
public/textures/CREDITS.md for each file's source and author.
Licensed under the MIT Licence.