How I Run Developer Environments in Docker

How I Run Developer Environments in Docker

By Lisandro Pérez Meyer

It is not uncommon for software developers to be involved in more than one project at a time, or need to set up an old project to check a change. Some projects might require a specific OS, or availability of one or more SDKs or libraries. A virtual machine might come handy in these cases. But virtual machines come with drawbacks: they run yet another kernel with a subset of the main system’s cores and RAM. And keeping a full image around for documentation purposes is suboptimal.

To better handle this type of scenario, including containers on embedded devices, I have been using Docker and Podman. I thought you might be interested in learning about how I set up and document specialized systems.

Create a Git Repository

I start by creating a git repository in which to take notes, and keep necessary files and other stuff. With a git repository I can use Markdown-based files to gather documentation and notes while treating them as code: atomic commits, history can be reviewed, and I can sync it up in multiple machines. I always keep in this repository either a Dockerfile or Buildah scripts in order to build my environments.

Next, I need to successfully run Qt Creator directly on the container. For this, I typically use Docker because it makes it easier to share repositories in my base system’s file system while keeping the right user permissions. (On Podman, as far as I understand, I would need to make the container’s root user to map my local user).

How would this work in practice? Let’s suppose I need an OS that ships Qt 5.15. I’m a Debian Developer so I’ll go straight to Debian Bullseye. Here’s how I’d start constructing the required Dockerfile. Basic stuff like Bullseye-slim to get the smallest image possible to start with. These are the basic set of tools I always use: build-essential to get compilers et al., git, tree and vim.

FROM debian:bullseye-slim

RUN apt-get update

RUN apt-get install -y 
  build-essential \
  git \
  tree \

Then I find out I need to work around tzdata asking for information while being installed. While there might be a better way to achieve this, this is what I use:

# Hack around tzdata asking for manual information.
RUN ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime
RUN apt-get install -y tzdata
RUN dpkg-reconfigure --frontend noninteractive tzdata

Now let’s suppose the project requires a project-exclusive library to be installed, and this library requires Qt Core, Network and SerialPort.

# libproject-exclusive requires two build dependencies:
# qtbase5-dev: Qt Core and Network
# libqt5serialport5-dev: Qt SerialPort
RUN apt-get install -y \
  qtbase5-dev \

Our main app – myapp – requires Qt Core, Qt Gui, and libpoppler-qt5. Here’s what that looks like:

# myapp build dependencies
# qtbase5-dev: Qt Core and Gui
# libpoppler-qt5-dev: Poppler support for Qt 5
RUN apt-get install -y \
  qtbase5-dev \

Why did I add qtbase5-dev since it was already installed in the previous step? I had a good reason! Doing so helps me track the dependencies of each project. If I ever need to replace libproject-exclusive with another non-Qt based library I can just delete the entry without worrying about the rest of the file.

Moving on, I discover I need a newer googletest than the one provided by Debian Bullseye. Let’s install it:

# We need the latest googletest
RUN git clone /opt/googletest
ENV GOOGLETEST_DIR=/opt/googletest

It’s time for convenience code. I’ll try to run it within the container with the same user ID as I use on my system. Here goes:

# Replace 1000 with your user / group id
# Replace lisandro with your user
RUN apt-get install -y sudo
RUN export uid=1000 gid=1000 && \
    mkdir -p /home/lisandro && \
    echo "lisandro:x:${uid}:${gid}:Lisandro,,,:/home/lisandro:/bin/bash" >> /etc/passwd && \
    echo "lisandro:x:${uid}:" >> /etc/group && \
    echo "lisandro ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/lisandro && \
    chmod 0440 /etc/sudoers.d/lisandro && \
    chown ${uid}:${gid} -R /home/lisandro

USER lisandro
ENV HOME /home/lisandro

You might be wondering why I am adding ‘sudo’ here. Sometimes it’s very convenient to temporarily install packages to test stuff. For that the user requires super-user privileges, so sudo here is quite convenient. If the package ends up being required, it can be easily added to the Dockerfile.

Now, I want to run Qt Creator on this image:

# Let's use my favorite IDE.
RUN apt-get -y install qtcreator
CMD /usr/bin/qtcreator

The Dockerfile is finally ready – but I’m not quite finished. I have to document everything. To do so, I normally add a file which states how to build the docker image:

$ docker build -t myprojectimage .

And how to properly run it:

$ docker run --privileged -ti --rm -e
/tmp/.X11-unix:/tmp/.X11-unix -v
/home/lisandro/my/repos/theproject:/home/lisandro --network host

Here’s a closer look at this last part.

  • I’m running the container as a privileged one. From my point of view, it is mostly as if I were running the code on my main system – meaning there’s nothing much to worry about here. If your situation is different, you can skip this if you want/can.
  • Export $XDG_RUNTIME,  $DISPLAY and $XDG_RUNTIME_DIR to match the one in your main system.
  • Share the X socket.
  • Share “theproject”’s main directory as if it were my home. Yes, this means I get to reconfigure Qt Creator as I want, but also that I keep all my project’s settings in the same place.
  • I include this last line ~/.bashrc as an alias to make it easier to start the project.


I showed you my way for setting up an environment with a specific OS, and some specific SDK and/or libraries without cluttering my main system with a project’s specifics. While this setup has worked well for me, I’m sure it could be modified or improved. If you have questions or suggestions, feel free to reach me using our contact form. Please include my name and reference this blog in the “How can we help you?” field.