feat(server): support configurable hostname for managed OpenCode server spawn (#599)

* feat(server): support configurable hostname for managed OpenCode server spawn

Allow the managed OpenCode server bind hostname to be configured via
OPENCHAMBER_OPENCODE_HOSTNAME environment variable (default: 127.0.0.1).

This enables LAN/Tailscale access without a reverse proxy by setting
the hostname to 0.0.0.0.

Closes #597

* fix: address review feedback — input validation, port probe hostname, security docs

- Add defensive parsing for OPENCHAMBER_OPENCODE_HOSTNAME with trim/empty
  check and warning log, matching OPENCODE_HOST validation pattern
- Pass configured hostname to resolveManagedOpenCodePort() so port
  availability is probed on the actual bind address, avoiding EADDRINUSE
- Add security note in README docs warning about 0.0.0.0 exposure on
  untrusted networks
This commit is contained in:
yslee
2026-03-23 15:48:04 +02:00
committed by GitHub
parent fb011d6f38
commit ea6d4c4d43
5 changed files with 54 additions and 4 deletions
+5
View File
@@ -127,6 +127,11 @@ OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
```
Bind managed OpenCode server to all interfaces (use only on trusted networks):
```bash
OPENCHAMBER_OPENCODE_HOSTNAME=0.0.0.0 openchamber --port 3000
```
</details>
<details>
+1
View File
@@ -26,4 +26,5 @@ services:
# OH_MY_OPENCODE: true # enable oh-my-opencode
# OPENCODE_HOST: http://172.17.0.1:4096 # Connect to external OpenCode server
# OPENCODE_SKIP_START: true # skip start opencode
# OPENCHAMBER_OPENCODE_HOSTNAME: 0.0.0.0 # Bind OpenCode to all interfaces
restart: unless-stopped
+28
View File
@@ -46,6 +46,34 @@ openchamber update # Update to latest version
- Replacing or stopping a tunnel revokes existing connect links and invalidates remote tunnel sessions.
- Connect links are one-time tokens; generating a new link revokes the previous unused link.
<details>
<summary>Connect to external OpenCode server</summary>
```bash
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
```
| Variable | Description |
|----------|-------------|
| `OPENCODE_HOST` | Full base URL of external server (overrides `OPENCODE_PORT`) |
| `OPENCODE_PORT` | Port of external server |
| `OPENCODE_SKIP_START` | Skip starting embedded OpenCode server |
| `OPENCHAMBER_OPENCODE_HOSTNAME` | Bind hostname for managed OpenCode server (default: `127.0.0.1`, use `0.0.0.0` for LAN/remote access — trusted networks only) |
</details>
<details>
<summary>Bind managed OpenCode to LAN / Tailscale</summary>
```bash
OPENCHAMBER_OPENCODE_HOSTNAME=0.0.0.0 openchamber --port 3000
```
**Security note:** binding to `0.0.0.0` exposes the server on all network interfaces — use only on trusted networks and protect with firewall rules or `--ui-password`.
</details>
**Optional env vars:**
```yaml
environment:
+1
View File
@@ -864,6 +864,7 @@ ENVIRONMENT:
OPENCODE_HOST External OpenCode server base URL, e.g. http://hostname:4096
OPENCODE_PORT Port of external OpenCode server to connect to
OPENCODE_SKIP_START Skip starting OpenCode, use external server
OPENCHAMBER_OPENCODE_HOSTNAME Bind hostname for managed OpenCode server (default: 127.0.0.1)
EXAMPLES:
openchamber # Start in daemon mode on default port 3000 (or free port)
+19 -4
View File
@@ -3788,6 +3788,21 @@ const ENV_CONFIGURED_OPENCODE_HOST = (() => {
// OPENCODE_HOST takes precedence over OPENCODE_PORT when both are set
const ENV_EFFECTIVE_PORT = ENV_CONFIGURED_OPENCODE_HOST?.port ?? ENV_CONFIGURED_OPENCODE_PORT;
const ENV_CONFIGURED_OPENCODE_HOSTNAME = (() => {
const raw = process.env.OPENCHAMBER_OPENCODE_HOSTNAME;
if (typeof raw !== 'string') {
return '127.0.0.1';
}
const trimmed = raw.trim();
if (!trimmed) {
console.warn(
`[config] Ignoring OPENCHAMBER_OPENCODE_HOSTNAME=${JSON.stringify(raw)}: empty after trimming`,
);
return '127.0.0.1';
}
return trimmed;
})();
const ENV_SKIP_OPENCODE_START = process.env.OPENCODE_SKIP_START === 'true' ||
process.env.OPENCHAMBER_SKIP_OPENCODE_START === 'true';
const ENV_DESKTOP_NOTIFY = process.env.OPENCHAMBER_DESKTOP_NOTIFY === 'true';
@@ -6049,7 +6064,7 @@ async function createManagedOpenCodeServerProcess({
};
}
async function resolveManagedOpenCodePort(requestedPort) {
async function resolveManagedOpenCodePort(requestedPort, hostname = '127.0.0.1') {
if (typeof requestedPort === 'number' && Number.isFinite(requestedPort) && requestedPort > 0) {
return requestedPort;
}
@@ -6079,13 +6094,13 @@ async function resolveManagedOpenCodePort(requestedPort) {
});
});
server.listen(0, '127.0.0.1');
server.listen(0, hostname);
});
}
async function startOpenCode() {
const desiredPort = ENV_CONFIGURED_OPENCODE_PORT ?? 0;
const spawnPort = await resolveManagedOpenCodePort(desiredPort);
const spawnPort = await resolveManagedOpenCodePort(desiredPort, ENV_CONFIGURED_OPENCODE_HOSTNAME);
console.log(
desiredPort > 0
? `Starting OpenCode on requested port ${desiredPort}...`
@@ -6100,7 +6115,7 @@ async function startOpenCode() {
try {
const serverInstance = await createManagedOpenCodeServerProcess({
hostname: '127.0.0.1',
hostname: ENV_CONFIGURED_OPENCODE_HOSTNAME,
port: spawnPort,
timeout: 30000,
cwd: openCodeWorkingDirectory,