前言
在 Homelab 搭建手记(6)Debian Xfce 与 XRDP 同用户并发会话配置 中,我已经把 Debian、XFCE 和 XRDP 的远程桌面环境调整到可以长期使用;上一篇又通过 Homelab 搭建手记(7)部署 code-server 并配置 HTTPS 访问 把浏览器开发环境接了出来。
不过 code-server 只能提供编辑器和终端,遇到必须使用完整图形界面的程序时,还是需要打开原生 RDP 客户端。这次增加一个浏览器入口:使用 Apache Guacamole 作为 Web Gateway,通过 RDP 连接已有的 xrdp 会话。这样既保留原生 RDP,也可以在平板、轻薄本或临时设备上直接用浏览器进入 Debian 桌面。
配套脚本放在我维护的 homelab-setup 项目中。这个仓库用于初始化 Debian 环境和部署常用服务;这次加入 Guacamole 时,我也整理了有状态服务的目录管理,让生成的配置有固定位置,重新执行脚本时保留已有账号和连接数据。下面会同时说明脚本用法和具体配置。
一、目标架构
Guacamole 连接的是现有的远程 XFCE 会话,访问链路如下:
text12345678910111213浏览器 │ HTTPS ▼ Caddy │ 反向代理 ▼ Apache Guacamole │ RDP ▼ xrdp │ ▼ 已有的 XFCE 远程会话
xrdp 是远程桌面后端,Guacamole 把浏览器连接转换为 RDP,Caddy 负责 HTTPS 和外部入口。原生 RDP 客户端仍然可以直接连接 xrdp,两种访问方式互不替代。
二、为什么选择 Guacamole
如果只是想在浏览器中显示一个 Linux 桌面,也可以选择专门的 Web Desktop 容器。但那通常意味着重新创建用户、桌面环境和数据目录,和当前已经调通的 xrdp 会话变成两套互不相干的系统。
Guacamole 更适合当前场景,因为它只负责把浏览器连接转发为 RDP,不需要安装另一套桌面。这样可以保留之前已经验证过的本地与远程会话共存、同用户会话复用,以及输入法、D-Bus 和密钥环等配置。Guacamole 自身通过 Docker 隔离,也不会向宿主机额外安装 Tomcat、Java 或 PostgreSQL 运行时。
三、为什么把有状态服务统一放在 services/docker/
过去部署 Docker 服务时,Compose 文件有时放在 /opt,有时放在用户目录。服务多了,就不容易记住配置、环境变量和初始化 SQL 分别放在哪里,备份时也容易漏掉。因此这次把生成的服务配置统一放到 services/docker/ 下,每个服务使用自己的子目录。
因此我在 homelab-setup 中把服务入口和数据工作区分开:setup/ 保存可公开的安装与生成脚本,工作区中的 services/docker/ 保存本机服务配置;镜像和 PostgreSQL volume 仍由 Docker 管理,不会被错误地当成 Git 内容提交。
默认目录关系如下:
text12345678~/workspace/ ├── setup/ # 公开的 homelab-setup 仓库 └── services/ └── docker/ └── guacamole/ # 本机有状态服务配置 ├── compose.yaml ├── .env # 本机凭据,权限 600,不提交 └── initdb.sql
数据工作区默认是 $HOME/workspace,需要换到数据盘时可使用 --workspace-root 统一指定,省去逐个修改服务路径的操作。备份时要保存这里的 Compose 文件和 .env,并单独备份 Docker volume 中的数据。
Compose 文件本身占用很小空间,PostgreSQL 数据和镜像层实际位于 Docker 默认数据目录。部署前可以检查:
bash12sudo du -sh /var/lib/docker 2>/dev/null docker system df
四、通过 homelab-setup 部署并复用服务状态
我为 homelab-setup 增加了 services/docker/00-guacamole.sh。它先检查 Docker 与 Docker Compose,再在上述目录中生成缺失的部署文件、校验 Compose 配置并启动容器。重复执行时不会覆盖已有 .env、initdb.sql 或数据库卷;默认只拉取本地缺少的固定版本镜像,只有显式使用 --upgrade 才会拉取更新。
克隆公开仓库后,在仓库目录执行:
bash12345678git clone https://github.com/DoraTiger/homelab-setup.git "$HOME/workspace/setup" cd "$HOME/workspace/setup" # 按编号选择 Guacamole 服务 bash service.sh --silent docker 00 # 数据工作区位于其他磁盘时 # bash service.sh --silent --workspace-root /data/homelab docker 00
执行完成后,日常管理回到生成的服务目录:
bash1234cd "$HOME/workspace/services/docker/guacamole" docker compose ps docker compose logs -f docker compose restart
如果不使用脚本,下面就是它所实现的关键原理。Guacamole 的 PostgreSQL 表结构由官方镜像生成,并固定版本以避免 latest 标签在升级时悄悄改变行为:
bash1234docker run --rm guacamole/guacamole:1.6.0 \ /opt/guacamole/bin/initdb.sh --postgresql > initdb.sql ls -lh initdb.sql head initdb.sql
脚本会自动生成随机数据库密码并以 600 权限保存到部署目录的 .env。数据库密码只放在本机的 .env 中,不写入文章、Compose 文件或 Git 仓库:
bash1chmod 600 "$HOME/workspace/services/docker/guacamole/.env"
dotenv1GUACAMOLE_DB_PASSWORD=替换为随机生成的密码
五、编排 Guacamole、guacd 与 PostgreSQL
compose.yaml 由三个容器组成:postgres 保存用户和连接配置,guacd 负责协议代理,guacamole 提供 Web 界面。
yaml123456789101112131415161718192021222324252627282930313233343536373839services: postgres: image: postgres:17 restart: unless-stopped environment: POSTGRES_DB: guacamole_db POSTGRES_USER: guacamole_user POSTGRES_PASSWORD: ${GUACAMOLE_DB_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data - ./initdb.sql:/docker-entrypoint-initdb.d/001-guacamole.sql:ro networks: [guacamole] guacd: image: guacamole/guacd:1.6.0 restart: unless-stopped networks: [guacamole] guacamole: image: guacamole/guacamole:1.6.0 restart: unless-stopped depends_on: [postgres, guacd] environment: GUACD_HOSTNAME: guacd POSTGRESQL_ENABLED: "true" POSTGRESQL_HOSTNAME: postgres POSTGRESQL_DATABASE: guacamole_db POSTGRESQL_USERNAME: guacamole_user POSTGRESQL_PASSWORD: ${GUACAMOLE_DB_PASSWORD} WEBAPP_CONTEXT: ROOT ports: - "127.0.0.1:30090:8080" networks: [guacamole] networks: guacamole: volumes: postgres-data:
setup 模板设置了 WEBAPP_CONTEXT: ROOT,因此 Guacamole 直接位于 /。端口映射使用 127.0.0.1:30090:8080,宿主机上的 Web 端口只监听回环地址,浏览器通过 Caddy 访问。
30090 延续了 homelab-setup 使用高位端口的约定,前文的 code-server 对应 30080。这样可以减少与常见低位端口的冲突,也便于区分反向代理的目标服务。高位端口本身不提供安全保护,仍需保留回环地址绑定和 HTTPS 配置。手动编写 Compose 文件时,也应与脚本生成的路径和端口保持一致。
启动并检查容器:
bash12345cd "$HOME/workspace/services/docker/guacamole" docker compose up -d docker compose ps curl -I http://127.0.0.1:30090 docker compose logs --tail=100
六、通过 Caddy 提供 HTTPS
浏览器远程桌面和 code-server 一样,需要一个稳定的 HTTPS 域名。实际部署时将示例域名替换成自己的域名:
本文沿用前文的证书签发方式:由 Caddy 通过阿里云 DNS-01 完成域名验证和 SSL 证书签发,alidns 插件读取阿里云 DNS API 凭据。也就是说,下面的配置适用于当前使用阿里云 DNS 的环境;如果改用 Cloudflare、腾讯云或其他 DNS 服务商,需要将 alidns 插件和对应的凭据配置一并替换,不能只修改域名。
caddy12345678910desktop.example.com { tls { dns alidns { access_key_id {env.ALIYUN_ACCESS_KEY_ID} access_key_secret {env.ALIYUN_ACCESS_KEY_SECRET} } } reverse_proxy 127.0.0.1:30090 }
bash12sudo caddy validate --config /etc/caddy/Caddyfile sudo systemctl reload caddy
Caddy 原生支持 WebSocket 反向代理,不需要手动添加 Upgrade 和 Connection 请求头。访问 https://desktop.example.com/ 后,应该能看到 Guacamole 登录页。
七、首次登录与 RDP 连接
初始化数据库后,Guacamole 的初始管理员账号是 guacadmin,初始密码也是 guacadmin。首次登录后,应立即新建一个自己的管理员账号并授予全部管理权限;退出后用新账号重新登录,确认权限和连接配置都正常,再删除默认的 guacadmin 账号。管理密码、RDP 密码和 DNS API 凭据不要复用,也不要把默认账号留在长期运行的实例中。
在 Settings → Connections → New Connection 中创建连接:
text12Name: Debian XFCE Protocol: RDP
连接由 guacd 容器发起,因此不能填写 127.0.0.1。对 guacd 来说,这个地址指向的是容器自身,而不是 Debian 宿主机。这里应该填写 Debian 在局域网中的地址:
text12Hostname: YOUR_DEBIAN_LAN_IP Port: 3389
第一次连接可以使用 Security mode: Any,并临时启用 Ignore server certificate,以兼容 xrdp 默认自签名证书。长期运行时应结合实际证书和网络边界重新评估。
八、最容易踩的坑:RDP 不要误选成 VNC
这次排障中最有代表性的问题,是浏览器能够打开 Guacamole,点击连接后却一直停留在“已连接,等待应答”。guacd 日志显示它实际创建的是:
text1Creating new client for protocol "vnc"
而目标端口是 xrdp 的 3389,链路就变成了:
text1Guacamole ── VNC ──→ Debian:3389 ──→ xrdp
协议不匹配时,xrdp 日志会出现 X.224 握手失败。这不是 xrdp 本身损坏,而是连接类型选错。重新把连接类型改为 RDP,并确认日志出现:
text1Creating new client for protocol "rdp"
这是判断协议是否正确的最快方法。
九、确认 WebSocket 和会话复用
连接建立后,可以在浏览器开发者工具的 Network 面板中搜索 websocket 或 tunnel,确认 /websocket-tunnel 返回 101 Switching Protocols。这说明浏览器、Caddy 和 Guacamole 之间的 WebSocket 链路正常。
正确使用 RDP 后,Guacamole 可以复用此前已经存在的远程 XFCE 会话。这次部署没有修改已经稳定的 /etc/xrdp/startwm.sh,也没有额外创建一套桌面环境。
十、显示、动态分辨率与剪贴板
这个浏览器入口主要用于 ChatGPT、Obsidian、浏览器和终端,连接参数应优先保证文字可读性和窗口适配:
text1234567891011Resize method: Display update Color depth: 32-bit DPI: 96 Clipboard: Bidirectional Font smoothing: Enabled Theming: Enabled Wallpaper: Disabled Animations: Disabled Full-window drag: Disabled Drive redirection: Disabled unless needed Audio: Enable only if needed
Display update 会在浏览器窗口大小变化时请求远程桌面调整分辨率。Guacamole 使用 Canvas 显示远程画面,清晰度不一定完全等同于原生 RDP 客户端;浏览器缩放建议保持 100%,再根据实际显示器测试 DPI。
十一、最终访问体系
text12345原生 RDP 客户端 ─────────────→ xrdp ──→ XFCE 浏览器 ─→ HTTPS ─→ Caddy ─→ Guacamole ─→ RDP ─→ xrdp ──→ XFCE 浏览器 ─→ HTTPS ─→ Caddy ─→ code-server
日常长时间使用时,我还是更倾向于原生 RDP,画质和延迟表现更好;临时设备、平板或无法安装客户端时,再通过 Guacamole 连接。
十二、维护与安全边界
bash12345678cd "$HOME/workspace/services/docker/guacamole" docker compose ps docker compose logs --tail=100 docker compose logs -f guacd docker compose restart docker compose down docker compose up -d docker system df
长期运行时需要注意:PostgreSQL volume 保存用户、连接、权限和连接参数,必须纳入备份;.env 权限应保持为 600;30090 继续只监听 127.0.0.1;真实客户端 IP 经过 Caddy 和 Docker bridge 后可能被代理地址替代,审计和暴力破解防护需要结合实际代理链路验证。
参考
支付宝
微信