Skip to content

Commit 6442056

Browse files
chapterjasonclaude
andcommitted
Add web-shell devcontainer feature
Installs web-shell (persistent browser terminal backed by tmux) from the SoureCode/web-shell GitHub release tarball via npm, registers a systemd unit for supervision under Coder + sysbox with a /etc/profile.d fallback for non-systemd bases, and publishes a Coder workspace app so the terminal button appears automatically on the workspace page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bb9e3bb commit 6442056

5 files changed

Lines changed: 310 additions & 0 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ that hosts the devcontainers these features get installed into — see
1717
| `claude-code` | `ghcr.io/sourecode/devcontainer-features/claude-code:2` | Installs the Claude Code CLI via the official native installer into `/usr/local/bin`. Declares `~/.claude` and `~/.claude.json` as persistence targets via the `home-persist` manifest. Requires Node.js — automatically pulls in the `nvm` feature via `dependsOn`. |
1818
| `rtk` | `ghcr.io/sourecode/devcontainer-features/rtk:2` | Installs [rtk](https://github.com/rtk-ai/rtk), an LLM token-reducing CLI proxy, into `/usr/local/bin`. Auto-patches Claude Code via `postCreateCommand` so the hook is written against the live `~/.claude`, not the image. |
1919
| `context-mode` | `ghcr.io/sourecode/devcontainer-features/context-mode:2` | Installs the [`context-mode`](https://github.com/mksglu/context-mode) Claude Code plugin via `postCreateCommand`, so the plugin lands in `~/.claude/plugins` (which `home-persist` symlinks into the persistence volume when installed). |
20+
| `web-shell` | `ghcr.io/sourecode/devcontainer-features/web-shell:1` | Installs [web-shell](https://github.com/SoureCode/web-shell) (persistent browser terminal backed by `tmux`) from the GitHub release tarball and registers it as a systemd unit + Coder workspace app. Requires Node.js — automatically pulls in the `nvm` feature via `dependsOn`. |
2021
| `home-persist` | `ghcr.io/sourecode/devcontainer-features/home-persist:1` | Symlinks declared `$HOME` paths into a per-owner persistence volume at `/mnt/home-persist`. Features and users contribute paths via JSON manifests in `/etc/devcontainer-persist.d/`; an `onCreateCommand` resolver materializes the symlinks on every create. |
2122
| `nvm` | `ghcr.io/sourecode/devcontainer-features/nvm:2` | Installs [nvm](https://github.com/nvm-sh/nvm) system-wide at `/usr/local/share/nvm` and optionally a Node version (defaults to LTS), with `node`/`npm`/`npx` symlinked into `/usr/local/bin`. No yarn. |
2223

@@ -73,6 +74,20 @@ claude-code feature as well. `installsAfter` handles ordering for either
7374
| `version` | string | `0.40.4` | nvm release tag to install (without the leading `v`). |
7475
| `node` | string | `lts` | Node version to install via nvm. `lts` uses `nvm install --lts`. `none` skips node install. Anything else is passed as-is to `nvm install`. |
7576

77+
#### `web-shell`
78+
79+
| Option | Type | Default | Purpose |
80+
|---|---|---|---|
81+
| `version` | string | `latest` | web-shell release to install. `latest` resolves the newest tag via the GitHub API; otherwise `X.Y.Z` or `vX.Y.Z`. |
82+
| `port` | string | `4000` | TCP port web-shell binds on. Baked into the systemd unit as `$PORT`. |
83+
| `host` | string | `127.0.0.1` | Bind address. Baked into the systemd unit as `$HOST`. Use `0.0.0.0` to listen on all interfaces. |
84+
| `authToken` | string | `""` | Bearer token for incoming connections. Baked into the systemd unit as `$AUTH_TOKEN`. Empty disables auth. |
85+
86+
Declares `dependsOn` for `ghcr.io/sourecode/devcontainer-features/nvm:2`, so
87+
adding `web-shell` automatically pulls in `nvm` (and Node.js). The Coder app
88+
registration (`customizations.coder.apps`) makes a **web-shell** button appear
89+
on the workspace page when running under Coder + sysbox.
90+
7691
#### `home-persist`
7792

7893
| Option | Type | Default | Purpose |
@@ -291,6 +306,10 @@ src/
291306
rtk/
292307
devcontainer-feature.json
293308
install.sh
309+
web-shell/
310+
devcontainer-feature.json
311+
install.sh
312+
README.md
294313
docs/
295314
migration-guide.md
296315
persistence.md

src/web-shell/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# web-shell devcontainer feature
2+
3+
Installs [web-shell](https://github.com/SoureCode/web-shell) — a persistent
4+
browser terminal backed by `tmux` — inside a devcontainer and registers it
5+
as a Coder workspace app.
6+
7+
`web-shell` is a Node.js + xterm.js service. Every session is a `tmux` session
8+
on the server, so shells survive Node restarts. This feature:
9+
10+
- Installs OS deps (`tmux`, `build-essential`, `python3`, `curl`, `jq`).
11+
- Downloads the release tarball from `SoureCode/web-shell` and `npm install -g`s
12+
it against the Node toolchain provided by the
13+
[`nvm`](../nvm) feature (`dependsOn`).
14+
- Symlinks the binary into `/usr/local/bin/web-shell`.
15+
- Writes and enables a systemd unit at `/etc/systemd/system/web-shell.service`.
16+
Starting is left to container boot — systemd isn't running during image
17+
build.
18+
- Falls back to a `/etc/profile.d/web-shell.sh` login-shell supervisor when
19+
PID 1 is not `systemd`.
20+
21+
## OCI reference
22+
23+
```
24+
ghcr.io/sourecode/devcontainer-features/web-shell:1
25+
```
26+
27+
## Options
28+
29+
| Option | Type | Default | Purpose |
30+
|---|---|---|---|
31+
| `version` | string | `latest` | Release to install. `latest` resolves via the GitHub API; otherwise `X.Y.Z` or `vX.Y.Z`. |
32+
| `port` | string | `4000` | Port `web-shell` binds on. Baked into the systemd unit as `$PORT`. |
33+
| `host` | string | `127.0.0.1` | Bind address. Baked into the systemd unit as `$HOST`. Use `0.0.0.0` to listen on all interfaces. |
34+
| `authToken` | string | `""` | Bearer token required on incoming connections. Baked into the systemd unit as `$AUTH_TOKEN`. Empty disables auth. |
35+
36+
## Usage
37+
38+
```jsonc
39+
{
40+
"image": "debian:trixie-slim",
41+
"features": {
42+
"ghcr.io/sourecode/devcontainer-features/nvm:2": {},
43+
"ghcr.io/sourecode/devcontainer-features/web-shell:1": {}
44+
}
45+
}
46+
```
47+
48+
`nvm` is pulled in automatically via `dependsOn`, so listing it is optional.
49+
To expose the service to the workspace's public IP instead of loopback:
50+
51+
```jsonc
52+
"ghcr.io/sourecode/devcontainer-features/web-shell:1": {
53+
"host": "0.0.0.0",
54+
"port": "4000",
55+
"authToken": "replace-me"
56+
}
57+
```
58+
59+
## Coder workspace app
60+
61+
The feature ships a `customizations.coder.apps` entry so that workspaces
62+
running under Coder's Dev Containers integration (sub-agent) automatically
63+
render a **web-shell** button on the workspace page. The button opens the
64+
terminal in the browser — no manual steps.
65+
66+
The app URL uses `${localEnv:PORT:4000}`, so overriding `PORT` in
67+
`devcontainer.json`'s `containerEnv` (or passing it via the feature option and
68+
also exposing it as env) is picked up by the Coder button. Set `PORT` in both
69+
places when customising.
70+
71+
## Supervision
72+
73+
| PID 1 | What runs |
74+
|---|---|
75+
| `systemd` (Coder + sysbox, systemd-on-boot bases) | `web-shell.service` — started on container boot, restarted on failure. |
76+
| Anything else (plain Docker, host-agent runners) | `/etc/profile.d/web-shell.sh` — spawns a background supervisor on first login per user, guarded by `pgrep` so relogins don't duplicate. Logs go to `/tmp/web-shell.log`. |
77+
78+
The systemd unit is always written, even when the fallback is active, so a
79+
later rebase onto a systemd base just works without reinstalling.
80+
81+
## Checks
82+
83+
After the container is up:
84+
85+
```bash
86+
which web-shell # /usr/local/bin/web-shell
87+
systemctl is-enabled web-shell.service # enabled (on systemd hosts)
88+
curl -fsS http://127.0.0.1:4000/api/sessions # health endpoint
89+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"id": "web-shell",
3+
"version": "1.0.0",
4+
"name": "web-shell",
5+
"description": "Installs web-shell (persistent browser terminal backed by tmux) from the GitHub release tarball into the global Node install, and registers a systemd unit so the service starts automatically on container boot. Also publishes the service as a Coder workspace app.",
6+
"documentationURL": "https://github.com/SoureCode/devcontainer-features/tree/master/src/web-shell",
7+
"options": {
8+
"version": {
9+
"type": "string",
10+
"default": "latest",
11+
"description": "web-shell release to install. `latest` resolves the newest tag via the GitHub API; otherwise pass a version like `1.2.3` or `v1.2.3`."
12+
},
13+
"port": {
14+
"type": "string",
15+
"default": "4000",
16+
"description": "TCP port web-shell binds on (baked into the systemd unit as $PORT)."
17+
},
18+
"host": {
19+
"type": "string",
20+
"default": "127.0.0.1",
21+
"description": "Bind address (baked into the systemd unit as $HOST). Set to `0.0.0.0` to listen on all interfaces."
22+
},
23+
"authToken": {
24+
"type": "string",
25+
"default": "",
26+
"description": "Bearer token required on incoming connections (baked into the systemd unit as $AUTH_TOKEN). Empty disables auth."
27+
}
28+
},
29+
"dependsOn": {
30+
"ghcr.io/sourecode/devcontainer-features/nvm:2": {}
31+
},
32+
"customizations": {
33+
"coder": {
34+
"apps": [
35+
{
36+
"slug": "web-shell",
37+
"displayName": "web-shell",
38+
"url": "http://localhost:${localEnv:PORT:4000}",
39+
"icon": "/icon/terminal.svg",
40+
"share": "owner",
41+
"subdomain": true,
42+
"healthcheck": {
43+
"url": "http://localhost:${localEnv:PORT:4000}/api/sessions",
44+
"interval": 5,
45+
"threshold": 6
46+
},
47+
"order": 2
48+
}
49+
]
50+
}
51+
}
52+
}

src/web-shell/install.sh

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env bash
2+
# web-shell feature installer.
3+
# https://github.com/SoureCode/web-shell
4+
#
5+
# Installs the release tarball globally via npm against the Node toolchain
6+
# provided by the `nvm` feature (required via `dependsOn`). The binary ends up
7+
# inside the nvm prefix, so we also symlink it into /usr/local/bin for a stable
8+
# path that systemd, sudo, and non-login shells can resolve without sourcing
9+
# nvm.
10+
#
11+
# Supervision: a real systemd unit if PID 1 is systemd (sysbox workspaces), a
12+
# /etc/profile.d login-shell fallback otherwise. Starting is never done here —
13+
# systemd isn't up during image build, and fallback starts happen on user
14+
# login.
15+
set -e
16+
17+
WS_VERSION_OPT="${VERSION:-latest}"
18+
WS_PORT="${PORT:-4000}"
19+
WS_HOST="${HOST:-127.0.0.1}"
20+
WS_AUTH_TOKEN="${AUTHTOKEN:-}"
21+
22+
# 1. OS deps: tmux for the terminal multiplexer, build-essential + python3
23+
# because node-pty compiles native bindings, plus curl/jq for release lookup.
24+
APT_PKGS=""
25+
for pkg in tmux build-essential python3 ca-certificates curl jq; do
26+
if ! dpkg -s "$pkg" >/dev/null 2>&1; then
27+
APT_PKGS="$APT_PKGS $pkg"
28+
fi
29+
done
30+
if [ -n "$APT_PKGS" ]; then
31+
apt-get update
32+
# shellcheck disable=SC2086
33+
apt-get install -y --no-install-recommends $APT_PKGS
34+
rm -rf /var/lib/apt/lists/*
35+
fi
36+
37+
# 2. Activate nvm so `npm` and `npm config get prefix` resolve against the
38+
# default Node alias the nvm feature set up.
39+
export NVM_DIR="${NVM_DIR:-/usr/local/share/nvm}"
40+
if [ -s "$NVM_DIR/nvm.sh" ]; then
41+
# shellcheck disable=SC1091
42+
. "$NVM_DIR/nvm.sh"
43+
nvm use default >/dev/null
44+
fi
45+
46+
if ! command -v npm >/dev/null 2>&1; then
47+
echo "web-shell feature: npm not on PATH. Add ghcr.io/sourecode/devcontainer-features/nvm:2 to your features." >&2
48+
exit 1
49+
fi
50+
51+
# 3. Resolve target version. `latest` → newest tag via GitHub API. Otherwise
52+
# normalize `vX.Y.Z` / `X.Y.Z` → `X.Y.Z`.
53+
if [ "$WS_VERSION_OPT" = "latest" ]; then
54+
WS_VERSION="$(curl -fsSL https://api.github.com/repos/SoureCode/web-shell/releases/latest | jq -r .tag_name)"
55+
else
56+
WS_VERSION="$WS_VERSION_OPT"
57+
fi
58+
WS_VERSION="${WS_VERSION#v}"
59+
if [ -z "$WS_VERSION" ] || [ "$WS_VERSION" = "null" ]; then
60+
echo "web-shell feature: failed to resolve release version (got '$WS_VERSION_OPT')." >&2
61+
exit 1
62+
fi
63+
64+
# 4. Download + global install.
65+
TMPDIR="$(mktemp -d)"
66+
trap 'rm -rf "$TMPDIR"' EXIT
67+
TARBALL_URL="https://github.com/SoureCode/web-shell/releases/download/v${WS_VERSION}/web-shell-${WS_VERSION}.tgz"
68+
curl -fsSL -o "$TMPDIR/web-shell.tgz" "$TARBALL_URL"
69+
70+
npm install -g "$TMPDIR/web-shell.tgz"
71+
72+
# 5. Stable symlink at /usr/local/bin/web-shell — the nvm prefix isn't on the
73+
# systemd service PATH.
74+
NPM_PREFIX="$(npm config get prefix)"
75+
WS_BIN="$NPM_PREFIX/bin/web-shell"
76+
if [ ! -x "$WS_BIN" ]; then
77+
echo "web-shell feature: $WS_BIN missing after npm install." >&2
78+
exit 1
79+
fi
80+
if [ "$WS_BIN" != "/usr/local/bin/web-shell" ]; then
81+
ln -sf "$WS_BIN" /usr/local/bin/web-shell
82+
fi
83+
84+
# 6. Systemd unit. We always write it — even when systemd isn't PID 1 right
85+
# now, a later rebase onto a systemd base won't need to reinstall.
86+
install -d -m 0755 /etc/systemd/system
87+
cat >/etc/systemd/system/web-shell.service <<EOF
88+
[Unit]
89+
Description=web-shell
90+
After=network.target
91+
92+
[Service]
93+
Type=simple
94+
Environment=HOST=${WS_HOST}
95+
Environment=PORT=${WS_PORT}
96+
Environment=AUTH_TOKEN=${WS_AUTH_TOKEN}
97+
ExecStart=/usr/local/bin/web-shell
98+
Restart=on-failure
99+
RestartSec=1
100+
101+
[Install]
102+
WantedBy=multi-user.target
103+
EOF
104+
chmod 0644 /etc/systemd/system/web-shell.service
105+
106+
INIT_COMM="$(ps -p 1 -o comm= 2>/dev/null | tr -d '[:space:]' || true)"
107+
if [ "$INIT_COMM" = "systemd" ]; then
108+
systemctl daemon-reload
109+
systemctl enable web-shell.service
110+
else
111+
if command -v systemctl >/dev/null 2>&1; then
112+
systemctl enable web-shell.service >/dev/null 2>&1 || true
113+
fi
114+
115+
# Login-shell fallback for non-systemd bases. pgrep guard avoids spawning
116+
# duplicate supervisors on repeat logins. The inner while-loop restarts
117+
# web-shell if it exits, mirroring `Restart=on-failure`.
118+
cat >/etc/profile.d/web-shell.sh <<EOF
119+
# web-shell feature fallback: systemd wasn't PID 1 at feature install time,
120+
# so a login-shell supervisor is used instead.
121+
if ! pgrep -u "\$(id -u)" -f '/usr/local/bin/web-shell' >/dev/null 2>&1; then
122+
HOST='${WS_HOST}' PORT='${WS_PORT}' AUTH_TOKEN='${WS_AUTH_TOKEN}' \\
123+
nohup sh -c 'while true; do /usr/local/bin/web-shell; sleep 1; done' \\
124+
> /tmp/web-shell.log 2>&1 &
125+
disown >/dev/null 2>&1 || true
126+
fi
127+
EOF
128+
chmod 0644 /etc/profile.d/web-shell.sh
129+
130+
echo "web-shell feature: PID 1 is '${INIT_COMM:-unknown}' (not systemd). Installed /etc/profile.d/web-shell.sh as a login-shell fallback; use a systemd-enabled base (e.g. Coder + sysbox) for proper supervision." >&2
131+
fi

test/web-shell/test.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
# Feature tests for web-shell. Run via:
3+
# devcontainer features test --features web-shell --base-image debian:trixie-slim .
4+
set -e
5+
6+
# shellcheck disable=SC1091
7+
source dev-container-features-test-lib
8+
9+
check "tmux installed" which tmux
10+
check "web-shell binary in /usr/local/bin" test -x /usr/local/bin/web-shell
11+
check "systemd unit present" test -f /etc/systemd/system/web-shell.service
12+
check "unit contains PORT env" grep -q '^Environment=PORT=' /etc/systemd/system/web-shell.service
13+
check "unit contains HOST env" grep -q '^Environment=HOST=' /etc/systemd/system/web-shell.service
14+
check "unit contains ExecStart" grep -q '^ExecStart=/usr/local/bin/web-shell' /etc/systemd/system/web-shell.service
15+
# On systemd hosts the unit is enabled; on non-systemd bases a profile.d
16+
# fallback is installed instead. Accept either.
17+
check "supervision wired" bash -c "systemctl is-enabled web-shell.service 2>/dev/null || test -f /etc/profile.d/web-shell.sh"
18+
19+
reportResults

0 commit comments

Comments
 (0)