Hopp Docker Compose 部署指南
Hopp(开源远程结对编程平台)Docker Compose 自托管实战:官方五服务栈部署、首个管理员注册、房间创建、数据持久化与生产域名形态,基于 v1.0.33 全流程实测。
Hopp Docker Compose 部署指南
目标:在 Ubuntu 22.04 服务器上通过官方 Docker Compose 栈从零自托管 Hopp(开源远程结对编程与屏幕共享平台),完成服务启动、首个管理员账号注册、房间创建与数据持久化验证。已在 Ubuntu 22.04(x86_64)+ Docker Compose v2 环境实测通过,Hopp 版本 v1.0.33,Web 服务实测监听 8147 端口。
一、环境信息
- 操作系统:Ubuntu 22.04 LTS(x86_64)
- 运行时:Docker 27.x + Docker Compose v2(
docker compose子命令形式) - Hopp 版本:v1.0.33(backend 镜像
iparaskev/hopp-backend:v1.0.33,发布于 2026-10-17,与桌面端 v1.0.33 同步) - 部署目录:
/opt/hopp/selfhost(git clone 官方仓库所得selfhost/目录) - 服务清单:backend(Go API + Web,监听 1926)、PostgreSQL 16、Redis 7、LiveKit v1.11(WebRTC SFU)、Caddy 2(可选,自动 TLS)
- 实测端口映射:
8147 -> 1926(backend Web/API)、7880(LiveKit 信令,host 网络)、50000-50100/UDP(WebRTC 媒体,本地形态收窄) - 实测项目名:
hopp-v033-stack(compose.override.yml 显式指定,防止多项目同名互删) - 演示账号:
admin@hopp.local/q1w2e3r4abcd(Hopp 强制密码长度 12-72 位,故在固定测试口令q1w2e3r4基础上补足 12 位)
二、镜像选型说明
官方 selfhost/compose.yml 一共编排 5 个服务,镜像与实测版本如下:
| 服务 | 镜像 | 实测版本 | 角色 |
|---|---|---|---|
| backend | iparaskev/hopp-backend | :latest(= v1.0.33) | Go 编写的 API 服务器,同时内嵌托管 Web 前端,监听 1926 |
| db | postgres:16-alpine | 16-alpine | 用户/团队/房间等业务数据存储 |
| cache | redis:7-alpine | 7-alpine | 会话缓存与 LiveKit callstate 依赖 |
| livekit | livekit/livekit-server | v1.11 | WebRTC SFU,屏幕共享/远程控制的媒体转发核心,host 网络直连 |
| caddy | caddy:2-alpine | 2-alpine | 反向代理 + Let's Encrypt 自动 TLS(生产域名形态才需要) |
关键差异与选型说明:
- backend 镜像由官方以
latest与v1.0.x双轨发布 tag,.env中HOPP_VERSION留空时取latest,生产建议显式 pin 版本(如HOPP_VERSION=v1.0.33),升级时再改此值; - LiveKit 使用 host 网络模式(
network_mode: host),UDP 媒体端口不走映射,这是 WebRTC 低延迟的官方推荐形态; - Caddy 仅在生产域名形态启用;本文主线实测的本地/内网形态不需要它,
docker compose up时显式列出服务名跳过 caddy。
三、前置条件
3.1 软件与硬件
- 一台 Ubuntu 22.04 服务器(2 vCPU / 4GB 内存起步,屏幕共享场景建议 4 vCPU / 8GB);
- 已安装 Docker 与 Docker Compose v2(
docker compose version可执行); - git(用于获取官方部署文件)。
3.2 端口规划
官方文档(docs.gethopp.app/open-source/self-hosting,查询日期 2026-10-29)给出的防火墙端口要求:
| 端口 | 协议 | 用途 |
|---|---|---|
| 80 | TCP | HTTP(TLS 签发质询) |
| 443 | TCP | HTTPS |
| 7881 | TCP | LiveKit TCP 回退 |
| 50000-60000 | UDP | WebRTC 媒体流 |
Caddy 在 80/443 做 TLS 终结;LiveKit 媒体流量不经过 Caddy,直达服务器。云厂商有安全组(AWS/GCP/Azure 等)时需显式放行上表端口。
3.3 域名两种形态
- 本地/内网形态(本文主线实测):
DOMAIN=localhost,不需要任何 DNS 与 TLS,跳过 Caddy,适合验证与内网试用; - 生产域名形态:需要一个公网域名,
<DOMAIN>与livekit.<DOMAIN>(或通配符*.<DOMAIN>)A 记录指向服务器 IP;快速测试可用sslip.io免费泛解析(如51.15.42.10.sslip.io),由 Caddy 自动签发 Let's Encrypt 证书。
⚠️ 注意:生产域名形态要求服务器公网可达 80/443 端口(TLS 质询与回源),本次实验网络不满足该条件,该形态的配置步骤照实给出、其中 TLS 签发结果以官方文档为准;本地形态为全流程实测。
四、部署步骤(本地/内网形态,全流程实测)
4.1 获取官方部署文件
git clone --depth 1 https://github.com/gethopp/hopp.git /opt/hopp
cd /opt/hopp/selfhost
⚠️ 网络说明:官方源
github.com在本次实验环境直连超时(Failed to connect to github.com port 443),已改用代理克隆,实测可用:git -c http.proxy=http://192.168.1.72:7897 clone --depth 1 https://github.com/gethopp/hopp.git /opt/hopp实测克隆成功,仓库 HEAD 为
2390461f8047eb1c99cc0a50f81717a9d31c9717(2026-10-21)。Docker 镜像拉取经守护进程代理自动完成,无需手动配置。
selfhost/ 目录内共 6 个文件,各文件用途:
Caddyfile Caddy 反代与 TLS 路由(生产域名形态用)
compose.yml 主编排文件(5 个服务)
compose.override.example.yml 端口/卷自定义示例(复制为 compose.override.yml 生效)
.env.example 环境变量模板(复制为 .env 填写)
livekit.yaml LiveKit 生产配置(UDP 50000-60000)
livekit.dev.yaml LiveKit 本地开发配置(UDP 收窄为 50000-50100)
4.2 生成环境配置
cp .env.example .env
用 openssl 生成各密钥并写入 .env,本地/内网形态最小配置为 4 个键:
DOMAIN=localhost
SESSION_SECRET=$(openssl rand -base64 32 | tr -d '\n')
LIVEKIT_API_KEY=$(openssl rand -base64 32 | tr -d '\n')
LIVEKIT_API_SECRET=$(openssl rand -base64 64 | tr -d '\n')
POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '\n')
LIVEKIT_SERVER_URL=ws://localhost:7880
逐项说明:
DOMAIN:唯一必填项。本地形态填localhost;生产填你的域名(不带协议与结尾斜杠);SESSION_SECRET:Cookie 会话签名密钥(32 字节);LIVEKIT_API_KEY/LIVEKIT_API_SECRET:WebRTC 信令鉴权密钥对(32/64 字节),backend 与 LiveKit 两端共享;POSTGRES_PASSWORD:数据库口令,实测值示例uqvyHC95CPan6LDx3vcvzvmCRQdx7YTh(演示环境生成值);LIVEKIT_SERVER_URL:客户端连 LiveKit 的地址。本地无 TLS 形态必须显式设为ws://localhost:7880,否则默认按wss://livekit.${DOMAIN}生成,本地形态下连不上。
⚠️ 注意:官方命令为
openssl rand -base64 64,但 64 字节的 base64 输出长达 88 字符会被 openssl 按 76 字符换行,直接重定向进.env会把换行后的残行当变量名,启动时实测报错:failed to read /opt/hopp/selfhost/.env: line 5: unexpected character "+" in variable name "Eo+Tgw5PfxYtdiAPRvwWNw=="解决:生成后接
| tr -d '\n'压成单行(上文命令已含)。
4.3 本地形态的 compose.override.yml
将示例复制为 override 文件(docker compose 会自动合并加载):
cp compose.override.example.yml compose.override.yml
写入本地形态需要的两处改动——LiveKit 换用收窄 UDP 段的开发配置、backend 端口映射到 8147:
name: hopp-v033-stack
networks:
default:
name: hopp-net
ipam:
config:
- subnet: 10.89.73.0/24
services:
livekit:
volumes: !override
- ./livekit.dev.yaml:/etc/livekit.yaml:ro
backend:
ports: !override
- "127.0.0.1:8147:1926"
逐项说明:
name:显式指定 compose 项目名,避免多个栈同名互相覆盖;networks.default显式固定网段,便于服务器上多套栈并存时排查 IP 归属;!override标签会整体替换主 compose.yml 中的同名列表(端口/卷挂载是列表合并语义,普通写法会追加而不是替换,这是官方 override 示例特别注释过的行为);- LiveKit 换用
livekit.dev.yaml后 UDP 段从 50000-60000 收窄到 50000-50100,本地验证足够且不与宿主上大量临时 UDP 端口冲突; 127.0.0.1:8147:1926:backend 在容器内监听 1926,映射到宿主 8147 环回地址。生产域名形态不需要此 override(由 Caddy 占用 80/443 对外)。
4.4 启动服务栈
本地形态显式启动 4 个服务(跳过 caddy):
docker compose up -d db cache livekit backend
实测回显(输出节选,实测首尾):
Container hopp-v033-stack-backend-1 Creating
Container hopp-v033-stack-backend-1 Created
Container hopp-v033-stack-cache-1 Starting
Container hopp-v033-stack-db-1 Starting
Container hopp-v033-stack-cache-1 Started
Container hopp-v033-stack-livekit-1 Starting
Container hopp-v033-stack-livekit-1 Started
Container hopp-v033-stack-db-1 Started
Container hopp-v033-stack-backend-1 Starting
Container hopp-v033-stack-backend-1 Started
确认四个服务全部 Up:
docker compose ps --format 'table {{.Name}}\t{{.Image}}\t{{.Status}}'
实测回显:
NAME IMAGE STATUS
hopp-v033-stack-backend-1 iparaskev/hopp-backend:latest Up 11 seconds
hopp-v033-stack-cache-1 redis:7-alpine Up 13 seconds
hopp-v033-stack-db-1 postgres:16-alpine Up 13 seconds
hopp-v033-stack-livekit-1 livekit/livekit-server:v1.11 Up 13 seconds
4.5 健康检查
backend 健康端点:
curl http://localhost:8147/api/health
实测回显:
OK
LiveKit 信令端口(host 网络直连宿主 7880):
curl http://localhost:7880/
实测回显:
OK
4.6 打开 Web 并注册首个管理员
backend 在 1926 端口同时托管 Web 前端(单页应用,运行时从 window.location 自动推导 API 地址,无需重新构建)。浏览器访问 http://localhost:8147 即是登录/注册页;命令行验证首页可达:
curl -s http://localhost:8147/ -o /dev/null -w 'HTTP %{http_code}, %{size_download} bytes\n'
实测回显:
HTTP 200, 3016424 bytes
注册接口实测(首个注册账号自动成为团队管理员 owner,与官方文档一致):
curl -s -X POST http://localhost:8147/api/sign-up \
-H 'Content-Type: application/json' \
-d '{"first_name":"Ad","last_name":"Min","email":"admin@hopp.local","password":"q1w2e3r4abcd","team_name":"Hopp Demo Team"}'
实测回显(输出节选,实测首尾):
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFkbWluQGhvcHAubG9jYWwiLCJleHAiOjE4MjQ4MTM4NDB9.s8pxwS-Hbk203dTsDEr9V63afOOTd4VUWdoSTmhJeWo"}
HTTP 201
💡 说明:Hopp 服务端强制密码 12-72 位(实测短于 12 位返回
400 {"message":"Password must be at least 12 characters long."}),演示账号口令在固定测试口令基础上补足 12 位。保存好返回的 JWT,后续 API 调用作为Authorization: Bearer <token>使用,详见《Hopp v1.0.33 API 使用详解》。
4.7 基础功能验证
用注册所得 JWT 创建一个房间并确认 LiveKit 地址下发正确:
TOKEN=<上一步返回的 JWT>
curl -s -X POST http://localhost:8147/api/auth/room \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"pairing-room-1"}'
实测回显:
{"id":"01a187b0-c09b-77ef-98af-45962a88f4ff","name":"pairing-room-1","user_id":"01a187b0-4e38-754b-81cf-8c7a27405db6","created_at":"2026-10-29T12:44:30.235528086Z","updated_at":"2026-10-29T12:44:30.235528086Z","team_id":1,"type":"default","temp":false}
HTTP 200
curl -s http://localhost:8147/api/auth/livekit/server-url -H "Authorization: Bearer $TOKEN"
实测回显(.env 里的 LIVEKIT_SERVER_URL 已正确透传到客户端下发链路):
{"url":"ws://localhost:7880"}
HTTP 200
至此服务端部署完成。桌面客户端(macOS 稳定版 / Windows alpha,从 GitHub Releases 下载)在 Settings > Custom Backend URL 填入 http://localhost:8147 即可连到这台自托管服务器;浏览器直接打开 Web 端也能加入房间。
4.8 数据持久化验证
重启 backend 服务,验证业务数据落盘(数据存于命名卷 db_data,不随服务重建丢失):
docker compose restart backend
实测回显:
Container hopp-v033-stack-backend-1 Restarting
Container hopp-v033-stack-backend-1 Started
重启后再查房间列表:
curl -s http://localhost:8147/api/auth/rooms -H "Authorization: Bearer $TOKEN"
实测回显(房间数据完好):
[{"id":"01a187b0-c09b-77ef-98af-45962a88f4ff","name":"pairing-room-1","user_id":"01a187b0-4e38-754b-81cf-8c7a27405db6","created_at":"2026-10-29T12:44:30.235528Z","updated_at":"2026-10-29T12:44:30.235528Z","team_id":1,"type":"default","temp":false}]
HTTP 200
五、生产域名形态(公网域名 + Caddy 自动 TLS)
与本地形态的差异只有三处:.env 的 DOMAIN/LIVEKIT_SERVER_URL、启动命令包含 caddy、防火墙放行公网端口。
DOMAIN=hopp.example.com
LIVEKIT_SERVER_URL=
LIVEKIT_SERVER_URL 留空时默认按 wss://livekit.${DOMAIN} 生成,由 Caddy 把 livekit.<域名> 反代到 LiveKit 的 7880。启动全部 5 个服务:
docker compose up -d
Caddy 依据 Caddyfile 自动完成两件事:https://<域名> 反代 backend:1926(对响应做 zstd/gzip 压缩,官方注释说明 Web 端是单个大 HTML 文件,压缩后体积约减半);wss://livekit.<域名> 反代 LiveKit 7880。Let's Encrypt 证书全自动签发续期,签发数据存于命名卷 caddy_data。
验证(域名解析生效后):
curl https://hopp.example.com/api/health
预期回显(官方文档口径):
OK
⚠️ 注意:此形态要求服务器公网 80/443 可达(TLS 质询),且 7881/TCP、50000-60000/UDP 已放行,否则 LiveKit 媒体协商会退化或失败。若已有 nginx/traefik 反代,可去掉 caddy 服务:
USE_TLS=false保持默认,反代指向backend:1926与127.0.0.1:7880,并在.env显式设置LIVEKIT_SERVER_URL=wss://livekit.yourdomain.com(官方文档 "Behind your own reverse proxy" 节,查询日期 2026-10-29)。
六、数据持久化说明
compose.yml 声明了 4 个命名卷,均由 Docker 管理:
| 卷名 | 挂载点 | 内容 | 实测行为 |
|---|---|---|---|
db_data | /var/lib/postgresql/data | 全部业务数据(用户/团队/房间) | 实测 down 后重新 up,数据完好 |
cache_data | /data | Redis 持久化数据 | 会话与 callstate 缓存 |
caddy_data | /data | Let's Encrypt 证书与 ACME 账户 | 生产形态续期依赖此卷 |
caddy_config | /config | Caddy 运行时配置 | 可随栈重建 |
注意:docker compose down(不带 -v)保留全部卷;docker compose down -v 会连数据一起删除,生产环境慎用。PostgreSQL 数据备份建议用 pg_dump(业务级)或对 db_data 卷做快照(停机窗口内)。
七、环境变量表
.env 全部变量(源自官方 .env.example,实测键名逐字核对):
| 变量 | 必填 | 默认行为 | 说明 |
|---|---|---|---|
DOMAIN | 是(唯一) | 无默认 | 部署域名;本地形态填 localhost |
SESSION_SECRET | 是 | 空 | Cookie 会话密钥,openssl rand -base64 32 |
LIVEKIT_API_KEY | 是 | 空 | LiveKit 鉴权 key,openssl rand -base64 32 |
LIVEKIT_API_SECRET | 是 | 空 | LiveKit 鉴权 secret,openssl rand -base64 64 |
POSTGRES_PASSWORD | 建议必填 | compose 兜底 hopp_local_pass | 数据库口令,生产务必显式设置 |
LIVEKIT_SERVER_URL | 否 | wss://livekit.${DOMAIN} | 客户端连 LiveKit 的地址;LiveKit Cloud 时填 wss://<project>.livekit.cloud;本地无 TLS 填 ws://localhost:7880 |
HOPP_VERSION | 否 | latest | backend 镜像 tag,生产 pin 到 v1.0.33 |
RESEND_API_KEY / RESEND_DEFAULT_SENDER | 否 | 未配置→邮件通知关闭 | 邮件发送(Resend);默认发件地址默认 noreply@${DOMAIN} |
GOOGLE_KEY / GOOGLE_SECRET | 否 | 空→该 OAuth 入口隐藏 | Google 登录 |
SLACK_KEY / SLACK_SECRET | 否 | 空→禁用 | Slack 登录 |
GITHUB_KEY / GITHUB_SECRET | 否 | 空→禁用 | GitHub 登录 |
SLACK_APP_CLIENT_ID 等 4 项 | 否 | 空→禁用 | Slack App 斜杠命令集成 |
SENTRY_DSN | 否 | 空→禁用 | 错误上报 |
STRIPE_SECRET_KEY 等 5 项 | 否 | 空→禁用 | 订阅计费(自托管无必要) |
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID | 否 | 空→禁用 | Telegram 通知 |
💡 说明:backend 容器内还有一组由 compose.yml 注入的运行时变量(
SERVER_HOST、DEPLOY_DOMAIN、DATABASE_DSN、REDIS_URI、USE_TLS等),它们从上表.env键派生而来,正常部署不需要直接修改;逐一取值含义与生效链路见《Hopp v1.0.33 配置参数详解》。
八、配置文件挂载与服务角色对照
compose.yml 中的挂载只有两类:LiveKit 配置(./livekit.yaml 只读挂载为 /etc/livekit.yaml,本地形态经 override 换为 livekit.dev.yaml)与 Caddy 配置(./Caddyfile 只读挂载为 /etc/caddy/Caddyfile)。.env 经 compose 变量替换进入各服务环境。
各服务"等价启动参数"对照表(与 compose.yml 等价,便于理解每个服务的角色与依赖):
| 服务 | 等价启动要点 | 关键依赖 |
|---|---|---|
| db | postgres:16-alpine,env 注入用户/口令/库名,卷 db_data 持久化 | 无 |
| cache | redis:7-alpine,仅绑定 127.0.0.1:6379,卷 cache_data | 无 |
| livekit | host 网络,--config /etc/livekit.yaml,env LIVEKIT_KEYS 注入鉴权对 | cache(Redis 127.0.0.1:6379) |
| backend | 监听 1926,env 注入域名/密钥/DSN,映射宿主端口 | db、cache、livekit |
| caddy | 80/443 对外,读 DOMAIN 反代 backend 与 livekit | backend |
两张 LiveKit 配置的差异:生产版 rtc.port_range_start/end 为 50000-60000 且 log_level: info;开发版收窄到 50000-50100 且 log_level: debug,两者 rtc.tcp_port 均为 7881、use_external_ip: true。
九、常用运维命令
docker compose ps # 查看服务状态
docker compose logs -f backend # 跟踪 backend 日志(启动竞态报错见第十节)
docker compose restart backend # 重启单个服务(实测数据不丢)
docker compose down # 停栈(保留数据卷)
docker compose pull backend && docker compose up -d backend # 升级 backend(改 HOPP_VERSION 后同理)
升级流程实测口径:修改 .env 中 HOPP_VERSION=v1.0.33 后执行 docker compose pull backend && docker compose up -d backend 即可完成版本切换;降级同理(把 HOPP_VERSION 改回旧 tag)。数据库结构变更由 backend 启动时自动处理,无需手工迁移。
十、常见问题(FAQ)
Q1:backend 启动日志刷 FATAL ... dial tcp 10.89.73.3:5432: connect: connection refused?
实测日志样例:
{"time":"2026-10-29T12:43:39.555525175Z","level":"FATAL","prefix":"echo","file":"server.go","line":"174","message":"failed to connect to `user=hopp database=hopp`: 10.89.73.3:5432 (db): dial error: dial tcp 10.89.73.3:5432: connect: connection refused"}
这是首次启动时 backend 先于 PostgreSQL 就绪的竞态,backend 会自动重试直至数据库可连,随后正常打出服务横幅并监听 1926,无需处理。同理 callstate: ListRooms failed ... localhost:7880 是 LiveKit 尚未就绪的一条 WARN,之后自行恢复。
Q2:docker compose config 或 up 报 unexpected character "+" in variable name?
.env 中某个密钥被 openssl 换行截断(base64 64 字节必然换行),按 4.2 节用 | tr -d '\n' 重新生成即可。
Q3:curl -I http://localhost:8147/ 返回 405 Method Not Allowed?
backend 根路径只允许 GET/OPTIONS,curl -I 发的是 HEAD。改用 curl -s -o /dev/null -w '%{http_code}' http://localhost:8147/ 验证,实测返回 200。
Q4:日志提示 RESEND_API_KEY not configured, email notifications will be disabled?
实测存在的 INFO 级提醒,未配置邮件服务时邀请链接、密码重置邮件不可用;自托管团队内用房间直连/桌面客户端不受影响,需要邮件时在 .env 配 RESEND_API_KEY 与 RESEND_DEFAULT_SENDER 后重建 backend。
Q5:LiveKit 起不来或媒体流不通?
先确认 docker compose ps 中 livekit 为 Up 且宿主 7880 未被占用(ss -ltnp | grep 7880);LiveKit 是 host 网络,宿主上已有服务占用 7880/7881 时会启动失败。媒体流不通按防火墙表检查 7881/TCP 与 UDP 段是否放行。
Q6:遥测会外发数据吗? 官方文档(查询日期 2026-10-29)明确:Web 端在自托管构建中默认关闭 PostHog 遥测,不向外部服务发送数据;桌面端连自托管后端时在 Settings 中关闭遥测。
十一、卸载与清理
docker compose down -v # 停栈并删除 4 个数据卷(不可恢复)
cd /opt/hopp && rm -rf /opt/hopp # 删除部署文件
宿主上确认无残留:docker ps -a | grep hopp-v033-stack、docker network ls | grep hopp-net 均应为空。