13 september 2025
Packaging Crystal Reports in a Windows Container for Azure App Service
Migrating legacy applications to the cloud is full of surprises. Usually, you expect problems in the code, infrastructure, or CI/CD pipeline, but in practice almost anything can surface. This time, everything stumbled over Crystal Reports… Let’s look at how we managed to migrate a legacy application built on classic ASP.NET, using Crystal Reports for report generation, to Azure App Service.
Introduction

The task was straightforward: move the application to the cloud. Azure App Service was chosen as the target platform: less hassle with web servers, simpler deployment, logs, and maintenance. Then came the usual infrastructure and deployment configuration. At first glance, everything looked simple.But it quickly became clear that there was one hard constraint. The application uses Crystal Reports, and in the classic .NET Framework world this component is tightly coupled to Windows and the native runtime.In practice, it looked unpleasant: the site worked, the business logic worked, but reports threw exceptions.As a result, the goal was no longer just to “run the site” in App Service, but to recreate around it the Windows environment in which Crystal Reports can actually function properly. From that point on, the story stopped being a standard App Service migration and became a discussion about the runtime environment.

The chosen solution

For understandable reasons, nobody wanted to rewrite the reporting layer or urgently look for a replacement for Crystal Reports as part of this migration. So the decision was made to package the application into a Windows container and run it in Azure App Service for Containers.This approach had several advantages. It preserved the existing .NET Framework 4.8 stack, gave us full control over the environment, and made it possible to explicitly lock down everything that used to “just live on the server”: the Windows version, Crystal Reports Runtime, and, as it turned out, system DLLs, fonts, regional settings, and logs.In practice, the container became not just a way to package the application, but a way to describe the entire execution environment.
The chosen solution
FROM mcr.microsoft.com/windows:1809 AS system_files# Final image: ASP.NET 4.8 on Windows Server Core 2022FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022SHELL ["cmd", "/S", "/C"]# Crystal Reports prerequisitesCOPY --from=system_files C:/Windows/System32/oledlg.dll C:/Windows/System32/oledlg.dllCOPY --from=system_files C:/Windows/SysWOW64/oledlg.dll C:/Windows/SysWOW64/oledlg.dll# Install Crystal Reports RuntimeCOPY ./CrystalReportsInstaller/CR13SP35MSI64_0-80007712.MSI C:/install/RUN msiexec.exe /i C:\install\CR13SP35MSI64_0-80007712.MSI /qn /norestart /L*V C:\install\cr_install.log || exit /b 0# Deploy applicationCOPY ./WebApplication C:/inetpub/wwwroot# Permissions and persistent log storageRUN powershell -NoProfile -Command "New-Item -Type Directory -Path C:\home\LogFiles\Application -Force | Out-Null"RUN icacls "C:\inetpub\wwwroot" /grant "IIS_IUSRS:(OI)(CI)M" /T && icacls "C:\inetpub\wwwroot" /grant "IUSR:(OI)(CI)R" /T && icacls "C:\home\LogFiles" /grant "IIS_IUSRS:(OI)(CI)M" /T
Windows version compatibility

When working with Linux containers, we quickly get used to the idea that choosing a base image is mostly about the internal set of components rather than compatibility. With Windows, things are stricter. The version of the container image must be compatible with the host where that container will later run.In this case, the web application runs on the mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022 image. This requires the corresponding Windows family in App Service. Otherwise, the container simply will not start.There is also an intermediate stage based on mcr.microsoft.com/windows:1809, from which some system files are pulled in. It turned out that windowsservercore was missing some libraries and fonts used during report generation.

Installing Crystal Reports Runtime

The main part of the whole setup is, of course, installing Crystal Reports Runtime inside the container. To do that, the MSI package is copied into the image and installed silently through msiexec.But a simple installation turned out not to be enough. During testing, it became clear that the Dockerfile also needed to copy oledlg.dll into both System32 and SysWOW64. This is an important detail because some Crystal Reports dependencies expect these libraries to be present for both architectures.

Fonts

