Hopp Docker Compose 部署指南

Hopp(开源远程结对编程平台)Docker Compose 自托管实战:官方五服务栈部署、首个管理员注册、房间创建、数据持久化与生产域名形态,基于 v1.0.33 全流程实测。

HoppHoppDocker Compose2026/10/29

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 个服务,镜像与实测版本如下:

服务镜像实测版本角色
backendiparaskev/hopp-backend:latest(= v1.0.33)Go 编写的 API 服务器,同时内嵌托管 Web 前端,监听 1926
dbpostgres:16-alpine16-alpine用户/团队/房间等业务数据存储
cacheredis:7-alpine7-alpine会话缓存与 LiveKit callstate 依赖
livekitlivekit/livekit-serverv1.11WebRTC SFU,屏幕共享/远程控制的媒体转发核心,host 网络直连
caddycaddy:2-alpine2-alpine反向代理 + Let's Encrypt 自动 TLS(生产域名形态才需要)

关键差异与选型说明:

  1. backend 镜像由官方以 latest 与 v1.0.x 双轨发布 tag,.env 中 HOPP_VERSION 留空时取 latest,生产建议显式 pin 版本(如 HOPP_VERSION=v1.0.33),升级时再改此值;
  2. LiveKit 使用 host 网络模式(network_mode: host),UDP 媒体端口不走映射,这是 WebRTC 低延迟的官方推荐形态;
  3. 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)给出的防火墙端口要求:

端口协议用途
80TCPHTTP(TLS 签发质询)
443TCPHTTPS
7881TCPLiveKit TCP 回退
50000-60000UDPWebRTC 媒体流

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/dataRedis 持久化数据会话与 callstate 缓存
caddy_data/dataLet's Encrypt 证书与 ACME 账户生产形态续期依赖此卷
caddy_config/configCaddy 运行时配置可随栈重建

注意: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否latestbackend 镜像 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 等价,便于理解每个服务的角色与依赖):

服务等价启动要点关键依赖
dbpostgres:16-alpine,env 注入用户/口令/库名,卷 db_data 持久化无
cacheredis:7-alpine,仅绑定 127.0.0.1:6379,卷 cache_data无
livekithost 网络,--config /etc/livekit.yaml,env LIVEKIT_KEYS 注入鉴权对cache(Redis 127.0.0.1:6379)
backend监听 1926,env 注入域名/密钥/DSN,映射宿主端口db、cache、livekit
caddy80/443 对外,读 DOMAIN 反代 backend 与 livekitbackend

两张 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 均应为空。

十二、参考链接