Self-hosted browser UI for OpenAI Codex (via MCP), with TOTP auth, resumable chat sessions, interactive browser terminal, and one-command deployment. Search tags: codex, remote, web chat, terminal, totp, nginx, systemd, nodejs.
This project is a small Node.js + React frontend/backend app that exposes Codex as a web service. It is designed for remote access from another machine (internet or LAN) and includes path-based proxy support under /codex.
- Web chat UI for Codex sessions
- Streaming assistant responses with auto-reconnect
- TOTP login with first-login QR setup
- Persistent sessions/chats via
DATA_DIR(default:data) /statuscommand support with usage/rate-limit status/web helpin chat for web-only commands- Interactive terminal (WebSocket + PTY) with session-scoped terminal tabs
- Terminal management APIs (
POST /api/terminal,GET /api/terminals,GET /ws/terminal) - Remote deployment script with Nginx + systemd bootstrap
- Interactive prompt mode for deployment domain
- Existing
/codexroute replacement on target machine
server/API: Node.js/Express service, wraps localcodex mcp-serverweb/Frontend: Vite + React, deployed fromweb/dist- Persistence:
server/.envconfig +DATA_DIRJSON files - Optional reverse proxy path: default
NGINX_PATH=/codex
- Install dependencies
npm install- Configure environment
cd server
cp .env.example .env
# edit server/.env
# IMPORTANT: codex runtime needs OPENAI credentials in env, typically OPENAI_API_KEY- Start
cd ..
npm run start- Open service
- Local:
http://127.0.0.1:18888 - LAN/WAN: set
HOST=0.0.0.0inserver/.env, then openhttp://<server-ip>:18888
If you can’t access from another machine, open inbound TCP 18888 in firewall/security group.
- Configure:
cd server
node -e "const { authenticator } = require('otplib'); console.log(authenticator.generateSecret());"- Set env:
TOTP_SECRET=<your secret>PRINT_TOTP_QR=true(optional: print QR in server logs)
- Restart and open login page:
- On first login, the web UI fetches and shows TOTP QR automatically
- After first successful TOTP login, QR is no longer retrievable
/web help/web status/resume/statusis handled server-side and shows live status for running sessions
- Create credential tokens via API (
POST /api/auth/credential) after normal login. - Use credential token to create a fresh session (
POST /api/auth/credential/login). - List and revoke credentials via
GET /api/auth/credentialsandPOST /api/auth/credential/revoke. - Store credentials securely; the token is shown once when created and cannot be read again later.
- Create terminal:
POST /api/terminal - List terminal sessions:
GET /api/terminals - Connect terminal websocket:
- direct:
GET /ws/terminal?terminalId=<id> - under
/codex:GET /codex/ws/terminal?terminalId=<id>
- direct:
- Terminal and chat are peer sessions in the sidebar; switching tabs swaps the main content area
- If deployed under
/codex, compatibility APIs are also available:POST /codex/api/terminalGET /codex/api/terminals
When reverse-proxying /codex, websocket upgrade headers are required in the /codex/ location:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;Use on a fresh host:
git clone git@github.com:JasonShui716/remote-codexapp.git /opt/remote-codexapp
cd /opt/remote-codexapp
chmod +x scripts/deploy-remote.sh
# Option A: interactive domain input
APP_DIR=/opt/remote-codexapp \
NGINX_PATH=/codex \
APP_PORT=18888 \
bash scripts/deploy-remote.sh
# Option B: explicit domain (non-interactive)
APP_DIR=/opt/remote-codexapp \
DOMAIN=your.domain.com \
NGINX_PATH=/codex \
APP_PORT=18888 \
bash scripts/deploy-remote.shFor first deploy, leave DOMAIN empty in interactive mode and input your host when prompted.
You can also use the wrapper script (it runs deploy-remote.sh with sane defaults):
# interactive domain prompt
bash scripts/deploy-one-click.sh
# or pass domain directly
bash scripts/deploy-one-click.sh your.domain.com
# no-domain deploy on custom port (direct access, no nginx)
bash scripts/deploy-one-click.sh --host 0.0.0.0 --port 18890 --skip-nginxCommon overrides:
APP_DIR=/opt/remote-codexapp \
APP_PORT=18888 \
NGINX_PATH=/codex \
GIT_BRANCH=master \
bash scripts/deploy-one-click.sh your.domain.comProxy overrides (to persist into server/.env for systemd runtime):
bash scripts/deploy-one-click.sh \
--https-proxy http://127.0.0.1:7890 \
--http-proxy http://127.0.0.1:7890 \
--socks5-proxy socks5://127.0.0.1:7891No-domain deploy with deploy-remote.sh (equivalent):
sudo SKIP_NGINX=1 APP_HOST=0.0.0.0 APP_PORT=18890 bash scripts/deploy-remote.shDirect proxy env with deploy-remote.sh:
sudo \
HTTPS_PROXY=http://127.0.0.1:7890 \
HTTP_PROXY=http://127.0.0.1:7890 \
SOCKS5_PROXY=socks5://127.0.0.1:7891 \
bash scripts/deploy-remote.sh --skip-nginx- Pulls or clones repo
npm installandnpm run build- Runs git/npm as
APP_USER(nosudo npmrequirement) - Writes/patches
server/.env(HOST/PORT/CODEX_CWD) - Installs/updates systemd unit
/etc/systemd/system/codex-remoteapp.service - Generates/reloads Nginx config for reverse proxy path
- Includes websocket upgrade forwarding for terminal (
/codex/ws/terminal) - Auto-replaces existing bindings on
/codex(backed up as.bak.<timestamp>)
Skip nginx config (code-only update):
sudo SKIP_NGINX=1 bash scripts/deploy-remote.shnpm run restartDefaults:
- log:
/tmp/codex_remoteapp.log - pid:
.codex_remoteapp.pid - port:
PORTfromserver/.env(fallback18888)
Optional override:
CODEREMOTEAPP_LOG=/path/to/log npm run restartGET /api/mePOST /api/chats(create chat)GET /api/chatsGET /api/chats/:idPOST /api/chats/:id/sendGET /api/chats/:id/streamPOST /api/chats/:id/abortPOST /api/terminalGET /api/terminalsPOST /api/auth/totp/verifyPOST /api/auth/credential/loginPOST /api/auth/credentialGET /api/auth/credentialsPOST /api/auth/credential/revoke
And compatibility Nginx path endpoints are also available:
POST /codex/api/terminalGET /codex/api/terminals
- Requires local
codexbinary available in PATH (codex --version) - Approval policy examples:
never,on-request, etc. (configured by env) - Chats and terminal sessions are session-scoped; terminal history is kept in memory by session map