Debug a Docker image build

I am creating new Docker images and encountering various issues; debugging a Docker image build can be surprisingly tricky.

Here are a bunch of things that I learned.

Docker basics

In case you are not familiar with Docker: it’s basically a system akin to a lightweight virtual machine. It uses files to build images, which are then run as containers.

The same Dockerfile can be used to build multiple images using different arguments ; and the same image can be used to start multiple containers using different arguments.

There is also a “compose” system that allows you to run multiple containers as a group, configure them in a single place, and other cool things.

A common question that is not often answered by tutorials is “why use Docker”: because it allows to standardize your applications build and deployment, and if correctly used, it can ease up your development by a lot (you can attach a debugger to a program running in Docker), especially if you have a microservice architecture.

Here is the official documentation:

Advanced basics

If you’re using Windows, don’t forget that your containers will mostly use Linux, and that Linux uses LF file ending.

When built using a Dockerfile, each instruction in the file results in an intermediate image, called a layer. These layers truly are images, and can be used to start containers, inspected, etc. It’s an important thing to remember because it’s critical to remember that for debugging. More on that below.

Images and containers can accumulate over time and result in a huge disk loss. Use the Docker pruning to do some cleanup: https://docs.docker.com/engine/manage-resources/pruning/

When building an image or starting a container, either you specify a name, or an ID will be generated for you. Remember to use names if you need to find the image or container again later.

Take specific care with the order of arguments in commands: most do not accept arguments after unnamed parameters. For example, docker build -f .\Dockerfile src will work, but docker build src -f .\Dockerfile will not. If it looks like your argument is not used, it may be why.

By chaining multiple FROM in a Dockerfile you can use a single file to build a program using a big SDK and still have a lightweight image to run afterwards, for example:

FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base
# prepare the final image using EXPOSE, WORKDIR, RUN apk add, etc

FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim AS build
# build the project using COPY and RUN dotnet build

FROM base AS final
# copy the build artifacts into the final image using COPY --from=build

Usual Docker commands

Building and running

  • docker build -f .\Dockerfile . uses the specified Dockerfile and builds the project, using the current folder as context; add -t xxx to name the resulting image “xxx”
  • docker run xxx creates a new container based on the image named “xxx”, then starts it; you can add --name=yyy to specify the name of the container (otherwise it will have a random name)
  • docker create xxx creates the container based on the image named “xxx” but does not start it
  • docker start xxx starts the container with the ID or name xxx, but it has to already exist

Checking the status

  • docker image ls lists the already-built images that can be run
  • docker ps displays a list of running containers (add -a to also list the ones that are stopped)

Debugging a Docker image creation

  • docker inspect xxx shows metadata details about the specified image: layers, entrypoints, environment variables, exposed ports, etc.
  • Set the DOCKER_BUILDKIT environment variable to 0 to enable more debugging (Powershell: $env:DOCKER_BUILDKIT=0; docker build -f path/to/Dockerfile . ; Linux: DOCKER_BUILDKIT=0 docker build -f path/to/Dockerfile .). This will give you “short guids” at each steps, and these layers IDs can be used with docker run (see below) to start a container at that build step and see what’s happenning.
    Note: this is deprecated, but the alternative (docker buildx debug build -f path/to/Dockerfile .) is experimental and does not work properly at the time of writing.
  • Another, more basic way to debug, is simply to comment everything in your Dockerfile except what’s working, but it can be a bit annoying with large, complex files.

Debugging a Docker container

  • docker inspect xxx shows metadata details about the specified container: mounted volumes, networks, system stuff (virtual CPU, I/O, etc), environment variables, exposed ports, etc.
  • docker run --rm -it xxx sh creates a container based on the image named “xxx”, starts it and opens a shell (sh) on it (but you can run any command you want like “ls”) ; -it runs the command in interactive (-i) TTY (-t) ; --rm removes the container when it exists
  • docker run --rm -it --entrypoint sh xxx does the same but skips the ENTRYPOINT instruction of the Dockerfile (in case this renders it unable to start)
  • docker exec -it xxx sh opens a shell (sh) inside a running container
  • docker attach xxx attaches the current terminal to the main process of a container
  • docker export xxx -o xxx.tar exports the content of the specified container to a tar file so it can be explored using an archive explorer (ex: 7zip). This command cannot use an image, it has to use a container, so if you cannot start your container you’ll have to use docker create first (see above)
  • docker logs xxx shows the logs of the container (actually stdout); adding -f will tail the logs

