Files
pnger/docs/INSTALL.md
T

165 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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`
```bash
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:
```bash
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](#the-temp-directory-does-nothing) below.
---
## 3. Build and run the image directly
```bash
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-builder**`npm install` then `vite build` in `frontend/`, producing `frontend/dist`.
2. **backend-builder**`npm install` then `tsc` in `backend/`, producing `backend/dist`.
3. **runtime**`node: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`/`groupmod`s 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:
```bash
# 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 | `tsc``dist/` |
| `npm start` | backend | `node dist/index.js` |
| `npm run dev` | frontend | Vite dev server on 5173 |
| `npm run build` | frontend | `vite build``frontend/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.*