如何在Ubuntu20.04上使用Docker和Caddy远程访问GUI应用程序
作为 Write for DOnations 计划的一部分,作者选择了 Free and Open Source Fund 来接受捐赠。
介绍
即使随着云服务的日益普及,运行本机应用程序的需求仍然存在。
通过使用 noVNC 和 TigerVNC,您可以在 Docker 容器内运行本机应用程序,并使用 Web 浏览器远程访问它们。 此外,您可以在具有比本地可用资源更多的系统资源的服务器上运行您的应用程序,这可以在运行大型应用程序时提供更大的灵活性。
在本教程中,您将使用 Docker 将电子邮件客户端 Mozilla Thunderbird 容器化。 之后,您将使用 Caddy Web 服务器保护它并提供远程访问。
完成后,您只需使用网络浏览器即可从任何设备访问 Thunderbird。 或者,您还可以使用 WebDAV 从本地访问文件。 您还将拥有一个完全独立的 Docker 映像,可以在任何地方运行。
先决条件
在开始本指南之前,您需要以下内容:
- 一台 Ubuntu 20.04 服务器,至少 2GB RAM 和 4GB 磁盘空间。
- 具有
sudo
权限的非 root 用户。 - Docker 在您的服务器上设置。 您可以关注如何在Ubuntu 20.04上安装和使用Docker。
第 1 步 — 创建 supervisord
配置
现在您的服务器正在运行并且 Docker 已安装,您可以开始配置应用程序的容器了。 由于您的容器由多个组件组成,因此您需要使用流程管理器来启动和监控它们。 在这里,您将使用 supervisord。 supervisord
是一个用 Python 编写的流程管理器,通常用于编排复杂的容器。
首先,为您的容器创建并输入一个名为 thunderbird
的目录:
mkdir ~/thunderbird cd ~/thunderbird
现在使用 nano
或您喜欢的编辑器创建并打开一个名为 supervisord.conf
的文件:
nano ~/thunderbird/supervisord.conf
现在将第一段代码添加到 supervisord.conf
中,它将定义 supervisord 的全局选项:
~/thunderbird/supervisord.conf
[supervisord] nodaemon=true pidfile=/tmp/supervisord.pid logfile=/dev/fd/1 logfile_maxbytes=0
在此块中,您正在配置 supervisord
本身。 您需要将 nodaemon
设置为 true
因为它将在 Docker 容器内作为入口点运行。 因此,您希望它保持在前台运行。 您还将 pidfile
设置为非 root 用户可访问的路径(稍后将详细介绍),并将 logfile
设置为标准输出,以便您查看日志。
接下来,将另一个小代码块添加到 supervisord.conf
。 此块启动 TigerVNC,它是一个组合的 VNC/X11 服务器:
~/thunderbird/supervisord.conf
... [program:x11] priority=0 command=/usr/bin/Xtigervnc -desktop "Thunderbird" -localhost -rfbport 5900 -SecurityTypes None -AlwaysShared -AcceptKeyEvents -AcceptPointerEvents -AcceptSetDesktopSize -SendCutText -AcceptCutText :0 autorestart=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true
在此块中,您将设置 X11 服务器。 X11 是一个显示服务器协议,它允许 GUI 应用程序运行。 请注意,将来它将被 Wayland 取代,但远程访问仍在开发中。
对于此容器,您使用的是 TigerVNC 及其内置的 VNC 服务器。 与使用单独的 X11 和 VNC 服务器相比,这具有许多优点:
- 更快的响应时间,因为 GUI 绘图直接在 VNC 服务器上完成,而不是在中间帧缓冲区(存储屏幕内容的内存)上完成。
- 自动调整屏幕大小,允许远程应用程序自动调整大小以适应客户端(在本例中为您的 Web 浏览器窗口)。
如果您愿意,可以将 -desktop
选项的参数从 Thunderbird
更改为您选择的其他参数。 服务器会将您的选择显示为用于访问您的应用程序的网页的标题。
现在,让我们在 supervisord.conf
中添加第三个代码块来启动 easy-novnc
:
~/thunderbird/supervisord.conf
... [program:easy-novnc] priority=0 command=/usr/local/bin/easy-novnc --addr :8080 --host localhost --port 5900 --no-url-password --novnc-params "resize=remote" autorestart=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true
在此块中,您将设置 easy-novnc,这是一个独立的服务器,它提供了 noVNC 的包装器。 该服务器执行两个角色。 首先,它提供了一个简单的连接页面,允许您配置连接选项,并允许您设置默认选项。 其次,它通过 WebSocket 代理 VNC,允许通过普通的 Web 浏览器访问它。
通常,调整大小是在客户端完成的(即 图像缩放),但您正在使用 resize=remote
选项来充分利用 TigerVNC 的远程分辨率调整。 这也为速度较慢的设备(例如低端 Chromebook)提供了更低的延迟:
注:本教程使用easy-novnc
。 如果您愿意,您可以使用 websockify
和单独的 Web 服务器。 easy-novnc
的优点是内存使用和启动时间明显更低,而且它是独立的。 easy-novnc
还提供比默认 noVNC 更清晰的连接页面,并允许设置有助于此设置的默认选项(例如 resize=remote
)。
现在将以下块添加到您的配置中以启动窗口管理器 OpenBox:
~/thunderbird/supervisord.conf
... [program:openbox] priority=1 command=/usr/bin/openbox environment=DISPLAY=:0 autorestart=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true
在此块中,您将设置 OpenBox,一个轻量级的 X11 窗口管理器。 您可以跳过此步骤,但没有它,您将没有标题栏或无法调整窗口大小。
最后,让我们将最后一个块添加到 supervisord.conf
,这将启动主应用程序:
~/thunderbird/supervisord.conf
... [program:app] priority=1 environment=DISPLAY=:0 command=/usr/bin/thunderbird autorestart=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true
在最后一个块中,您将 priority
设置为 1
以确保 Thunderbird 在 TigerVNC 之后启动 ,否则它将遇到竞争条件并随机启动失败。 我们还设置了 autorestart=true
以在应用程序错误关闭时自动重新打开它。 DISPLAY
环境变量告诉应用程序显示在您之前设置的 VNC 服务器上。
这是您完成的 supervisord.conf
的样子:
~/thunderbird/supervisord.conf
[supervisord] nodaemon=true pidfile=/tmp/supervisord.pid logfile=/dev/fd/1 logfile_maxbytes=0 [program:x11] priority=0 command=/usr/bin/Xtigervnc -desktop "Thunderbird" -localhost -rfbport 5900 -SecurityTypes None -AlwaysShared -AcceptKeyEvents -AcceptPointerEvents -AcceptSetDesktopSize -SendCutText -AcceptCutText :0 autorestart=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true [program:easy-novnc] priority=0 command=/usr/local/bin/easy-novnc --addr :8080 --host localhost --port 5900 --no-url-password --novnc-params "resize=remote" autorestart=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true [program:openbox] priority=1 command=/usr/bin/openbox environment=DISPLAY=:0 autorestart=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true [program:app] priority=1 environment=DISPLAY=:0 command=/usr/bin/thunderbird autorestart=true stdout_logfile=/dev/fd/1 stdout_logfile_maxbytes=0 redirect_stderr=true
如果要容器化不同的应用程序,请将 /usr/bin/thunderbird
替换为应用程序可执行文件的路径。 否则,您现在可以配置 GUI 的主菜单了。
第 2 步 — 设置 OpenBox 菜单
现在您的流程管理器已配置完毕,让我们设置 OpenBox 菜单。 这个菜单允许我们在容器内启动应用程序。 如果需要,我们还将包括一个用于调试的终端和进程监视器。
在您的应用程序目录中,使用 nano
或您喜欢的文本编辑器创建并打开一个名为 menu.xml
的新文件:
nano ~/thunderbird/menu.xml
现在将以下代码添加到 menu.xml
:
〜/雷鸟/menu.xml
<?xml version="1.0" encoding="utf-8"?> <openbox_menu xmlns="http://openbox.org/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://openbox.org/ file:///usr/share/openbox/menu.xsd"> <menu id="root-menu" label="Openbox 3"> <item label="Thunderbird"> <action name="Execute"> <execute>/usr/bin/thunderbird</execute> </action> </item> <item label="Terminal"> <action name="Execute"> <execute>/usr/bin/x-terminal-emulator</execute> </action> </item> <item label="Htop"> <action name="Execute"> <execute>/usr/bin/x-terminal-emulator -e htop</execute> </action> </item> </menu> </openbox_menu>
此 XML 文件包含右键单击桌面时将出现的菜单项。 每个项目由一个标签和一个动作组成。
如果要容器化不同的应用程序,请将 /usr/bin/thunderbird
替换为应用程序可执行文件的路径,并更改项目的 label
。
第 3 步 - 创建 Dockerfile
现在 OpenBox 已配置好,您将创建 Dockerfile,它将所有内容联系在一起。
在容器目录中创建一个 Dockerfile:
nano ~/thunderbird/Dockerfile
首先,让我们添加一些代码来构建 easy-novnc
:
〜/雷鸟/Dockerfile
FROM golang:1.14-buster AS easy-novnc-build WORKDIR /src RUN go mod init build && \ go get github.com/geek1011/easy-novnc@v1.1.0 && \ go build -o /bin/easy-novnc github.com/geek1011/easy-novnc
在第一阶段,您正在构建 easy-novnc
。 为了简单和节省空间,这是在一个单独的阶段完成的——你不需要在最终图像中使用整个 Go 工具链。 请注意构建命令中的 @v1.1.0
。 这确保了结果是确定性的,这很重要,因为 Docker 缓存了每一步的结果。 如果您没有指定明确的版本,Docker 将在首次构建映像时引用 easy-novnc
的最新版本。 此外,您要确保下载特定版本的 easy-novnc
,以防对 CLI 界面进行重大更改。
现在让我们创建第二个阶段,它将成为最终图像。 在这里,您将使用 Debian 10 (buster) 作为基础镜像。 请注意,由于它在容器中运行,因此无论您在服务器上运行的发行版如何,它都可以工作。
接下来,将以下块添加到您的 Dockerfile
:
〜/雷鸟/Dockerfile
... FROM debian:buster RUN apt-get update -y && \ apt-get install -y --no-install-recommends openbox tigervnc-standalone-server supervisor gosu && \ rm -rf /var/lib/apt/lists && \ mkdir -p /usr/share/desktop-directories
在本说明中,您将安装 Debian 10 作为基础映像,然后安装在容器中运行 GUI 应用程序所需的最低限度。 请注意,您将 apt-get update
作为同一指令的一部分运行,以防止 Docker 出现缓存问题。 为了节省空间,您还删除了之后下载的包列表(缓存的包本身被 默认删除 )。 您还在创建 /usr/share/desktop-directories
因为某些应用程序依赖于现有目录。
让我们添加另一个小代码块:
〜/雷鸟/Dockerfile
... RUN apt-get update -y && \ apt-get install -y --no-install-recommends lxterminal nano wget openssh-client rsync ca-certificates xdg-utils htop tar xzip gzip bzip2 zip unzip && \ rm -rf /var/lib/apt/lists
在本说明中,您将安装一些有用的通用实用程序和软件包。 这里特别感兴趣的是 xdg-utils
(它提供 Linux 上桌面应用程序使用的基本命令)和 ca-certificates
(它安装根证书以允许我们访问 HTTPS 站点)。
现在,我们可以添加主应用程序的说明:
〜/雷鸟/Dockerfile
... RUN apt-get update -y && \ apt-get install -y --no-install-recommends thunderbird && \ rm -rf /var/lib/apt/lists
和以前一样,我们在这里安装应用程序。 如果您正在容器化不同的应用程序,您可以将这些命令替换为安装特定应用程序所需的命令。 一些应用程序需要更多的工作才能在 Docker 中运行。 例如,如果您要安装使用 Chrome、Chromium 或 QtWebEngine 的应用程序,则需要使用命令行参数 --no-sandbox
,因为 Docker 不支持它。
接下来,让我们开始添加指令以将最后几个文件添加到容器中:
〜/雷鸟/Dockerfile
... COPY --from=easy-novnc-build /bin/easy-novnc /usr/local/bin/ COPY menu.xml /etc/xdg/openbox/ COPY supervisord.conf /etc/ EXPOSE 8080
在这里,您将之前创建的配置文件添加到映像中,并从第一阶段复制 easy-novnc
二进制文件。
下一个代码块创建数据目录并为您的应用程序添加一个专用用户。 这很重要,因为某些应用程序拒绝以 root 身份运行。 即使在容器中也不以 root 身份运行应用程序也是一种很好的做法。
〜/雷鸟/Dockerfile
... RUN groupadd --gid 1000 app && \ useradd --home-dir /data --shell /bin/bash --uid 1000 --gid 1000 app && \ mkdir -p /data VOLUME /data
为了确保文件的 UID/GID
一致,您将两者都显式设置为 1000
。 您还在数据目录上安装了一个卷,以确保它在重新启动之间保持不变。
最后,让我们添加启动所有内容的说明:
〜/雷鸟/Dockerfile
... CMD ["sh", "-c", "chown app:app /data /dev/stdout && exec gosu app supervisord"]
通过将默认命令设置为 supervisord
,管理器将启动运行应用程序所需的进程。 在这种情况下,您使用的是 CMD
而不是 ENTRYPOINT
。 在大多数情况下,它不会产生影响,但出于几个原因,使用 CMD
更适合此目的。 首先,supervisord
不接受任何与我们相关的参数,如果您向容器提供参数,它们将替换 CMD
并附加到 ENTRYPOINT
。 其次,使用 CMD
允许我们在向容器传递参数时提供完全不同的命令(将由 /bin/sh -c
执行),这使得调试更容易。
最后,您需要在启动supervisord
之前以root身份运行chown
,以防止数据卷的权限问题并允许子进程打开stdout
。 这也意味着您需要使用 gosu
而不是 USER
指令来切换用户。
这是您完成的 Dockerfile
的样子:
〜/雷鸟/Dockerfile
FROM golang:1.14-buster AS easy-novnc-build WORKDIR /src RUN go mod init build && \ go get github.com/geek1011/easy-novnc@v1.1.0 && \ go build -o /bin/easy-novnc github.com/geek1011/easy-novnc FROM debian:buster RUN apt-get update -y && \ apt-get install -y --no-install-recommends openbox tigervnc-standalone-server supervisor gosu && \ rm -rf /var/lib/apt/lists && \ mkdir -p /usr/share/desktop-directories RUN apt-get update -y && \ apt-get install -y --no-install-recommends lxterminal nano wget openssh-client rsync ca-certificates xdg-utils htop tar xzip gzip bzip2 zip unzip && \ rm -rf /var/lib/apt/lists RUN apt-get update -y && \ apt-get install -y --no-install-recommends thunderbird && \ rm -rf /var/lib/apt/lists COPY --from=easy-novnc-build /bin/easy-novnc /usr/local/bin/ COPY menu.xml /etc/xdg/openbox/ COPY supervisord.conf /etc/ EXPOSE 8080 RUN groupadd --gid 1000 app && \ useradd --home-dir /data --shell /bin/bash --uid 1000 --gid 1000 app && \ mkdir -p /data VOLUME /data CMD ["sh", "-c", "chown app:app /data /dev/stdout && exec gosu app supervisord"]
保存并关闭您的 Dockerfile
。 现在我们准备好构建和运行我们的容器,然后访问 Thunderbird — 一个 GUI 应用程序。
第 4 步 - 构建和运行容器
下一步是构建您的容器并将其设置为在启动时运行。 您还将设置一个卷以在重新启动和更新之间保留应用程序数据。
首先构建你的容器。 确保在 ~/thunderbird
目录中运行这些命令:
docker build -t thunderbird .
现在创建一个将在应用程序的容器之间共享的新网络:
docker network create thunderbird-net
然后创建一个卷来存储应用程序数据:
docker volume create thunderbird-data
最后,运行它并将其设置为自动重启:
docker run --detach --restart=always --volume=thunderbird-data:/data --net=thunderbird-net --name=thunderbird-app thunderbird
请注意,如果需要,可以将 --name
选项后的 thunderbird-app
替换为不同的名称。 无论您选择什么,您的应用程序现在都已容器化并正在运行。 现在让我们使用 Caddy Web 服务器来保护它并远程连接到它。
第 5 步 — 设置 Caddy
在此步骤中,您将设置 Caddy Web 服务器以提供身份验证,并可选择通过 WebDAV 进行远程文件访问。 为简单起见,并允许您将它与现有的反向代理一起使用,您将在另一个容器中运行它。
创建一个新目录,然后在其中移动:
mkdir ~/caddy cd ~/caddy
现在使用 nano
或您喜欢的编辑器创建一个新的 Dockerfile
:
nano ~/caddy/Dockerfile
然后添加以下指令:
~/caddy/Dockerfile
FROM golang:1.14-buster AS caddy-build WORKDIR /src RUN echo 'module caddy' > go.mod && \ echo 'require github.com/caddyserver/caddy/v2 v2.1.1' >> go.mod && \ echo 'require github.com/mholt/caddy-webdav v0.0.0-20200523051447-bc5d19941ac3' >> go.mod RUN echo 'package main' > caddy.go && \ echo 'import caddycmd "github.com/caddyserver/caddy/v2/cmd"' >> caddy.go && \ echo 'import _ "github.com/caddyserver/caddy/v2/modules/standard"' >> caddy.go && \ echo 'import _ "github.com/mholt/caddy-webdav"' >> caddy.go && \ echo 'func main() { caddycmd.Main() }' >> caddy.go RUN go build -o /bin/caddy . FROM debian:buster RUN apt-get update -y && \ apt-get install -y --no-install-recommends gosu && \ rm -rf /var/lib/apt/lists COPY --from=caddy-build /bin/caddy /usr/local/bin/ COPY Caddyfile /etc/ EXPOSE 8080 RUN groupadd --gid 1000 app && \ useradd --home-dir /data --shell /bin/bash --uid 1000 --gid 1000 app && \ mkdir -p /data VOLUME /data WORKDIR /data CMD ["sh", "-c", "chown app:app /data && exec gosu app /usr/local/bin/caddy run -adapter caddyfile -config /etc/Caddyfile"]
此 Dockerfile 在启用 WebDAV 插件的情况下构建 Caddy,然后在 /etc/Caddyfile
的 Caddyfile
端口 8080
上启动它。 保存并关闭文件。
接下来,您将配置 Caddy Web 服务器。 在刚刚创建的目录中创建一个名为 Caddyfile
的文件:
nano ~/caddy/Caddyfile
现在将以下代码块添加到您的 Caddyfile
:
〜/球童/球童文件
{ order webdav last } :8080 { log root * /data reverse_proxy thunderbird-app:8080 handle_path /files/* { file_server browse } redir /files /files/ handle /webdav/* { webdav { prefix /webdav } } redir /webdav /webdav/ basicauth /* { {env.APP_USERNAME} {env.APP_PASSWORD_HASH} } }
此 Caddyfile
将根目录代理到您在步骤 4 中创建的 thunderbird-app
容器(Docker 将其解析为正确的 IP)。 它还将在 /files
上提供基于 Web 的只读文件浏览器,并在 /webdav
上运行 WebDAV 服务器,您可以将其安装在本地以访问您的文件。 用户名和密码从环境变量 APP_USERNAME
和 APP_PASSWORD_HASH
中读取。
现在构建容器:
docker build -t thunderbird-caddy .
Caddy v2 要求您散列您想要的密码。 运行以下命令并记住将 mypass
替换为您选择的强密码:
docker run --rm -it thunderbird-caddy caddy hash-password -plaintext 'mypass'
该命令将输出一串字符。 将其复制到剪贴板以准备运行下一个命令。
现在您已准备好运行容器。 确保将 myuser
替换为您选择的用户名,并将 mypass-hash
替换为您在上一步中运行的命令的输出。 您还可以更改端口(此处为 8080
)以在不同的端口上访问您的服务器:
docker run --detach --restart=always --volume=thunderbird-data:/data --net=thunderbird-net --name=thunderbird-web --env=APP_USERNAME="myuser" --env=APP_PASSWORD_HASH="mypass-hash" --publish=8080:8080 thunderbird-caddy
我们现在可以访问和测试我们的应用程序了。
第 6 步 — 测试和管理应用程序
让我们访问您的应用程序并确保它正常工作。
首先,在网络浏览器中打开http://your_server_ip:8080
,使用您之前选择的凭据登录,然后单击连接。
您现在应该能够与应用程序交互,并且它应该会自动调整大小以适合您的浏览器窗口。
如果您右键单击黑色桌面,您应该会看到一个允许您访问终端的菜单。 如果您单击鼠标中键,您应该会看到一个窗口列表。
现在在网络浏览器中打开 http://your_server_ip:8080/files/
。 您应该能够访问您的文件。
或者,您可以尝试在 WebDAV 客户端中安装 http://your_server_ip:8080/webdav/
。 您应该能够直接访问和修改您的文件。 如果您在 Windows 资源管理器中使用 Map network drive 选项,您将需要使用反向代理来添加 HTTPS 或将 HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters\BasicAuthLevel
设置为 DWORD:2
。
无论哪种情况,您的本机 GUI 应用程序现在都可以远程使用了。
结论
您现在已经成功地为 Thunderbird 设置了一个 Docker 容器,然后使用 Caddy,您已经配置了通过 Web 浏览器访问它。 如果您需要升级您的应用程序,请停止容器,运行 docker rm thunderbird-app thunderbird-web
,重新构建映像,然后重新运行上述步骤中的 docker run
命令。 您的数据仍将保留,因为它存储在一个卷中。
如果你想了解更多关于基本 Docker 命令的信息,你可以阅读这个 tutorial 或 this cheatsheet。 对于长期使用,您可能还需要考虑启用 HTTPS(这需要域)以提高安全性。
此外,如果您要部署多个应用程序,您可能希望使用 Docker Compose 或 Kubernetes,而不是手动启动每个容器。 请记住,本教程可以作为在您的服务器上运行任何其他 Linux 应用程序的基础,包括:
最后一个选项展示了容器化和远程访问 GUI 应用程序的巨大潜力。 通过此设置,您现在可以使用计算能力比本地更多的服务器来运行 Cutter 等资源密集型工具。