
First things first
In my daily routine as a data scientist, I noticed that in my production pipeline the LGBMRegressor model was not running with success on GPU inside the container. After that, I decided to take a look on how was the Dockerfile:
FROM nvidia/cuda:12.6.3-runtime-ubuntu24.04
# INSTALL CONDA
RUN apt-get update && apt-get install -y wget
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-py39_24.11.1-0-Linux-x86_64.sh
RUN bash Miniconda3-py39_24.11.1-0-Linux-x86_64.sh -b -p /opt/conda
ENV PATH="/opt/conda/bin:${PATH}"
# CREATE PIPELINE CONDA ENV
WORKDIR /usr/src/pipeline
COPY ./gpu-time-series-env.yaml .
RUN conda env create -f gpu-time-series-env.yaml
ENV CONDA_OVERRIDE_CUDA=12.2
SHELL ["conda", "run", "-n", "gpu_time_series", "/bin/bash", "-c"]
RUN conda install -c conda-forge tensorflow-gpu=2.17.0 lightgbm=4.6.0
RUN conda install conda-forge::flash-attn-layer-norm==2.6.3
RUN pip install xgboost==3.0.5
# COPY CODE AND RUN
COPY . .
CMD [ \
"bash", "-c", \
"source /opt/conda/etc/profile.d/conda.sh && \
conda activate gpu_time_series && \
conda config --set channel_priority strict && \
python3 main.docker.py" \
]Some Concepts (you can skip it)
Before dive into the problem I will give some definition of containers, docker and virtual machine (VM).
Containers are a way to encapsulate your application and dependencies that can be installed in a different machine without the need to install every instruction manually. Unlike Virtual Machines (VMs), it does not virtualize the hardware, neither operates in a new Operational System (OS). Instead, it shares the same kernel of the host machine, with no overhead when it is operating inside a linux (that’s also way I use Ubuntu), or a little overhead with Windows (it needs to be use WSL and Hypervision) or MacOS.
In Data Science, we need a lighter, fast and isolated piece of software, that’s way containers are handy. We do not need to have another entire system to our disposal.
When we run FROM: ubuntu:24.04 in Dockerfile we are not installing a fresh new Ubuntu for that container. We are only installing some parts of the Ubuntu, such as bash, apt and /usr, /etc folders, while having the same kernel of the host (it is not even necessary to have the same version of the linux, as long as the kernel supports it).
Lastly, but not least, Docker is a tool that manages the containers :). Dockerfile is the filename used to create an image, that containers can be created from it.
Command definitions in the Docker
COPYTransfers files and directories from your host machine (the build context) into the container’s filesystem during the build phase. It runs in build timeRUNruns on files inside the containerCMDsets the default command that executes automatically when the container starts running. It does not execute during the build process. It runs in RuntimeSHELLchanges the default shell used to execute subsequent RUN, CMD and ENTRYPOINT instructions during the build. Linux containers use [“/bin/sh”, “-c”]. It runs in build configuration.
Analyzing the current Dockerfile
When I looked into the Dockerfile, I realized that there were several issues:
- Several packages are not being installed inside
conda:- One of them is the
lightgbm; - All other packages are also GPU related;
- One of them is the
- We have two different versions of cuda:
- Cuda
FROMthe image, which is12.6.3; - Cuda version that will be used by
conda(CONDA_OVERRIDE_CUDA=12.2);
- Cuda
conda config --set channel_priority strictin theCMDspecifies thatcondawill use only one channel to install packages:- Once it is inside the container, we do not need to install anything else, only use it. Therefore, unnecessary;
How everything got solved:
Considering that solving the problem only for lightgbm, would leave many problems behind, I decided to take one by one.
LightGBM
- First of all, I put back all packages inside the
conda, because I wanted to usecondathe single source of truth, and it would guarantee that its solver would not break in theDockerfile. - Then, I realized that
lightgbminstalls two libraries:liblightgbmwhich is the C++ code;lightgbmwhich is the Python API.
Since one of them was installed by conda-forge channel, and another by defaults., it created a version mismatch between then, which did not make possible to use the GPU inside the container. Therefore, I reinstalled them using only conda-forge, which is the recommended channel nowadays.
⚠️ GOOD PRACTICE ALERT: Use only a single channel when creating a conda env.
In addition to that, conda-forge itself has three different ways to install its package: cuda, gpu or cpu. It also needs to be specified in the yaml. In this case, I did liblightgbm=4.6.0=cuda* and lightgbm=4.6.0=cuda*. cuda because is more modern than gpu. It guarantees that conda-forge would pick a cuda version of this library. I could be even more restrict to enforce cuda to be a specific version, but I set it loose.
flash-attn
It was also a bit pain, because it needs to be installed after pytorch (that’s way it was out of the conda env) once I installed from pip, which has an import pytorch after installation.
The good news is that after installing from conda-forge, this pre-installation process does not exists, and made possible to include it inside the conda env, with no ordering needed.
Cuda versions
What I realized is that inside the (gpu-time-series-env.yaml), I already had cuda (cuda-version=12.9). Therefore, it allowed me to get rid of image that install cuda and the CONDA_OVERRIDE_CUDA env var. Letting conda be the only one responsible by solving cuda.
It could be a good solution, but it would not last long because my cuda environment was created using conda env export --no-builds, which only describes the environment, i.e. it lists the packages that I want. But it does not specify the builds, because these builds are from the respective machine where I am using, and may not be compatible inside the machine I need to install it (I had problem with it before installing using only conda env export).
To solve this, I created the conda-lock.yaml file from conda-lock package, that locks the exact dependency resolution needed to reproduce an environment. It solves conda versioning of the packages and save it with all information needed to a fresh install. In order to create this file I also had to create the file virtual-packages.yaml:
subdirs:
linux-64:
packages:
__glibc: "2.28"
__cuda: "12.9"which specicies the cuda version, allowing the new image to know that cuda is available (just like CONDA_OVERRIDE_CUDA beore).
Pinned ubuntu image
When we just use FROM ubuntu:24.04, even though we pin the operational system, it keeps receiving patch updates, and can make our code to break unexpectedly. In order to overcome this, there are two main ways of doing it:
- Using the
manifest digest, which it is ashakey, not human readable, in this casesha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517; or - Using
<name_of_ubuntu>-<published_date>, in this casenoble-20260810
I used the second option, of course, because it is human readable.
Channels strict and conda lock
Once all packages are from conda-forge:
- I no longer need
conda config --set channel_priority strict - I no longer need
miniconda(which hasdefaultandconda-forgechannels available):- I switched to
miniforgewhich is theminicondaof onlyconda-forgechannel, being lightweight.
- I switched to
Pinning miniforge
I have also pinned miniforge to a version specific, because it will install Python on your container image; and an update on Python that may result an update to the latest miniforge can also break your software.
Optimized Dockerfile
After applying all theses changes, I was able to come up with the following solution:
FROM ubuntu:noble-20260810
RUN apt-get update && apt-get install -y wget && \
wget https://github.com/conda-forge/miniforge/releases/download/26.3.2-3/Miniforge3-26.3.2-3-Linux-x86_64.sh -O /tmp/miniforge.sh && \
bash /tmp/miniforge.sh -b -p /opt/conda && rm /tmp/miniforge.sh
ENV PATH="/opt/conda/bin:${PATH}"
# CREATE PIPELINE CONDA ENV
WORKDIR /usr/src/pipeline
COPY conda-lock.yml .
RUN pip install --no-cache-dir conda-lock && \
conda-lock install --micromamba --name gpu_time_series conda-lock.yml && \
pip uninstall -y conda-lock && \
conda clean -afy && \
rm -rf /opt/conda/pkgs/cache/* ~/.cache/pip ~/.cache/conda-lock ~/.mamba ~/.local/share/mamba
# Activate environment for RUN commands
SHELL ["conda", "run", "-n", "gpu_time_series", "/bin/bash", "-c"]
COPY . .
CMD [ \
"bash", "-c", \
"source /opt/conda/etc/profile.d/conda.sh && \
conda activate gpu_time_series && \
python3 main.docker.py" \
]Which is 20% faster to build and a 70% lighter image than the previous Dockerfile because:
- Ubuntu and miniforge are lighter;
- I have used
micromambaas well cleared all cache from package installation; - Started using
.dockerignore.
Next Steps
I want to separate the gpu-time-series-env.yaml file into development and production, so I can save even more space on disk!
Happy coding!