Creating Python Docker Image For Windows Nano Server
By: Jung-Hyun Nam
Tags: Windows Server, DevOps, Docker, Programming, Python

My company, DEVSISTERS, is an early adopter of Windows container and Windows Kubernetes in Korea and its gaming industries. Recently, our team operated a Windows stack-based game software closed beta for a limited time. (Related Link, Korean)

During the closed beta period, we often collected application dump files to debug and improve server applications. Initially, we wrote a script with PowerShell and FileSystemWatcher.

But as you know, FileSystemWatcher and PowerShell do not work correctly sometimes. Also, in the Windows environment, PowerShell would be the right choice, but most of our team members are not familiar.

Initially, a simple automation script wrote in Python. Currently, Python official images only packaged with Windows Server Core image, not the Nano Server image. This option makes containerized Python applications consume more memory and resources, which makes a quite overhead.

In most cases, people accept this limitation willingly because the Nano server has too limited features than traditional Windows Server SKU. If you try to run a Python application in Nano Server, you will soon face a very tough problem. These differences can make overwork and can waste your time.

But I decided to make a hard work because I want to optimize the Python workload in Windows container environment. So I used about two business days and worked done charmingly. :-)

The Dockerfile — Build Stage

I used a multi-staged build for minimizing output image size. I defined some environment variables and changed the default shell to PowerShell.

FROM mcr.microsoft.com/windows/servercore:1809 ENV PYTHON_VERSION 3.7.4 ENV PYTHON_RELEASE 3.7.4 # if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value '<VERSION>'" ENV PYTHON_PIP_VERSION 20.0.2 https://github.com/pypa/get-pip ENV PYTHON_GET_PIP_URL https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.py WORKDIR C:\\Temp SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'Continue'; $verbosePreference='Continue';"]

Then, download an embedded version of Python Windows release and extract the ZIP file. Also, I download the get_pip.py script file too. Before doing that, I modified SecurityProtocol property to allow communication with the GitHub URL.

