Hacklily is an online sheet-music editor. You write LilyPond in a Monaco-powered editor in your browser and get a live rendered preview (SVG/PDF/PNG/MIDI). Scores can be saved to and published directly to GitHub, and MusicXML files can be imported via musicxml2ly.
Hacklily has three parts:
- Frontend (
src/) — a React 18 SPA bundled with webpack. Monaco provides the editor; the preview talks to the renderer over a JSON-RPC 2.0 WebSocket. Three webpack entry points:index(the editor),status(a public server-status page), andmusicxml2ly(the MusicXML importer). - Renderer (
server/renderer/andserver/renderer-unstable/) — Docker images that build LilyPond from source (stable 2.26, unstable 2.27). Each container runs a warm LilyPond Scheme server (lily-server.scm) and a bash frame (render-impl.bash) that reads one JSON request per line on stdin and emits one JSON response per line on stdout. - Renderer server (
server/renderer-server/, Rust) — a single binary that does three jobs: inservemode it is the frontend-facing coordinator (WebSocket JSON-RPC, GitHub OAuth, a publicget_statuspage) and the local render pool; inws-workermode it offers compute to a remote coordinator; inbatchmode it renders test cases from a file. It keeps a pool of renderer containers warm, routes render requests (stable vs. unstable, picked from the score's\version), enforces an 8s render timeout, and tears down and replaces containers after each request for isolation. A coordinator with--stable-worker-count 0 --unstable-worker-count 0runs in pure-coordinator mode, dispatching renders to remotews-workerpeers (and failing requests if none are attached). The legacy Qt5 coordinator that used to live inserver/ws-server/has been removed.
You need Node and Docker.
# frontend (without GitHub integration)
npm install
npm start # http://localhost:3000, talks to the remote render backend by default
# or, with the local backend:
npm start # in one shell — REACT_APP_BACKEND_WS_URL defaults to the prod backend
cd server/renderer-server
cargo run -- \
--stable-docker-tag hacklily-renderer \
--unstable-docker-tag hacklily-renderer-unstable \
--stable-worker-count 4 --unstable-worker-count 4 \
--render-timeout-msec 8000 \
serve --ws-port 2000 --github-client-id "" --github-secret ""npm start:remote-backend runs the dev server pointed at the production render backend (wss://render.hacklily.org/rpc), so you don't need Docker for most frontend work.
The serve coordinator listens on plain TCP (--ws-port, bound to 127.0.0.1 by default via --bind-address) and does not terminate TLS itself. In production it sits behind a TLS-terminating reverse proxy: the proxy presents the public wss://… certificate and forwards plain ws:// to the coordinator's port. The coordinator ignores the HTTP path (it upgrades any WebSocket handshake), so the proxy can route /rpc or any other path to it. The same reverse proxy that fronts the SPA can do this — just add a WebSocket-capable location block proxying to the coordinator port and ensure it passes through Upgrade/Connection headers.
The coordinator also supports an optional HTTP status endpoint (--http-status-port, e.g. 9990) that serves GET /status as JSON — the same data as the WebSocket get_status RPC but over plain HTTP, so monitoring scripts and load balancers don't need a WebSocket connection. The nginx site config in server/renderer-server/deploy/nginx/ proxies https://render.hacklily.org/status to it.
Graceful shutdown: send the process SIGTERM (this is what systemd, k8s, and docker stop send). The coordinator drains in-flight renders and exits 0; because a single render can take up to the render timeout (~8s), set the supervisor's termination grace period to exceed that so in-flight user renders aren't cut off mid-deploy. (SIGINT / Ctrl-C does the same thing for interactive use.)
A ready-to-use systemd user service (unit file, env template, install + update scripts, and docs) lives in server/renderer-server/deploy/. It runs the serve coordinator, restarts on crashes, pulls the published crate and renderer images from the public Codeberg registries (no credentials stored on the host), and updates with a single hacklily-renderer-update command that pulls the latest versions and restarts. See server/renderer-server/deploy/README.md for install and usage.
Hacklily is stable and live at https://www.hacklily.org. The renderer is fully on the Rust renderer server, which now serves the frontend directly (the legacy Qt5 coordinator has been retired).
Source and issue tracker: https://codeberg.org/jocelyn-stericker/hacklily
Contributions are welcome. By the terms of the licenses below, client contributions must be GPLv3-or-later and server contributions must be AGPLv3-or-later.
The client (everything outside server/) is licensed under the GNU General Public License version 3 or later, with additional permissions (notably for minimized/compact forms and System Libraries) as described in LICENSE.txt. The full GPL text is in LICENSE.txt.
The server (everything under server/) is licensed under the GNU Affero General Public License version 3 or later. The full AGPL text is in LICENSE.AGPL.txt.