The definitive guide

How To Run Any Windows CLI App in a Linux Docker Container

Run your legacy Windows apps in the cloud with Wine, Mono, and Docker

Jens Meder
Better Programming
Published in
12 min readDec 17, 2019

--

by Jens Meder on EyeEm

I have been struggling with this for quite some time. Days. Nights. Weekends. We have several Windows legacy applications that we need to run headless on our Kubernetes cluster — without access to the source code or the original developers.

To make things worse:

  • All binaries are 32-bits (x86).
  • Some require visual C++ redistributable runtime components.
  • Some require the .NET runtime.
  • Some need a windowing system, even though we only use the command-line interface (CLI).

Initially, I thought I could reverse-engineer the applications and implement the functionality in Kotlin. I was able to extract the .NET source code with the help of JetBrains dotPeek.

Unfortunately, that was insufficient as some apps use additional DLLs — compiled for native Windows and visual C++ redistributable execution. As you might know, extracting code from such binaries is next to impossible.

This realization left me with only one option: try to run the applications with Wine in Linux Docker containers.

What’s in It for You?

Figuring out all the necessary steps to run our applications in a Docker container took me quite some time. I hope I can save you the trouble by sharing what I have learned along the way.

In this post, I teach you to:

  • Determine your app’s runtime dependencies including .NET and Visual C++.
  • Set up Wine in a Docker container.
  • Run x86 and x64 Windows apps in the same container.
  • Run your apps headless with a virtual framebuffer.

Prerequisites

Docker

If you are running on Linux, you can install Docker via your platform’s package manager, e.g., on Ubuntu, you can run sudo apt install -y docker.io.

For macOS and Windows, you need to have a Docker Hub account to download Docker Desktop. Docker Desktop uses a virtual machine running in VirtualBox to create and run your containers.

Therefore, you might have to adjust the virtual machine’s resource constraints to accommodate your application’s needs.

For example, if your Windows applications require at least 8GB of RAM, you need to adjust the Docker Desktop configuration accordingly. Otherwise, the execution of your Windows application might fail with an obscure exception.

Docker Desktop configuration on macOS

Text editor

Please make sure that you have an editor at hand that allows you to create plain text documents. We need it to write our Dockerfiles.

Any Integrated Development Environment (IDE), e.g., JetBrains IntelliJ IDEA, is excellent. Otherwise, I recommend you install either Microsoft Visual Studio Code or Atom.

Windows app

If your application comes with a dedicated installer, you should be good to go. Prefer MSI installers over custom solutions, because we can install .msi files without user interaction using msiexec.

Without an installer, you need to make sure that you have access to the required dependencies, e.g., static and dynamic linked libraries. If you are unsure about dependencies, I’ll show you how you can determine them in the next step.

Step 1. Determine Runtime Requirements

Dependencies

If you have built the Windows applications yourself, you should be well aware of the required dependencies, e.g., .NET or third-party DLLs.

Otherwise, you have to rely on tools to determine the dependencies for you. Use the following tools to identify the required dependencies of all your EXE and DLL files.

The freeware Dependency Walker scans 32-bit and 64-bit binaries (such as applications or DLLs) for external dependencies and lists the results as a hierarchical tree.

With this information, you can identify the required DLLs and packages that you need to install. Please note that this only works for unmanaged code (code that does not require the .NET runtime).

Dependency Walker

If your app requires .NET, you have to use tools that can walk and display the dependency tree of managed assemblies, e.g., the freeware dotPeek by JetBrains.

You can even write a tool yourself using the Assembly.GetReferencedAssemblies API.

JetBrains dotPeek

After you have identified all dependency DLLs, you have to figure out where to get them.

Microsoft DLLs are the easiest to get because they come with packages such as .NET or Visual C++ redistributable. A quick Google search with the name of the DLL tells you the Microsoft package you need to install.

Machine type/CPU

Wine only runs executables with a machine type compatible with your Docker base image.

For example, you cannot run a 64-bit Intel/AMD executable in a 64-bit ARM Docker container (e.g., on Raspberry PI). If you run Docker on an Intel or AMD platform, you only have to worry about two types: x86 (i386) for 32-bit applications and x86/x64 (amd64) for 64-bit applications.

You can determine the machine type of your executables with the following REPL. Just execute the REPL, select your EXE or DLL, and the tool tells you the machine type.

REPL to identify machine type of an EXE or DLL file

Alternatively, you can determine the machine type by looking at the so-called COFF file header.

For that, you need a hex editor, e.g., iHex on macOS. Open your EXE or DLL with the hex editor and have a look at the beginning of the file. You notice that every EXE file starts with a standardized header:

  • The MS-DOS stub (the part that says: This program cannot be run in DOS mode).
  • The PE signature with the value PE\0\0.
  • The COFF file header.

The first two bytes of the COFF file header determine the architecture (or machine type). Let’s have a look at an example. This is what we get when we open the Dependency Walker executable with iHex:

Dependency Walker in iHex

Have a look at the two bytes after the “PE” (50 45 00 00) signature: 4C 01.

