ShellHub Docker Compose 部署实战
ShellHub v0.27.4 社区版官方 Compose 编排全流程部署实测:keygen 密钥、.env.override 最小配置、PostgreSQL 持久化、管理员初始化、配对码接入设备、公钥集中注册、审批式 SSH 网关连接与审计落库,附 8 条真实排障实录。
ShellHub Docker Compose 部署实战
目标:在真实服务器上以官方编排方式部署 ShellHub v0.27.4 社区版,完成从「空栈」到「经 SSH 网关连上第一台设备」的全流程实操,并把过程中踩到的每一个坑沉淀为排障实录。选型认知见 《ShellHub 介绍与选型指南》,逐项配置参数见 《ShellHub 配置参数详解》,管理命令见 《ShellHub 常用 CLI 命令详解》。
一、环境信息
本次实测全程在真实服务器视角完成,环境基线如下:
| 项目 | 值 |
|---|---|
| 服务器操作系统 | Ubuntu 24.04.5 LTS(x86_64,内核 7.0.0-38-generic) |
| Docker | 29.9.2(含 Docker Compose v5.6.1) |
| ShellHub 版本 | v0.27.4(2026-10-27 官方最新稳定版,Apache-2.0) |
| 服务端口 | 8131(Web/API/agent 回连)、8133(SSH 网关入口,server 发布) |
| 服务器地址 | shellhub.lab(实验环境经 hosts 解析到服务器 IP,生产请用真实域名) |
| 实验命名空间/账号 | demo / admin(密码 q1w2e3r4) |
⚠️ 部署前必须先想好「设备与用户通过什么地址访问服务端」。ShellHub 的接入地址必须是域名(服务端校验 hostname),且设备 agent 回连、用户浏览器访问、SSH 客户端连接用的应是同一个稳定地址。生产环境请准备真实域名并解析到服务器;本文实验环境用 hosts 模拟。
二、镜像与版本选型
v0.27.4 的官方编排是「四个服务 + PostgreSQL」的托管组合,各服务镜像与职责:
| 服务 | 镜像 | 职责 |
|---|---|---|
| gateway | shellhubio/gateway:v0.27.4 | TLS 终结与统一入口,v0.27 起内置 Caddy(替代旧 nginx+certbot),按域名 Host 匹配转发 |
| server | shellhubio/server:v0.27.4 | API 与 SSH 端点合一的单进程(v0.27 架构合并,旧 api/ssh 两服务已不存在) |
| ui | shellhubio/ui:v0.27.4 | Web 控制台 |
| postgres | postgres:18.0 | 主存储(官方编排锁定 18.0) |
| redis | valkey/valkey:9.1-alpine | 缓存与任务队列(Redis 协议兼容的 Valkey) |
版本选型建议:
- 锁定 release 标签:编排内的镜像版本由
.env的SHELLHUB_VERSION统一驱动,升级只改这一个值(见第九节); - 直接从 v0.27.x 开始:v0.27 重构了存储(PostgreSQL 取代 MongoDB)与设备注册(配对码/预配密钥取代 TENANT_ID 预授权),从旧版本迁移的复杂度远高于全新部署;
- agent 与服务端同版本:官方安装脚本默认拉取与服务端一致的 agent 版本,混用版本可能导致隧道协议不兼容(v2 隧道为 v0.27 默认)。
三、前置条件
- 服务器已安装 Docker 与 Compose 插件(官方要求);
- 端口规划:8131(HTTP,放行给设备与浏览器)、8133(SSH,放行给运维客户端)——两个端口必须在防火墙放行,SSH 端口由 server 服务发布而非 gateway;
- 一个解析到服务器的域名(生产)或 hosts 模拟域名(实验);
git、openssl、make(keygen 目标依赖);- 磁盘余量 ≥ 5GB(六镜像约 1.5GB + PostgreSQL 数据)。
四、部署步骤
4.1 获取官方编排(按 release 标签克隆)
$ git clone -b v0.27.4 https://github.com/shellhub-io/shellhub.git
$ cd shellhub
官方明确建议按 release 标签克隆而非 main 分支,保证编排文件与镜像版本严格配套。
4.2 生成密钥(make keygen)
$ make keygen
openssl genrsa -out api_private_key 4096
openssl rsa -in api_private_key -pubout -out api_public_key
openssl genrsa -out ssh_private_key 4096
这一步生成三份 Docker secrets:API 签名密钥对(api_private_key/api_public_key)与 SSH 主机密钥(ssh_private_key)。务必备份:重新生成 SSH 主机密钥会改变服务端主机指纹,所有连过的 SSH 客户端都会因 known_hosts 不匹配而拒绝连接。
4.3 最小配置(.env.override,不要改 .env)
官方约定 .env 是随版本升级会被覆盖的默认值文件,自定义配置写入 .env.override(读取顺序最后、优先生效)。本次实测的最小覆盖:
$ cat .env.override
# 端口:HTTP 入口与 SSH 网关
SHELLHUB_HTTP_PORT=8131
SHELLHUB_SSH_PORT=8133
# 设备回连与客户端访问的公共地址(必须是域名)
SHELLHUB_PUBLIC_URL=http://shellhub.lab:8131
SHELLHUB_DOMAIN=shellhub.lab
# 关闭首账号向导的外部问卷(离线/内网环境建议关闭)
SHELLHUB_ONBOARDING_URL=
# 数据库口令(必改)
SHELLHUB_POSTGRES_PASSWORD=q1w2e3r4
⚠️
SHELLHUB_DOMAIN只接受域名。实测填入 IP 会触发网关配置校验失败(排障实录 R2),服务反复重启。
4.4 数据持久化(官方默认无卷,强烈建议补上)
官方编排的 PostgreSQL 没有挂载任何卷,实例删除即丢全部数据(设备、账号、会话记录)。在 docker-compose.override.yml 中补一个命名卷(Compose 自动合并,无需改官方文件):
# docker-compose.override.yml
services:
postgres:
volumes:
- shellhub-postgres-data:/var/lib/postgresql
volumes:
shellhub-postgres-data:
name: shellhub-postgres-data
⚠️ 挂载点必须是
/var/lib/postgresql而不是旧的/var/lib/postgresql/data——postgres:18 镜像挂错路径会拒绝启动(排障实录 R1)。
4.5 启动(务必走官方包装器)
$ make start
make start 内部会先补跑 keygen(若无密钥),然后调用官方包装器 ./bin/docker-compose up -d。严禁用裸 docker compose:包装器负责拼装编排文件列表(含 PostgreSQL 扩展层)与环境文件链(.env + .env.override),裸命令会漏掉 PostgreSQL 编排,服务端因无数据库起不来(排障实录 R5)。
实测启动后的服务状态:
$ ./bin/docker-compose ps
NAME IMAGE SERVICE STATUS PORTS
shellhub-v0274-stack-gateway-1 shellhubio/gateway:v0.27.4 gateway Up 33 minutes (healthy) 0.0.0.0:8131->80/tcp
shellhub-v0274-stack-postgres-1 postgres:18.0 postgres Up 35 minutes (healthy) 5432/tcp
shellhub-v0274-stack-redis-1 valkey/valkey:9.1-alpine redis Up 35 minutes 6379/tcp
shellhub-v0274-stack-server-1 shellhubio/server:v0.27.4 server Up 30 minutes (healthy) 0.0.0.0:8133->2222/tcp, [::]:8133->2222/tcp
shellhub-v0274-stack-ui-1 shellhubio/ui:v0.27.4 ui Up 35 minutes (healthy) 8080/tcp
五个服务全部 healthy。资源占用实测(空闲态)极轻:server 14MiB、gateway 11MiB、ui 13MiB、postgres 51MiB、redis 4MiB 内存。
4.6 健康验证
$ curl -s http://shellhub.lab:8131/api/info
{"version":"v0.27.4","endpoints":{"api":"shellhub.lab:8131","ssh":"shellhub.lab:8133"},"setup":false,"authentication":{"local":true}}
$ curl -s -o /dev/null -w '%{http_code}\n' http://shellhub.lab:8131/api/healthcheck
200
$ curl -s -o /dev/null -w '%{http_code}\n' http://shellhub.lab:8131/
200
三个关键信号:/api/info 报告版本与两端点地址;setup:false 表示还没有任何账号;Web 首页 200。此时用浏览器打开 http://shellhub.lab:8131 会强制跳转到 /setup 初始化页。
五、初始化管理员
首个账号只能创建一次,/setup 在账号建好后即消失。官方路径是浏览器访问 /setup 填写名称、用户名、邮箱、命名空间与密码(密码至少 5 位);本次实测用等价的 API 直调:
$ curl -s -w '\nHTTP %{http_code}\n' -X POST http://shellhub.lab:8131/api/setup \
-H 'Content-Type: application/json' \
-d '{"name":"Admin","email":"admin@shellhub.lab","username":"admin","password":"q1w2e3r4","namespace":"demo"}'
HTTP 200
命名空间名(demo)会出现在每一条 SSHID 里,务必小写、不带点。初始化成功后登录验证:
$ curl -s -X POST http://shellhub.lab:8131/api/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"q1w2e3r4"}'
{"token":"eyJhbGciOiJSUzI1NiIs...","user":"admin","origin":"local","auth_methods":["local"],
"name":"Admin","id":"17865734-009a-4b9d-b7b8-a3ce825061ee",
"tenant":"94f7d6f1-c805-40d9-b596-b88a503674ee","email":"admin@shellhub.lab",
"role":"owner","mfa":false,"admin":true,"max_namespaces":-1}
返回的 JWT 即后续 API 调用凭证(Authorization: Bearer <token>);tenant 是命名空间不可变的 Tenant ID(设备注册、配对确认都要用到)。社区版实例只此一个命名空间,服务端拒绝再建。
六、接入第一台设备
6.1 官方安装脚本(推荐路径)
在目标设备上执行服务端下发的安装脚本:
$ curl -sSf http://shellhub.lab:8131/install.sh | sh
脚本自动识别系统与架构,以官方 agent 镜像启动设备代理(内部核心行为等价于:特权模式挂载宿主根文件系统与 Docker 套接字、--net=host,密钥落在设备 /etc/shellhub.key,详见官方仓库 install.sh)。设备必须能访问服务端 8131 端口,且无需任何入站端口。
6.2 配对码注册(v0.27 新流程)
agent 启动后打印配对码并等待确认(实测回显):
$ (agent 日志)
level=info msg="Starting ShellHub" mode=multi-user version=v0.27.4
level=info msg="This device is not enrolled in any namespace yet."
level=info msg="To pair it, open http://shellhub.lab:8131/accept-device and enter code FHJX-QK6A"
level=info msg="Or open the link directly: http://shellhub.lab:8131/accept-device?code=FHJXQK6A"
level=info msg="Waiting for acceptance... (code expires in 10 minutes)"
命名空间成员在浏览器打开上述链接确认即完成注册;API 等价调用(需 Bearer token 与 Tenant ID):
$ curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"tenant_id":"94f7d6f1-c805-40d9-b596-b88a503674ee"}' \
http://shellhub.lab:8131/api/devices/pairing/FHJXQK6A/accept
{"uid":"08196998...","tenant_id":"94f7d6f1-...","namespace":"demo","owner_id":"17865734-..."}
agent 侧随即上线(实测回显):
level=info msg="Listening for connections" status=accepted tenant_id=94f7d6f1-...
level=info msg="Server connection established" sshid=demo.shellhub-device@shellhub.lab
transport=tunnel transport_version=2 version=v0.27.4
💡 设备身份与设备私钥绑定:agent 首次启动生成设备私钥(本实测置于设备
/etc/shellhub.key),该密钥是设备在平台上的唯一身份凭证。删除或更换它,设备将以新设备身份重新走注册流程;备份它则可在重装后保留设备台账记录。
6.3 设备重命名
agent 默认上报的设备名可能不直观(容器环境是网卡 MAC,实测为 be-4d-e4-b8-2c-4f),建议立即改名:
$ curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"shellhub-device"}' \
http://shellhub.lab:8131/api/devices/081969989ac9a7c078984024a4739c33b124f2e61a79f1347a0fdb1948b8f8bb
HTTP 200
改名后 SSHID 变为 demo.shellhub-device@shellhub.lab。
七、经网关 SSH 连接实测
7.1 SSHID 与连接语法
ShellHub 把目标设备编码进 SSH 用户名:<设备用户名>@<命名空间>.<设备名>@<服务端>。本次实测:
$ ssh -p 8133 root@demo.shellhub-device@shellhub.lab
-p 8133 指向 server 发布的 SSH 端口。因为 SSHID 就是普通用户名,scp/sftp/Ansible 全部通用,无需任何插件。
7.2 公钥集中注册
在运维客户端生成密钥并把公钥注册到平台(绑定账号身份,而非逐台设备写 authorized_keys):
$ ssh-keygen -t ed25519 -f ~/.ssh/shellhub_lab -N '' -C 'shellhub-lab'
$ PUB=$(cat ~/.ssh/shellhub_lab.pub)
$ python3 - <<EOF
import json, base64, urllib.request
token = open('token').read().strip() # /api/login 返回的 JWT
pub = open('/root/.ssh/shellhub_lab.pub').read().strip()
body = {"name": "lab-key", "username": "root",
"filter": {"hostname": ".*"}, # 主机名匹配规则,.* 表示全部设备
"data": base64.b64encode(pub.encode()).decode()}
req = urllib.request.Request("http://shellhub.lab:8131/api/sshkeys/public-keys",
data=json.dumps(body).encode(), method="POST")
req.add_header("Authorization", "Bearer " + token)
req.add_header("Content-Type", "application/json")
print(urllib.request.urlopen(req).status)
EOF
200
⚠️ v0.27 的公钥注册接口
data字段要求 base64 编码的公钥文本(实测直接传原文报 422illegal base64 data)。filter.hostname与tags二选一,决定这把钥匙能开哪些设备。
7.3 首次登录的审批环节(v0.27 新增)
v0.27 改为身份制接入:未注册过身份的密钥首次登录时,服务端会挂起一次登录审批,要求命名空间成员在控制台确认。实测服务端日志:
level=info msg="new connection established" sshid=root@demo.shellhub-device
level=info msg="the presented key needs an approval before it can log in" key="ssh-ed25519 AAAAC3..."
level=info msg="an approval was refused" error="ssh login prompt dismissed"
审批交互形态:SSH 客户端会收到一段键盘交互提示,指引打开 http://shellhub.lab:8131/ssh-identities/new/<审批码> 确认身份并领取确认码,回到终端输入确认码后登录完成,该密钥即被登记为已信任身份,后续登录不再需要审批(实测第二次连接直接通过)。自动化的确认 API(实测可用):
$ curl -s -X POST -H "Authorization: Bearer $TOKEN" \
http://shellhub.lab:8131/api/ssh-approvals/<审批码>/confirm -H 'Content-Type: application/json' -d '{}'
{"confirmation_code":"..."}
7.4 连接成功与审计落库
审批完成后,实测经网关在目标设备执行命令:
$ ssh -p 8133 -i ~/.ssh/shellhub_lab root@demo.shellhub-device@shellhub.lab 'hostname; id; uname -srm'
master
uid=0(root) gid=0(root) groups=0(root)
Linux 7.0.0-38-generic x86_64
同时换一台机器用标准 OpenSSH 客户端(无任何 ShellHub 插件)复测同一条 SSHID,同样直连成功——验证了「原生 SSH 支持」。
服务端审计已自动落库,会话记录包含主体与事件类型(实测 /api/sessions 返回片段):
{
"uid": "22819c6d9819...",
"device_uid": "08196998...",
"username": "root",
"principal": {"kind": "user", "id": "17865734-..."},
"ip_address": "10.89.66.1",
"started_at": "2026-10-27T08:44:25Z",
"active": false, "authenticated": true, "recorded": false,
"events": {"types": ["exec", "exit-status"], "seats": [0], "first": "exec"}
}
principal.kind: user 说明这次会话是以 admin 账号身份授权的——这正是 v0.27 身份制接入的价值:审计能回答「谁、从哪个 IP、在何时、以哪个系统账号、对哪台设备做了什么」。
八、数据持久化与备份
- PostgreSQL 数据卷:按 4.4 节挂载命名卷后,
down -v以外的任何停止/重建操作都不会丢数据;实测卷shellhub-postgres-data独立于实例生命周期; - 三份密钥文件(
api_private_key、api_public_key、ssh_private_key):丢失 API 密钥对意味着所有已签发 token 失效;丢失 SSH 主机密钥意味着全网客户端 known_hosts 报警。两者都应纳入备份; - 设备私钥(每台设备的
/etc/shellhub.key):决定设备身份,按设备纳入备份; - 备份命令:官方推荐对 PostgreSQL 定期
pg_dump(管理命令见本专栏 CLI 篇),并演练一次恢复。
九、常用运维命令
$ make start # 启动(自动补 keygen)
$ make stop # 停止
$ make restart # 重启
$ ./bin/docker-compose ps # 服务状态
$ ./bin/docker-compose logs -f # 跟踪日志(可加服务名)
升级路径:修改 .env.override 中 SHELLHUB_VERSION=v0.27.5(示例)→ 重新 make start,官方编排按新标签重建服务。跨大版本升级前阅读官方 Upgrade 文档与 Release Notes(v0.27 涉及存储引擎与注册流程迁移,不可跳步)。
十、排障实录
以下全部为本次实测真实触发并解决的问题,按踩坑顺序编号。
R1 postgres:18 挂载点变更,附加持久化卷后实例反复重启
现象:给 postgres 服务补挂 /var/lib/postgresql/data 卷后,数据库容器启动即退出,日志明示检测到「PostgreSQL data in /var/lib/postgresql/data (unused mount/volume)」。
原因:postgres:18 镜像的推荐挂载点改为 /var/lib/postgresql(数据落在其下子目录,便于 pg_upgrade --link),旧版 17 及以前的 /.../data 路径被判定为无用挂载直接拒绝启动。
解决:override 卷目标改为 /var/lib/postgresql,删除误生成的空卷后重建,postgres 进入 healthy。
R2 SHELLHUB_DOMAIN 填 IP 导致网关崩溃循环
现象:实验初期图省事把域名填成服务器 IP,gateway 容器每秒重启,日志 failed to load configuration: Key: 'GatewayConfig.Domain' Error:Field validation for 'Domain' failed on the 'hostname' tag。
原因:网关配置对 Domain 做了 hostname 合法性校验,IP 不合法。
解决:改用域名(实验环境 hosts 模拟 shellhub.lab)。生产本来就该有域名,这个校验反而提前暴露了「没有稳定域名」的部署缺陷。
R3 网关按 Host 匹配,用 IP/错误域名访问一律 404
现象:服务全部 healthy,但 curl http://192.168.146.128:8131/api/info 返回空 404,换 curl http://shellhub.lab:8131/api/info 立即 200。
原因:Caddy 按请求 Host 头匹配站点(SHELLHUB_DOMAIN),且配置里有一条显式兜底规则——Host 不匹配时返回空 404(官方注释:避免 agent 把错误的 200 空响应当成服务信息)。
解决:设备、浏览器、SSH 客户端统一用 SHELLHUB_DOMAIN 指定的地址访问。这是「地址一致性」原则的强制实现。
R4 agent 首次启动即退出(缺 Docker 套接字)
现象:以精简参数手动启动 agent,几秒后退出,日志仅一行 Cannot connect to the Docker daemon at unix:///var/run/docker.sock。
原因:官方 agent 镜像为极简构造(FROM scratch,无 shell),会话执行依赖宿主环境,启动时即探测 Docker 套接字;缺挂载直接退出。
解决:按官方 install.sh 的挂载清单补齐(/:/host、/var/run/docker.sock 等),agent 正常注册上线。
R5 裸 docker compose 缺 PostgreSQL 编排层
官方文档反复强调(实测验证):包装器 ./bin/docker-compose 负责拼装多份编排文件与环境链,裸 docker compose up -d 会跳过 PostgreSQL 扩展层,server 无库可连。任何手工操作都必须走包装器。
R6 v0.27 公钥接口的 data 字段要求 base64
现象:按旧文档直接把公钥文本放进 data 字段,返回 422;服务端日志 illegal base64 data at input byte 3。
原因:v0.27 请求结构中 data 定义为字节切片,JSON 传输按 base64 编解码。
解决:公钥文本先 base64 再提交,注册成功并返回指纹。
R7 SSH 首次登录被拒:密钥身份未审批
现象:公钥已注册,ssh -p 8133 root@demo.shellhub-device@shellhub.lab 仍 Permission denied (publickey);服务端日志出现 the presented key needs an approval before it can log in。
原因:v0.27 身份制接入要求新密钥首次使用时经命名空间成员在控制台审批(键盘交互提示 + 确认码),无人的自动化客户端会被判「提示被关闭」而拒绝。
解决:完成一次浏览器/确认码审批后,密钥登记为已信任身份,后续连接(实测 paramiko 与原生 OpenSSH 两种客户端)均直接通过。自动化管道可用 POST /api/ssh-approvals/<code>/confirm 接口完成确认(实测 200)。
R8 known_hosts 无法写入导致警告
现象:ssh -o UserKnownHostsFile=/root/.ssh/known_hosts_shellhub 报 Failed to add the host to the list of known hosts。
原因:目标文件不存在且父目录权限不允许创建。
解决:先 touch 该文件或指定可写路径。另注意:重生成 SSH 主机密钥后,需清掉各客户端的旧指纹条目,否则连接被拒。
十一、参考链接
- 官方自托管快速上手:https://docs.shellhub.io/selfhosted/quickstart
- 官方编排(v0.27.4 标签):https://github.com/shellhub-io/shellhub/blob/v0.27.4/docker-compose.yml
- 环境变量官方清单:https://docs.shellhub.io/selfhosted/environment-variables
- 端口与连通性:https://docs.shellhub.io/about/ports-and-connectivity
- 设备接入 Quickstart:https://docs.shellhub.io/get-started
- 备份指南:https://docs.shellhub.io/selfhosted/backup
- 本站姊妹篇:《ShellHub 介绍与选型指南》、《ShellHub 配置参数详解》、《ShellHub 常用 CLI 命令详解》