Files
pnger/docs/INSTALL.md
T

9.3 KiB
Raw Blame History

Installation & configuration

PNGer is a single stateless container: Express serves both the API and the compiled Svelte frontend on one port. There is no database, no persistent volume, and nothing to migrate.

Path Use when Where
Unraid Docker GUI Production on Unraid. UNRAID.md — click-by-click.
docker compose Any Docker host. §2 below.
Plain docker run Ad hoc. §3 below.
Local Node Development. §4 below.

1. Prerequisites

  • Docker for the container paths. Nothing else — no runtime deps on the host.
  • Node 20+ and npm for local development. Sharp ships prebuilt binaries for common platforms; if npm install builds it from source you also need a C++ toolchain.
  • A free host port. The container listens on 3000; the compose file publishes it on 8080 by default.

No volume is required. No reverse proxy is required either, though you will want one if you expose it beyond the LAN — PNGer has no authentication of any kind.


2. docker compose

git clone https://git.alwisp.com/jason/pnger.git
cd pnger
docker compose up -d --build

Open http://localhost:8080.

docker-compose.yml builds locally, tags pnger:latest, publishes ${HOST_PORT:-8080}:3000, sets restart: unless-stopped, caps the container at 512m / 1.0 CPU (MEM_LIMIT / CPU_LIMIT), rotates JSON logs at 10 MB × 3, and runs the same healthcheck as the image. Override anything through a .env file next to the compose file or through the shell environment:

HOST_PORT=9000 TZ=America/Chicago docker compose up -d

The optional /app/temp volume mount is commented out in the compose file and is not needed — see temp directory below.


3. Build and run the image directly

docker build -t pnger:latest .
docker run -d --name pnger -p 8080:3000 \
  -e PUID=99 -e PGID=100 -e TZ=America/Chicago \
  --restart unless-stopped pnger:latest

The Dockerfile is a three-stage build:

  1. frontend-buildernpm install then vite build in frontend/, producing frontend/dist.
  2. backend-buildernpm install then tsc in backend/, producing backend/dist.
  3. runtimenode:20-alpine with apk upgrade --no-cache and su-exec + shadow installed; production-only backend deps (npm install --omit=dev); the compiled backend at /app/dist; the built frontend at /app/dist/public; /app/temp created; docker-entrypoint.sh installed.

npm install is used rather than npm ci on purpose — the committed lockfiles are stubs, and caret ranges in package.json are what actually pin the tree.

At start, docker-entrypoint.sh runs as root: it usermod/groupmods the built-in node user to the requested PUID/PGID, chown -R node:node /app, then exec su-exec node "$@". The app itself never runs as root.

CI

.gitea/workflows/docker-build.yml runs on every push to main (plus workflow_dispatch) on the host-labelled Gitea runner — bundled Docker CLI, mounted /var/run/docker.sock, legacy builder. It logs in to registry.alwisp.com with REGISTRY_USER / REGISTRY_TOKEN, builds with the labels org.alwisp.git-sha, org.alwisp.version (v1.<commit-count-1>) and org.alwisp.repo, pushes registry.alwisp.com/jason/pnger:latest, then prunes dangling images on the host.

Only :latest is published — there is no immutable per-SHA tag, so rollback means rebuilding from an older commit.

To run the published image instead of building locally, point docker run (or the Unraid Repository field) at registry.alwisp.com/jason/pnger:latest after a docker login registry.alwisp.com.


4. Local development

Two terminals:

# terminal 1 — API on :3000, ts-node-dev with --respawn
cd backend
npm install
npm run dev

# terminal 2 — Vite dev server on :5173 with HMR
cd frontend
npm install
npm run dev

Open http://localhost:5173.

frontend/src/lib/api.ts switches on import.meta.env.DEV: in dev it posts to http://localhost:3000/api, in a production build to the same-origin /api. There is no Vite proxy — the backend's app.use(cors()) allows the cross-origin call.

Script Where Does
npm run dev backend ts-node-dev --respawn --transpile-only src/index.ts
npm run build backend tscdist/
npm start backend node dist/index.js
npm run dev frontend Vite dev server on 5173
npm run build frontend vite buildfrontend/dist
npm run preview frontend Serve the production build locally