They are in reverse order due to the endianness, so the real value is 01 4C. According to the PE format specification, we are looking at an i386 architecture with 32-bits.

Step 2. Select a Docker Base Image

Now that we know the runtime requirements for your applications, we can choose an appropriate Docker base image.

Distribution

In general, you can use any Docker base image as long as it supports Wine.

I use Ubuntu for my setup, but if you prefer other distributions, that’s fine too. Just make sure your distribution comes with decent community support. Otherwise, you spend much time figuring out the quirks of Wine yourself.

One word of warning, if you choose to use Alpine images: I have stumbled upon an issue with these images when running Windows apps that require the Visual C++ redistributable.

I have tried several ways to install the package, but the installation always fails — no matter which version I have tried. I assume the Alpine images lack support for a specific native library.

Even running the Visual C++ redistributable installation with verbose output does not provide any useful debugging information.

Architecture

You have to make sure your Docker base image supports the machine type from step one.

If you pick one of the mainstream Docker base images, such as Ubuntu, this is a non-issue. If you are unsure, you can check the supported architectures of your Docker image via Docker Hub.

Ubuntu images on Docker Hub

If you only have 32-bit apps, you can also pick a 32-bit only base image such as i386/ubuntu.

This reduces the Docker image size because you only install the architecture you need. You can find a selection of x86 images on the i386 user profile on Docker Hub.

Step 3. Install Wine

Wine translates Windows API calls on-the-fly instead of simulating a Windows environment. It allows you to run almost any Windows application on Linux without virtualizing a complete Windows environment.

Installation

Wine provides binary packages of the most recent version for various Unix operating systems, including macOS, Ubuntu, or Android. You can download them from the Wine download page together with installation instructions.

Alternatively, you can install Wine via your platform’s package manager.

Unfortunately, the package names vary from distribution to distribution, so you have to figure out the package name yourself. Please beware that the version from the package managers can be quite old.

By default, Wine logs a lot of warnings, e.g., fixme warnings for missing APIs. You can disable specific warnings via the environment variable WINEDEBUG. To turn off all fixme warnings, just add ENV WINEDEBUG=fixme-all to your Dockerfile.

Please note: When you install Wine on a 64-bit Docker image, it only installs the 64-bit Wine package and dependencies. That means you have to install the 32-bit libraries separately. The easiest way to achieve this is by adding the 32-bit architecture to the package manager.

Set up Wine prefixes

If you only have one app or a set of homogenous applications, you can use a shared Wine prefix, e.g., the default prefix ~/.wine. For more complex scenarios, e.g., running x86 and x64 apps in the same container, you need to setup Wine prefixes to separate the configurations.

Prefixes are easy to set up:

  1. Choose a directory within your Docker container, but make sure the directory itself does not exist.
  2. Determine the required architecture for the prefix.
  3. Create the prefix via WINEARCH=<architecture> WINEPREFIX=<directory> winecfg , e.g., WINEARCH=win32 WINEPREFIX=~/myX86Prefix winecfg.
Dockerfile to install WineHQ and setup a Wine prefix

Please note: If you run Wine on a 64-bit platform, Wine sets the architecture to 64-bits by default. To create a 32-bit prefix, you have to set the WINEARCH=win32 environment variable before creating your Wine prefix.

Install Winetricks

Winetricks provides workarounds for common problems in Wine, e.g., it patches and tweaks the installation of the official Microsoft .NET framework automatically.

I recommend installing the most recent version from the official GitHub repository. This way, you have access to the most recent packages, patches, and workarounds.

Alternatively, you can install Winetricks via your platform’s package manager. Please be aware that the version from the package managers can be quite old.

Dockerfile to install Winetricks

Step 4. Install an X11 Window Server

If your application shows a GUI when you call it from the CLI, it will most likely crash. That’s the part that cost me quite some time to figure out.

Wine tries to bridge GUI calls to the corresponding X11 window server. Since our container runs headless without any means to display a GUI, it crashes. Fortunately, there is a simple solution: the virtual framebuffer Xvfb.

Xvfb provides a window server that emulates both the display and input devices. With the help of Xfvb, you can run your application without a physical display. You can even forward the display content via VNC to your host or any other computer on your network, e.g., for debugging.

The easiest way to use Xfvb is by using a convenience script called xvfb-run that ships with Xvfb. xvfb-run sets up the required configuration, starts the window server in a background process, and then executes your app.

When your app exits, xvfb-run terminates the X window server, deletes all the configuration files, and returns the exit code of your application.

Dockerfile to install Xvfb

Step 5. Install Additional Dependencies

.NET

If you want to execute .NET code, you need to install the corresponding runtime: either the official Microsoft .NET package or Mono.

I have found Mono to be the more comfortable option because Wine provides a dedicated installer for it, the installation is fast, and the resulting image size is decent. As far as I can tell, the overall API support is quite comprehensive.

You can download the Wine Mono installer either via the official Wine website or from GitHub. Just download the MSI file and then run msiexec /i WineInstaller.exe to install it in your Wine prefix.

