Skip to content

feat(packaging): add Debian (.deb) build via nfpm with systemd unit#7559

Open
JohnMcLear wants to merge 2 commits intoether:developfrom
JohnMcLear:chore/packaging-apt
Open

feat(packaging): add Debian (.deb) build via nfpm with systemd unit#7559
JohnMcLear wants to merge 2 commits intoether:developfrom
JohnMcLear:chore/packaging-apt

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

First-class Debian packaging for Etherpad. Produces etherpad-lite_<version>_<arch>.deb for amd64 and arm64 from a single nfpm manifest. Installing the package gives users:

  • /opt/etherpad-lite with a prebuilt, self-contained node_modules/ — no pnpm required at runtime, only nodejs (>= 20).
  • etherpad system user/group, created via adduser in preinst.
  • /etc/etherpad-lite/settings.json (seeded from the template on first install; preserved across upgrades; removed on purge).
  • /var/lib/etherpad-lite owned by etherpad:etherpad, with the default dirty-DB path retargeted there so ProtectSystem=strict works.
  • /lib/systemd/system/etherpad-lite.service — hardened unit (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, RestrictAddressFamilies) with Restart=on-failure.
  • /usr/bin/etherpad-lite CLI wrapper running node --import tsx/esm.

Part of #7529 — top-3 deployment targets (Snap #7558, Apt, Home Assistant).

CI

.github/workflows/deb-package.yml triggers on v* tags, builds both arches via native runners (ubuntu-latest + ubuntu-24.04-arm), and smoke-tests the amd64 package end-to-end:

  1. dpkg -i installs
  2. systemctl start etherpad-lite
  3. curl /health returns 200
  4. dpkg --purge removes config + user

Artefacts are attached to the GitHub Release.

Not included (follow-up)

Publishing to an APT repo (Cloudsmith / Launchpad PPA / self-hosted reprepro) is out of scope — needs a governance decision on who holds the signing key. Recipes are in packaging/README.md to be wired in once that's decided.

Legacy

bin/buildDebian.sh and bin/deb-src/ are stale (Etherpad v1.3, init-based, unmaintained). Flagged for removal in a follow-up PR so this one stays mechanical.

Test plan

  • pnpm install --frozen-lockfile && pnpm run build:etherpad succeeds
  • Local nfpm package --packager deb produces a well-formed .deb (dpkg-deb -I / -c)
  • Fresh install on Ubuntu 24.04: service starts, /health returns OK
  • Upgrade install: /etc/etherpad-lite/settings.json untouched; service restarted
  • apt remove keeps /etc and /var/lib; apt purge removes them plus the etherpad user
  • CI workflow succeeds on amd64 and arm64 (the workflow itself runs the smoke test)

Refs #7529

🤖 Generated with Claude Code

First-class Debian packaging for Etherpad, producing signed-ready
etherpad-lite_<version>_<arch>.deb artefacts for amd64 and arm64 from a
single nfpm manifest. Installing the package gives users:

- /opt/etherpad-lite with a prebuilt, self-contained node_modules/ — no
  pnpm required at runtime, just `nodejs (>= 20)`.
- etherpad system user/group, created via `adduser` in preinst.
- /etc/etherpad-lite/settings.json seeded from the template on first
  install, preserved across upgrades, removed on `purge`.
- /var/lib/etherpad-lite owned by etherpad:etherpad, with the default
  dirty-DB retargeted there so ProtectSystem=strict works.
- /lib/systemd/system/etherpad-lite.service — hardened unit
  (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp,
  RestrictAddressFamilies) with Restart=on-failure.
- /usr/bin/etherpad-lite CLI wrapper running `node --import tsx/esm`.

CI (.github/workflows/deb-package.yml) triggers on v* tags, builds both
arches via native runners (ubuntu-latest + ubuntu-24.04-arm), smoke-tests
the amd64 package end-to-end (install → systemctl start → curl /health
→ purge → confirm user removed), and attaches the artefacts to the
GitHub Release.

