diff --git a/docker-compose.yml b/docker-compose.yml index 1877b14c..e1366b49 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - ./data/ssh:/home/openchamber/.ssh - ./workspaces:/home/openchamber/workspaces #environment: + # OPENCHAMBER_HOST: 0.0.0.0 # Bind address (default in Docker: 0.0.0.0) # UI_PASSWORD: your_secure_password_here # Uncomment to set UI password # OPENCHAMBER_TUNNEL_PROVIDER: cloudflare # OPENCHAMBER_TUNNEL_MODE: quick # quick | managed-remote | managed-local diff --git a/packages/web/bin/cli.js b/packages/web/bin/cli.js index 5bc59cba..a5753645 100644 --- a/packages/web/bin/cli.js +++ b/packages/web/bin/cli.js @@ -570,6 +570,7 @@ function parseArgs(argv = process.argv.slice(2)) { const args = Array.isArray(argv) ? [...argv] : []; const options = { port: DEFAULT_PORT, + host: undefined, uiPassword: process.env.OPENCHAMBER_UI_PASSWORD || undefined, json: false, all: false, @@ -658,6 +659,15 @@ function parseArgs(argv = process.argv.slice(2)) { options.explicitPort = true; break; } + case 'host': { + const { value, nextIndex } = consumeValue(i, inlineValue); + i = nextIndex; + if (typeof value !== 'string' || value.trim().length === 0) { + throw new TunnelCliError('Missing value for --host.', EXIT_CODE.USAGE_ERROR); + } + options.host = value.trim(); + break; + } case 'ui-password': { const { value, nextIndex } = consumeValue(i, inlineValue); i = nextIndex; @@ -842,11 +852,13 @@ COMMANDS: OPTIONS: -p, --port Web server port (default: ${DEFAULT_PORT}) + --host Bind address (default: 127.0.0.1) --ui-password Protect browser UI with single password -h, --help Show help -v, --version Show version ENVIRONMENT: + OPENCHAMBER_HOST Bind address (e.g. 0.0.0.0 for all interfaces) OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag OPENCHAMBER_DATA_DIR Override OpenChamber data directory OPENCODE_HOST External OpenCode server base URL, e.g. http://hostname:4096 @@ -2730,6 +2742,10 @@ const commands = { } } const serverArgs = [serverPath, '--port', String(targetPort)]; + const effectiveHost = typeof options.host === 'string' && options.host.length > 0 ? options.host : undefined; + if (effectiveHost) { + serverArgs.push('--host', effectiveHost); + } const serveSpin = showOutput ? createSpinner(options) : null; @@ -2741,6 +2757,7 @@ const commands = { ...process.env, OPENCHAMBER_PORT: String(targetPort), OPENCODE_BINARY: opencodeBinary, + ...(effectiveHost ? { OPENCHAMBER_HOST: effectiveHost } : {}), ...(effectiveUiPassword ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}), ...(process.env.OPENCODE_SKIP_START ? { OPENCHAMBER_SKIP_OPENCODE_START: process.env.OPENCODE_SKIP_START } : {}), }, diff --git a/packages/web/server/index.d.ts b/packages/web/server/index.d.ts index e3c1e6e6..e9141db8 100644 --- a/packages/web/server/index.d.ts +++ b/packages/web/server/index.d.ts @@ -13,6 +13,7 @@ export interface WebUiServerController { export interface StartWebUiServerOptions { port?: number; + host?: string; attachSignals?: boolean; exitOnShutdown?: boolean; uiPassword?: string | null; @@ -27,6 +28,7 @@ export declare function setupProxy(app: Express): void; export declare function restartOpenCode(): Promise; export declare function parseArgs(argv?: string[]): { port: number; + host?: string; uiPassword: string | null; tryCfTunnel: boolean; tunnelProvider?: string; diff --git a/packages/web/server/index.js b/packages/web/server/index.js index c891e0f8..47830dc0 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -5788,6 +5788,7 @@ function parseArgs(argv = process.argv.slice(2)) { const options = { port: DEFAULT_PORT, + host: undefined, uiPassword: envPassword, tryCfTunnel: envCfTunnel, tunnelProvider: envTunnelProvider, @@ -5826,6 +5827,13 @@ function parseArgs(argv = process.argv.slice(2)) { continue; } + if (optionName === 'host') { + const { value, nextIndex } = consumeValue(i, inlineValue); + i = nextIndex; + options.host = typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; + continue; + } + if (optionName === 'ui-password') { const { value, nextIndex } = consumeValue(i, inlineValue); i = nextIndex; @@ -7095,6 +7103,7 @@ async function gracefulShutdown(options = {}) { async function main(options = {}) { const port = Number.isFinite(options.port) && options.port >= 0 ? Math.trunc(options.port) : DEFAULT_PORT; + const host = typeof options.host === 'string' && options.host.length > 0 ? options.host : undefined; const tryCfTunnel = options.tryCfTunnel === true; const shouldUseCanonicalTunnelConfig = typeof options.tunnelMode === 'string' || typeof options.tunnelProvider === 'string' @@ -14235,9 +14244,10 @@ async function main(options = {}) { let activePort = port; - const bindHost = typeof process.env.OPENCHAMBER_HOST === 'string' && process.env.OPENCHAMBER_HOST.trim().length > 0 - ? process.env.OPENCHAMBER_HOST.trim() - : null; + const bindHost = host + || (typeof process.env.OPENCHAMBER_HOST === 'string' && process.env.OPENCHAMBER_HOST.trim().length > 0 + ? process.env.OPENCHAMBER_HOST.trim() + : '127.0.0.1'); await new Promise((resolve, reject) => { const onError = (error) => { @@ -14256,9 +14266,12 @@ async function main(options = {}) { // ignore } - console.log(`OpenChamber server running on port ${activePort}`); - console.log(`Health check: http://localhost:${activePort}/health`); - console.log(`Web interface: http://localhost:${activePort}`); + const displayHost = (bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]') + ? 'localhost' + : (bindHost.includes(':') ? `[${bindHost}]` : bindHost); + console.log(`OpenChamber server listening on ${bindHost}:${activePort}`); + console.log(`Health check: http://${displayHost}:${activePort}/health`); + console.log(`Web interface: http://${displayHost}:${activePort}`); if (startupTunnelRequest) { const startupModeLabel = startupTunnelRequest.mode === TUNNEL_MODE_QUICK @@ -14308,11 +14321,7 @@ async function main(options = {}) { resolve(); }; - if (bindHost) { - server.listen(port, bindHost, onListening); - } else { - server.listen(port, onListening); - } + server.listen(port, bindHost, onListening); }); if (attachSignals && !signalsAttached) { @@ -14355,6 +14364,7 @@ if (isCliExecution) { exitOnShutdown = true; main({ port: cliOptions.port, + host: cliOptions.host, tryCfTunnel: cliOptions.tryCfTunnel, tunnelProvider: cliOptions.tunnelProvider, tunnelMode: cliOptions.tunnelMode, diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 19d54a38..8f5dcab0 100644 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -55,6 +55,10 @@ if [ "${OH_MY_OPENCODE:-false}" = "true" ]; then fi fi +# Docker containers need to listen on all interfaces for port mapping to work. +OPENCHAMBER_HOST="${OPENCHAMBER_HOST:-0.0.0.0}" +export OPENCHAMBER_HOST + echo "[entrypoint] starting..." if [ "$#" -gt 0 ]; then @@ -67,4 +71,4 @@ if [ -n "${UI_PASSWORD:-}" ]; then fi "$@" -bun packages/web/bin/cli.js logs +exec bun packages/web/bin/cli.js logs