bun add matterbridge --global --omit=dev
bunx --bun matterbridge
The image (tag bun 69 MB) includes only Matterbridge, using the latest release published on npm. This image is based on oven/bun:slim. Plugins are not included in the image: they will be reinstalled on first run.
docker pull luligu/matterbridge:bun && docker run --name matterbridge -v ~/Matterbridge:/root/Matterbridge -v ~/.matterbridge:/root/.matterbridge -v ~/.mattercert:/root/.mattercert --network host --restart always --stop-timeout 60 -d luligu/matterbridge:bun
The bun image runs Matterbridge directly from the local source files with Bun runtime.
oven/bun:slim (Debian trixie slim + Bun).bun install --omit=dev bun link| File | Purpose |
|---|---|
docker/Dockerfile.local.bun |
The Bun image definition |
docker/Dockerfile.local.bun.dockerignore |
Per-Dockerfile build context (keeps source, drops .git/chip/scripts/…) |
docker/entrypoint.local.bun.sh |
Entrypoint banner (prints the Bun version) |
npm run docker:build:localbun # build the image (matterbridge:local-bun)
npm run docker:run:localbun # run it (container matterbridge-local-bun, port 8283)
npm run docker:exec:localbun # open a shell in the running container
npm run docker:log:localbun # follow the container logs
The core bridge runs on Bun: it creates its directories, initializes the Matter node storage, and brings up the server node and endpoints. The web frontend is built and served. See the TODO list below for the known limitations.
The approach is to detect if running in bun with isBun() and switch the
package-manager command and global-modules paths to Bun where needed.
bun link (in Dockerfile.bun) registers the
local build as the global matterbridge package — and installs all CLI bins
and their exec bits — so plugins resolve import 'matterbridge'. This is the
full npm link replacement.npm root -g discovery. getGlobalNodeModules() returns
getGlobalBunModules() when running on Bun (there is no bun root -g; the path is
derived from $BUN_INSTALL / ~/.bun).
(npmPrefix.ts, runtimeBun.ts)PluginManager resolves plugins from the Bun
global modules dir when running on Bun. (pluginManager.ts)sudo.
(pluginManager.ts, frontend.ts, backendExpress.ts, spawnCommand.ts)isBun() ? 'bun' : 'npm'.
(matterbridge.ts)--add local plugin. When running on Bun, the plugin is no longer treated as
"local", so the npm link matterbridge step is skipped (bun link already
provides resolution). (matterbridge.ts)node:os and bun:os return username: "unknown" and shell: "unknown"
from os.userInfo(), even though they correctly return the UID, GID, and home
directory. Consequently, Matterbridge sends User: unknown to the frontend
system-information view instead of the container account (for example, root).
Reproduce with bun -e "import * as os from 'bun:os'; console.log(os.userInfo())".// Change: FileStorageDriver.js
async #writeAndMoveFile(filepath, valueOrStream) {
const tmpName = `${filepath}.tmp`;
await writeFile(tmpName, valueOrStream, { encoding: "utf8", flush: true });
await rename(tmpName, filepath);
}
// Change: FileStorageDriver.js
import { isBunjs } from '../../util/runtimeChecks.js';
if (isBunjs()) {
if (typeof valueOrStream === 'string') {
await writeFile(tmpName, valueOrStream, { encoding: 'utf8', flush: true });
} else {
const value = new Uint8Array(await new Response(valueOrStream).arrayBuffer());
await writeFile(tmpName, value, { flush: true });
}
await rename(tmpName, filepath);
return;
}
/**
* @license
* Copyright 2022-2026 Matter.js Authors
* SPDX-License-Identifier: Apache-2.0
*/
import * as assert from 'node:assert';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { FileStorageDriver } from '../../src/storage/fs/FileStorageDriver.js';
describe('FileStorageDriver Bun runtime', () => {
let rootDir: string;
beforeEach(async () => {
rootDir = await mkdtemp(join(tmpdir(), 'matterjs-file-storage-bun-test-'));
});
afterEach(async () => {
await rm(rootDir, { recursive: true, force: true });
});
it('uses the Bun write path for string and stream values', async () => {
const previousBun = process.versions.bun;
Object.defineProperty(process.versions, 'bun', { value: '1', configurable: true });
try {
const storage = new FileStorageDriver(rootDir);
await storage.initialize();
await storage.set(['context'], 'text', 'value');
assert.equal(await storage.get(['context'], 'text'), 'value');
await storage.writeBlobFromStream(
['context'],
'blob',
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array([1, 2, 3]));
controller.close();
},
}),
);
await storage.close();
const file = await readFile(join(rootDir, 'context.blob'));
assert.deepEqual(new Uint8Array(file), new Uint8Array([1, 2, 3]));
} finally {
if (previousBun === undefined) {
delete process.versions.bun;
} else {
Object.defineProperty(process.versions, 'bun', { value: previousBun, configurable: true });
}
}
});
});