Dockerfile layers cache optimization

As previously stated, each instruction of a Dockerfile will result in a temporary image, called layer, to be created. Let’s take an example, a basic nginx server:

FROM nginx:alpine
ARG PORT=8080
EXPOSE ${PORT}
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY ./html/ /usr/share/nginx/html

Each of these instructions creates a layer, and these layers are cached. This cache is invalidated when a layer changes, whether it’s the result of a RUN or the content of files to COPY.

What it means exactly, is that this Dockerfile, inverting the commands, functionally results in the same image:

FROM nginx:alpine
COPY ./html/ /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
ARG PORT=8080
EXPOSE ${PORT}

However, every time a single file in the html folder changes, the whole cache after that is invalidated!

Obviously in this basic example it doesn’t change much, but when you’re preparing a complex image, remember to declare everything that changes rarely as high as possible in the stack of commands to optimize the Docker cache and speed up your build!

Cleaning up outdated Git branches

I’m pretty sure you have tons of Git branches that you have merged and removed on your server, but are still on your local clone. Here’s a Powershell script to clean them up, using non-brittle commands (just doing git branch -vv like you often see is not a good practice).

Merge git repositories

We have more than 20 Git repositories for the same project, but it causes lots of headaches for building. We’ve decided to merge most of them into a single one, keeping the file history when possible.

This script uses newren/git-filter-repo and as such needs Python 3.

It’s done in 3 steps:

  • clone the local repositories from d:\dev\xxx to d:\devnew\xxx using git-filter-repo with source and target parameters
  • create a new repository at d:\devnew\merged
  • merge from d:\devnew\xxx to d:\devnew\merged\xxx using git merge --allow-unrelated-histories

I’ve removed a lot of code for brievity, so maybe it won’t work out of the box, but you should get the general idea.

Next up: pushing the new repository, then migrate the developers workstations, for which I wrote another script:

Next up: migrating Jenkins jobs. With more than 170 jobs, doing it by hand is a real chore. Fortunately, you can also automate it.

Now we can version the Jenkins configs. Then we edit, and update them.

Migrate YUI components

I work on an application that uses quite a lot of Javascript components based on the YUI framework, which has been obsolete since around 2014. We recently had the budget to start migrating them to plain Javascript.

How YUI widgets work

Our YUI components use the syntax of YUI3. Sometimes they also use widgets from YUI2 : these have to be rebuilt, or migrated to use another library.

A YUI 3 component is structured as follows:

Continue reading Migrate YUI components

Unable to type text in inputs in IE 11, missing caret

The app I’m working on allows a user working on a document to open a pop-in (Bootstrap popup), select something, validate, and it adds a field on the document, where the user can type something. After working for some time on the widget and changing many things, I test in IE11 and find myself unable to type anything anywhere, after selecting a value. The text input fields are focused (they have the Bootstrap blue halo), but the caret does not appear, and typing doesn’t do anything. Clearing another field that already has some value (using the “X” in the field) makes the inputs work again. All other browsers are working fine. It smells very strongly like a IE bug. A very similar bug is already documented here but it looks like it should have been fixed in 2014, and I’m using Windows 10 (so one would assume it has this fix). Anyway, the bug points towards a focused element being removed from the DOM. I have tried to just focus  something else and then blur  it, but it didn’t work. My salvation came when I realized that the content of the popup was inside an iframe. Selecting and bluring the iframe’s body did the trick :
$('#modalWindow').find('iframe').contents().find('body').focus().blur()
 

Always run Visual Studio as administrator on Windows 10

You might be working with a program that always require administrator privileges, such as a service listening on a port. Up to Windows 10, you just right clicked the devenv exe, and checked “always run as administrator” in the compatibility tab. But in Windows 10, the compatibility tab is missing for the Visual Studio exe. Why? No idea. But here’s how to fix it.

To just run the shortcut as admin : right click on the sortcut > in the “properties” tab, click “advanced”, then check “always run as administrator”. Simple, fast, easy.

