rome-os / custom-dependencies
Install software that Rome does not ship, such as a system package, CLI tool, language runtime, Python or global npm package, or an app dependency with native code, so it keeps working after Rome upgrades. Use when the guardian asks to install a tool or package in Rome, when `apt-get`, `sudo`, `pip`, or `npm install -g` fails, or when an app install fails with a native-build hint or `ERR_PNPM_IGNORED_BUILDS`.
Custom dependencies
The rules
- Rome runs as the
romeaccount. Itssudorights cover two Rome maintenance commands and nothing else, so it cannot runapt-getor install into/usr. - A Rome upgrade replaces the container. Of the files you can change, only those under
/home/rome,/home/user, and/app/memorycarry over./home/romeis the home directory ofrome. - Rome's processes, app installs, app code, and the
romeandusershells have~/.local/binat the end ofPATH. Commands there run by name, and an image command wins any name clash.
Install custom software under /home/rome. Commands in ~/.local/bin run by name. Anything elsewhere, such as ~/.local/go/bin/go, needs a symlink into ~/.local/bin or its absolute path. Rome and its apps see only /home/rome/.local/bin, so a tool the user account installs over SSH is not on their PATH.
What a Rome upgrade keeps
| Path | After an upgrade |
|---|---|
/home/rome: ~/.local, ~/.cache, ~/.npmrc, the shell profiles, app repos, installed apps, app data, the database, the pnpm store | Kept |
/home/user | Kept |
/app | Rome's own code, replaced by the new image. /app/memory is kept. |
System directories such as /usr, /opt, and /etc, including anything installed with apt-get | Replaced by the new image, except Rome's SSH host keys and Tailscale state |
Rome publishes a new image every weekday on a schedule, plus manual releases (release schedule). An instance upgrades at these times:
- Rome Cloud: when the guardian starts the upgrade under Settings → System. Instances with automatic upgrades also check at 3:00 in the guardian's time zone, then upgrade after a countdown the guardian can postpone.
- Self-hosted: when the operator pulls a new image and recreates the container, for example by running the quickstart script again.
Where each kind of dependency goes
dpkg --print-architecture prints the architecture to download for, amd64 or arm64. Node packages name amd64 as x64.
CLI tools and language runtimes
Download a static binary or the official Linux tarball, and unpack it under ~/.local:
mkdir -p ~/.local/bin
rm -rf ~/.local/go
curl -fsSL "https://go.dev/dl/go<version>.linux-$(dpkg --print-architecture).tar.gz" | tar -xz -C ~/.local
ln -sf ~/.local/go/bin/go ~/.local/bin/
go versionA tool that links against the image's system libraries can break when an upgrade changes them. Prefer a static build.
Python packages and tools
The image's Python 3 has no pip and no ensurepip, and Debian marks it as externally managed. Use uv, which installs into ~/.local/bin and keeps its Pythons and tools under ~/.local/share/uv. The image configures uv to use only the Pythons it installs there, so its environments survive upgrades.
curl -LsSf https://astral.sh/uv/install.sh | env UV_NO_MODIFY_PATH=1 sh
uv tool install httpie # a CLI tool → ~/.local/bin/http
uv venv ~/.venvs/work # an environment; add --python <version> to pick one
uv pip install --python ~/.venvs/work/bin/python requestsGlobal npm packages
Rome sets npm's global prefix to ~/.local in ~/.npmrc unless the file already sets one, so npm install -g <package> puts its commands in ~/.local/bin.
Global packages run on the image's Node. A global package with native code can stop loading when an upgrade moves to a new Node major version.
System packages
The stock image has no persistent way to add an apt package. Use one of these instead:
- A static build of the tool, installed as in CLI tools and language runtimes.
- A package that bundles the binary, from npm or from PyPI through uv.
- On a self-hosted instance, an extended image.
App dependencies
Declare every dependency an app uses in its package.json. Rome installs it with the app, and the installed copy stays in place across upgrades. Anything the app finds only under /home/rome exists on this instance alone, so a shared or published app that relies on it breaks elsewhere.
A dependency with native code needs the steps in App dependencies with native code.
App dependencies with native code
Rome installs an app's dependencies inside the container, as rome, in two steps:
- A source install runs
pnpm installand then the app'sbuildscript in the app repo. Dev dependencies install in this step too. - Every install of an app with runtime dependencies, including an App Store install, runs
pnpm install --prodfor the packed artifact.
Both steps run every build script that the app's pnpm-workspace.yaml approves under allowBuilds. A build that compiles C or C++ fails, because the image has no make and no C or C++ compiler. The failed install ends with a hint that names the package. A make or compiler in ~/.local/bin would be found, but it exists only on this instance. The app would still fail on the stock image and for anyone who installs it from the App Store, so do not rely on one. Rome runs step 2 again for every new version of the app, including each App Store update. The app must install on the stock image every time. Step 2 reuses the installed copy when the packed artifact has not changed. So a successful reinstall of an unchanged app does not prove that an install fix works.
Try these options in order.
1. Use a dependency without native code
- For SQLite, use the app database for app data (
coding:app_creation→REFERENCE.md), andnode:sqlitefor tests and scripts. - Prefer a pure JavaScript or WebAssembly package over a native one.
- Do not add a driver the app does not use. Drizzle lists
better-sqlite3and other drivers as optional peers. - Treat a native test-only dependency like a runtime one. Dev dependencies install during a source install, so its build runs there.
2. Decline builds the package does not need
Some packages ship compiled binaries inside the npm package and need their build only on platforms without one. If the package ships a binary for the instance, decline its build:
# pnpm-workspace.yaml
allowBuilds:
better-sqlite3: falsebetter-sqlite3 13.0.3 and later ship prebuilds/linux-x64.node and prebuilds/linux-arm64.node, so they load with the build declined. Approving the build fails the install. pnpm runs node-gyp rebuild for any approved package that has a binding.gyp and no install script, and node-gyp needs make. The app template declines esbuild the same way.
A declined build can install cleanly and still fail when the app loads the package:
- better-sqlite3 12.x ships no binary. Its install script runs
prebuild-install, so approve the build instead, as in option 3. Declined, it fails at runtime withCould not locate the bindings file. - better-sqlite3 13.0.0 to 13.0.2 ship an arm64 binary that needs glibc 2.38. The image has glibc 2.36, so it fails on arm64 with an error that
GLIBC_2.38was not found.
After declining a build, run code that uses the package once in the Rome container, for example open a database.
Record a decision for every dependency with a build script. pnpm fails the install with ERR_PNPM_IGNORED_BUILDS when one is missing.
To check what a package ships, run npm pack <package>@<version> --dry-run and look for .node files under prebuilds/linux-<arch>. ldd --version prints the image's glibc version.
3. Approve builds that load a prebuilt binary
Approve a build only when its install script uses a prebuilt binary for the instance and compiles only as a fallback:
- An install script of
node-gyp-buildloads a binary from the package'sprebuilds/directory. classic-level 1.4.1 installs this way on both architectures. - An install script that starts with
prebuild-installdownloads a binary from the package's releases, so the instance must reach the download host.
If no binary matches the instance's architecture, the script compiles and the install fails. bufferutil 4.0.9 ships a linux-x64 binary and no linux-arm64 binary, so it installs on x64 and fails on arm64.
Prefer packages whose binaries use N-API, which usually have napi in the file name. A binary built for one Node ABI stops loading after a Rome upgrade moves to a new Node major version. The binary that prebuild-install downloads for better-sqlite3 12.x is one. Reinstalling the unchanged app reuses that binary. Release a new version of the app to install a matching one.
4. Vendor a binary the app's code loads
If no package ships a binary for the instance, build the .node file once on another machine and commit it to the app repo.
- Build the addon with N-API. A non-N-API addon loads only on the Node major version it was built for. A Rome upgrade can move to a new Node major.
- Build it in the
node:24-bookwormDocker image, which has the compilers, once with--platform linux/amd64and once with--platform linux/arm64. Use a machine with Docker, such as the guardian's computer or a CI runner. A build for one architecture is enough only for an app that stays on this instance. A shared or published app runs on both. - Commit the files under
src/, for example assrc/native/<name>-linux-x64.nodeandsrc/native/<name>-linux-arm64.node.rome buildcopies the files undersrc/into theappRootdirectory, except.ts,.tsx, and.cssfiles andsrc/web/. WithincludeSource: true, the published source carries the files too, so each binary counts twice against the 50 MB App Store bundle limit. - Load the file for
process.archwithcreateRequire(import.meta.url)and a path that starts from the loading module's directory. The same path works fromsrc/and from the built output.
5. On a self-hosted instance, extend the image
An extended image with make g++ lets approved builds compile.
Self-hosted: extend the image
A self-hosted instance can run an image that adds apt packages. Rome Cloud runs only the stock image.
ARG ROME_VERSION
FROM yunfanye/rome:${ROME_VERSION}
RUN apt-get update \
&& apt-get install -y --no-install-recommends make g++ \
&& rm -rf /var/lib/apt/lists/*- Replace
make g++with the packages you need. - Build the image for the Rome version you run:
docker build --build-arg ROME_VERSION=<version> -t rome-custom:<version> . - Start Rome from it. Pass
--image rome-custom:<version> --no-pullto the quickstart script, or setimage:in the Compose file. - Repeat steps 2 and 3 for every Rome upgrade. The quickstart script without
--imagepulls the stock image.
On the first native build for each Node version, node-gyp downloads the Node headers from nodejs.org and caches them under ~/.cache/node-gyp.
Host execution
If the guardian has enabled host execution, system:execute_root_script runs a root script on the Linux VM that hosts Rome. Software it installs there survives Rome upgrades, because an upgrade replaces only the container. Rome's processes inside the container cannot run that software. Only host jobs can.
What does not work
apt-get installordpkg -i. Both need root, andromehas no generalsudo. An upgrade also replaces the system directories they write to.- Writing into
/app.romecan write there, but an upgrade replaces the files. - Shipping a prebuilt
node_moduleswith an app. Pack and install both dropnode_modulesand install the declared dependencies again. - A startup script. Rome runs no user script when the container starts. An app's
app-startedhook runs asrome, like the rest of the app's code.