Publishing to an APT repo (Cloudsmith, Launchpad PPA, self-hosted
reprepro) is intentionally out of scope — needs a governance decision on
who holds the signing key. Recipes are documented in packaging/README.md.

Refs ether#7529

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add Debian (.deb) packaging via nfpm with systemd service

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add first-class Debian (.deb) packaging via nfpm for amd64 and arm64
• Create systemd service with hardened security settings and auto-restart
• Implement Debian lifecycle scripts (preinst/postinst/prerm/postrm)
• Add CI workflow for automated package building and smoke testing
Diagram
flowchart LR
  A["Source Code<br/>+ node_modules"] -->|Stage| B["staging/<br/>opt/etherpad-lite"]
  B -->|nfpm package| C[".deb Artifact<br/>amd64/arm64"]
  D["nfpm.yaml<br/>manifest"] -->|Configure| C
  E["Lifecycle Scripts<br/>preinst/postinst/prerm/postrm"] -->|Package| C
  F["systemd Unit<br/>+ Hardening"] -->|Include| C
  C -->|CI Workflow| G["GitHub Release<br/>Assets"]
  C -->|Smoke Test| H["Install → Start<br/>→ Health Check"]
Loading

Grey Divider

File Changes

1. packaging/nfpm.yaml ⚙️ Configuration changes +112/-0

nfpm manifest for Debian package configuration

packaging/nfpm.yaml


2. packaging/bin/etherpad-lite ✨ Enhancement +18/-0

CLI wrapper for production Node.js execution

packaging/bin/etherpad-lite


3. packaging/systemd/etherpad-lite.service ⚙️ Configuration changes +48/-0

Hardened systemd unit with security sandboxing

packaging/systemd/etherpad-lite.service


View more (7)
4. packaging/systemd/etherpad-lite.default ⚙️ Configuration changes +7/-0

Environment variable defaults for systemd service

packaging/systemd/etherpad-lite.default


5. packaging/scripts/preinstall.sh ✨ Enhancement +28/-0

Create etherpad system user and group

packaging/scripts/preinstall.sh


6. packaging/scripts/postinstall.sh ✨ Enhancement +68/-0

Configure directories, settings, and enable service

packaging/scripts/postinstall.sh


7. packaging/scripts/preremove.sh ✨ Enhancement +20/-0

Stop service before package removal

packaging/scripts/preremove.sh


8. packaging/scripts/postremove.sh ✨ Enhancement +43/-0

Clean up config, data, and user on purge

packaging/scripts/postremove.sh


9. .github/workflows/deb-package.yml ⚙️ Configuration changes +151/-0

CI workflow for building and testing .deb packages

.github/workflows/deb-package.yml


10. packaging/README.md 📝 Documentation +89/-0

Documentation for packaging, building, and deployment

packaging/README.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 19, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Shell scripts use 4-space indent📘 Rule violation ⚙ Maintainability
Description
The newly added packaging shell scripts use 4-space indentation (e.g., within case branches),
violating the 2-space indentation standard. This can reduce consistency and trigger formatting/lint
issues where the repo enforces 2-space indentation.
Code

packaging/scripts/preinstall.sh[R6-18]

+case "$1" in
+    install|upgrade)
+        if ! getent group etherpad >/dev/null 2>&1; then
+            addgroup --system etherpad
+        fi
+        if ! getent passwd etherpad >/dev/null 2>&1; then
+            adduser --system --ingroup etherpad \
+                    --home /var/lib/etherpad-lite \
+                    --no-create-home \
+                    --shell /usr/sbin/nologin \
+                    --gecos "Etherpad service user" \
+                    etherpad
+        fi
Evidence
PR Compliance ID 8 requires 2-space indentation with no tabs. The added shell scripts contain blocks
indented with 4 spaces (for example, inside the install|upgrade) branch).

