In my last blog, I spent some time talking about why being efficient as a developer matters to me. The proliferation of container technology can significantly help us achieve that goal.
What is a container?
First though, let’s quickly go through what a container is for those who don’t know. Docker is the most common containerization solution, so I’ll take the definition of a container from their website:
A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another. A Docker container image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries and settings.
Containers can run on Windows or Linux systems, are relatively easy to set up and deploy, and are easy to share or distribute to others (via services like Docker Hub).
Of course, there are some limitations to using containers as well. You have to set up a container runtime on every system where you want to run your containers, containers can get quite large, which may be an issue if you are having to upload and download them often, and they don’t provide hard separation of resources on the underlying system like a virtual machine (VM) would. Despite these limitations, for many use cases containers are an awesome way to go, so let’s talk about how it helps us as developers.
What does this mean to a developer?
Using containers as part of our development process means you can create and maintain a stable, reproducible environment from which to develop software. If you ever corrupt your development environment, it is as easy as starting another instance of the container to get a clean environment to work in.
Another cool thing is you can create and run multiple developments and test environments (aka multiple Linux versions) on a single system. Most importantly, you generally don’t have to worry about messing up your host system since the container isolates most resources.
For developers whose customers use containers, shipping your application in a container allows you to run in a more controlled environment. This can help reduce bugs, reduce your testing surface area, and improve your time to market (TTM), which helps both you and your customers.
There are other options available to solve some of the “multiple development environment” problems such as virtual machine (VM)-based solutions. You can create a base Windows Subsystem for Linux (WSL) instance, configure your development environment in it and deploy the instance multiple times. You also could use enterprise solutions like a VMware Virtual Desktop Infrasture (VDI) which are even more robust. However, these solutions aren’t available to everyone on all platforms, so for the purposes of this blog, I will focus on leveraging containers to help develop and deploy software using oneAPI.
Using the oneAPI Development Containers
To leverage the oneAPI development containers to provide the aforementioned, stable development environment, we first need to set up Docker on our development system. In general, you can just go to Docker’s website and follow their installation instructions. You may remember from my previous blog that I’m using the WSL environment for my development, so it was slightly trickier.
Once Docker is set up, you can grab the latest and greatest oneAPI development containers from Docker Hub.
There are oneAPI development containers for developing oneAPI code on CentOS8, Ubuntu 18.04, or Ubuntu 20.04. For this example, I’m going to build and package some simple SYCL code, so I’m going to pull the oneAPI Base Toolkit, Ubuntu 20.04 image. Using the Docker command line, I just run:
> docker pull intel/oneapi-basekit:devel-ubuntu20.04
This downloads the container to my local system. To get into the development environment you can run the container using the following command:
> docker run -ti --name=ubuntu-dev-20.04 intel/oneapi-basekit:devel-ubuntu20.04
-ti flag tells Docker to provide me with an interactive terminal in the container once it is up and running. The name flag gives our running container the name ubuntu-dev-20.04.
Building some code
To make things a bit simpler, I will be leveraging the oneAPI samples. I went to the oneAPI samples Github:
…and cloned the repo in my development container. I’ll be using the
Nbody sample located in the
The nice thing about using the development container is I do NOT have to run:
> source /opt/intel/oneapi/setvars.sh
As I did in my last example because the oneAPI development container was built having already run this command. I simply can go to the
Nbody folder and follow the instructions to build and run the
> mkdir build > cd build > cmake .. > make > make run
Here’s the output on my Intel i9 Alder Lake Alienware R13 system:
Creating a Production Container
Now that we know our code builds in the development container, let’s see how we can ship it in a container to a customer to run on their system.
I’m going to leverage the Docker multi-stage build capability to tell the docker build process to build my code in one container and then copy it to another container. This allows me to have the entire build and package workflow in a single place, while still allowing me to ship a container without all the development tools. Here’s my production container Dockerfile:
# run the development container and name it mybuild FROM intel/oneapi-basekit:devel-ubuntu20.04 as mybuild # get oneAPI sample code RUN git clone https://github.com/oneapi-src/oneAPI-samples # build the Nbody sample to root directory RUN cmake /oneAPI-samples/DirectProgramming/DPC++/N-BodyMethods/Nbody RUN make # use oneapi-runtime container as my production container FROM intel/oneapi-runtime:latest # copy file from mybuild to the production container COPY - from=mybuild src/nbody / CMD ["/nbody"]
You’ll see that for the production container I’m using the
intel/oneapi-runtime:latest container as a base because it provides all the runtimes required to run any oneAPI-based workload.
The next step is to take this Dockerfile, which I named
Dockerfile.runtime, and run the docker build command to create my new container.
> docker build . -f Dockerfile.runtime -t tonymintel/nbody:runtime
This command tells Docker to build using the current path, my
Dockerfile.runtime file and tagging the image as
A successful docker build! Yay!
A quick run of the container shows that our build was completed successfully and is running out of code as we want.
Now I just need to package up my container and ship it to my customer. I can push it up to Docker Hub or use the docker save command to save the image on my local system and send the file.
In my case, I’m just pushing it up to Docker Hub:
> docker push tonymintel/nbody:runtime
This pushes the image to my
tonymintel Docker Hub account and saves it to the
nbody repository with the tag runtime. Now anyone with access to the
tonymintel/nbody Docker Hub repository can pull and run the code via regular Docker commands.
Tagged nbody repo on Docker Hub
Making my production container 4x smaller
Just as being efficient as a developer is important to me, having an efficient solution is important to users of our applications. As my colleague James Reinders says, we need to make sure we bring them “Joy Out of the Box.”
From my screenshot, you can see the container I’ve uploaded is 1.32GB. That’s a pretty big Docker container for my customer to download.
My production container is based on the intel/oneapi-runtime container, which provides all the runtimes for all toolkits that build oneAPI code. This isn’t necessarily a bad thing. oneAPI provides a variety of libraries that I may want to integrate with my code, so having a runtime package that I know will run my code is great. However, since I’m the developer and I know what libraries I used, let’s see if we can optimize our production container to make it smaller.
The intel/oneapi-runtime:latest Docker Hub file as of June 2022
After some inspection, I created a new Dockerfile.prod that looks like this:
# run the development container and name it mybuild FROM intel/oneapi-basekit:devel-ubuntu20.04 as mybuild # get oneAPI sample code RUN git clone https://github.com/oneapi-src/oneAPI-samples # build the Nbody sample to root directory RUN cmake /oneAPI-samples/DirectProgramming/DPC++/N-BodyMethods/Nbody RUN make # build the production container FROM ubuntu:20.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y - no-install-recommends \ curl ca-certificates gpg-agent software-properties-common # repository to install Intel(R) oneAPI Libraries RUN curl -fsSL https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS-2023.PUB | apt-key add - RUN echo "deb [trusted=yes] https://apt.repos.intel.com/oneapi all main " > /etc/apt/sources.list.d/oneAPI.list # repository to install Intel(R) GPU drivers RUN curl -fsSL https://repositories.intel.com/graphics/intel-graphics.key | apt-key add - RUN echo "deb [trusted=yes arch=amd64] https://repositories.intel.com/graphics/ubuntu focal main" > /etc/apt/sources.list.d/intel-graphics.list # pull required Intel runtime packages needed RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y - no-install-recommends \ intel-oneapi-runtime-dpcpp-cpp ENV LANG=C.UTF-8 ENV LD_LIBRARY_PATH=/opt/intel/oneapi/lib # copy file from build container to our production container COPY - from=build src/nbody / CMD ["/nbody"]
There’s a lot of text here, so let me try to explain what is going on:
- Lines 1–9 and 32–34 are the same in both Dockerfiles. They build our binary and run it when the container starts
Lines 11–30 are mostly boilerplate code that sets up Intel’s apt repositories and then installs some Intel packages via APT. These lines are pulled from the
oneapi-runtime Dockerfile, which can be found here:
The key line in my
Dockerfile.prod is line 28. If you go look at the original
intel/oneapi-runtime Dockerfile (click here and click on a digest SHA and find the biggest image layer), it does an apt-get for all the possible runtime packages. In my case, I know I only used DPC++, so my
Dockerfile.prod only includes that
Now I just build my new Dockerfile, verify it works as before, and push it to the cloud.
> docker build . -f Dockerfile.prod -t tonymintel/nbody:production > docker run -it tonymintel/nbody:production > docker push tonymintel/nbody:production
Now I see ~4x improvement in download size for my customer…a little more Joy Out of the Box hopefully.
You might ask, can I do better even? To avoid suspense, the answer is yes, but it involved going in and pulling individual files to omit various unused runtime components. It’s a little bit cumbersome, but it is possible. If you’re curious, it ended up making the package about 250MB for a ~5x improvement.
I consider this a big win because it can really affect your customer’s experience. Obviously, if your customer needs to download a container over a standard internet connection this is a big win. In a large-scale environment like a data center or cloud environment, this may be even more valuable since deploying the same pod on tens or hundreds of nodes is very common and network usage, saturation, and latency is always a concern. Perhaps most importantly, if your customer is paying for network or storage to deploy their containers, they’ll thank you (or at least won’t complain) about the container costing them more. :)
Containers are a useful technology to help us as developers build and test our code. Depending on our customer (aka will they use a container), it also can really help us ship more reliable code to our customer.
There’s definitely some work that goes into learning all the technology, but it is definitely worth it in many cases. I hope this helped you learn a little about how to leverage containers and how to optimize those containers for your customer.
Until next time!
Want to Connect? If you want to see what random tech news I’m reading, you can follow me on Twitter.
Tony is a Software Architect and Technical Evangelist at Intel. He has worked on several software developer tools and most recently led the software engineering team that built the data center platform which enabled Habana’s scalable MLPerf solution.
See Related Content
- Free Your Software from Vendor Lock-in using SYCL and oneAPI
- Accelerating Open Development through oneAPI Centers of Excellence
- Building a Standards-Based Ecosystem for Heterogeneous Parallelism
- SYCLomatic: A New CUDA-to-SYCL Code Migration Tool
Get the Software
Get started with this core set of tools and libraries for developing high-performance, data-centric applications across diverse architectures.