Dockerfile to install Wine Mono

If your application requires specific parts of .NET that are unsupported in Mono, you have to use the official Microsoft .NET runtime.

I recommend using Winetricks for the installation because it already fixes some known issues on the fly. Please be aware that there are still several issues with the official .NET Framework:

  • Some Microsoft-specific frameworks, e.g., WPF, do not work in Wine.
  • Longer installation times compared to Mono.
  • A larger installation size compared to Mono.
Dockerfile to install the official Microsoft .NET Framework

Visual C++ redistributable

Visual Studio knows two ways to compile C++ code into an executable or DLL:

  • Managed C++
  • Visual C++

Managed C++ compiles to the Microsoft Intermediate Language (MSIL) and runs on the Common Language Runtime (CLR) of the .NET Framework. For this option, you already know what to do: install either Mono or the Microsoft .NET runtime.

Visual C++, on the other hand, requires the Visual C++ redistributable package and runtime. If you have determined the dependencies of your app in step one, you should know which version of the Visual C++ redistributable package you have to install.

You can install the Visual C++ redistributable either via the installer from the Microsoft website or by using Winetricks. I suggest using Winetricks because it takes care of the download and installation thus does not bloat up your Dockerfile.

Dockerfile to install Visual C++ Redistributable 2008

Step 6. Install Your Application

Standalone

If your application comes without a dedicated installer, you need to copy the files yourself. I suggest you create a new directory in your Wine prefix for each app to prevent conflicts.

Msiexec

Msiexec installs and configures MSI Installer packages. It is the most convenient option to install your app in your Wine prefix. Just run the following command, and msiexec takes care of the rest.

WINEPREFIX=yourprefix msiexec /i yourMsiInstaller.msi

Custom installer

Installers with a command-line interface should be straightforward. Just select your Wine prefix and run the executable. In case the installer displays some GUI dialogs during installation, you might have to add xvfb-run.

If the installer requires user interaction via a GUI, you have to take a detour:

  1. Set up and launch a virtual machine with your chosen Linux distribution.
  2. Follow the steps as mentioned earlier as you would when creating the Docker container.
  3. Open a terminal and change to your Wine prefix.
  4. Launch the installer with Wine, e.g., wine myInstaller.exe.
  5. Finish the installation.
  6. Copy the Wine prefix to your container.

If you have problems installing your application, try to run wineboot -u before running the installer.

Step 7. Configure Your Application

Some applications require configuration during the first launch, e.g., connection parameters or registration information. They either create dedicated files or store the settings in the Windows Registry.

Best case scenario, you can provide the configuration upfront via a command-line interface, and the application takes care of the rest.

If the configuration requires user interaction via a GUI, you have to take a detour. You can determine the configuration locations with a simple trick:

  1. Set up and launch a virtual machine with your chosen Linux distribution.
  2. Follow the steps as mentioned earlier as you would when creating the Docker container.
  3. Install git if your Linux distribution ships without it.
  4. Open a terminal and change to the Wine prefix where you have installed your application.
  5. Run git init to initialize a Git repository.
  6. Launch your application with Wine, e.g., wine myApplication.exe.
  7. Configure the application via the UI.
  8. Run git status to list the changed files.
  9. Copy all the listed files from the virtual machine and copy them to your Wine prefix in the container image.

Examples

AMD64 Docker Image: 7-Zip (x64)

This example downloads the MSI Installer for the 64-bit version of 7-Zip, installs, and runs it in the official Ubuntu 64-bit container. You should see the following output on success.

7-Zip 19.00 (x64) command-line interface
Dockerfile to install 7-Zip 19.00 (x64)

i386 Docker Image: 7-Zip (x86)

This example downloads the MSI Installer for the 32-bit version of 7-Zip, installs, and runs it in an Ubuntu 32-bit container from i386. You should see the following output on success.

7-Zip 19.00 (x86) command-line interface
Dockerfile to install 7-Zip 19.00 (x86)

.NET framework 4.5.2: C# Hello World

This example installs the official Microsoft .NET Framework 4.5.2 and then compiles and runs a simple “Hello World” in C#. On success, it prints Hello World to the console.

Dockerfile to compile and execute HelloWorld.cs with .NET 4.5.2

Wine Mono: C# Hello World

This example installs Wine Mono and runs the HelloWorld.exe from the previous example. On success, it prints Hello World to the console.

Dockerfile to execute HelloWorld.cs with Wine Mono

Final Thoughts

I hope this tutorial helps you to get your legacy apps ready for Docker. As you might have noticed, the resulting container images can be quite large. It is common to have 1-2GBs per image.

Therefore, I recommend only to install what you need, e.g., avoid installing the .NET framework if your application runs without it.

You can also reduce the image size by reducing the number of layers in your Dockerfile. That means to merge all RUN commands into a single one using && , e.g., RUN apt get update && apt-get install -y wget xvfb .

Cheers!

--

--

Head of Development @ Apoplex Medical Technologies | Certified Kubernetes Application Developer | Writer | Photographer | Chef