RUN [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; \

Invoke-WebRequest -UseBasicParsing -Uri "https://www.python.org/ftp/python/$env:PYTHON_RELEASE/python-$env:PYTHON_VERSION-embed-amd64.zip" -Out 'Python.zip'; \

Expand-Archive -Path "Python.zip"; \

Invoke-WebRequest -UseBasicParsing -Uri "$env:PYTHON_GET_PIP_URL" -OutFile 'Python\get-pip.py';

I used an embedded version of Python because the official Win32 installer will not work in Nano Server. And this is the hard part.

Some complicated configurations applied in this stage. I’ll explain what’s going on here.

RUN [String]::Format('@set PYTHON_PIP_VERSION={0}', $env:PYTHON_PIP_VERSION) | Out-File -FilePath 'Python\pipver.cmd' -Encoding ASCII; \

$FileVer = [System.Version]::Parse([System.Diagnostics.FileVersionInfo]::GetVersionInfo('Python\python.exe').ProductVersion); \

$Postfix = $FileVer.Major.ToString() + $FileVer.Minor.ToString(); \

Remove-Item -Path "Python\python$Postfix._pth"; \

Expand-Archive -Path "Python\python$Postfix.zip" -Destination "Python\Lib"; \

Remove-Item -Path "Python\python$Postfix.zip"; \

New-Item -Type Directory -Path "Python\DLLs";

I create PIPVER.CMD file to pass the PYTHON_PIP_VERSION environment variable to Nano Server.

For reducing the hard-coded part, I looked up the Win32 resource table in the python executable file and made a postfix string. This postfix string continuously used to extract the compiled Python library archive file and removing the _PTH file.

Embedded Python does not honor the system path variable (PYTHONPATH) due to the _PTH file after version 3.7.x. Removing this file makes embedded Python works like traditional Python.

I extracted the archived pre-compiled Python library to Libs directory.

Finally, for latter use, I created the DLLs directory separately. This directory used by pip and virtualenv.

Phew! The hard part is over. Until now, in this build stage, I created a temporary directory and composed a Python installation directory manually.

The Dockerfile — Nano Server

Let’s dive into the Nano Server.

FROM mcr.microsoft.com/windows/nanoserver:1809 COPY --from=0 C:\\Temp\\Python C:\\Python USER ContainerAdministrator

By default, Windows Container uses the ContainserUser account. For security reasons, even in a container, the user does not have all permissions. If you want to modify the registry and system settings in the Windows container, you should change your user account to ContainerAdministrator.

ENV PYTHONPATH C:\\Python;C:\\Python\\Scripts;C:\\Python\\DLLs;C:\\Python\\Lib;C:\\Python\\Lib\\plat-win;C:\\Python\\Lib\\site-packages RUN setx.exe /m PATH %PATH%;%PYTHONPATH% && \

setx.exe /m PYTHONPATH %PYTHONPATH% && \

setx.exe /m PIP_CACHE_DIR C:\Users\ContainerUser\AppData\Local\pip\Cache && \

reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\FileSystem /v LongPathsEnabled /t REG_DWORD /d 1 /f

I defined the PYTHONPATH environment variable locally. Then I configured the PATH, PYTHONPATH environment variable to system-wide. Also, I set the PIP_CACHE_DIR environment variable for hiding the PIP cache directory from the container root path.

The last line configures the long-path support for Windows operating system.

RUN assoc .py=Python.File && \

assoc .pyc=Python.CompiledFile && \

assoc .pyd=Python.Extension && \

assoc .pyo=Python.CompiledFile && \

assoc .pyw=Python.NoConFile && \

assoc .pyz=Python.ArchiveFile && \

assoc .pyzw=Python.NoConArchiveFile && \

ftype Python.ArchiveFile="C:\Python\python.exe" "%1" %* && \

ftype Python.CompiledFile="C:\Python\python.exe" "%1" %* && \

ftype Python.File="C:\Python\python.exe" "%1" %* && \

ftype Python.NoConArchiveFile="C:\Python\pythonw.exe" "%1" %* && \

ftype Python.NoConFile="C:\Python\pythonw.exe" "%1" %*

In the case of the AWS CLI, it requires “.PY” file extension should be associated with a Python interpreter directly. These commands make mappings between major Python file extensions and Python interpreter, respectively.

RUN call C:\Python\pipver.cmd && \

%COMSPEC% /s /c "echo Installing pip==%PYTHON_PIP_VERSION% ..." && \

%COMSPEC% /s /c "C:\Python\python.exe C:\Python\get-pip.py --disable-pip-version-check --no-cache-dir pip==%PYTHON_PIP_VERSION%" && \

echo Removing ... && \

del /f /q C:\Python\get-pip.py C:\Python\pipver.cmd && \

echo Verifying install ... && \

echo python --version && \

python --version && \

echo Verifying pip install ... && \

echo pip --version && \

pip --version && \

echo Complete.

The remaining parts are relatively simple. Simply call the get-pip script with — disable-pip-version-check, — no-cache-dir, and specify the PIP version. After the PIP installation completed, remove temporary files and verifying Python and PIP works correctly.

RUN pip install virtualenv USER ContainerUser CMD ["python"]

In the official Python Windows Server Core image, it adds the virtualenv package for convenience. So I simply added it to provide virtualenv tool in the Nano Server container.

Then, changing the user to ContainerUser again. This configuration makes the container more secure.

Finally, I specify the default command of this image as a Python interpreter.

Test Flight — AWS CLI & Virtual Environment

First, I tested the installation of AWS CLI.

Then, I tested the installation of Django in a virtual environment.

It looks like work correctly.

Test Flight — Django Web Application

Lastly, I created a simple Django sample web site with the Nano Server. I wrote a simple Dockerfile to achieve this.

First, I create a new Django project.

django-admin startproject helloworld

Then, I modified the settings.py file to allow all hosts. In this case, I’m not using any reverse proxy server, so I need to adjust the security setting that would enable incoming connection to Windows container.

... # SECURITY WARNING: don't run with debug turned on in production!

DEBUG = True ALLOWED_HOSTS = ["*"] ...

Lastly, I create a Dockerfile to build a docker image.

FROM rkttu/python-nanoserver:3.7.4_1809 EXPOSE 8000

RUN pip install django WORKDIR C:\\website

ADD . . ENV DJANGO_DEBUG=1

CMD python -Wall manage.py runserver --insecure 0.0.0.0:8000

Let’s start the Nano server-based Django application!

docker build -t helloworld:latest . docker run --rm -d -p 8000:8000 helloworld:latest

Voila! After launching the container, I can browse the Django app.

From now on, you can run your ordinary Python application in the Nano Server container. It makes your Windows-based Python application much slimmer and works fast.

Do you want to use the image?

I published a public Git repository and Docker Hub repository. You can clone the code or pull the image immediately.

GitHub repository URL: https://github.com/rkttu/python-nanoserver

Docker Hub: docker pull rkttu/python-nanoserver:3.7.4_1809 or docker pull rkttu/python-nanoserver:3.8.2_1809

And as always, All kinds of contributions are welcome!