The reports started working, but they rendered incorrectly. The issue turned out to be fonts. For Crystal Reports, this is an especially sensitive topic. Even if the report is technically generated, missing fonts can easily cause the layout to shift, line breaks to change, margins to move, and column widths to break.That is why Arial fonts are copied separately into the container and registered in the system registry.
# Fonts required by reportsCOPY --from=system_files C:/Windows/Fonts/arial.ttf C:/Windows/Fonts/arial.ttfCOPY --from=system_files C:/Windows/Fonts/arialbd.ttf C:/Windows/Fonts/arialbd.ttfCOPY --from=system_files C:/Windows/Fonts/ariali.ttf C:/Windows/Fonts/ariali.ttfCOPY --from=system_files C:/Windows/Fonts/arialbi.ttf C:/Windows/Fonts/arialbi.ttfRUN reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Arial (TrueType)" /t REG_SZ /d arial.ttf /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Arial Bold (TrueType)" /t REG_SZ /d arialbd.ttf /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Arial Italic (TrueType)" /t REG_SZ /d ariali.ttf /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Arial Bold Italic (TrueType)" /t REG_SZ /d arialbi.ttf /f# Locale and time zoneRUN powershell -NoProfile -Command "Set-Culture fr-FR; Set-WinSystemLocale fr-FR; Set-WinUserLanguageList fr-FR -Force"RUN powershell -NoProfile -Command "tzutil /s 'Romance Standard Time'"
Locale, time zone, and currency settings

Another thing that brought a surprise was the container’s regional settings.The application’s Web.config already had globalization fixed to fr-FR. But, as it turned out, that alone was not enough. For the reports and the application as a whole to work correctly, the container also had to explicitly configure culture, system locale, language list, time zone, and currency formats.Our application generates documents and works with dates, amounts, currencies, and string representations of numbers. And if the container runs, for example, with default settings, we again get problems in the reports, even if everything is fine with fonts:dates are displayed in the wrong format;monetary values use the wrong separator;the currency symbol differs from what is expected;the time zone is not the expected one.That is why the container explicitly sets fr-FR, the Romance Standard Time time zone, and currency parameters through HKEY_USERS\.DEFAULT\Control Panel\International.There is one more nuance here. Since the application runs in IIS, loading the user profile is disabled for the application pool, and the required regional settings are configured for the default profile.

Permissions and persistent storage

Next come the less Crystal-specific issues related to using disk space for application logs.The Dockerfile separately creates the C:\home\LogFiles\Application directory and grants permissions to IIS_IUSRS and IUSR both for the application directory and for the log directory.Here it is especially important to remember that in App Service, the C:\home path inside the container is mapped to the App Service file system. In other words, the data written there will be available in the App Service file system and, first, will not disappear when the container is redeployed, and second, will be available for viewing through developer tools.

Logging specifics

Now, separately about logs.In the project, logging is configured in Web.config through log4net. We configured two logging outputs.The first is RollingFileAppender, which writes to C:\home\LogFiles\Application\app.log.The second is ConsoleAppender, which writes to the container’s standard output.As a result, logs can be viewed both in the standard App Service application log output and as files in the service’s file system.The file appender is useful because it provides a proper rolling log. In our configuration, the log is appended to the existing file, rotated by size, limited to 10 MB per file, and keeps up to 10 archives. In addition, MinimalLock is used, which reduces the risk of file locking issues during writes.
# Currency settings for the default profile used by IISRUN powershell -NoProfile -Command "New-Item -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Force | Out-Null; \ Set-ItemProperty -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Name sCurrency -Value ([char]0x20AC); \ Set-ItemProperty -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Name iCurrency -Value 3; \ Set-ItemProperty -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Name sMonDecimalSep -Value ','; \ Set-ItemProperty -Path 'Registry::HKEY_USERS\.DEFAULT\Control Panel\International' -Name sMonThousandSep -Value ' '"RUN %WINDIR%\System32\inetsrv\appcmd set apppool /apppool.name:DefaultAppPool /processModel.loadUserProfile:falseEXPOSE 80ENTRYPOINT ["C:\\ServiceMonitor.exe", "w3svc"]
Summary

In the end, we managed to run the application with Crystal Reports in Azure App Service without rewriting the reporting layer and without abandoning .NET Framework. But what worked here was not a single clever trick, but a long chain of small details that are easy to underestimate individually.We had to do more than simply install Crystal Reports Runtime in the container. We had to assemble a full Windows environment around the application: choose the base image, verify Windows version compatibility, bring in missing system DLLs, fonts, culture, time zone, currency formats, and proper logging.Below is an anonymized Dockerfile example based on the version used in the project.