There are no tests and no lint step. Verification is manual: resize a large PNG, convert it to WebP and JPEG, try each crop anchor, toggle the theme, paste from the clipboard.


Environment variables

Everything is optional — the image ships a working default for each. There is no .env file in the container; set them on the container.

Variable Default (image) Read by Notes
PORT 3000 backend/src/index.ts Express listen port. Changing it means changing the healthcheck and port mapping too — easier to leave it and remap on the host.
PUID 99 docker-entrypoint.sh UID the node user is remapped to. 99 = Unraid nobody.
PGID 100 docker-entrypoint.sh GID the node group is remapped to. 100 = Unraid users.
TZ UTC base image Container timezone, e.g. America/Chicago. Affects log timestamps only.
NODE_ENV production Express / Node Set to development locally.
MAX_FILE_SIZE 10485760 nothing ⚠️ Declared in the Dockerfile, the compose file and the Unraid docs, but the live upload middleware (backend/src/routes/image.ts) creates multer with no limits, so no size cap is applied. The only code that ever honoured it is the dead backend/src/server.js. Treat uploads as unbounded until this is wired up.
TEMP_DIR /app/temp nothing ⚠️ See below.
CORS_ORIGIN nothing ⚠️ Listed in backend/.env.example and older docs. index.ts calls app.use(cors()) with no options, so all origins are allowed and this variable is ignored.
HOST_PORT 8080 docker-compose.yml Host side of the port mapping. Compose only.
MEM_LIMIT 512m docker-compose.yml Container memory cap. Compose only.
CPU_LIMIT 1.0 docker-compose.yml Container CPU cap. Compose only.
VITE_API_URL nothing ⚠️ Documented historically; lib/api.ts derives the base URL from import.meta.env.DEV instead. Setting it has no effect.

backend/.env.example also predates the current server: it lists MAX_FILE_SIZE=10 (megabytes) while the Dockerfile sets 10485760 (bytes). Neither is read today.

The temp directory does nothing

/app/temp is created by the Dockerfile, TEMP_DIR points at it, and UNRAID.md offers an optional volume mapping for it. Nothing writes there. Uploads live in memory (multer.memoryStorage()), Sharp transforms the buffer, Express returns it. Mounting a volume is harmless but pointless.


Health checks

The image's HEALTHCHECK requests http://localhost:3000/ every 30 s and passes on HTTP 200 — that is the SPA index.html, so it confirms the process is up and serving.

Be careful with /api/health: it does not exist. The router only defines POST /api/transform, so a GET to /api/health falls through to the app.get("*") SPA handler and returns index.html with a 200. Any monitor pointed at it will report healthy no matter what state the API is in. Use POST /api/transform with a small test image if you need a real probe.


Upgrading

  • Compose / local build: git pull && docker compose up -d --build.
  • Registry image: docker pull registry.alwisp.com/jason/pnger:latest && docker restart pnger, or Unraid → Docker tab → Force update.

There is no state, so upgrades and rollbacks are just container recreations. Nothing to back up — but note that only :latest is published, so rolling back means rebuilding from the older commit.


Troubleshooting

Symptom Cause / fix
Container exits immediately Host port already in use, or PUID/PGID collide with an existing user in a way usermod rejects. docker logs pnger.
Transform failed in the UI The API returned non-200. Check docker logs — Sharp throws a 500 on files it cannot decode (the client only checks the MIME type starts with image/).
Large upload hangs or the container OOMs No upload size limit is enforced and processing is entirely in memory. Raise MEM_LIMIT, or put a body-size cap on your reverse proxy.
Preview does not match the download Expected. The preview is a Canvas re-encode, the output is Sharp. PNG quality is ignored by Canvas, and the preview will upscale where the server's withoutEnlargement: true will not.
Preview never appears Browser console — the preview path is pure Canvas API and fails silently into console.error.
Port already in use in dev lsof -ti:3000 | xargs kill -9
Sharp fails to load after a Node upgrade cd backend && npm rebuild sharp
Docker build fails oddly docker builder prune, then rebuild.

MPM — Born to Innovate. Built to Last.