packaging/scripts/preinstall.sh[6-18]
packaging/scripts/postinstall.sh[13-50]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New packaging shell scripts use 4-space indentation inside blocks, violating the repo requirement for 2-space indentation.
## Issue Context
This affects newly added Debian packaging maintainer scripts and should be corrected to match the enforced formatting standard.
## Fix Focus Areas
- packaging/scripts/preinstall.sh[6-18]
- packaging/scripts/postinstall.sh[13-50]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Smoke test false-positive🐞 Bug ≡ Correctness
Description
The workflow’s /health polling loop never fails the job if the endpoint never becomes healthy, so CI
can report success for a broken .deb/service. This can lead to attaching and releasing non-working
packages.
Code

.github/workflows/deb-package.yml[R110-122]

+          set -eux
+          sudo apt-get update
+          sudo apt-get install -y nodejs
+          sudo dpkg -i dist/*.deb || sudo apt-get install -f -y
+          test -x /usr/bin/etherpad-lite
+          test -f /etc/etherpad-lite/settings.json
+          test -L /opt/etherpad-lite/settings.json
+          id etherpad
+          systemctl cat etherpad-lite.service
+          sudo systemctl start etherpad-lite
+          for i in $(seq 1 30); do
+            curl -fsS http://127.0.0.1:9001/health && break || sleep 2
+          done
Evidence
The smoke test uses a curl loop that breaks on success but has no validation after the loop
completes; if curl never succeeds, the last executed command is sleep 2 (exit 0), so the step
continues successfully.

.github/workflows/deb-package.yml[107-126]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The smoke test loop does not fail if `/health` never returns 200, so the workflow can pass even when Etherpad never becomes ready.
### Issue Context
Because `curl ... && break || sleep 2` is inside a `for` loop, a permanently failing curl results in the loop ending with the exit code of the final `sleep` (0). With `set -e`, this still won’t fail.
### Fix Focus Areas
- .github/workflows/deb-package.yml[107-126]
### Suggested change
Track success and explicitly `exit 1` if the endpoint never becomes healthy, e.g.:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Secrets readable in /etc/default🐞 Bug ⛨ Security
Description
The package installs /etc/default/etherpad-lite as world-readable (0644) even though it is the
documented place to put environment overrides for settings.json, which can include passwords and
secrets. Any local user on the system can read those secrets.
Code

packaging/nfpm.yaml[R62-68]

+  # Default environment file (conffile: preserved on upgrade)
+  - src: ./packaging/systemd/etherpad-lite.default
+    dst: /etc/default/etherpad-lite
+    type: config|noreplace
+    file_info:
+      mode: 0644
+
Evidence
The systemd unit loads /etc/default/etherpad-lite, and the packaged file is explicitly intended for
settings.json environment overrides; Etherpad supports env-var substitution including passwords, so
secrets placed there would be exposed if the file is 0644.

packaging/nfpm.yaml[62-68]
packaging/systemd/etherpad-lite.service[7-14]
packaging/systemd/etherpad-lite.default[1-7]
settings.json.template[10-16]
settings.json.template[78-83]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/etc/default/etherpad-lite` is installed with mode `0644`, making any secrets placed there readable by all local users.
### Issue Context
The systemd unit uses `EnvironmentFile=-/etc/default/etherpad-lite`, and Etherpad supports `${ENV_VAR}` substitution for config values, including passwords.
### Fix Focus Areas
- packaging/nfpm.yaml[62-68]
- packaging/systemd/etherpad-lite.service[7-14]
### Suggested change
Tighten permissions and ownership so only root and the `etherpad` service user can read it:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. RPM/APK scripts not portable 🐞 Bug ≡ Correctness
Description
The packaging manifest and documentation imply RPM/APK support, but the maintainer scripts use
Debian-specific user/group commands (adduser/addgroup/deluser/delgroup) that will fail or behave
incorrectly on RPM/APK systems. This makes RPM/APK builds non-functional if someone attempts them
with the current nfpm.yaml.
Code

packaging/scripts/preinstall.sh[R6-18]

+case "$1" in
+    install|upgrade)
+        if ! getent group etherpad >/dev/null 2>&1; then
+            addgroup --system etherpad
+        fi
+        if ! getent passwd etherpad >/dev/null 2>&1; then
+            adduser --system --ingroup etherpad \
+                    --home /var/lib/etherpad-lite \
+                    --no-create-home \
+                    --shell /usr/sbin/nologin \
+                    --gecos "Etherpad service user" \
+                    etherpad
+        fi
Evidence
The README explicitly claims the same manifest can produce RPM/APK, and nfpm.yaml includes an RPM
override, but the install/remove scripts call Debian-specific tools rather than portable equivalents
or per-packager scripts.

packaging/README.md[1-5]
packaging/nfpm.yaml[1-3]
packaging/nfpm.yaml[96-112]
packaging/scripts/preinstall.sh[6-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Maintainer scripts are Debian-specific, but the packaging docs/manifest suggest RPM/APK support.
### Issue Context
`preinstall.sh` uses `addgroup/adduser` and removal uses `deluser/delgroup`, which are not generally present (or have different semantics) on RPM/APK distros.
### Fix Focus Areas
- packaging/nfpm.yaml[96-112]
- packaging/scripts/preinstall.sh[6-18]
- packaging/README.md[1-5]
### Suggested change
Either:
1) Narrow the claim (README/manifest) to Debian-only for now (remove RPM/APK messaging and RPM override), **or**
2) Provide per-packager scripts via nfpm `overrides` (e.g., rpm scripts that use `useradd/groupadd/userdel/groupdel`, apk equivalents), keeping Debian scripts for `.deb`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Brittle tsx loader path 🐞 Bug ⚙ Maintainability
Description
The /usr/bin/etherpad-lite wrapper hardcodes tsx’s internal dist/esm/index.mjs file path, which
creates a maintenance trap if tsx’s internal layout changes. Etherpad already uses the stable tsx
entrypoints (tsx/cjs, --import=tsx) in its own scripts.
Code

packaging/bin/etherpad-lite[R13-17]

+# Run the server through tsx's ESM loader (shipped in node_modules).
+# No pnpm needed at runtime.
+exec node \
+    --import "file://${APP_DIR}/src/node_modules/tsx/dist/esm/index.mjs" \
+    "${APP_DIR}/src/node/server.ts" \
Evidence
The wrapper relies on a concrete internal file path under node_modules, while the main package uses
tsx via exported entrypoints in package scripts, indicating a more stable invocation pattern is
already used elsewhere in the repo.

packaging/bin/etherpad-lite[13-18]
src/package.json[75-84]
src/package.json[134-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The wrapper imports `tsx/dist/esm/index.mjs` by absolute file path, which is an internal layout detail.
### Issue Context
Repo scripts already use stable tsx entrypoints such as `--require tsx/cjs` and `--import=tsx`.
### Fix Focus Areas
- packaging/bin/etherpad-lite[13-18]
- src/package.json[134-141]
### Suggested change
Align the wrapper with the repo’s existing runtime pattern, e.g.:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread packaging/scripts/preinstall.sh Outdated
Comment thread .github/workflows/deb-package.yml
Comment thread packaging/nfpm.yaml Outdated
…rms, 2-space indent

Addresses Qodo review feedback on ether#7559:

1. Smoke test false-positive: the `for` loop polling /health never failed
   the job if the endpoint stayed down — `curl && break || sleep 2`
   keeps returning 0 from the trailing `sleep`, so `set -e` never
   trips. CI could attach a broken .deb to a release. Fix: track
   success explicitly and exit 1 (plus dump journald logs for
   diagnostics) when the service never becomes healthy.

2. /etc/default/etherpad-lite was world-readable (0644). systemd loads
   it via `EnvironmentFile=…`, and Etherpad supports
   ${ENV_VAR}-substitution for secrets (DB_PASSWORD etc.), so any
   local user could read anything admins drop there. Fix: install the
   conffile as root:etherpad 0640 — only root and the service user can
   read it.

3. Indentation: reflow maintainer scripts from 4-space to 2-space to
   match the repo style rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant