diff --git a/pnpr/DEPLOY.md b/pnpr/DEPLOY.md new file mode 100644 index 0000000000..b5838c6920 --- /dev/null +++ b/pnpr/DEPLOY.md @@ -0,0 +1,96 @@ +# Deploying pnpr + +`pnpr` is a single long-running HTTP server backed by a filesystem +store, so it deploys like any stateful web service: one instance, one +persistent volume, a domain with TLS in front. + +> **Licensing.** `pnpr` is source-available under the +> [PolyForm Shield License](../LICENSE.md), **not** open source. You may +> run, modify, and self-host it for any purpose **except** providing a +> product that competes with `pnpr`. Self-hosting a private registry for +> your own team or CI is fine; offering it as a commercial hosted +> registry needs a commercial license from +> [Zoltan Kochan](https://kochan.io). + +## Container image + +The repo ships a multi-stage [`Dockerfile`](./Dockerfile). Its build +context is the **monorepo root**, because pnpr depends on `pacquet-*` +crates from the shared Cargo workspace. Build it from the repo root: + +```bash +docker build -f pnpr/Dockerfile -t pnpr . +``` + +Run it with a persistent volume for state: + +```bash +docker run -d --name pnpr \ + -p 4873:4873 \ + -v pnpr-data:/data \ + -e PNPR_PUBLIC_URL=https://registry.example.com \ + pnpr +``` + +Then point pnpm at it: + +```bash +pnpm config set registry http://localhost:4873/ +``` + +## Configuration + +The image is configured through environment variables (translated into +CLI flags by [`docker/entrypoint.sh`](./docker/entrypoint.sh)): + +| Variable | Default | Purpose | +| --- | --- | --- | +| `PNPR_PUBLIC_URL` | `http://` | URL clients use to reach the server. **Set this in production** — pnpr rewrites `dist.tarball` URLs in served packuments to it. Without it, clients get links pointing at the container's bind address. | +| `PNPR_LISTEN` | `0.0.0.0:4873` | Address the server binds to inside the container. | +| `PNPR_STORAGE` | `/data` | Directory for the package cache, `htpasswd`, and `tokens.db`. Mount a volume here. | +| `PNPR_CONFIG` | `/etc/pnpr/config.yaml` | Path to the YAML config. The baked-in [`docker/config.yaml`](./docker/config.yaml) proxies all of npm and persists state under `PNPR_STORAGE`. | +| `PNPR_PACKUMENT_TTL_SECS` | (config value) | Seconds before a cached packument is refetched. | +| `PNPR_LOG_LEVEL` / `PNPR_LOG_FORMAT` | `info` / `json` | Log verbosity and shape. `RUST_LOG` overrides the level. | + +To customize package routing, auth, or uplinks beyond what the env vars +cover, mount your own YAML over `/etc/pnpr/config.yaml` (or point +`PNPR_CONFIG` elsewhere). It follows verdaccio's `config.yaml` shape; +`${VAR:-default}` placeholders are substituted from the environment. + +## Coolify + +[Coolify](https://coolify.io) is a self-hosted PaaS. pnpr fits its +Dockerfile/Compose deployment model directly. + +1. **New Resource → Application** in your Coolify project, sourced from + this Git repository. +2. **Build pack: Dockerfile.** Set: + - **Base Directory:** `/` (the repo root — the build context needs the + whole Cargo workspace). + - **Dockerfile Location:** `pnpr/Dockerfile`. +3. **Port:** expose `4873` so Coolify's Traefik proxy routes to it. +4. **Domain:** assign one (e.g. `registry.example.com`). Coolify + provisions HTTPS automatically. +5. **Environment variables:** set `PNPR_PUBLIC_URL` to that same HTTPS + domain. Add `PNPR_LOG_LEVEL` / `PNPR_LOG_FORMAT` if you want to tune + logging. +6. **Persistent storage:** add a volume mounted at `/data`. This is the + one step that bites people — without it, every redeploy wipes your + published packages, accounts, and cache. +7. Deploy. The image's `HEALTHCHECK` hits `/-/ping`, which Coolify + surfaces as the container's health status. + +Prefer Compose? Point Coolify at [`docker-compose.yml`](./docker-compose.yml) +instead — it already wires up the port, the `/data` volume, and the env +vars; just set `PNPR_PUBLIC_URL`. + +## Operational notes + +- **Single instance.** pnpr is a stateful single process over a + filesystem store. Don't scale it to multiple replicas behind a load + balancer expecting them to share state. +- **Back up `/data`.** It holds published packages, the htpasswd user + file, and the SQLite token database. +- **Accounts.** `npm adduser --registry ` registers a user (written + to `htpasswd`). Set `auth.htpasswd.max_users: -1` in the config once + your accounts exist to close registration. diff --git a/pnpr/Dockerfile b/pnpr/Dockerfile new file mode 100644 index 0000000000..94028f7c86 --- /dev/null +++ b/pnpr/Dockerfile @@ -0,0 +1,51 @@ +# Build context must be the monorepo root, since pnpr depends on +# pacquet-* crates from the shared Cargo workspace: +# +# docker build -f pnpr/Dockerfile -t pnpr . +# +# The companion pnpr/docker-compose.yml sets the context for you. + +# The pinned toolchain matches rust-toolchain.toml at the repo root. +FROM rust:1.95.0-slim-bookworm AS builder + +# cmake + perl are needed by aws-lc-sys/ring (pulled in via reqwest's +# rustls backend); build-essential provides the C compiler that +# rusqlite's bundled SQLite and the *-sys crates compile against. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + perl \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build +COPY . . +RUN cargo build --release --locked --bin pnpr + +FROM debian:bookworm-slim AS runtime + +# ca-certificates lets pnpr's reqwest client validate TLS to upstream +# registries (registry.npmjs.org and any configured uplinks). +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /build/target/release/pnpr /usr/local/bin/pnpr +COPY pnpr/docker/config.yaml /etc/pnpr/config.yaml +COPY pnpr/docker/entrypoint.sh /usr/local/bin/pnpr-entrypoint +RUN chmod +x /usr/local/bin/pnpr-entrypoint + +# Storage, auth state (htpasswd, tokens.db), and the packument/tarball +# cache all live under /data. Mount it as a volume so they survive +# redeploys. +ENV PNPR_STORAGE=/data \ + PNPR_LISTEN=0.0.0.0:4873 \ + PNPR_CONFIG=/etc/pnpr/config.yaml +VOLUME /data +EXPOSE 4873 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -fsS http://127.0.0.1:4873/-/ping || exit 1 + +ENTRYPOINT ["/usr/local/bin/pnpr-entrypoint"] diff --git a/pnpr/Dockerfile.dockerignore b/pnpr/Dockerfile.dockerignore new file mode 100644 index 0000000000..97c77dbcb0 --- /dev/null +++ b/pnpr/Dockerfile.dockerignore @@ -0,0 +1,7 @@ +# BuildKit reads this when building with -f pnpr/Dockerfile. Keeps the +# repo-root build context small so `COPY . .` doesn't ship build output +# or installed dependencies into the image. +target/ +**/node_modules/ +.git/ +*.log diff --git a/pnpr/crates/pnpr/README.md b/pnpr/crates/pnpr/README.md index 1c193f2d69..fdb178fea9 100644 --- a/pnpr/crates/pnpr/README.md +++ b/pnpr/crates/pnpr/README.md @@ -4,6 +4,13 @@ A pnpm-compatible npm registry server, written in Rust. Lives in the [pnpm monorepo](https://github.com/pnpm/pnpm) under `registry/`. +## Deployment + +See [`DEPLOY.md`](../../DEPLOY.md) for running pnpr in a container, the +`PNPR_*` configuration variables, and a step-by-step Coolify guide. The +repo ships a [`Dockerfile`](../../Dockerfile) and +[`docker-compose.yml`](../../docker-compose.yml). + ## License Source-available under the [PolyForm Shield License 1.0.0](../../LICENSE.md) — diff --git a/pnpr/docker-compose.yml b/pnpr/docker-compose.yml new file mode 100644 index 0000000000..f6d270c0e3 --- /dev/null +++ b/pnpr/docker-compose.yml @@ -0,0 +1,28 @@ +# Build context is the monorepo root because pnpr depends on pacquet-* +# crates from the shared Cargo workspace. Run from the pnpr/ directory: +# +# docker compose up --build +# +# Or point a PaaS (e.g. Coolify) at this file. See DEPLOY.md. + +services: + pnpr: + build: + context: .. + dockerfile: pnpr/Dockerfile + image: pnpr:local + restart: unless-stopped + ports: + - "4873:4873" + environment: + # The URL clients use to reach this server. Set it to your public + # domain in production so proxied tarball URLs are rewritten + # correctly. + PNPR_PUBLIC_URL: ${PNPR_PUBLIC_URL:-http://localhost:4873} + PNPR_LOG_LEVEL: info + PNPR_LOG_FORMAT: json + volumes: + - pnpr-data:/data + +volumes: + pnpr-data: diff --git a/pnpr/docker/config.yaml b/pnpr/docker/config.yaml new file mode 100644 index 0000000000..806e82a637 --- /dev/null +++ b/pnpr/docker/config.yaml @@ -0,0 +1,41 @@ +# Production-oriented pnpr config for container deployments. +# +# Unlike the bundled default config (which is tuned for +# @pnpm/registry-mock's test packages), this one proxies the whole npm +# namespace through registry.npmjs.org and persists all state under +# ${PNPR_STORAGE}. `${VAR:-default}` placeholders are substituted from +# the environment at startup, so a PaaS can configure it without editing +# the file. + +storage: ${PNPR_STORAGE:-/data} + +auth: + htpasswd: + file: ${PNPR_STORAGE:-/data}/htpasswd + # Cap open registration. Omit for unlimited; set to -1 to disable + # `npm adduser` entirely once your accounts exist. + #max_users: -1 + tokens: + file: ${PNPR_STORAGE:-/data}/tokens.db + +uplinks: + npmjs: + url: https://registry.npmjs.org/ + +# Evaluated in order; first match wins. Publishing requires auth; +# everything not published locally is proxied from npmjs and cached. +packages: + '@*/*': + access: $all + publish: $authenticated + proxy: npmjs + + '**': + access: $all + publish: $authenticated + proxy: npmjs + +log: + type: stdout + format: ${PNPR_LOG_FORMAT:-json} + level: ${PNPR_LOG_LEVEL:-info} diff --git a/pnpr/docker/entrypoint.sh b/pnpr/docker/entrypoint.sh new file mode 100644 index 0000000000..a6206dbc81 --- /dev/null +++ b/pnpr/docker/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Translate PNPR_* environment variables into pnpr CLI flags so the +# server can be configured entirely through a PaaS's env-var UI. +set -eu + +set -- --listen "${PNPR_LISTEN:-0.0.0.0:4873}" --storage "${PNPR_STORAGE:-/data}" + +if [ -n "${PNPR_CONFIG:-}" ]; then + set -- "$@" --config "$PNPR_CONFIG" +fi + +# Required behind a reverse proxy: pnpr rewrites dist.tarball URLs in +# served packuments to this URL. Without it, clients receive links +# pointing at the container's bind address. +if [ -n "${PNPR_PUBLIC_URL:-}" ]; then + set -- "$@" --public-url "$PNPR_PUBLIC_URL" +fi + +if [ -n "${PNPR_PACKUMENT_TTL_SECS:-}" ]; then + set -- "$@" --packument-ttl-secs "$PNPR_PACKUMENT_TTL_SECS" +fi + +exec pnpr "$@"