etherpad-lite/packaging
John McLear 80c385e657
fix(deb): keep plugin_packages in-tree so admin-installed plugins can resolve ep_etherpad-lite (#7750)
* fix(deb): keep plugin_packages in-tree to fix admin-installed plugins

The .deb postinstall symlinked /opt/etherpad/src/plugin_packages to
/var/lib/etherpad/plugin_packages so the etherpad user could install
plugins under ProtectSystem=strict. Node.js resolves symlinks to their
realpath before walking node_modules, so a plugin installed via the
admin UI lived under /var/lib/etherpad/... and could no longer reach
the bundled ep_etherpad-lite in /opt/etherpad/node_modules. Every
require('ep_etherpad-lite/...') in installed plugins (and the matching
esbuild client-bundle build) failed with MODULE_NOT_FOUND, etherpad
exited, and the systemd unit restart-looped (ether/ep_comments_page#416).

Keep plugin_packages as a real in-tree directory, make it (and its
.versions/ subdir) group-writable by etherpad like node_modules already
is, and add it to ReadWritePaths= in the unit. Migrate the contents of
any pre-existing /var/lib/etherpad/plugin_packages symlink target back
in-tree on upgrade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(deb): update assertions for in-tree plugin_packages + cover migration

The previous assertions in packaging/test-local.sh and the deb-package
workflow checked that /opt/etherpad/src/plugin_packages was a symlink to
/var/lib/etherpad/plugin_packages -- the layout this PR is removing.
They would have failed under the new postinst.

- Assert plugin_packages is a real directory (not a symlink), owned by
  group etherpad with mode 2775, matching node_modules.
- After the happy-path /health check, simulate a pre-fix install by
  recreating the symlink with a marker plugin, re-run the postinst via
  dpkg-reconfigure, and assert the marker payload was migrated back
  in-tree and the symlink is gone. Locks in regression coverage for the
  upgrade path (ether/ep_comments_page#416).

* ci(deb): add ep_layout_trip_wire fixture to gate plugin_packages layout

Layout assertions alone don't actually exercise the failure mode from
ether/ep_comments_page#416: they confirm /opt/etherpad/src/plugin_packages
is a real directory but never load a plugin whose index.js does
require('ep_etherpad-lite/...') from the on-disk realpath.

Ship a tiny test plugin under packaging/test-fixtures/ep_layout_trip_wire
that exercises the four require() patterns from the bug report (eejs,
Settings, log4js, pad_utils) and emits a marker line from
expressCreateServer. Both packaging/test-local.sh and the deb-package
workflow now stage it into plugin_packages/.versions/, wire up the
toplevel + node_modules symlinks live-plugin-manager would create,
list it in installed_plugins.json, restart etherpad, and assert:

  * marker line appears in the journal (every require resolved), and
  * no "Cannot find module 'ep_etherpad-lite" appears anywhere.

Verified locally that the fixture loads under the in-tree layout
(marker present) and fails under a symlinked-out-of-tree layout
(marker absent, MODULE_NOT_FOUND in the log) -- so the gate catches
the regression in both directions.

* fix(deb): clean up runtime plugin artifacts on purge

With plugin_packages now living under /opt/etherpad/src/plugin_packages,
admin-installed plugins land in dpkg-unmanaged paths (the .versions/
stage that live-plugin-manager populates plus matching ep_* symlinks
under src/node_modules/). dpkg --purge would leave them behind because
the manifest never recorded them.

In postremove's purge branch: explicitly rm -rf plugin_packages and any
runtime ep_* symlinks in node_modules, then rm -rf the whole APP_DIR
as belt-and-braces against other runtime drift. Verified in a sandbox
that a staged ep_runtime@1.0.0 + node_modules/ep_runtime symlink are
both gone after purge runs.

Add post-purge assertions to both packaging/test-local.sh and the
deb-package workflow: /opt/etherpad/src/plugin_packages and
/var/lib/etherpad must not exist after dpkg --purge.

Addresses Qodo PR #7750 review item 3 (\"Purge cleanup misses
plugins\", Reliability).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:40:37 +01:00
..
apt ci(packaging): publish signed apt repository to etherpad.org/apt (closes #7610) (#7624) 2026-04-29 00:20:00 +01:00
bin fix(deb): bump nodejs Depends to >= 25; install Node 25 in smoke test (#7754) 2026-05-15 11:57:27 +01:00
scripts fix(deb): keep plugin_packages in-tree so admin-installed plugins can resolve ep_etherpad-lite (#7750) 2026-05-15 13:40:37 +01:00
systemd fix(deb): keep plugin_packages in-tree so admin-installed plugins can resolve ep_etherpad-lite (#7750) 2026-05-15 13:40:37 +01:00
test-fixtures/ep_layout_trip_wire fix(deb): keep plugin_packages in-tree so admin-installed plugins can resolve ep_etherpad-lite (#7750) 2026-05-15 13:40:37 +01:00
nfpm.yaml fix(deb): bump nodejs Depends to >= 25; install Node 25 in smoke test (#7754) 2026-05-15 11:57:27 +01:00
README.md fix(deb): bump nodejs Depends to >= 25; install Node 25 in smoke test (#7754) 2026-05-15 11:57:27 +01:00
test-local.sh fix(deb): keep plugin_packages in-tree so admin-installed plugins can resolve ep_etherpad-lite (#7750) 2026-05-15 13:40:37 +01:00

Etherpad Debian / RPM packaging

Produces native .deb (and, with the same manifest, .rpm / .apk) packages for Etherpad using nfpm.

Layout

packaging/
  nfpm.yaml                # nfpm package manifest
  bin/etherpad             # /usr/bin launcher
  scripts/                 # preinst / postinst / prerm / postrm
  systemd/etherpad.service
  systemd/etherpad.default
  etc/settings.json.dist   # populated in CI from settings.json.template

Built artefacts land in ./dist/.

Building locally

Prereqs: Node 24 (current LTS; engines.node floor is 20), pnpm 10+, nfpm.

pnpm install --frozen-lockfile
pnpm run build:etherpad

# Stage the tree the way CI does:
STAGE=staging/opt/etherpad
mkdir -p "$STAGE"
cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \
      node_modules "$STAGE/"
printf 'packages:\n  - src\n  - bin\n' > "$STAGE/pnpm-workspace.yaml"
cp settings.json.template packaging/etc/settings.json.dist

VERSION=$(node -p "require('./package.json').version") \
ARCH=amd64 \
    nfpm package --packager deb -f packaging/nfpm.yaml --target dist/

End-to-end test (Docker, no real systemd needed)

packaging/test-local.sh builds the .deb and runs the same smoke test the CI workflow does, inside a throwaway systemd-enabled container:

packaging/test-local.sh                # build + smoke + purge
packaging/test-local.sh --shell        # leave the container up so you can poke around
packaging/test-local.sh --build-only   # just produce dist/*.deb

This is the fastest way to validate that the systemd hardening, plugin path symlinks, and tsx wrapper actually work together before pushing.

The release workflow publishes a signed apt repository at https://etherpad.org/apt/ on every tagged release. Three lines on any Debian/Ubuntu/Mint:

curl -fsSL https://etherpad.org/key.asc \
  | sudo gpg --dearmor -o /usr/share/keyrings/etherpad.gpg
echo "deb [signed-by=/usr/share/keyrings/etherpad.gpg] https://etherpad.org/apt stable main" \
  | sudo tee /etc/apt/sources.list.d/etherpad.list
sudo apt update && sudo apt install etherpad

apt upgrade works going forward. Repo metadata is signed with the GPG keypair documented in packaging/apt/key.asc (long key id AF0CD687D51A6E63).

Installing a single .deb directly

The release page publishes both versioned and stable filenames per arch:

# Stable URL — always points at the most recent release:
curl -fsSL -o etherpad-latest_amd64.deb \
  https://github.com/ether/etherpad/releases/latest/download/etherpad-latest_amd64.deb
sudo apt install ./etherpad-latest_amd64.deb

# Or pin to a specific version:
sudo apt install ./dist/etherpad_<version>_amd64.deb

sudo systemctl start etherpad
curl http://localhost:9001/health

apt will pull in nodejs (>= 25) (matches Etherpad's engines.node). Most distro repos don't yet ship Node.js 25, so on most systems you will need to add NodeSource's node_25.x apt repo before apt install:

KEYRING=/usr/share/keyrings/nodesource.gpg
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
  | sudo gpg --dearmor --yes -o "${KEYRING}"
echo "deb [signed-by=${KEYRING}] https://deb.nodesource.com/node_25.x nodistro main" \
  | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update

Configuration

  • Edit /etc/etherpad/settings.json, then sudo systemctl restart etherpad.
  • Environment overrides: /etc/default/etherpad.
  • Logs: journalctl -u etherpad -f.
  • Data (sqlite default): /var/lib/etherpad/etherpad.db.

The shipped settings template defaults to dbType: "dirty", which the template itself warns is for testing only. postinstall rewrites the seeded /etc/etherpad/settings.json to sqlite and points it at /var/lib/etherpad/etherpad.db so fresh installs get an ACID-safe DB out of the box. Existing /etc/etherpad/settings.json is never touched on upgrade.

Upgrading

dpkg --install etherpad_<new>.deb (or apt install) replaces the app tree under /opt/etherpad while preserving /etc/etherpad/* and /var/lib/etherpad/*. The service is restarted automatically.

Removing

  • sudo apt remove etherpad — keeps config and data.
  • sudo apt purge etherpad — also removes config, data, and the etherpad system user.

Publishing to an APT repository (follow-up)

Out of scope here — requires credentials and ownership decisions. Recipes once a repo is picked:

  • Cloudsmith (easiest, free OSS tier): cloudsmith push deb ether/etherpad/any-distro/any-version dist/*.deb
  • Launchpad PPA: requires signed source packages (a debian/ tree), which nfpm does not produce — use debuild separately.
  • Self-hosted reprepro: reprepro -b /srv/apt includedeb stable dist/*.deb

Wire the chosen option into .github/workflows/deb-package.yml after the release job.