From 01f3febe4c58f8fe321da8c1f914f6fb5ff57cb7 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 19 Apr 2026 17:53:19 +0100 Subject: [PATCH 1/2] feat(packaging): publish Etherpad as a Snap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds first-class Snap packaging so Ubuntu / snapd users can install via `sudo snap install etherpad-lite`. - snap/snapcraft.yaml — core24, strict confinement, builds with pnpm against a pinned Node.js 22 runtime. Version is auto-derived from src/package.json so `snap info` tracks upstream release numbering. - snap/local/bin/etherpad-service — launch wrapper that seeds $SNAP_COMMON/etc/settings.json on first run (rewriting the default dirty-DB path to a writable $SNAP_COMMON location) and execs Etherpad via `node --import tsx/esm`. - snap/local/bin/etherpad-healthcheck-wrapper — HTTP probe for external supervisors, falling back to Node if curl isn't staged. - snap/local/bin/etherpad-cli — thin passthrough to Etherpad's bin/ scripts (importSqlFile, checkPad, etc.). - snap/hooks/configure — exposes `snap set etherpad-lite port=` and `ip=` with validation, restarts the service when running. - snap/README.md — build / install / configure / publish instructions. - .github/workflows/snap-publish.yml — builds on every v* tag, uploads a short-lived artifact, publishes to `edge`, and then promotes to `stable` through a manually-approved GitHub Environment. Requires a one-time `snapcraft register etherpad-lite` plus provisioning of the `SNAPCRAFT_STORE_CREDENTIALS` repo secret (instructions inline). Pad data (dirty DB, logs) lives in /var/snap/etherpad-lite/common/ and survives snap refreshes. The read-only $SNAP squashfs is never written to at runtime. Refs #7529 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/snap-publish.yml | 87 ++++++++++++ snap/README.md | 61 +++++++++ snap/hooks/configure | 24 ++++ snap/local/bin/etherpad-cli | 24 ++++ snap/local/bin/etherpad-healthcheck-wrapper | 20 +++ snap/local/bin/etherpad-service | 43 ++++++ snap/snapcraft.yaml | 139 ++++++++++++++++++++ 7 files changed, 398 insertions(+) create mode 100644 .github/workflows/snap-publish.yml create mode 100644 snap/README.md create mode 100755 snap/hooks/configure create mode 100755 snap/local/bin/etherpad-cli create mode 100755 snap/local/bin/etherpad-healthcheck-wrapper create mode 100755 snap/local/bin/etherpad-service create mode 100644 snap/snapcraft.yaml diff --git a/.github/workflows/snap-publish.yml b/.github/workflows/snap-publish.yml new file mode 100644 index 00000000000..50d56d06dc5 --- /dev/null +++ b/.github/workflows/snap-publish.yml @@ -0,0 +1,87 @@ +# Builds and publishes the Etherpad snap on tagged releases. +# Mirrors the trigger pattern from .github/workflows/docker.yml / release.yml +# (tags matching v?X.Y.Z). +# +# One-time maintainer setup: +# 1. `snapcraft register etherpad-lite` claims the name. +# 2. Generate a store credential: +# snapcraft export-login --snaps etherpad-lite \ +# --channels edge,stable \ +# --acls package_access,package_push,package_release - +# Store the output as repo secret SNAPCRAFT_STORE_CREDENTIALS. +# 3. Create a GitHub Environment called `snap-store-stable` with required +# reviewers so stable promotion is gated. +# +# Ref: https://snapcraft.io/docs/releasing-to-the-snap-store +name: Snap +on: + push: + tags: + - 'v?[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + outputs: + snap-file: ${{ steps.build.outputs.snap }} + steps: + - name: Check out + uses: actions/checkout@v6 + + - name: Build snap + id: build + uses: snapcore/action-build@v1 + + - name: Upload snap artifact + uses: actions/upload-artifact@v4 + with: + name: etherpad-lite-snap + path: ${{ steps.build.outputs.snap }} + if-no-files-found: error + retention-days: 7 + + publish-edge: + needs: build + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Download snap artifact + uses: actions/download-artifact@v4 + with: + name: etherpad-lite-snap + + - name: Publish to edge + uses: snapcore/action-publish@v1 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: ${{ needs.build.outputs.snap-file }} + release: edge + + publish-stable: + needs: [build, publish-edge] + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: read + # Manual gate: promote edge -> stable via GitHub Environments approval. + environment: snap-store-stable + steps: + - name: Download snap artifact + uses: actions/download-artifact@v4 + with: + name: etherpad-lite-snap + + - name: Publish to stable + uses: snapcore/action-publish@v1 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: ${{ needs.build.outputs.snap-file }} + release: stable diff --git a/snap/README.md b/snap/README.md new file mode 100644 index 00000000000..7f6a3f7530a --- /dev/null +++ b/snap/README.md @@ -0,0 +1,61 @@ +# Etherpad snap + +Packages Etherpad as a [Snap](https://snapcraft.io/) for publishing to the +Snap Store. + +## Build locally + +``` +sudo snap install --classic snapcraft +sudo snap install lxd && sudo lxd init --auto +snapcraft # from repo root; uses LXD by default +``` + +Output: `etherpad-lite__.snap`. + +## Install the local build + +``` +sudo snap install --dangerous ./etherpad-lite_*.snap +sudo snap start etherpad-lite +curl http://127.0.0.1:9001/health +``` + +Logs: `sudo snap logs etherpad-lite -f`. + +## Configure + +The snap seeds `$SNAP_COMMON/etc/settings.json` from the upstream +template on first run. Edit that file to customise Etherpad, then: + +``` +sudo snap restart etherpad-lite +``` + +A few values are exposed as snap config for convenience: + +| Key | Default | Notes | +| ----------------------------------- | --------- | --------------- | +| `snap set etherpad-lite port=9001` | `9001` | Listen port | +| `snap set etherpad-lite ip=0.0.0.0` | `0.0.0.0` | Bind address | + +Pad data (dirty DB, logs) lives in `/var/snap/etherpad-lite/common/` and +survives `snap refresh`. + +## Publish to the Snap Store + +Maintainers only. See +[Releasing to the Snap Store](https://snapcraft.io/docs/releasing-to-the-snap-store). + +One-time setup: + +``` +snapcraft register etherpad-lite +snapcraft export-login --snaps etherpad-lite \ + --channels edge,stable \ + --acls package_access,package_push,package_release - +``` + +Store the printed credential in the repo secret +`SNAPCRAFT_STORE_CREDENTIALS`. CI (`.github/workflows/snap-publish.yml`) +handles the rest on every `v*` tag. diff --git a/snap/hooks/configure b/snap/hooks/configure new file mode 100755 index 00000000000..f47df2c176f --- /dev/null +++ b/snap/hooks/configure @@ -0,0 +1,24 @@ +#!/bin/bash +# Validates values set via `snap set etherpad-lite key=value`. +# Supported keys: +# port : integer 1-65535 (default 9001). Ports <1024 require AppArmor override. +# ip : bind address (default 0.0.0.0) +set -euo pipefail + +PORT="$(snapctl get port || true)" +if [ -n "${PORT}" ]; then + if ! [[ "${PORT}" =~ ^[0-9]+$ ]] || [ "${PORT}" -lt 1 ] || [ "${PORT}" -gt 65535 ]; then + echo "port must be an integer 1-65535" >&2 + exit 1 + fi +fi + +IP="$(snapctl get ip || true)" +if [ -n "${IP}" ] && ! [[ "${IP}" =~ ^[0-9a-fA-F.:]+$ ]]; then + echo "ip must be a valid IPv4/IPv6 address" >&2 + exit 1 +fi + +if snapctl services etherpad-lite.etherpad-lite 2>/dev/null | grep -q active; then + snapctl restart etherpad-lite.etherpad-lite +fi diff --git a/snap/local/bin/etherpad-cli b/snap/local/bin/etherpad-cli new file mode 100755 index 00000000000..9da100c4bf1 --- /dev/null +++ b/snap/local/bin/etherpad-cli @@ -0,0 +1,24 @@ +#!/bin/bash +# Thin passthrough to Etherpad's bin/ scripts. +# Usage: etherpad-lite.etherpad [args...] +set -euo pipefail + +APP_DIR="${SNAP}/opt/etherpad-lite" +NODE_BIN="${SNAP}/opt/node/bin/node" +export PATH="${SNAP}/opt/node/bin:${PATH}" + +if [ "$#" -eq 0 ]; then + echo "Usage: etherpad-lite.etherpad [args...]" + echo "Available scripts:" + ls "${APP_DIR}/bin" | grep -E '\.(ts|sh)$' | sed 's/^/ /' + exit 2 +fi + +SCRIPT_NAME="$1"; shift +SCRIPT_PATH="${APP_DIR}/bin/${SCRIPT_NAME}" +[ -f "${SCRIPT_PATH}" ] || { echo "no such script: ${SCRIPT_NAME}"; exit 2; } + +case "${SCRIPT_PATH}" in + *.sh) exec "${SCRIPT_PATH}" "$@" ;; + *.ts) exec "${NODE_BIN}" --import tsx/esm "${SCRIPT_PATH}" "$@" ;; +esac diff --git a/snap/local/bin/etherpad-healthcheck-wrapper b/snap/local/bin/etherpad-healthcheck-wrapper new file mode 100755 index 00000000000..10bb12d91dd --- /dev/null +++ b/snap/local/bin/etherpad-healthcheck-wrapper @@ -0,0 +1,20 @@ +#!/bin/bash +# HTTP healthcheck. Returns 0 if /health returns 200. +set -euo pipefail + +PORT="$(snapctl get port 2>/dev/null || true)" +: "${PORT:=9001}" + +if command -v curl >/dev/null 2>&1; then + exec curl --fail --silent --show-error --max-time 5 \ + "http://127.0.0.1:${PORT}/health" +fi + +NODE_BIN="${SNAP}/opt/node/bin/node" +exec "${NODE_BIN}" -e ' + const http = require("http"); + http.get("http://127.0.0.1:'"${PORT}"'/health", r => { + if (r.statusCode === 200) process.exit(0); + console.error("HTTP " + r.statusCode); process.exit(1); + }).on("error", e => { console.error(e.message); process.exit(1); }); +' diff --git a/snap/local/bin/etherpad-service b/snap/local/bin/etherpad-service new file mode 100755 index 00000000000..dec33656d82 --- /dev/null +++ b/snap/local/bin/etherpad-service @@ -0,0 +1,43 @@ +#!/bin/bash +# Launch wrapper for the Etherpad snap daemon. +# +# 1. On first run, copy settings.json.template -> $SNAP_COMMON/etc/settings.json +# so the admin can edit it outside the read-only squashfs. +# 2. Create writable data dirs under $SNAP_COMMON. +# 3. Apply `snap set` overrides (port, ip) via env vars — settings.json +# uses ${PORT:9001}-style substitution natively. +# 4. Exec Node with tsx loader to run server.ts. +set -euo pipefail + +APP_DIR="${SNAP}/opt/etherpad-lite" +NODE_BIN="${SNAP}/opt/node/bin/node" + +export PATH="${SNAP}/opt/node/bin:${SNAP}/usr/bin:${SNAP}/bin:${PATH}" + +ETC_DIR="${SNAP_COMMON}/etc" +VAR_DIR="${SNAP_COMMON}/var" +LOG_DIR="${SNAP_COMMON}/logs" +mkdir -p "${ETC_DIR}" "${VAR_DIR}" "${LOG_DIR}" + +SETTINGS="${ETC_DIR}/settings.json" +if [ ! -f "${SETTINGS}" ]; then + echo "[etherpad-snap] bootstrapping ${SETTINGS} from template" + cp "${APP_DIR}/settings.json.template" "${SETTINGS}" + # Rewrite the dirty DB filename to a writable absolute path. + sed -i \ + -e 's|"filename": "var/dirty.db"|"filename": "'"${VAR_DIR}"'/dirty.db"|' \ + "${SETTINGS}" +fi + +PORT_OVERRIDE="$(snapctl get port || true)" +IP_OVERRIDE="$(snapctl get ip || true)" +: "${PORT_OVERRIDE:=9001}" +: "${IP_OVERRIDE:=0.0.0.0}" +export PORT="${PORT_OVERRIDE}" +export IP="${IP_OVERRIDE}" + +cd "${APP_DIR}" +export EP_SETTINGS="${SETTINGS}" +export NODE_ENV=production + +exec "${NODE_BIN}" --import tsx/esm src/node/server.ts "$@" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 00000000000..6455441de1e --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,139 @@ +# snap/snapcraft.yaml — Snap recipe for Etherpad +# +# Design notes: +# - base: core24 chosen because Etherpad requires Node.js >= 20 and +# core24 (Ubuntu 24.04 LTS) ships glibc/OpenSSL versions matching modern +# Node 20/22 binaries. core22 also works but ships older TLS/CA bundles. +# - confinement: strict. Etherpad is a pure Node.js HTTP service. The only +# native Node module (`rusty-store-kv`) ships as a prebuilt napi-rs +# binary, so no node-gyp compile is performed at install time and +# strict confinement works cleanly. +# - We use `dump` + a manual override-build (rather than the npm plugin) +# because this repo is a pnpm workspace and we pin Node.js 22 manually. +name: etherpad-lite +title: Etherpad +summary: Real-time collaborative document editor +description: | + Etherpad is a highly customizable open-source online editor providing + collaborative editing in real-time. This snap bundles Etherpad with a + pinned Node.js 22 runtime. On first launch a default `settings.json` + is copied into `$SNAP_COMMON/etc` where it can be edited. Pad data is + stored in `$SNAP_COMMON/var` and survives snap refreshes. + + Default listen port: 9001. + + Upstream: https://etherpad.org + Source: https://github.com/ether/etherpad-lite +license: Apache-2.0 +website: https://etherpad.org +source-code: https://github.com/ether/etherpad-lite +issues: https://github.com/ether/etherpad-lite/issues +contact: https://etherpad.org/#community + +adopt-info: etherpad +grade: stable +confinement: strict +base: core24 +compression: lzo + +platforms: + amd64: + arm64: + +apps: + etherpad-lite: + command: bin/etherpad-service + daemon: simple + install-mode: enable + restart-condition: on-failure + plugs: + - network + - network-bind + environment: + HOME: $SNAP_DATA + NODE_ENV: production + EP_SETTINGS: $SNAP_COMMON/etc/settings.json + EP_DATA_DIR: $SNAP_COMMON/var + PORT: "9001" + + healthcheck: + command: bin/etherpad-healthcheck-wrapper + plugs: + - network + + etherpad: + command: bin/etherpad-cli + plugs: + - network + - network-bind + +parts: + etherpad: + plugin: dump + source: . + source-type: local + build-packages: + - curl + - ca-certificates + - git + - python3 + - build-essential + stage-packages: + - ca-certificates + - libstdc++6 + - openssl + override-pull: | + craftctl default + VERSION="$(grep -m1 '"version"' src/package.json | sed -E 's/.*"([^"]+)".*/\1/')" + craftctl set version="${VERSION}" + override-build: | + set -eu + + # -- 1. Install Node.js 22 from the official tarball. + NODE_VERSION=22.12.0 + ARCH="$(dpkg --print-architecture)" + case "${ARCH}" in + amd64) NODE_ARCH=x64 ;; + arm64) NODE_ARCH=arm64 ;; + *) echo "Unsupported arch ${ARCH}"; exit 1 ;; + esac + NODE_TGZ="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" + curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TGZ}" \ + -o "/tmp/${NODE_TGZ}" + mkdir -p "${CRAFT_PART_INSTALL}/opt/node" + tar -xJf "/tmp/${NODE_TGZ}" -C "${CRAFT_PART_INSTALL}/opt/node" \ + --strip-components=1 + + export PATH="${CRAFT_PART_INSTALL}/opt/node/bin:${PATH}" + + # -- 2. Install pnpm via corepack (bundled with Node 22). + corepack enable --install-directory "${CRAFT_PART_INSTALL}/opt/node/bin" + corepack prepare pnpm@10.33.0 --activate + + # -- 3. Copy source into install dir and build. + APP_DIR="${CRAFT_PART_INSTALL}/opt/etherpad-lite" + mkdir -p "${APP_DIR}" + cp -a "${CRAFT_PART_SRC}/." "${APP_DIR}/" + cd "${APP_DIR}" + + pnpm install --frozen-lockfile --prod=false + pnpm run build:etherpad + + # Strip dev deps to shrink the payload. + pnpm prune --prod || true + + rm -rf .git tests/frontend-new/.cache \ + src/tests/frontend-new/test-results || true + + # -- 4. Install wrappers. + install -Dm755 "${CRAFT_PROJECT_DIR}/snap/local/bin/etherpad-service" \ + "${CRAFT_PART_INSTALL}/bin/etherpad-service" + install -Dm755 "${CRAFT_PROJECT_DIR}/snap/local/bin/etherpad-healthcheck-wrapper" \ + "${CRAFT_PART_INSTALL}/bin/etherpad-healthcheck-wrapper" + install -Dm755 "${CRAFT_PROJECT_DIR}/snap/local/bin/etherpad-cli" \ + "${CRAFT_PART_INSTALL}/bin/etherpad-cli" + +hooks: + configure: + plugs: + - network From bacf79fc7b7a562578789c7043b8c1aaa1fb4373 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 19 Apr 2026 18:33:48 +0100 Subject: [PATCH 2/2] fix(snap): pass --settings flag, env-subst ip/port, 2-space indent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Qodo review feedback on #7558: 1. Settings file ignored: Etherpad's Settings loader reads `argv.settings`, not the `EP_SETTINGS` env var. Without `--settings`, the launcher's seeded $SNAP_COMMON/etc/settings.json is never loaded; Etherpad falls back to /settings.json, which lives on the read-only squashfs — so the default dirty-DB path ends up unwritable and the daemon fails to persist pads. Fix: pass `--settings "${SETTINGS}"` to node; drop the EP_SETTINGS export. 2. `snap set` overrides were no-ops: the seeded settings.json carries the template's literal `"ip": "0.0.0.0"` / `"port": 9001` values, which override the env-based defaults Etherpad exposes via ${…} substitution. Users following the README saw the listener stay put after `snap set etherpad-lite port=…`. Fix: after copying the template on first run, rewrite the top-level `ip` and `port` lines to `"${IP:0.0.0.0}"` / `"${PORT:9001}"`. Use `0,/…/` anchors so the `dbSettings.port` entry further down stays literal. 3. Indentation: reflow the new shell scripts from 4-space to 2-space to match the repo style rule. Co-Authored-By: Claude Opus 4.7 (1M context) --- snap/hooks/configure | 14 ++++---- snap/local/bin/etherpad-cli | 12 +++---- snap/local/bin/etherpad-healthcheck-wrapper | 8 ++--- snap/local/bin/etherpad-service | 39 ++++++++++++++------- 4 files changed, 44 insertions(+), 29 deletions(-) diff --git a/snap/hooks/configure b/snap/hooks/configure index f47df2c176f..1bdf54e1594 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -7,18 +7,18 @@ set -euo pipefail PORT="$(snapctl get port || true)" if [ -n "${PORT}" ]; then - if ! [[ "${PORT}" =~ ^[0-9]+$ ]] || [ "${PORT}" -lt 1 ] || [ "${PORT}" -gt 65535 ]; then - echo "port must be an integer 1-65535" >&2 - exit 1 - fi + if ! [[ "${PORT}" =~ ^[0-9]+$ ]] || [ "${PORT}" -lt 1 ] || [ "${PORT}" -gt 65535 ]; then + echo "port must be an integer 1-65535" >&2 + exit 1 + fi fi IP="$(snapctl get ip || true)" if [ -n "${IP}" ] && ! [[ "${IP}" =~ ^[0-9a-fA-F.:]+$ ]]; then - echo "ip must be a valid IPv4/IPv6 address" >&2 - exit 1 + echo "ip must be a valid IPv4/IPv6 address" >&2 + exit 1 fi if snapctl services etherpad-lite.etherpad-lite 2>/dev/null | grep -q active; then - snapctl restart etherpad-lite.etherpad-lite + snapctl restart etherpad-lite.etherpad-lite fi diff --git a/snap/local/bin/etherpad-cli b/snap/local/bin/etherpad-cli index 9da100c4bf1..3a08c7ecda1 100755 --- a/snap/local/bin/etherpad-cli +++ b/snap/local/bin/etherpad-cli @@ -8,10 +8,10 @@ NODE_BIN="${SNAP}/opt/node/bin/node" export PATH="${SNAP}/opt/node/bin:${PATH}" if [ "$#" -eq 0 ]; then - echo "Usage: etherpad-lite.etherpad [args...]" - echo "Available scripts:" - ls "${APP_DIR}/bin" | grep -E '\.(ts|sh)$' | sed 's/^/ /' - exit 2 + echo "Usage: etherpad-lite.etherpad [args...]" + echo "Available scripts:" + ls "${APP_DIR}/bin" | grep -E '\.(ts|sh)$' | sed 's/^/ /' + exit 2 fi SCRIPT_NAME="$1"; shift @@ -19,6 +19,6 @@ SCRIPT_PATH="${APP_DIR}/bin/${SCRIPT_NAME}" [ -f "${SCRIPT_PATH}" ] || { echo "no such script: ${SCRIPT_NAME}"; exit 2; } case "${SCRIPT_PATH}" in - *.sh) exec "${SCRIPT_PATH}" "$@" ;; - *.ts) exec "${NODE_BIN}" --import tsx/esm "${SCRIPT_PATH}" "$@" ;; + *.sh) exec "${SCRIPT_PATH}" "$@" ;; + *.ts) exec "${NODE_BIN}" --import tsx/esm "${SCRIPT_PATH}" "$@" ;; esac diff --git a/snap/local/bin/etherpad-healthcheck-wrapper b/snap/local/bin/etherpad-healthcheck-wrapper index 10bb12d91dd..fa9db7c8466 100755 --- a/snap/local/bin/etherpad-healthcheck-wrapper +++ b/snap/local/bin/etherpad-healthcheck-wrapper @@ -6,15 +6,15 @@ PORT="$(snapctl get port 2>/dev/null || true)" : "${PORT:=9001}" if command -v curl >/dev/null 2>&1; then - exec curl --fail --silent --show-error --max-time 5 \ - "http://127.0.0.1:${PORT}/health" + exec curl --fail --silent --show-error --max-time 5 \ + "http://127.0.0.1:${PORT}/health" fi NODE_BIN="${SNAP}/opt/node/bin/node" exec "${NODE_BIN}" -e ' const http = require("http"); http.get("http://127.0.0.1:'"${PORT}"'/health", r => { - if (r.statusCode === 200) process.exit(0); - console.error("HTTP " + r.statusCode); process.exit(1); + if (r.statusCode === 200) process.exit(0); + console.error("HTTP " + r.statusCode); process.exit(1); }).on("error", e => { console.error(e.message); process.exit(1); }); ' diff --git a/snap/local/bin/etherpad-service b/snap/local/bin/etherpad-service index dec33656d82..1df548dace6 100755 --- a/snap/local/bin/etherpad-service +++ b/snap/local/bin/etherpad-service @@ -2,11 +2,15 @@ # Launch wrapper for the Etherpad snap daemon. # # 1. On first run, copy settings.json.template -> $SNAP_COMMON/etc/settings.json -# so the admin can edit it outside the read-only squashfs. +# so the admin can edit it outside the read-only squashfs. Patch the +# seeded file so dirty-DB / ip / port point at writable paths and pick +# up env-var overrides. # 2. Create writable data dirs under $SNAP_COMMON. -# 3. Apply `snap set` overrides (port, ip) via env vars — settings.json -# uses ${PORT:9001}-style substitution natively. -# 4. Exec Node with tsx loader to run server.ts. +# 3. Apply `snap set` overrides (port, ip) via env vars — Etherpad's +# settings.json supports ${PORT:9001}-style substitution natively. +# 4. Exec Node with tsx loader to run server.ts, passing the seeded +# settings file via --settings (Etherpad reads `argv.settings`, not +# an env var, so EP_SETTINGS alone would be ignored). set -euo pipefail APP_DIR="${SNAP}/opt/etherpad-lite" @@ -21,12 +25,20 @@ mkdir -p "${ETC_DIR}" "${VAR_DIR}" "${LOG_DIR}" SETTINGS="${ETC_DIR}/settings.json" if [ ! -f "${SETTINGS}" ]; then - echo "[etherpad-snap] bootstrapping ${SETTINGS} from template" - cp "${APP_DIR}/settings.json.template" "${SETTINGS}" - # Rewrite the dirty DB filename to a writable absolute path. - sed -i \ - -e 's|"filename": "var/dirty.db"|"filename": "'"${VAR_DIR}"'/dirty.db"|' \ - "${SETTINGS}" + echo "[etherpad-snap] bootstrapping ${SETTINGS} from template" + cp "${APP_DIR}/settings.json.template" "${SETTINGS}" + # Point the default dirty-DB at $SNAP_COMMON (absolute path, writable). + sed -i \ + -e 's|"filename": "var/dirty.db"|"filename": "'"${VAR_DIR}"'/dirty.db"|' \ + "${SETTINGS}" + # Rewrite ip/port literals to Etherpad's env-substitution syntax so + # `snap set etherpad-lite port=` / `ip=` actually take effect. + # Only substitute the first (top-level) occurrence — `dbSettings.port` + # has the same key name lower down and must not be touched. + sed -i \ + -e '0,/"ip": "0.0.0.0"/{s|"ip": "0.0.0.0"|"ip": "${IP:0.0.0.0}"|}' \ + -e '0,/"port": 9001/{s|"port": 9001|"port": "${PORT:9001}"|}' \ + "${SETTINGS}" fi PORT_OVERRIDE="$(snapctl get port || true)" @@ -37,7 +49,10 @@ export PORT="${PORT_OVERRIDE}" export IP="${IP_OVERRIDE}" cd "${APP_DIR}" -export EP_SETTINGS="${SETTINGS}" export NODE_ENV=production -exec "${NODE_BIN}" --import tsx/esm src/node/server.ts "$@" +# Pass --settings explicitly; Etherpad's Settings loader reads argv only, +# so exporting EP_SETTINGS is not enough to redirect the config file. +exec "${NODE_BIN}" --import tsx/esm src/node/server.ts \ + --settings "${SETTINGS}" \ + "$@"