本文将告诉大家如何将 Avalonia 应用部署到 Docker 容器中,使用 Avalonia 作为渲染端,通过离屏渲染输出图像。我将在本文分享在这个过程中遇到的核心坑点以及对应的解决方案
本文内容由 AI 辅助编写
背景
我所在的团队正在使用 Avalonia 作为跨平台的渲染引擎,期望把渲染逻辑放进 Docker 容器里,通过 Web 服务对外提供绘图能力。Avalonia 本身依赖 X11 等桌面环境,而 Docker 容器通常是无桌面的,这就带来了几个挑战。经过一番折腾,终于让 Avalonia 在 Docker 里跑了起来,这里记录一下关键步骤。
核心坑点主要有三个:
- 需要为容器创建虚拟的 X11 环境(通过
xvfb) - 容器内缺少中文字体,导致文字变成豆腐块
- 要使用
OffscreenTopLevelImplBase实现离屏渲染,并且一定得记得设置ClientSize尺寸
下面我按照实际解决问题的逻辑顺序展开说明。
一、准备 Docker 基础镜像
为了方便,我会在一份 Dockerfile 里同时完成基础环境、.NET 运行时、X11 和字体的安装。完整文件在文末给出,这里先说核心部分。
首先,我们需要一个基于 Ubuntu 的镜像,安装 xvfb 来模拟出 X11 Display 环境。
FROM ubuntu:24.04
# 安装 xvfb 作为虚拟 X 服务器
RUN apt-get install -y xvfb
ENV DISPLAY=:0
后续启动容器时,Avalonia 应用会通过 DISPLAY 环境变量连接到这个虚拟显示设备。
安装 .NET 运行时时可以只安装 aspnetcore-runtime,因为我们的应用会通过 ASP.NET Core 暴露 HTTP 接口。当然,你也可以换成 dotnet-runtime,看实际需要。
RUN apt-get install -y aspnetcore-runtime-10.0
二、填补字体的黑洞
Avalonia 渲染文字依赖系统字体,而 Docker 官方镜像为了体积几乎什么都不带。如果直接跑,所有中文字符都会变成方框。我的做法是:
- 从包管理安装一部分常用字体
- 从 Windows 机器上把
C:\Windows\Fonts\下的ttf、ttc直接拷贝进镜像(这句是重点!)
# 安装一些基础字体
RUN apt install -y fonts-ipafont fonts-noto fonts-ubuntu fonts-roboto
RUN fc-cache -fv
即便如此,默认的字体覆盖仍然不够。在我的实际项目里,还额外从网上下载了“文泉驿”等中文字体。更好的做法就是上面说的,直接从 Windows 字体目录复制,确保渲染效果与开发机一致。
另外,有些渲染效果依赖于 libgdiplus,也一并装好:
RUN apt-get install -y libgdiplus
此外,我还发现安装 inkscape 能间接带齐许多图形相关的依赖,省去一个个排查的麻烦:
RUN apt install -y inkscape
三、在容器中启动 X11 环境
即使 Dockerfile 里安装了 xvfb,程序运行时也需要主动拉起虚拟显示服务。我在 AvaloniaApp 项目中写了一个 LinuxDockerEnvironmentHelper 来负责这件事。
这个工具类会检查当前是否运行在 Docker 中(通过环境变量 DOTNET_RUNNING_IN_CONTAINER 判断),如果是,则用 Process.Start 启动 Xvfb,并等待它就绪。
static class LinuxDockerEnvironmentHelper
{
public static void EnsureX11Ready()
{
if (OperatingSystem.IsLinux() && IsRunningInDocker())
{
var display = Environment.GetEnvironmentVariable("DISPLAY");
if (display is null)
throw new InvalidOperationException("找不到 DISPLAY 环境变量");
Process.Start("Xvfb", [display, "-screen", "0", "1920x1080x24"]);
// 等待 X11 准备好,通过 P/Invoke 检测
var stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed < TimeSpan.FromSeconds(5))
{
var displayHandler = XOpenDisplay(IntPtr.Zero);
if (displayHandler != 0)
{
XCloseDisplay(displayHandler);
return;
}
Thread.SpinWait(10);
}
throw new NotSupportedException("无法启动 X11 环境");
}
}
// 使用 libX11 判断显示是否可用
[DllImport("libX11.so.6")]
private static extern IntPtr XOpenDisplay(IntPtr display);
[DllImport("libX11.so.6")]
private static extern int XCloseDisplay(IntPtr display);
}
以上的一个小点在于,不能立刻启动 Xvfb 的时候就开始初始 Avalonia 框架,否则将会在底层因为无法打开设备而失败。这就是为什么以上代码需要在循环里面调用 XOpenDisplay 确定环境准备完成的原因
在 Avalonia 入口的 Main 方法最开始处调用这个助手,确保后续渲染管线能正常工作。
[STAThread]
public static void Main(string[] args)
{
LinuxDockerEnvironmentHelper.EnsureX11Ready();
// ...
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
四、使用离屏渲染输出图像
Avalonia 本来是为桌面应用设计的,但在 Docker 里我们不需要真正的窗口,而是想把某个控件渲染成图片。这个需求在我之前的博客 Avalonia 实现离屏渲染能力 里已经提过,当时在 Windows 上能够工作,但 Linux 下需要自己实现 ITopLevelImpl 接口。这里我们沿用同样的方式。
首先,在项目文件 AvaloniaApp.csproj 里添加以下属性,允许访问 Avalonia 内部还未稳定的 API:
<AvaloniaAccessUnstablePrivateApis>true</AvaloniaAccessUnstablePrivateApis>
然后定义一个 OffscreenTopLevelImpl 类,继承自 OffscreenTopLevelImplBase 并实现 ITopLevelImpl:
class OffscreenTopLevelImpl : OffscreenTopLevelImplBase, ITopLevelImpl
{
public override IEnumerable<object> Surfaces { get; } = [];
public override IMouseDevice MouseDevice { get; } = new MouseDevice();
}
特别注意:务必给 ClientSize 设置正确的尺寸,否则会渲染出一张空白图片! 这是多次踩坑后总结的血泪教训。
随后在 AppManager.TakeAsync 方法里,利用 EmbeddableControlRoot 承载我们的视图,并在 Loaded 事件里完成截图保存:
public async Task<string> TakeAsync()
{
await App.WaitAppLaunched();
var imageFilePath = Path.Join(Path.GetTempPath(), $"{Path.GetRandomFileName()}.png");
await Dispatcher.UIThread.InvokeAsync(async () =>
{
var offscreenTopLevelImpl = new OffscreenTopLevelImpl()
{
ClientSize = new Size(1000, 600) // 一定要设置尺寸!
};
var embeddableControlRoot = new EmbeddableControlRoot(offscreenTopLevelImpl)
{
Width = 1000,
Height = 600
};
var mainView = new MainView();
mainView.Loaded += (sender, args) =>
{
using var renderTargetBitmap = new RenderTargetBitmap(new PixelSize(1000, 600));
renderTargetBitmap.Render(mainView);
renderTargetBitmap.Save(imageFilePath);
taskCompletionSource.SetResult();
};
embeddableControlRoot.Content = mainView;
embeddableControlRoot.Prepare(); // 此处会触发 Loaded 事件
embeddableControlRoot.StartRendering();
await taskCompletionSource.Task;
embeddableControlRoot.StopRendering();
embeddableControlRoot.Dispose();
});
return imageFilePath;
}
这段代码的关键在于:必须先等待 Avalonia 的 UI 线程就绪(通过 App.WaitAppLaunched()),然后在 UI 线程中创建离屏渲染的“窗口”,把业务控件放进去,立刻渲染并保存为 PNG。
五、在 ASP.NET Core 中暴露接口
最后,我们用一个极简的 ASP.NET Core Web 应用把渲染能力暴露出去。Program.cs 里只加一个 MapGet:
app.MapGet("/", async () =>
{
AppManager appManager = new();
var imageFilePath = await appManager.TakeAsync();
return Results.File(imageFilePath, "image/png");
});
这样,整个链路就通了:Docker 容器启动 → 拉起 Xvfb 虚拟显示 → Avalonia 应用初始化(包含字体、界面框架)→ Web 请求到来时进行离屏渲染并返回图片。
完整 Dockerfile 和项目代码请按照下文给出的所有代码的下载方法进行下载。你也可以根据自己的需要调整尺寸、字体和渲染逻辑。
代码
本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 4fc6e1c6c9511c935bae33186fff35b32101077c
以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 4fc6e1c6c9511c935bae33186fff35b32101077c
获取代码之后,进入 AvaloniaIDemo/JolehenilegeKurhenokoyarfai 文件夹,即可获取到源代码
更多技术博客,请参阅 博客导航
本文会经常更新,请阅读原文: https://blog.lindexi.com/post/Avalonia-%E5%9C%A8-Docker-%E5%AE%B9%E5%99%A8%E4%B8%AD%E9%80%9A%E8%BF%87-xvfb-%E8%BF%90%E8%A1%8C%E7%9A%84%E6%96%B9%E6%B3%95.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航
本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:
https://blog.lindexi.com
),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请
与我联系
。
无盈利,不卖课,做纯粹的技术博客
以下是广告时间
推荐关注 Edi.Wang 的公众号
欢迎进入 Eleven 老师组建的 .NET 社区
以上广告全是友情推广,无盈利