Files
pnger/docs/INSTALL.md
T

165 lines
9.3 KiB
Markdown
Raw Normal View 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](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.*