DEV Community

IronSoftware
IronSoftware

Posted on

jsreport Docker Setup: The Complexity of Service Architecture for .NET

jsreport positions itself as a flexible reporting platform that converts HTML to PDF using Node.js and headless Chrome. For .NET teams, this means running a separate JavaScript-based service alongside your application. The official documentation acknowledges this architecture directly: "you should always consider pros and cons compared to running jsreport separately."

This article examines the real-world challenges .NET developers face when integrating jsreport: Docker container management, network latency for every conversion, cross-platform debugging, and the overhead of maintaining a Node.js service within a .NET-focused infrastructure. It also presents an alternative that eliminates the service boundary entirely.

Understanding the jsreport Architecture

jsreport operates as a standalone Node.js server that exposes HTTP endpoints for PDF generation. To convert HTML to PDF from a .NET application, you must:

  1. Deploy and maintain a jsreport Docker container (or install Node.js directly)
  2. Configure network connectivity between your .NET application and jsreport
  3. Send HTTP requests containing your HTML content
  4. Handle the PDF binary response
  5. Manage errors, timeouts, and retries across the network boundary

The basic Docker deployment requires:

docker run -p 5488:5488 jsreport/jsreport
Enter fullscreen mode Exit fullscreen mode

While this appears straightforward, production deployments reveal significant complexity.

Docker Configuration Challenges

Community discussions on the jsreport forum document recurring configuration issues that affect .NET teams.

Permission and User Mapping Problems

A common problem involves file system permissions when mounting volumes. Forum users report:

"The problem is that inside the container, node is executed by user jsReport that has uid 100 and gid 101. On Ubuntu, those uid/gid map to systemd-network systemd-journal that probably has no permission to write on the mounted folder."

The solution requires explicit user configuration in docker-compose:

version: "3.8"
services:
  jsreport:
    image: jsreport/jsreport
    user: jsreport
    ports:
      - "5488:5488"
    volumes:
      - ./data:/app/data
      - ./jsreport.config.json:/app/jsreport.config.json
Enter fullscreen mode Exit fullscreen mode

This works on some systems but fails on others depending on how Docker handles user namespace mapping.

Browser Launch Failures

A frequently reported error when running jsreport in Docker:

Failed to launch the browser process!
Running as root without --no-sandbox is not supported.
Enter fullscreen mode Exit fullscreen mode

The fix requires configuring Chrome launch options either in the jsreport configuration file or through the .NET client:

