WebSSH: Self-hosted browser SSH client, Xshell-style.
694
A self-hosted, browser-based SSH client — Xshell-style, in a single Go binary on a tiny Alpine image.
Manage SSH sessions, multi-tab / split-pane terminals, SFTP file transfer and port-forwarding tunnels, all from your browser.
Open source: https://github.com/casualty01/web_ssh
golang.org/x/crypto/ssh and bridges them over WebSocket to an xterm.js terminal-L, remote -R and dynamic SOCKS5 -D forwarding, with optional auto-start0 = off)webssh userlinux/amd64 · linux/arm64
Images are built and published automatically by GitHub Actions whenever a v* Git tag is pushed. Tag rules:
| Tag | Description | Example |
|---|---|---|
<major>.<minor>.<patch> | Full version | 1.1.3 |
<major>.<minor> | Rolling minor tag, always points to the latest patch of that series | 1.1 |
sha-<commit> | Short commit SHA, for exact rollbacks | sha-a718467 |
docker run -d --name webssh \
--restart unless-stopped \
-p 8080:8080 \
-e ADMIN_USER=admin \
-e ADMIN_PASSWORD='use-a-strong-password' \
-v webssh-data:/data \
casualty01/web_ssh:latest
Open http://localhost:8080 and sign in with ADMIN_USER / ADMIN_PASSWORD.
Create docker-compose.yml:
services:
webssh:
image: casualty01/web_ssh:latest
container_name: webssh
restart: unless-stopped
ports:
- "8080:8080"
environment:
- ADMIN_USER=admin
- ADMIN_PASSWORD=use-a-strong-password
volumes:
- webssh-data:/data
volumes:
webssh-data:
Start it:
docker compose up -d
To keep data in a specific host directory, just bind-mount it. No manual chown is needed — the entrypoint fixes the ownership of /data on every start:
-v /data/webssh-data:/data
Menu names vary between system versions; the steps below follow recent releases. Key points for every platform: container port
8080, mount a volume at/data, and change the default password.
Option 1: GUI
webssh-data under the shared folder docker for persistent datacasualty01/web_ssh and download the latest tagcasualty01/web_ssh and click Run8080 (change it if it is taken), container port 8080docker/webssh-data and set the mount path to /dataADMIN_USER and ADMIN_PASSWORD (use your own values)http://NAS-IP:8080Option 2: Project (Compose, recommended)
webssh and pick the path docker/webssh (created automatically)volumes entry to: volumes:
- /volume1/docker/webssh-data:/data
/volume1/docker/webssh-data must match the folder you created in File Station; adjust it if your storage pool is not volume1Older DSM (7.1 and below): install the Docker package from Package Center, download the image from "Registry", then launch it from "Image". The settings are the same as above.
Tips
sudo docker pull casualty01/web_ssh:latest, or configure a registry mirror / proxy for Docker22 in the sessionOption 1: Compose project (recommended)
docker/webssh, with a data subfolder for persistencewebssh, choose the docker/webssh folder, and paste the Compose file from Quick Start, changing the volumes entry to: volumes:
- ./data:/data
http://NAS-IP:8080The relative path
./datastores data under the project folder, so you don't need to know the absolute path of the storage volume.
Option 2: Create a container in the GUI
casualty01/web_ssh:latest8080 → container port 8080docker/webssh/data) at /data in the containerADMIN_USER and ADMIN_PASSWORDTips
22 in the session"18080:8080"/data: otherwise sessions, tunnels and a changed admin password are lost when the container is recreated or updatedadmin / admin to the internet: for remote access use an HTTPS reverse proxy plus a VPN or IP allow-list/data folder is kept, no data is lostlocalhost (which points at the container itself)| Variable | Default | Description |
|---|---|---|
ADMIN_USER | admin | Bootstrap login username (only used until the password is changed via the UI/API) |
ADMIN_PASSWORD | admin | Bootstrap login password — change it in production |
LISTEN_ADDR | :8080 | HTTP listen address |
DATA_DIR | /data | Data directory; configuration is stored as store.json inside it |
WEB_DIR | /app/web | Static web client directory (preset in the image, normally no change needed) |
After logging in you can change the credentials from the 🔑 button at the bottom of the sidebar. The new salted-hash credentials are written to
store.jsonand take precedence over the environment variables; changing them signs out every active login, including your own. Logins are valid for 24 hours.
| Item | Value | Description |
|---|---|---|
| Port | 8080 | Web UI and WebSocket share it |
| Volume | /data | Holds sessions, groups, tunnels and admin credentials. Always mount it, or data is lost when the container is removed |
Logs and running version
docker logs -f webssh
curl http://localhost:8080/api/version
/api/version needs no login and returns something like {"version":"1.1.3-20261009-120000"}, in the format <release number>-<UTC build time>. The version is also shown on the login screen and in the sidebar footer, so you can confirm that what is running is the image you just deployed.
Upgrade
# docker run
docker pull casualty01/web_ssh:latest
docker rm -f webssh
# then re-run the docker run command above (the data volume is kept)
# Compose: after changing the image tag if needed
docker compose pull
docker compose up -d
Backup: simply back up the /data volume / directory (e.g. tar czf webssh-data.tgz -C /path/to/data .). It contains plaintext SSH credentials, so keep it safe.
Credentials are POSTed to the login endpoint, so always put TLS in front (Caddy / Nginx / Traefik). WebSocket upgrades for /ws/ must also be forwarded.
Nginx example (request buffering is disabled for large SFTP uploads; otherwise the progress bar jumps straight to 100%):
server {
listen 443 ssl;
server_name webssh.example.com;
# ssl_certificate ...;
# ssl_certificate_key ...;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_request_buffering off;
client_max_body_size 0;
proxy_read_timeout 3600s;
}
}
Caddy: reverse_proxy 127.0.0.1:8080 is enough (HTTPS and WebSocket are handled automatically).
Q: How do I reach an SSH server on the host or my LAN from inside the container?
Sessions use addresses as seen from the container. For sshd on the Docker host, on Linux add --add-host=host.docker.internal:host-gateway and use host.docker.internal as the session host. For LAN machines, just use their LAN IP, as long as the container network can route to it.
Q: I forgot the admin password I changed. What now?
Stop the container and back up store.json. Then edit the top-level admin field (the stored username and password hash) — delete or empty it — and restart. With no password hash in admin, the app falls back to the ADMIN_USER / ADMIN_PASSWORD environment variables. Sessions, groups and tunnels in store.json are not affected.
Q: Is my terminal content still there after a page refresh? Yes. The remote shell keeps running for 30 minutes after the WebSocket drops; reconnecting re-attaches to the same shell and replays the output. If you don't reconnect within 30 minutes, the shell is reclaimed.
Q: Where does a local (-L) tunnel listen?
Inside the container. To reach it from the host or outside, publish the corresponding port with an extra -p in docker run / Compose.
⚠️ Read this before exposing WebSSH to the internet.
InsecureIgnoreHostKey is used), which leaves you open to man-in-the-middle attacks on untrusted networksstore.json; restrict access to the volume / directoryadmin / admin immediately, put the service behind TLS, and ideally add a VPN or IP allow-list自托管的浏览器 SSH 客户端 —— 类 Xshell 体验,单个 Go 二进制,Alpine 轻量镜像。
通过浏览器即可管理 SSH 会话、多标签/分屏终端、SFTP 文件传输和端口转发隧道。
开源地址:https://github.com/casualty01/web_ssh
golang.org/x/crypto/ssh 建立连接,经 WebSocket 桥接到 xterm.js 终端-L、远程 -R、动态 SOCKS5 -D,可自动启动0 为关闭)webssh 用户运行linux/amd64 · linux/arm64
镜像在推送 v* 格式的 Git tag 时由 GitHub Actions 自动构建并发布,标签规则如下:
| 标签 | 说明 | 示例 |
|---|---|---|
<major>.<minor>.<patch> | 完整版本号 | 1.1.3 |
<major>.<minor> | 小版本滚动标签,自动指向该系列最新补丁版 | 1.1 |
sha-<commit> | 对应提交的短 SHA,用于精确回溯 | sha-a718467 |
docker run -d --name webssh \
--restart unless-stopped \
-p 8080:8080 \
-e ADMIN_USER=admin \
-e ADMIN_PASSWORD='请改成强密码' \
-v webssh-data:/data \
casualty01/web_ssh:latest
启动后访问 http://localhost:8080,使用 ADMIN_USER / ADMIN_PASSWORD 登录。
新建 docker-compose.yml:
services:
webssh:
image: casualty01/web_ssh:latest
container_name: webssh
restart: unless-stopped
ports:
- "8080:8080"
environment:
- ADMIN_USER=admin
- ADMIN_PASSWORD=admin
volumes:
- webssh-data:/data
volumes:
webssh-data:
启动:
docker compose up -d
如果希望把数据放在宿主机指定目录,直接挂载即可,无需手动 chown(入口脚本每次启动会自动修正 /data 的属主):
-v /data/webssh-data:/data
界面名称因系统版本而异,以下以较新版本为准。通用要点:容器端口
8080,数据卷挂载到/data,密码务必改掉。
方式一:图形界面
docker 下新建目录 webssh-data,用于持久化数据casualty01/web_ssh,下载 latest 标签casualty01/web_ssh,点击 运行8080(若被占用可改为其他端口),容器端口 8080docker/webssh-data,装载路径填 /dataADMIN_USER 和 ADMIN_PASSWORD(改成你自己的值)http://NAS-IP:8080方式二:项目(Compose,推荐)
webssh,路径选择 docker/webssh(会自动创建)volumes 改为: volumes:
- /volume1/docker/webssh-data:/data
/volume1/docker/webssh-data 需要与 File Station 里创建的目录一致,存储池不是 volume1 时请对应修改旧版 DSM(7.1 及以下):套件中心安装 Docker,在"注册表"下载镜像后,在"映像"中启动,设置项与上面相同。
使用提示
sudo docker pull casualty01/web_ssh:latest,或为 Docker 配置可用的镜像加速/代理22方式一:Compose 项目(推荐)
docker/webssh,并在其中再建 data 子目录用于持久化webssh,路径选择上面建好的 docker/webssh 目录,粘贴「快速开始」中的 Compose 内容,并把 volumes 改为: volumes:
- ./data:/data
http://NAS-IP:8080这里使用相对路径
./data,数据会保存在项目目录下的data中,无需关心存储卷的绝对路径。
方式二:图形界面创建容器
casualty01/web_ssh:latest8080 → 容器端口 8080docker/webssh/data)到容器路径 /dataADMIN_USER、ADMIN_PASSWORD使用提示
22"18080:8080"/data:否则重建/更新容器后,会话、隧道和修改过的管理员密码都会丢失admin / admin 暴露到公网:需要远程访问时,使用 HTTPS 反向代理,并配合 VPN 或 IP 白名单/data 目录保持不变,数据不会丢localhost(那指向容器自身)| 变量 | 默认值 | 说明 |
|---|---|---|
ADMIN_USER | admin | 初始登录用户名(仅在通过界面/API 修改过密码之前生效) |
ADMIN_PASSWORD | admin | 初始登录密码,生产环境务必修改 |
LISTEN_ADDR | :8080 | HTTP 监听地址 |
DATA_DIR | /data | 数据目录,配置保存为其中的 store.json |
WEB_DIR | /app/web | 静态前端目录(镜像内已设置,一般无需改动) |
登录后可在侧边栏底部点击 🔑 修改账号密码。修改后新的加盐哈希凭据会写入
store.json,优先于环境变量,并会使所有已登录会话失效(包括当前会话)。登录有效期为 24 小时。
| 项目 | 值 | 说明 |
|---|---|---|
| 端口 | 8080 | Web 界面和 WebSocket 共用 |
| 数据卷 | /data | 保存会话、分组、隧道配置和管理员凭据,务必挂载,否则删除容器后数据丢失 |
查看日志与运行版本
docker logs -f webssh
curl http://localhost:8080/api/version
/api/version 无需登录,返回形如 {"version":"1.1.3-20261009-120000"},格式为 <发布版本号>-<UTC 构建时间>。版本号同时显示在登录页和侧边栏底部,可用来确认当前运行的确实是刚部署的镜像。
升级
# docker run 方式
docker pull casualty01/web_ssh:latest
docker rm -f webssh
# 然后用新的 tag 重新执行上面的 docker run 命令(数据卷保持不变)
# Compose 方式:修改 image 的 tag 后
docker compose pull
docker compose up -d
备份:直接备份 /data 数据卷/目录即可(例如 tar czf webssh-data.tgz -C /path/to/data .)。其中含明文保存的 SSH 凭据,请妥善保管。
登录凭据通过 POST 提交,请务必在前面加一层 TLS(Caddy / Nginx / Traefik)。同时需要转发 /ws/ 的 WebSocket 升级请求。
Nginx 示例(已针对 SFTP 大文件上传关闭请求缓冲,否则进度条会瞬间跳到 100%):
server {
listen 443 ssl;
server_name webssh.example.com;
# ssl_certificate ...;
# ssl_certificate_key ...;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_request_buffering off;
client_max_body_size 0;
proxy_read_timeout 3600s;
}
}
Caddy:一行 reverse_proxy 127.0.0.1:8080 即可(自动处理 HTTPS 和 WebSocket)。
Q:容器内如何访问宿主机或内网的 SSH 服务器?
会话里填写的是「容器所能访问到的」地址。访问宿主机上的 sshd 时,Linux 可以加 --add-host=host.docker.internal:host-gateway,然后在会话中填 host.docker.internal;访问内网机器则直接填内网 IP,确保容器网络能路由到即可。
Q:忘记了修改后的管理员密码怎么办?
先停止容器并备份 store.json,然后编辑其中顶层的 admin 字段(即保存的账号和密码哈希),将其删除或清空,再重启容器。admin 中没有密码哈希时,程序会重新使用 ADMIN_USER / ADMIN_PASSWORD 环境变量登录。store.json 中的会话、分组和隧道配置不受影响。
Q:刷新页面后终端内容还在吗? 在。远端 Shell 在 WebSocket 断开后继续保留 30 分钟,重新连接会附着到同一个 Shell 并回放历史输出。超过 30 分钟未重连则 Shell 会被回收。
Q:隧道里的本地转发 -L 监听在哪里?
监听在容器内。需要从宿主机或外部访问时,请在 docker run / Compose 中额外映射对应端口(-p)。
⚠️ 暴露到公网之前请务必阅读。
InsecureIgnoreHostKey),在不可信网络中存在中间人风险store.json 中,请限制对数据卷/目录的访问权限admin / admin,并将服务置于 TLS 之后,最好再配合 VPN 或 IP 白名单Content type
Image
Digest
sha256:6c851af80…
Size
6.8 MB
Last updated
17 days ago
docker pull casualty01/web_ssh