Plenty of data science tooling assumes a Linux environment. Develop on Windows and that assumption becomes impactful, particularly with research code leaning on native compilers, system libraries or tightly controlled build chains. Docker bridges that gap cleanly and repeatably. By packaging a Linux environment into a lightweight container, it becomes possible to run the same tools locally that would otherwise require a dedicated Linux machine or server.
What follows covers why Docker suits this setting and how it differs from a virtual machine. The worked example prepares a docker image and container for the symbolic regression tool Brush, which currently does not have Windows support. Three interactive figures in the sections that follow build up this mental model before the walkthrough begins.
Virtual machines vs. Docker containers
Virtual machines and Docker containers both get Linux software running on a non-Linux system. How they get there could hardly be more different. A virtual machine virtualizes the hardware beneath it. A hypervisor presents virtual devices to the guest, which boots its own kernel on them and brings up system services and a full filesystem. This makes VMs flexible, but also heavy. Startup times are long, disk usage is large, and resource overhead can be significant.
Docker containers share the host kernel and isolate only what is needed at the application level. Instead of virtualizing hardware, Docker isolates the user space. This leads to faster startup, smaller images, and closer integration with local development workflows. For data science tasks that involve compiling code, running experiments, and iterating quickly, this difference matters. In practice, Docker feels closer to running native Linux tools, while a virtual machine (VM) feels like managing a second computer.
Figure 1 makes this contrast concrete. Toggle between the two models. A virtual machine stacks a complete guest operating system, kernel included, on top of a hypervisor. A container keeps only the application and its libraries, and shares the host kernel underneath. That shared kernel is the reason containers start faster and stay smaller, and on Windows it is supplied by WSL 2, which the next figure examines directly.
Figure 1
Two ways to run Linux software on one machine
Every virtual machine instance boots its own guest kernel and operating-system user space. A Linux container runs instead as a group of processes fenced off by a shared Linux kernel, carrying only its application and user-space dependencies. On a native Linux host that kernel belongs to the host. On Windows, Docker Desktop supplies it through WSL 2 (Figure 2). Use the toggle to compare the two stacks, and hover or focus any layer for a short explanation.
The reason tools are not available on Windows
Many advanced data science and machine learning tools are developed primarily for Linux. There are several reasons for this:
• Linux provides a stable and consistent toolchain for C, C++, and Fortran, which are common in high performance libraries.
• Package managers and build systems on Linux are easier to script and automate.
• Research code often targets server and cluster environments, which are overwhelmingly Linux-based.
While ports of such tools are sometimes possible, they are rarely the primary focus of development. Rather than fighting the platform mismatch, Docker allows the Linux environment to be brought directly into the local workflow.
Figure 2 shows how this works in practice on Windows, where the shared Linux kernel does not come from Windows at all. Hover or focus each layer to see its role, and use the switch to reveal what optional GPU access additionally requires.
Figure 2
How a Linux container runs on Windows
Docker does not run Linux tools on the Windows kernel directly. On Windows it uses WSL 2 by default, a lightweight virtual machine that provides a genuine Linux kernel, and every Linux container shares that one kernel. Hover or focus a layer to read what it does, and use the switch to see what optional GPU access requires.
docker-desktop WSL 2 distribution and uses the full Linux kernel that WSL 2 supplies. Your Linux containers share that one kernel, which keeps the approach lighter than giving every tool its own full virtual machine, even though virtualization is involved. Isolation is enforced by the kernel. Namespaces limit what each container can see, while cgroups limit the resources it can use. GPU acceleration runs through a defined path: from the GPU through the NVIDIA Windows driver, WSL 2 GPU paravirtualization and the Docker runtime. It needs a supported NVIDIA GPU, current Windows and WSL 2 components, an NVIDIA driver with WSL 2 support, Docker Desktop on the WSL 2 backend, a GPU-capable image and a run-time --gpus request. The NVIDIA Container Toolkit is installed separately in one case only, namely running Docker Engine directly inside WSL rather than through Docker Desktop. This figure shows Linux containers, the mode used throughout this article.Docker works well for data science
Docker provides several concrete advantages beyond simple compatibility. Repeatability is one of the most important. In a Dockerfile, one can specify the exact operating system, system libraries, compiler versions, and Python packages required. When the image builds successfully, the environment is far more repeatable than an ad hoc local setup, though it is not frozen. As Figure 3 notes, a Dockerfile pins only what it is told to pin, and the example below leaves the base image tag, the Miniconda installer, the Brush clone and the conda packages free to move between builds. An exact rebuild would need a digest-pinned base image, a fixed Miniconda version, a pinned Brush commit, and locked package versions.
Isolation is another key benefit. Dependencies required for one project do not interfere with others, and system-level packages do not pollute the host machine. That matters most with research code that may require older compilers or specific library versions.
Finally, Docker images are portable. Run the same image locally, on a shared workstation, or on a cloud server. Architecture has to match. The Dockerfile below installs the Linux x86_64 build of Miniconda, so its image runs on x86_64 hosts. This reduces friction when moving from experimentation to larger-scale runs.
These benefits are easier to reason about once the difference between an image and a running container is clear. As shown in Figure 3, a Dockerfile is built once into an immutable image, which is then run to create a container with a thin writable layer on top. Docker throws that writable layer away when the container goes. To keep results, mount a folder from the Windows host, or use a named volume. This matters the moment the example below starts producing output.
Figure 3
From Dockerfile to a running container, and where your data lives
A Dockerfile is built once into an image, and that image is run to create a container. They are not the same thing, and by default anything written inside a container is lost when it is removed. Step through the four stages, or press Play, to see how a result is made and how it is kept.
docker build combines a Dockerfile and its build context into an image of read-only layers. docker run then creates and starts a container by adding one thin writable layer on top, so the image is the template and a container is an instance of it that may be running or stopped. That writable layer survives stop and restart. Removing the container deletes it. Results therefore go somewhere else, either to a bind mount on a host or WSL folder, or to a named volume that Docker keeps until it is deleted. A Dockerfile improves repeatability, and exact rebuilds need a digest-pinned base image, locked dependencies and verified downloads, since ubuntu:24.04 is a mutable tag that can point somewhere new tomorrow and the file also pulls Miniconda3-latest and unpinned packages.Example: Running symbolic regression with Brush in Docker
RepeatableBuilds
How a Dockerfile packages an Ubuntu environment to build and run the Brush symbolic regression tool on Windows, repeatably and in isolation.
Repeatable Builds. How a Dockerfile packages an Ubuntu environment to build and run the Brush symbolic regression tool on Windows, repeatably and in isolation. Key topics covered: Dockerfile build layers, Install system packages, Build and run, Miniconda and conda-forge, Create the brush_demo env, Clone and build Brush, Mount host folders, Validate the notebook.
In this case, Docker is used to build a Linux image specifically for running the symbolic regression tool, Brush. More information about Brush can be found by accessing the link below: https://github.com/cavalab/brush
The Dockerfile below defines an Ubuntu-based image. It installs the system dependencies, sets up a Python environment, then compiles the native components Brush needs. A best practice when working with tools like Brush is to treat the container as a research instrument rather than a general-purpose environment. The Dockerfile should include only what is needed to build and run the model, and nothing more. This keeps build times reasonable and reduces the chance of hidden dependency issues.
# Set Linux Version
FROM ubuntu:24.04
# Set environment variables for non-interactive installation
ENV DEBIAN_FRONTEND=noninteractive \
CONDA_DIR=/opt/conda \
PATH=/opt/conda/bin:$PATH
# Install system packages: git and minimal dependencies for Miniconda
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
wget \
ca-certificates \
bzip2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Download and install latest Miniconda for Linux x86_64
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && \
/bin/bash /tmp/miniconda.sh -b -p $CONDA_DIR && \
rm /tmp/miniconda.sh && \
$CONDA_DIR/bin/conda clean -afy
# Configure conda to use only conda-forge channel
RUN $CONDA_DIR/bin/conda config --system --add channels conda-forge && \
$CONDA_DIR/bin/conda config --system --set channel_priority strict && \
$CONDA_DIR/bin/conda config --system --remove channels defaults || true
# Initialize conda for bash shells system-wide
RUN $CONDA_DIR/bin/conda init bash && \
echo ". $CONDA_DIR/etc/profile.d/conda.sh" >> /etc/profile.d/conda.sh && \
echo "conda activate brush_demo" >> /etc/profile.d/conda.sh
# Create the brush_demo conda environment
RUN $CONDA_DIR/bin/conda create -n brush_demo python=3.11 --override-channels -c conda-forge -y && \
$CONDA_DIR/bin/conda clean -afy
# Clone the brush repository
RUN git clone https://github.com/cavalab/brush.git /opt/brush
# Install dependencies from environment.yml into the brush_demo environment
WORKDIR /opt/brush
RUN $CONDA_DIR/bin/conda env update -n brush_demo -f environment.yml && \
$CONDA_DIR/bin/conda clean -afy
# Install matplotlib explicitly (conda-forge only)
RUN $CONDA_DIR/bin/conda run -n brush_demo \
conda install matplotlib --override-channels -c conda-forge -y && \
$CONDA_DIR/bin/conda clean -afy
# Install deap explicitly (conda-forge only)
RUN $CONDA_DIR/bin/conda run -n brush_demo \
conda install deap --override-channels -c conda-forge -y && \
$CONDA_DIR/bin/conda clean -afy
# Install brush in editable mode
RUN $CONDA_DIR/bin/conda run -n brush_demo pip install -e .
# Ensure conda is initialized in root's .bashrc
RUN echo ". $CONDA_DIR/etc/profile.d/conda.sh" >> /root/.bashrc && \
echo "conda activate brush_demo" >> /root/.bashrc
# Set working directory
WORKDIR /opt/brush
# Default shell is bash
SHELL ["/bin/bash", "-c"]
# Default command: interactive bash shell
CMD ["/bin/bash"]
Before we can build the image, ensure that Docker Desktop is installed from: https://docs.docker.com/desktop.
After installing the software, ensure that Docker Desktop is running and open a PowerShell window and navigate to the directory containing the Dockerfile. We can now build the Docker image by running the command below:
docker build -t dockerbrush:latest --progress=plain .docker buildstarts the image build process-t dockerbrush:latestassigns a name and optional tag to the image--progress=plainshows the full build progress.specifies the build context, which is the current directory where the Dockerfile is located
After the image is built, we can create a container from it and mount local data directories into the container. The mount lets results travel back to the host filesystem, at the cost of opening the isolation boundary for that path. A bind mount is a deliberate hole in it, and container processes run as root by default, so files can land on the host owned by root. From the standpoint of the symbolic regression method, it is running on a standard Linux machine. From the perspective of the researcher, it integrates smoothly with a Windows-based workflow.
An example command to create and run a Docker container is provided below. The command as shown mounts nothing, so add a -v argument to share data directories between the host and the container, in the format /path/to/host/data:/path/to/container/data:
docker run -it --name brush_demo --restart unless-stopped dockerbrushdocker runstarts a new container from a specified Docker image-itallocates an interactive terminal, which gives direct shell interaction with the container--name brush_demoassigns a human-readable name to the container for easier management--restart unless-stoppedconfigures the container to automatically restart if it exits or the system reboots, unless it is explicitly stopped by the user. With an interactive shell that means leaving the shell does not end the container. It restarts in the background under the same brush_demo name. To get back into it, run docker exec -it brush_demo bash. To retire it, run docker stop brush_demo followed by docker rm brush_demo, since stopping alone keeps the name reserved and re-running the command above would fail with a name conflictdockerbrushspecifies the Docker image used to create the container
We can now navigate to the Brush guide directory to access and run its demos from within the new Docker container.
cd /opt/brush/docs/guideUsing the jupyter nbconvert command, we can run Jupyter notebooks from the terminal, saving the output in a separate notebook. In this example, the archive.ipynb demo will be run from the terminal to verify that Brush was successfully installed.
jupyter nbconvert --to notebook --execute archive.ipynbCompleted 100% [====================][NbConvertApp] Writing 119498 bytes to archive.nbconvert.ipynb
The successful completion of the command confirms that the notebook executed from start to finish without errors, indicating that the Brush installation inside the Docker container is working as expected. The generated archive.nbconvert.ipynb file contains the fully executed notebook, including all outputs, and is a record of the run, though it is written inside the container and is lost when that container is removed. Copy it out with docker cp, or retire the container with docker stop brush_demo followed by docker rm brush_demo and start a fresh one with a -v mapping added to docker run and an –output-dir added to the nbconvert command. Bind-mounting the working directory itself would hide the cloned notebook and the run would fail. Reproducing the recorded run exactly would also require the image it ran in to be pinned to exact versions.
At this point, a running brush_demo container can be used both from the terminal and as a development environment. In particular, the running container can be attached directly in Visual Studio Code using the Docker or Dev Containers extension, which supports interactive exploration of the codebase, execution of additional notebooks, and iterative development with full IDE support. The result is a straightforward move from a command-line validation step to a more interactive workflow while keeping the environment isolated and repeatable.
Key Takeaways
A container is a set of processes isolated by kernel namespaces and cgroups. It packages an application and its user-space dependencies only, with no guest kernel, which is why it is typically smaller and quicker to start than a virtual machine.
Docker Desktop does not run Linux binaries on the Windows kernel. It starts a lightweight WSL 2 virtual machine, the default backend, which supplies a genuine Linux kernel, and every Linux container shares that one kernel instead of booting a machine of its own.
The writable layer added at run time survives stop and restart, then disappears when the container is removed. Output outlives the container through a bind mount to a host or WSL folder, a named volume, or a docker cp taken before the container is removed, and the docker run command above adds no mount.
This Dockerfile starts from the mutable tag ubuntu:24.04, pulls Miniconda3-latest and clones Brush unpinned, so the same file can build a different image later. Exact rebuilds need a digest-pinned base, locked dependencies and verified downloads.
A container does not virtualize hardware, and the GPU is not handed to it by default. On Windows it requires a supported NVIDIA GPU, a driver with WSL 2 support, current WSL 2 components, a GPU-capable image and a –gpus request at run time.
Older compilers and specific library versions stay inside the image instead of polluting the host, and the same image runs on a laptop, a workstation or a cloud server of the same architecture. A running container can also be attached from VS Code with the Dev Containers extension.
Data & License
No third-party dataset. The benchmarks and measurements shown are the author’s own, recorded on the hardware and software described in this article.
© 2025 Philip Sarajlic. All rights reserved for the article’s original text and figures.
Code examples in this article are licensed under the Common Public Attribution License Version 1.0 (CPAL-1.0), an OSI-approved copyleft license based on the Mozilla Public License 1.1. Initial Developer: Philip Sarajlic.
Attribution required by CPAL Exhibit B: © 2025 Philip Sarajlic · “Based on code by Philip Sarajlic” · philipsarajlic.com · no graphic image. This attribution must be displayed in Larger Works.
Modifications must be released in Source Code form under CPAL-1.0. Making the code usable by anyone other than you over a network is External Deployment under the license and is treated as distribution, so the Source Code must be made available to those users.
Full text: opensource.org/license/cpal-1-0 (SPDX identifier CPAL-1.0)

















