* docs: migrate useful wiki content into the VitePress manual (#7990) The GitHub wiki is being retired; documentation should ship with the software. This migrates the still-accurate, non-duplicate wiki pages into the published VitePress site (doc/**/*.md + the sidebar in doc/.vitepress/config.mts) so they are versioned, searchable and portable: - deployment.md: reverse-proxy configs (Nginx/Apache/Caddy/Traefik/ HAProxy) with the WebSocket-upgrade rules, subdirectory hosting via X-Proxy-Path, native HTTPS via the ssl block, a systemd unit, and the Istio manifest (with the Redis-adapter multi-replica caveat). - accessibility.md: editor keyboard shortcuts (verified against ace2_inner.ts / broadcast_slider.ts / pad_editbar.ts), toolbar navigation, NVDA notes. - faq.md: install methods, URL-path reference, listing/deleting pads (API-first), backup/restore, and history pruning. - development.md: source-tree tour, the pad<->format conversion pipeline, the internal DB API, and the Fontello toolbar-icon workflow. - database.md: the key/value schema plus connecting MySQL/PostgreSQL/Redis backends and a pgloader MySQL->PostgreSQL migration (database docs were previously absent from the VitePress site). Every page was checked against the current source before inclusion: corrected the apt instructions to the live signed repo (stable/main, signed-by key), dropped the unpublished snap, fixed the Redis dbSettings (flat host/port/password or url, not the obsolete client_options), dropped charset from the PostgreSQL example, and removed a phantom getEtherpad API reference. The VitePress site builds cleanly (pnpm run docs:build) with the dead-link checker enabled. Closes #7990 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: add verified hands-on changeset/atext walkthrough (#7990) Migrate the practical Changeset-library tutorial from the wiki into changeset_library.md, rewritten against the current API: unpack(), deserializeOps() (replacing the deprecated opIterator) and new AttributePool() (replacing the removed AttributePoolFactory). Every example output was produced by running the code against the current Changeset.ts / AttributePool.ts, not copied from the wiki. Also fixes a stale ether/etherpad-lite source link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
Deployment
This page collects working configurations for deploying Etherpad in production: running it behind a reverse proxy, hosting it under a subdirectory, terminating HTTPS natively, running it as a system service, and deploying it on Kubernetes.
Etherpad listens on port 9001 by default. Throughout this page the upstream
Etherpad server is assumed to be reachable at http://127.0.0.1:9001.
Running behind a reverse proxy
The recommended production setup is to run Etherpad on 127.0.0.1:9001 and put a
reverse proxy in front of it to terminate TLS, serve a virtual host, and forward
requests.
Etherpad uses WebSockets (via socket.io). The load-bearing part of every proxy
config below is the WebSocket upgrade: the proxy must forward the Upgrade
and Connection headers, or real-time editing will silently fail back to slow
long-polling (or break entirely).
When Etherpad runs behind a proxy you should also set trustProxy: true in your
settings so that Etherpad honours the X-Forwarded-* headers (correct client IP,
secure-cookie flag, etc.). See the trustProxy section in the Configuration documentation for the full details of which headers are trusted.
Nginx
# Map the Upgrade header so WebSockets work. Place this in the http context.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name pad.example.com;
ssl_certificate /etc/nginx/ssl/etherpad.crt;
ssl_certificate_key /etc/nginx/ssl/etherpad.key;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_buffering off;
proxy_set_header Host $host;
proxy_pass_header Server;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
# Redirect plain HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name pad.example.com;
return 301 https://$host$request_uri;
}
Apache
Enable mod_proxy, mod_proxy_http, mod_proxy_wstunnel and mod_headers.
The mod_proxy_wstunnel upgrade=websocket syntax requires Apache 2.4.47 or
newer.
<VirtualHost *:443>
ServerName pad.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/etherpad/etherpad.crt
SSLCertificateKeyFile /etc/ssl/etherpad/etherpad.key
ProxyVia On
ProxyRequests Off
ProxyPreserveHost On
# WebSocket traffic (socket.io) must be matched first.
<Location "/socket.io">
ProxyPass "ws://127.0.0.1:9001/socket.io" upgrade=websocket timeout=30
ProxyPassReverse "ws://127.0.0.1:9001/socket.io"
</Location>
<Location "/">
ProxyPass "http://127.0.0.1:9001/" retry=0 timeout=30
ProxyPassReverse "http://127.0.0.1:9001/"
</Location>
</VirtualHost>
Caddy
Caddy v2 proxies WebSocket connections automatically and obtains/renews a certificate for you, so the configuration is minimal:
pad.example.com {
reverse_proxy 127.0.0.1:9001
}
Traefik
Traefik v2 also proxies WebSockets transparently. For a Docker deployment, attach these labels to the Etherpad container:
labels:
- "traefik.enable=true"
- "traefik.http.routers.etherpad.rule=Host(`pad.example.com`)"
- "traefik.http.routers.etherpad.entrypoints=websecure"
- "traefik.http.routers.etherpad.tls.certresolver=myresolver"
- "traefik.http.services.etherpad.loadbalancer.server.port=9001"
- "traefik.http.services.etherpad.loadbalancer.passhostheader=true"
HAProxy
HAProxy detects the Connection: Upgrade exchange automatically and switches to
tunnel mode once the WebSocket is established. The important value is
timeout tunnel, which governs the lifetime of the upgraded connection.
frontend http
mode http
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/etherpad.pem alpn h2,http/1.1
http-request redirect scheme https code 301 unless { ssl_fc }
http-request add-header X-Forwarded-Proto https if { ssl_fc }
default_backend etherpad
backend etherpad
mode http
option forwardfor
timeout client 25s
timeout server 25s
timeout tunnel 3600s
server pad 127.0.0.1:9001
Hosting under a subdirectory
To serve Etherpad from a path such as https://example.com/pad rather than from
the root of a domain, the proxy must send the X-Proxy-Path header so that
Etherpad rewrites its own asset and API URLs to include the prefix. This header
is honoured regardless of the trustProxy setting — see the Configuration documentation.
location /pad/ {
rewrite ^/pad/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:9001;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Proxy-Path /pad;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
Native HTTPS without a proxy
Etherpad can terminate TLS itself using Node's native HTTPS server, with no
reverse proxy required. Configure the ssl block in settings.json:
"ssl": {
"key": "/path-to-your/etherpad-server.key",
"cert": "/path-to-your/etherpad-server.crt",
"ca": ["/path-to-your/intermediate-cert1.crt", "/path-to-your/intermediate-cert2.crt"]
}
key— path to the private key file.cert— path to the certificate file.ca— an (optional) array of intermediate/chain certificate paths.
Restart Etherpad after editing the settings. It will now serve HTTPS on its configured port.
For local testing you can generate a self-signed certificate with a single command:
openssl req -x509 -newkey rsa:4096 -nodes -days 365 \
-keyout etherpad-server.key -out etherpad-server.crt \
-subj "/CN=localhost"
Make sure the files are readable only by the user that runs Etherpad:
chmod 400 etherpad-server.key etherpad-server.crt
chown etherpad etherpad-server.key etherpad-server.crt
::: tip Self-signed certificates trigger browser warnings and are only suitable for testing. For production, obtain a free, trusted certificate from Let's Encrypt, or terminate TLS at a reverse proxy (see above) and let it manage certificate issuance and renewal. :::
Running as a service (systemd)
On a modern Linux distribution, run Etherpad as a systemd service so it starts
on boot and restarts automatically on failure.
Create a dedicated unprivileged user and install Etherpad into its home
directory (for example /opt/etherpad), owned by that user. Etherpad refuses to
start as root.
Create /etc/systemd/system/etherpad.service:
[Unit]
Description=Etherpad collaborative editor
After=network.target
[Service]
Type=simple
User=etherpad
Group=etherpad
WorkingDirectory=/opt/etherpad
Environment=NODE_ENV=production
ExecStart=/usr/bin/pnpm run prod
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Adjust WorkingDirectory to your install path and the ExecStart path to
wherever pnpm lives (which pnpm). Then enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now etherpad.service
# check status and follow logs
sudo systemctl status etherpad.service
sudo journalctl -u etherpad.service -f
Kubernetes (Istio)
The following manifest deploys Etherpad behind an Istio ingress gateway. It
defines three resources: a Gateway (TLS + hostname), a VirtualService
(routing with WebSocket-friendly timeouts), and a DestinationRule (sticky
sessions via the socket.io io cookie).
It assumes:
- Istio >= 1.18
- A
Servicenamedetherpadin theetherpadnamespace, on port9001 - A TLS secret
etherpad-tlsprovisioned in the gateway namespace - You replace
<your-host>with your own hostname
::: warning Sticky sessions are necessary but not sufficient for a multi-replica Etherpad deployment. Multi-replica also needs the socket.io Redis adapter so that pad state is shared across pods. Without it, two clients editing the same pad but routed to different pods will see divergent state.
Recommendation: start with replicas: 1 plus good failover, and only go
multi-replica once the Redis adapter is wired up.
:::
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: etherpad
namespace: etherpad
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: etherpad-tls
hosts:
- <your-host>
- port:
number: 80
name: http
protocol: HTTP
hosts:
- <your-host>
tls:
httpsRedirect: true
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: etherpad
namespace: etherpad
spec:
hosts:
- <your-host>
gateways:
- etherpad
http:
- match:
- uri:
prefix: /
route:
- destination:
host: etherpad
port:
number: 9001
# No per-request timeout — websockets and long-polling sit on the
# connection indefinitely. The default of 15s kills WS upgrades.
timeout: 0s
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: etherpad
namespace: etherpad
spec:
host: etherpad
trafficPolicy:
loadBalancer:
# Sticky sessions on the socket.io session cookie. Required so that
# long-polling fallback requests land on the same pod that owns the
# session state.
consistentHash:
httpCookie:
name: io
ttl: 0s # session cookie, expires with the browser tab
connectionPool:
tcp:
maxConnections: 10000
http:
# Must comfortably exceed socket.io's pingInterval (25s) +
# pingTimeout (20s). 1h is conservative.
idleTimeout: 3600s
h2UpgradePolicy: UPGRADE
http1MaxPendingRequests: 1000