How I run executables in containers
Here's a quick post explaining how I might typically containerise an executable. This is in the context of my aim to not "pollute" my base OS (at the bare metal level) with any installs, as far as I can, as well as to remain platform independent and abstract.
Background
Last Friday saw the second SAP Inside Track Madrid event, which was excellent. My good friend and colleague Antonio, one of the core event organisers, kicked things off, and then I started with my talk, "Level up your CAP skills by learning how to use the cds REPL".
I'd written my talk also in blog post form, for folks who might also be interested but couldn't attend the event in person, but also for the folks in the room, especially as I wanted to make clear that everything I was about to demonstrate (I conducted my talk as a live REPL session in the terminal) was easily repeatable, and they could follow the examples at home afterwards by reading the post.
Generating and displaying a QR code in the terminal
So I decided to be ready to share a link to the blog post right at the start of the talk, and thought I'd have a bit of fun trying to generate a QR code in the terminal, large enough for the attendees to scan on their phones from where they were sat in the audience. For this, I found a great tool called QRCode Terminal, in the form of a binary called qrterminal
.
Here's a very grainy screen grab from a small picture-in-picture window of me presenting:
Abstracting all the things
I am trying to avoid installing anything on this macOS laptop other than the basics, i.e. Docker. In previous times I would have typically had Homebrew installed and then used that to install various software packages, but I'm actively avoiding that, choosing instead to run everything in containers, either locally (i.e. on the container engine running on my laptop) or remotely (on my "homeops" server or on my Synology NAS).
I didn't have QRCode Terminal installed in my PDE (Personal Development Environment) which is also a container (based on Debian and running tmux
, neovim
, bash
and so on), so I could have just apt install
ed it and executed it there, but I wanted to take the opportunity to improve my Dockerfile fu (one kata at a time).
The Dockerfile
So I created a simple Dockerfile, trying to use some best practices with respect to multi-stage builds and minimising image size. Here it is:
FROM alpine:3.21 AS base
ARG ARCH=arm64
ARG QRTERMINALVER=3.2.1
RUN wget \
-O - \
-q \
https://github.com/mdp/qrterminal/releases/download/v${QRTERMINALVER}/qrterminal_Linux_${ARCH}.tar.gz \
| tar xzf - -C / qrterminal
FROM scratch
COPY /qrterminal /
ENTRYPOINT ["/qrterminal"]
Here are some notes:
- It's a two stage build
- The first stage is based on the small Linux distro Alpine, which has
wget
andtar
- all I need to download and extract the executable - The second stage is based on the "minimal image" scratch which seems ideal for this sort of containerised executable
- The
qrterminal
executable, extracted in the first stage, is copied to the second stage (see the--from=base
option to theCOPY
instruction) - I used the
ENTRYPOINT
instruction to set the default executable for the container image (see Docker Best Practices: Choosing Between RUN, CMD, and ENTRYPOINT for more info on this) - this means that I can then execute single shot containers, passing whatever QRCode Terminal options I need - My macOS laptop is Apple Silicon based which means a different native architecture (
arm64
) to my "homeops" server (amd64
), and even though I can cross-build thanks to QEMU/Rosetta, building for native is always preferable, but I'd like to have the option of building foramd64
too
On this last point, I'd like to dig in more to see how I might improve this, especially with respect to those circumstances (like here) where I'm downloading a particular tool's binary release which has been built for a specific architecture which is also in the tarball URL name.
Here's the (cached) build output:
$ docker build --build-arg QRTERMINALVER=3.2.1 -t qmacro/qrt .
[+] Building 1.9s (7/7) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 448B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.21 1.9s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [base 1/2] FROM docker.io/library/alpine:3.21@sha256:a8560b36e8b8210634f7 0.0s
=> CACHED [base 2/2] RUN wget -O - -q https://github.com/mdp/qrtermina 0.0s
=> CACHED [stage-1 1/1] COPY --from=base /qrterminal / 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:7731e4687c0a4272f2a06002c2bc933fdb5fe3e7c6f0b6f53 0.0s
=> => naming to docker.io/qmacro/qrt 0.0s
I was happy with the result, shown here via docker image ls
:
IMAGE ID REPOSITORY TAG SIZE
7731e4687c0a qmacro/qrt latest 1.64MB
And the best part was - it worked!
$ docker run --rm qmacro/qrt https://qmacro.org/blog/posts/2025/03/21/level-up-your-cap-skills-by-learning-how-to-use-the-cds-repl/