If you want to run Visual Studio as administrator also when opening a .sln or .csproj file, it’s not so easy. Here are the logical, coherent, and easy to find steps to fix this: (labels translated to english from french, sorry if it doesn’t match)

  • Open the start menu, type “troubleshoot”, and open “check your computer state and solve problems”
  • On the bottom left, click “troubleshoot compatibility problems”
  • Click on “advanced” on the left then “run as administrator”
  • Click again on “advanced”, then uncheck “automatically fix”, then “next”
  • Search for Visual Studio in the list, then “next”
  • Make sure “fix the program” is checked, then “next”
  • Select “troubleshoot the program” (bottom option)
  • Check “the program requires additional rights”, then “next”
  • Click “test the programe” then “next”
  • Select “save the parameters for the program” then “close”

Easy, logical, simple. Thanks Microsoft!

Git-svn and branches

I’m using git-svn to leverage the power of git while the rest of the company prefers the simplicity of svn.

I have created a local branch, intending to merge it in the trunk/master when I’m done, but it turns out it’s much more complicated than expected. So, I want to commit it on svn.
I already have checked out the full repository.

The most simple solution I have found; all other solutions fiddle with the git config files, and I don’t like it:

  • Checkout the branch: git checkout mybranch
  • Rename the local branch: git branch -m mybranch-temp
  • Create a remote branch “mybranch” in SVN using Tortoise SVN
  • Fetch the SVN repository to get the new branch: git svn fetch
  • Check that the new SVN branch has been fetched: git branch -r (or git branch -avv )
  • Get back to the master branch: git checkout master
  • Checkout the remote branch: git checkout -b mybranch-svn origin/mybranch
  • Rebase my local branch into the local svn branch: git rebase mybranch-temp
  • Commit the branch in svn: git svn dcommit ; use --dry-run  to check the branch it will commit to, because sometimes it still commits to the trunk (I haven’t found out why).

Unit testing MongoDB queries

When writing unit tests, it’s common to stop at the database access layer.
You might have a “dumb” data access layer that just passes stored procedure names and parameters to the SQL database, for instance.
It’s usually very hard to test this layer, since it requires either that your build server has access to a SQL Server instance, or that a SQL Server is running on the build server.
Plus, we’re getting out of unit tests and are entering integration tests, here.

Using a more advanced tool like Entity Framework, in order to test your complex EF queries, there are usually methods to insert fake data into a fake container, like Test Doubles, InMemory, or Effort.

Using MongoDB, you might encounter the same problem : how do I test that my complex queries are working ?

Here for instance, I get the possible colors of a product; how do I know it works using unit tests?

MongoDB has an “inMemory” storage engine, but it’s reserved to the (paid) Enterprise edition. Fortunately, since 3.2, even the Community edition has a not-very-well-documented “ephemeralForTests” storage engine, which loads up an in-memory Mongo instance, and does not store anything on the hard drive (but has poor performances). Exactly what we need!

Before running the data access layer tests, we will need to fire up an in-memory instance of MongoDB.
This instance will be common to all the tests for the layer, otherwise the test runner will fire up new tests faster than the system releases resources (file and ports locks).

You will have to extract the MongoDB binaries in your sources repository somewhere, and copy them besides your binaries on build.

The following wrapper provides a “Query” method that allows us to access the Mongo instance through command-line, bypassing the data access layer, in order to insert test data or query insertion results.

We’re using the IClassFixture  interface of xUnit to fire up a MongoDaemon instance that will be common to all our tests using it.
It means we need to clean up previously inserted test data at each run.

There you have it: a kind-of-easy way to test your Mongo data access layer.

Nested sets references

Just bookmarking a few things here.

Modify copy/paste format depending on target

Sometimes you want to copy tabular data, and paste it differently depending on the target. For instance, you want CSV when you paste in your text editor, and HTML when you paste it in an email.

Fortunately, some nice fellow has created a helper class to handle all that for you. And it’s even available on GitHub!

To sum it up, the Clipboard  class has a SetDataObject method. A DataObject can get multiple contents (with different DataFormats) through multiple calls of the SetData(string, object)  method.
So basically, you should create a DataObject, then call SetData once with the HTML, and once with text. The trick is that the HTML needs special formatting to work.