{
  "chrome": {
    "launchOptions": {
      "args": ["--no-sandbox", "--disable-setuid-sandbox"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, when using the jsreport .NET SDK:

JsReportChromePdf({
    launchOptions: {
        headless: true,
        args: ["--no-sandbox", "--disable-setuid-sandbox"]
    }
})
Enter fullscreen mode Exit fullscreen mode

The --no-sandbox flag disables Chrome's security sandbox, which some security teams prohibit in production environments.

Performance Degradation in Containers

Forum reports indicate significant performance differences between local and containerized deployments:

"On local machine, rendering of a PDF report with 13 pages takes ~5 sec. Inside docker container (with full machine resources access), the same report takes ~14 sec."

Additional performance issues arise from the headless: "new" Chrome configuration:

"The performance issue was related to using headless: new, with this turned on they saw consistently slower report generation times."

A production docker-compose configuration addressing these issues:

version: "3.8"
services:
  jsreport:
    image: jsreport/jsreport
    restart: unless-stopped
    ports:
      - "5488:5488"
    environment:
      - NODE_ENV=production
    volumes:
      - jsreport_data:/app/data
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: "2"
        reservations:
          memory: 2G
          cpus: "1"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5488/api/ping"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
volumes:
  jsreport_data:
Enter fullscreen mode Exit fullscreen mode

Each configuration parameter requires understanding both jsreport internals and container orchestration.

The .NET Integration Challenge

For .NET developers, integrating with jsreport means bridging two technology ecosystems.

SDK Installation and Configuration

The jsreport .NET SDK requires multiple packages:

dotnet add package jsreport.Binary
dotnet add package jsreport.Local
dotnet add package jsreport.Client
Enter fullscreen mode Exit fullscreen mode

Or when using ASP.NET Core MVC integration:

dotnet add package jsreport.AspNetCore
dotnet add package jsreport.Binary
dotnet add package jsreport.Local
Enter fullscreen mode Exit fullscreen mode

The configuration spans multiple files:

// Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddJsReport(new LocalReporting()
        .UseBinary(JsReportBinary.GetBinary())
        .AsUtility()
        .Create());
}
Enter fullscreen mode Exit fullscreen mode

For Docker-based deployments connecting to an external jsreport server:

services.AddJsReport(new ReportingService("http://jsreport:5488"));
Enter fullscreen mode Exit fullscreen mode

Azure Platform Limitations

The jsreport documentation explicitly warns about Azure limitations:

"Azure Web Apps running on Windows are very restrictive and don't allow running headless Chrome processes - meaning jsreport.Local won't be able to print PDFs in Azure Web Apps running on Windows."

The workaround requires switching to Linux containers:

"Azure Web Apps running in Docker with Linux hosts use a different sandboxing strategy where headless Chrome works."

This forces .NET teams to modify their deployment strategy to accommodate a JavaScript library's requirements.

Local Development vs Production Parity

The jsreport documentation recommends different approaches for development and production:

"You can always run full jsreport externally in another VM, Docker container, or external service like jsreportonline and connect to it from Azure Web App using jsreport.Client - this is usually better design in the era of micro-services."

This creates environment inconsistency where local development uses embedded jsreport.Local while production uses a separate jsreport server.

Network Latency Overhead

Every PDF conversion requires an HTTP round trip. Research on microservice architecture quantifies this overhead:

  • DNS resolution: ~0.1ms (cached)
  • TCP handshake: ~0.5ms (within datacenter)
  • TLS handshake: ~1.0ms (if using HTTPS)
  • HTTP headers: ~0.2ms
  • Request serialization: ~0.3ms
  • Network transmission: ~0.5ms
  • Response serialization: ~0.3ms
  • Response transmission: ~0.5ms
  • Response parsing: ~0.2ms

Total: approximately 3.0ms per call overhead, compared to ~0.001ms for a direct function call. This represents roughly 3,000x overhead for the network boundary alone.

For batch operations generating hundreds or thousands of PDFs, this latency compounds:

// jsreport approach - network call per conversion
public async Task<List<byte[]>> GenerateInvoicesBatch(List<InvoiceData> invoices)
{
    var client = new ReportingService("http://jsreport:5488");
    var results = new List<byte[]>();

    foreach (var invoice in invoices)
    {
        // Each conversion is a network round trip
        var report = await client.RenderAsync(new RenderRequest
        {
            Template = new Template
            {
                Content = BuildInvoiceHtml(invoice),
                Engine = Engine.Handlebars,
                Recipe = Recipe.ChromePdf
            }
        });

        using var ms = new MemoryStream();
        report.Content.CopyTo(ms);
        results.Add(ms.ToArray());
    }

    return results;
}
Enter fullscreen mode Exit fullscreen mode

Error Handling Complexity

Network-based architecture introduces failure modes that do not exist with in-process conversion:

public async Task<byte[]> GeneratePdfWithRetry(string html, int maxRetries = 3)
{
    var client = new ReportingService("http://jsreport:5488");

    for (int attempt = 0; attempt < maxRetries; attempt++)
    {
        try
        {
            var report = await client.RenderAsync(new RenderRequest
            {
                Template = new Template
                {
                    Content = html,
                    Engine = Engine.None,
                    Recipe = Recipe.ChromePdf
                }
            });

            using var ms = new MemoryStream();
            report.Content.CopyTo(ms);
            return ms.ToArray();
        }
        catch (HttpRequestException ex) when (attempt < maxRetries - 1)
        {
            // Network failure - wait and retry
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
        }
        catch (TaskCanceledException ex) when (attempt < maxRetries - 1)
        {
            // Timeout - jsreport may be overloaded
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
        }
    }

    throw new Exception("PDF generation failed after maximum retries");
}
Enter fullscreen mode Exit fullscreen mode

This retry logic, exponential backoff, and timeout handling adds code that would not exist with embedded conversion.

Memory and Resource Management

Running Chromium in Docker containers creates resource management challenges that the Puppeteer community has documented extensively.

Chrome Memory Leaks

Multiple GitHub issues document memory leak patterns:

"When using Puppeteer in a long-running process, server monitoring tools start reporting 'RAM is almost full'. Having lack of RAM on server is a terrible thing because it activates the operating system's out-of-memory killer, which starts killing processes randomly."

"A healthy pod typically runs 2-3 Chrome processes, but when things go wrong, dozens of orphaned Chrome instances can appear."

jsreport uses Puppeteer internally, inheriting these issues. The recommended Docker configuration includes aggressive memory limits:

deploy:
  resources:
    limits:
      memory: 4G
    reservations:
      memory: 2G
Enter fullscreen mode Exit fullscreen mode

Recommended Chrome Flags for Docker

Production deployments typically require these Chromium flags to manage memory:

{
  "chrome": {
    "launchOptions": {
      "args": [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-dev-shm-usage",
        "--disable-gpu",
        "--disk-cache-size=0",
        "--media-cache-size=0",
        "--disable-application-cache"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Each flag addresses a specific Docker environment limitation but may affect rendering behavior.

DevOps Overhead for .NET Teams

Running jsreport creates ongoing operational work.

Monitoring Requirements

A separate jsreport service needs its own monitoring:

# Prometheus scrape configuration
scrape_configs:
  - job_name: 'jsreport'
    static_configs:
      - targets: ['jsreport:5488']
    metrics_path: '/api/monitoring'
    scrape_interval: 15s
Enter fullscreen mode Exit fullscreen mode

Alert rules for common failure scenarios:

groups:
  - name: jsreport
    rules:
      - alert: JsReportHighLatency
        expr: jsreport_render_duration_seconds > 30
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "jsreport rendering slow"

      - alert: JsReportHighMemory
        expr: container_memory_usage_bytes{name="jsreport"} > 3500000000
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "jsreport memory usage critical"
Enter fullscreen mode Exit fullscreen mode

Version Management

jsreport releases update Chromium versions, which can change rendering behavior. Upgrading requires:

  1. Testing all templates against the new version
  2. Verifying visual output matches expectations
  3. Checking for breaking API changes
  4. Validating memory and performance characteristics

This creates a recurring maintenance task separate from your .NET application's version management.

Kubernetes Deployment Complexity

Production Kubernetes deployments require extensive configuration:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jsreport
spec:
  replicas: 3
  selector:
    matchLabels:
      app: jsreport
  template:
    metadata:
      labels:
        app: jsreport
    spec:
      containers:
        - name: jsreport
          image: jsreport/jsreport:4.0.0
          ports:
            - containerPort: 5488
          resources:
            requests:
              memory: "2Gi"
              cpu: "1000m"
            limits:
              memory: "4Gi"
              cpu: "2000m"
          livenessProbe:
            httpGet:
              path: /api/ping
              port: 5488
            initialDelaySeconds: 60
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /api/ping
              port: 5488
            initialDelaySeconds: 30
            periodSeconds: 10
          env:
            - name: NODE_ENV
              value: "production"
            - name: extensions_authentication_enabled
              value: "false"
---
apiVersion: v1
kind: Service
metadata:
  name: jsreport
spec:
  selector:
    app: jsreport
  ports:
    - port: 5488
      targetPort: 5488
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: jsreport-ingress
spec:
  podSelector:
    matchLabels:
      app: jsreport
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: your-dotnet-app
      ports:
        - protocol: TCP
          port: 5488
Enter fullscreen mode Exit fullscreen mode

This configuration addresses availability, resource limits, health monitoring, and network security. Each component requires tuning based on workload characteristics.

An Alternative Approach: Embedded Conversion

The operational complexity of jsreport stems from its architecture as a separate service. An alternative approach embeds PDF conversion directly into your .NET application, eliminating the service boundary.

IronPDF packages a Chrome-based rendering engine as a NuGet package. Conversion happens in-process without network calls:

using IronPdf;

public class InvoiceGenerator
{
    public byte[] GenerateInvoice(InvoiceData data)
    {
        // Create renderer - Chrome engine is embedded in the NuGet package
        var renderer = new ChromePdfRenderer();

        // Configure rendering options
        renderer.RenderingOptions.MarginTop = 20;
        renderer.RenderingOptions.MarginBottom = 20;
        renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;

        // Build HTML from your data
        string html = BuildInvoiceHtml(data);

        // Convert in-process - no network call, no separate service
        PdfDocument pdf = renderer.RenderHtmlAsPdf(html);

        return pdf.BinaryData;
    }

    private string BuildInvoiceHtml(InvoiceData data)
    {
        return $@"
            <!DOCTYPE html>
            <html>
            <head>
                <style>
                    body {{ font-family: Arial, sans-serif; margin: 0; padding: 20px; }}
                    .header {{ display: flex; justify-content: space-between; margin-bottom: 30px; }}
                    .line-items {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
                    .line-items th, .line-items td {{
                        border: 1px solid #ddd;
                        padding: 12px;
                        text-align: left;
                    }}
                    .line-items th {{ background-color: #f5f5f5; }}
                    .total {{ text-align: right; font-weight: bold; margin-top: 20px; }}
                </style>
            </head>
            <body>
                <div class='header'>
                    <div>
                        <h1>Invoice #{data.InvoiceNumber}</h1>
                        <p>{data.CustomerName}</p>
                    </div>
                    <div>
                        <p>Date: {data.Date:yyyy-MM-dd}</p>
                        <p>Due: {data.DueDate:yyyy-MM-dd}</p>
                    </div>
                </div>
                <table class='line-items'>
                    <tr>
                        <th>Description</th>
                        <th>Quantity</th>
                        <th>Unit Price</th>
                        <th>Total</th>
                    </tr>
                    {string.Join("", data.LineItems.Select(item => $@"
                    <tr>
                        <td>{item.Description}</td>
                        <td>{item.Quantity}</td>
                        <td>${item.UnitPrice:F2}</td>
                        <td>${item.Total:F2}</td>
                    </tr>"))}
                </table>
                <div class='total'>
                    <p>Total: ${data.GrandTotal:F2}</p>
                </div>
            </body>
            </html>";
    }
}
Enter fullscreen mode Exit fullscreen mode

Architectural Comparison

Aspect jsreport (Service) IronPDF (Embedded)
Deployment Separate Docker container NuGet package
Runtime Node.js .NET
Network calls Required for every conversion None
Scaling Independent service scaling Scales with application
Monitoring Separate metrics pipeline Application metrics
Versioning Container image updates Package updates
Latency Network round-trip per conversion In-process
Debug experience Cross-process, cross-language Standard .NET debugging
Azure App Service Requires Linux containers Windows or Linux

Batch Processing Comparison

The difference becomes pronounced with batch operations:

jsreport approach:

public async Task<List<byte[]>> GenerateReportsBatch(List<ReportData> reports)
{
    var client = new ReportingService("http://jsreport:5488");
    var semaphore = new SemaphoreSlim(5); // Limit concurrent requests to avoid overload

    var tasks = reports.Select(async report =>
    {
        await semaphore.WaitAsync();
        try
        {
            var result = await client.RenderAsync(new RenderRequest
            {
                Template = new Template
                {
                    Content = BuildReportHtml(report),
                    Engine = Engine.None,
                    Recipe = Recipe.ChromePdf
                }
            });

            using var ms = new MemoryStream();
            result.Content.CopyTo(ms);
            return ms.ToArray();
        }
        finally
        {
            semaphore.Release();
        }
    });

    return (await Task.WhenAll(tasks)).ToList();
}
Enter fullscreen mode Exit fullscreen mode

IronPDF approach:

public List<byte[]> GenerateReportsBatch(List<ReportData> reports)
{
    var renderer = new ChromePdfRenderer();
    renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;

    // Process in parallel - no network overhead, no external service limits
    return reports
        .AsParallel()
        .Select(report =>
        {
            string html = BuildReportHtml(report);
            PdfDocument pdf = renderer.RenderHtmlAsPdf(html);
            return pdf.BinaryData;
        })
        .ToList();
}
Enter fullscreen mode Exit fullscreen mode

The embedded approach eliminates:

  • HTTP client management
  • Semaphore-based throttling for external service protection
  • Network timeout configuration
  • Response stream handling
  • Retry logic for network failures

Dockerfile Comparison

jsreport deployment (docker-compose):

version: "3.8"
services:
  webapp:
    build: .
    ports:
      - "80:80"
    depends_on:
      - jsreport
    environment:
      - JSREPORT_URL=http://jsreport:5488

  jsreport:
    image: jsreport/jsreport:4.0.0
    ports:
      - "5488:5488"
    volumes:
      - jsreport_data:/app/data
    deploy:
      resources:
        limits:
          memory: 4G

volumes:
  jsreport_data:
Enter fullscreen mode Exit fullscreen mode

IronPDF deployment (single container):

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "YourApplication.dll"]
Enter fullscreen mode Exit fullscreen mode

No sidecar containers, no volume mounts for jsreport data, no inter-service networking.

Platform Support

IronPDF runs wherever your .NET application runs:

  • Windows (x64, x86)
  • Linux (Debian, Ubuntu, CentOS, Alpine, Amazon Linux)
  • macOS (Intel and Apple Silicon)
  • Docker containers
  • Azure App Service (Windows and Linux)
  • AWS Lambda, Google Cloud Run

The rendering engine extracts from the NuGet package at runtime, with automatic dependency configuration for Linux:

// Optional: automatic Linux dependency installation
IronPdf.Installation.LinuxAndDockerDependenciesAutoConfig = true;
Enter fullscreen mode Exit fullscreen mode

When jsreport Makes Sense

Despite the complexity, jsreport remains appropriate for certain scenarios:

  1. Existing Node.js infrastructure: Teams already operating Node.js services who want consistent technology
  2. Template designer UI: jsreport includes a visual template editor that non-developers can use
  3. Multi-language access: When PDF generation is needed from applications in multiple languages (Python, Java, PHP)
  4. Advanced scheduling: jsreport includes built-in report scheduling and job queuing

Migration Considerations

Teams moving from jsreport to embedded conversion should consider:

Configuration Translation

jsreport chrome-pdf recipe options map to IronPDF:

var renderer = new ChromePdfRenderer();

// jsreport: chrome.marginTop: "2cm"
renderer.RenderingOptions.MarginTop = 20; // millimeters

// jsreport: chrome.landscape: true
renderer.RenderingOptions.PaperOrientation = IronPdf.Rendering.PdfPaperOrientation.Landscape;

// jsreport: chrome.printBackground: true
renderer.RenderingOptions.PrintHtmlBackgrounds = true;

// jsreport: chrome.displayHeaderFooter with header/footer templates
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter
{
    HtmlFragment = "<div style='text-align: center;'>Page {page} of {total-pages}</div>"
};
Enter fullscreen mode Exit fullscreen mode

Licensing

IronPDF is commercial software with perpetual licensing starting at $749. Evaluate the licensing cost against:

  • Reduced infrastructure costs (fewer containers, less memory allocation)
  • Reduced DevOps time (no separate service monitoring)
  • Simplified deployment (single container)
  • Eliminated network latency

A free trial allows testing with actual production workloads before commitment.

Conclusion

jsreport's architecture as a separate Node.js service introduces operational complexity for .NET teams. Docker configuration, network latency, cross-platform debugging, and ongoing maintenance create work that extends beyond the initial integration.

Embedding PDF conversion in your .NET application eliminates the service boundary. The conversion code becomes part of your application, scaling and deploying together, debuggable with standard .NET tools, without network overhead or container orchestration.

For .NET teams, IronPDF provides Chrome-based HTML rendering as a NuGet package, delivering equivalent conversion quality while removing the architectural complexity of running a separate JavaScript service.


Written by Jacob Mellor, CTO at Iron Software, who originally built IronPDF.


References

  1. jsreport Docker Documentation{:rel="nofollow"} - Official Docker deployment guide
  2. jsreport .NET Local Documentation{:rel="nofollow"} - .NET SDK integration guide
  3. jsreport Forum - Docker Performance Issues{:rel="nofollow"} - Community performance discussions
  4. jsreport Forum - Docker-compose Example{:rel="nofollow"} - Permission and configuration solutions
  5. jsreport Forum - Running in Docker{:rel="nofollow"} - Browser launch issues
  6. jsreport GitHub Docker Repository{:rel="nofollow"} - Docker image source
  7. Puppeteer Memory Leak Issues{:rel="nofollow"} - Chrome memory management challenges
  8. IronPDF for .NET - Embedded Chrome PDF generation
  9. IronPDF Docker Guide - Container deployment documentation
  10. ChromePdfRenderer API Reference - IronPDF rendering options

For the latest IronPDF documentation and tutorials, visit ironpdf.com.

Top comments (0)