Creating Python Docker Image For Windows Nano Server

Jung-Hyun Nam
Mar 11 · 6 min read
Image for post
Image for post
Image Credit: https://unsplash.com/photos/9-ohfp-Dicg

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:1809ENV PYTHON_VERSION 3.7.4ENV 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-pipENV PYTHON_GET_PIP_URL https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.pyWORKDIR C:\\TempSHELL ["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:1809COPY --from=0 C:\\Temp\\Python C:\\PythonUSER 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-packagesRUN 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 virtualenvUSER ContainerUserCMD ["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.

Image for post
Image for post

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

Image for post
Image for post

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_1809EXPOSE 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.

Image for post
Image for post

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.

And as always, All kinds of contributions are welcome!

Image for post
Image for post

Beyond the Windows

DevOps Engineer’s Blog

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store