.NET 8+wkhtmltopdf部署Docker时出现Pipe Broken错误求解决
问题:Docker环境下wkhtmltopdf转换PDF抛出“Pipe is broken”异常
本地Windows环境使用wkhtmltopdf将cshtml文件转换为PDF完全正常,但部署到Docker容器后,调用generatePdf.GetPDF(html)时抛出Pipe is broken异常。
相关环境与代码
1. Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base RUN apt-get update && apt-get install -y --no-install-recommends wget ca-certificates fontconfig libc6 libfreetype6 libjpeg62-turbo libpng16-16 libssl3 libstdc++6 libx11-6 libxcb1 libxext6 libxrender1 xfonts-75dpi xfonts-base zlib1g RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb RUN dpkg -i wkhtmltox_0.12.6.1-3.bookworm_amd64.deb WORKDIR /app EXPOSE 8080 EXPOSE 8081 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ["nuget.config", "."] COPY ["src/Application/Application.csproj", "src/Application/"] COPY ["src/Application/Rotativa", "src/Application/Rotativa"] COPY ["src/Services/Services.csproj", "src/Services/"] RUN dotnet restore "./src/Application/Application.csproj" COPY . . WORKDIR "/src/src/Application" RUN dotnet build "./Application.csproj" -c $BUILD_CONFIGURATION -o /app/build FROM build AS publish ARG BUILD_CONFIGURATION=Release RUN dotnet publish "./Application.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . RUN chmod -R 755 /app/Rotativa/ ENTRYPOINT ["dotnet", "Application.dll"]
2. PDF生成类核心代码
using RazorEngineCore; using Services.Features.TransferRequest.Models.Interfaces; using Services.Features.TransferRequest.Repositories; using Shared.Infrastructure.Interfaces; using Shared.Infrastructure.ManipulateFiles; using System.Collections.Concurrent; using System.Xml.Linq; using Wkhtmltopdf.NetCore; using Wkhtmltopdf.NetCore.Options; namespace Services.Features.TransferRequest.UseCases.Services; public class PdfGeneratorService(IRazorEngine razorEngine, IGeneratePdf generatePdf, ILoggerActionEnrichService logger, IFilesReader filesReader) : IPdfGeneratorService { private readonly string _namespace = $"{typeof(TransferRequestModule).Namespace}.Models.Templates"; public async Task<MemoryStream> GeneratePdfAsync<TModel>(TModel model, string fileName) { try { var template = await filesReader.ReadFileWithMemoryFirstAsync(_namespace, fileName, TypeArchives.CSHTML); var html = await GenerateHtmlAsync<TModel>(model, template); return await GeneratePdf(html); } catch (Exception ex) { logger.Add($"Problemas para gerar o pdf \n Message: {ex.Message}"); logger.Add($"StackTrace: {ex.StackTrace}"); logger.Add($"InnerException: {ex.InnerException}"); logger.Add($"Exception: {ex}"); throw; } } private async Task<MemoryStream> GeneratePdf(string html) { generatePdf.SetConvertOptions(new ConvertOptions { PageMargins = new Margins(0, 0, 0, 0), }); var pdf = await Task.Run(() => generatePdf.GetPDF(html)); var pdfStream = new MemoryStream(); await pdfStream.WriteAsync(pdf, 0, pdf.Length); pdfStream.Position = 0; return pdfStream; } private async Task<string> GenerateHtmlAsync<TModel>(TModel model, string template) => await (await razorEngine.CompileAsync<RazorEngineTemplateBase<TModel>>(template)) .RunAsync(instance => { instance.Model = model; }); }
3. 报错位置及堆栈信息
报错行:
var pdf = await Task.Run(() => generatePdf.GetPDF(html));
堆栈信息:
StackTrace: at System.IO.Pipes.PipeStream.CheckWriteOperations() at System.IO.StreamWriter.Flush(Boolean flushStream, Boolean flushEncoder) at System.IO.StreamWriter.Dispose(Boolean disposing) at System.IO.TextWriter.Dispose() at Wkhtmltopdf.NetCore.WkhtmlDriver.Convert(String wkhtmlPath, String switches, String html) at Wkhtmltopdf.NetCore.GeneratePdf.GetPDF(String html) at Services.Features.TransferRequest.UseCases.Services.PdfGeneratorService.<>c__DisplayClass7_0.<GeneratePdf>b__0() in /src/src/Services/Features/TransferRequest/UseCases/Services/PdfGeneratorService.cs:line 42 at System.Threading.Tasks.Task`1.InnerInvoke() at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location --- at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) --- End of stack trace from previous location --- at Services.Features.TransferRequest.UseCases.Services.PdfGeneratorService.GeneratePdf(String html) in /src/src/Services/Features/TransferRequest/UseCases/Services/PdfGeneratorService.cs:line 42 at Services.Features.TransferRequest.UseCases.Services.PdfGeneratorService.GeneratePdfAsync[TModel](TModel model, String fileName) in /src/src/Services/Features/TransferRequest/UseCases/Services/PdfGeneratorService.cs:line 23
解决方案
方案1:确保wkhtmltopdf路径与权限正确
- 显式指定wkhtmltopdf路径(容器内默认安装路径为
/usr/local/bin/wkhtmltopdf):private async Task<MemoryStream> GeneratePdf(string html) { generatePdf.SetWkhtmlPath("/usr/local/bin/wkhtmltopdf"); // 添加此行 generatePdf.SetConvertOptions(new ConvertOptions { PageMargins = new Margins(0, 0, 0, 0), }); // ... 其余代码 } - 在Dockerfile的final阶段添加权限配置:
RUN chmod +x /usr/local/bin/wkhtmltopdf
方案2:禁用沙箱模式并移除多余线程包装
Linux环境下wkhtmltopdf的沙箱模式易引发管道异常,同时Task.Run属于多余包装,会导致线程上下文切换问题:
private async Task<MemoryStream> GeneratePdf(string html) { generatePdf.SetConvertOptions(new ConvertOptions { PageMargins = new Margins(0, 0, 0, 0), CustomSwitches = "--no-sandbox" // 禁用沙箱 }); var pdf = generatePdf.GetPDF(html); // 直接调用,无需Task.Run var pdfStream = new MemoryStream(); await pdfStream.WriteAsync(pdf, 0, pdf.Length); pdfStream.Position = 0; return pdfStream; }
方案3:检查HTML编码一致性
确保生成的HTML为UTF-8编码,避免特殊字符导致管道写入失败:
private async Task<MemoryStream> GeneratePdf(string html) { // 统一编码为UTF-8 var utf8Html = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(html)); generatePdf.SetConvertOptions(new ConvertOptions { PageMargins = new Margins(0, 0, 0, 0), CustomSwitches = "--no-sandbox" }); var pdf = generatePdf.GetPDF(utf8Html); var pdfStream = new MemoryStream(); await pdfStream.WriteAsync(pdf, 0, pdf.Length); pdfStream.Position = 0; return pdfStream; }
内容的提问来源于stack exchange,提问作者Paulo Enrique
相关产品推荐
相关产品推荐

