Build a docker container with cryptography, locust and pypy

Build a docker container with cryptography, locust and pypy

Some of you will ask why? Why are you doing this to yourself Mr One?  At the time, I was writing a fastapi pypy vs python benchmark/comparison for a code-clinic and hit a few snugs that I'd love to share with you. So if your are a glutton for punishment, this piece might just be what you need. If you are not intrigued, jog on and watch a 10 year old annoying orange clip to cheer yourself up you miserable old git.

If you are still here, I'm happy for you, congratulations and here's a picture of a gold medal.

Royal Gold Medal

Some Lessons Learnt

Lesson #1: Use multi stage docker builds - at a minimum while you're figuring how everything hangs together - unless you have to do your weekly chores in between making a code change and building a container.

Lesson #2: Take-your-time, find your zen. You are entering the unknown. Read all the stack-traces and follow the bread crumbs. I had a bad habit of skimming through and missing important information. Don't do that. That's pretending, not doing.

Lesson #3: Read lessons 1 and 2 again. Repeat if necessary.

ERROR: Could not build wheels which use PEP 517 and cannot be installed directly

How else can one install a library if not with wheels? Archaic as they may be they just work. For the life of me, I could not install the cryptography, gevent, geventhttpclient, psutil, and pyzmq libraries with pypy. That's the price one pays for walking slowly with a blindfold off the beaten track. The breadcrumbs led me to missing os dependencies (that would have been satisfied by pip install but we can't do that fully quite yet) so let's get our os sorted first.

Stage 1: OS

FROM pypy:slim-bullseye as os-base

RUN apt-get update && \
    apt-get install build-essential libssl-dev zlib1g-dev liblzma-dev libjpeg-dev git -y

EXPOSE 8080

Since not everything could be installed via pip, build-essential was required for using pip's --no-binary option later on. The rest of the os dependencies were required mainly by cryptography and locust, with the most unexpected of them being libbzip2 that does not exist and therefore needs to be cloned and built manually.

Please remember, this is a very specific use-case. The point of this blog is not for you to replicate this "I-don't-know-what-I'm-doing-saga", by coding side-by-side. It is aimed to help you potentially either (A) avoid these type of dirty tricks or at least (B) realise the pain incurred by blazing new trails for your own amusement.

Stage 2: Clone and Build libbzip2

FROM os-base as os-base-libbzip2

RUN git clone https://github.com/WardF/libbzip2.git
ARG LIBDIR=/libbzip2
WORKDIR $LIBDIR/
RUN make install
RUN make -f Makefile-libbz2_so

At this point, I was really thankful, not only to find a repo, but also to have a README containing instructions on how to build that thing. Remember, when you write code, your fellow hackers' remote mind-reading abilities are a thing of science fiction (e.g they may not exist yet), so a good README goes a very long way.

Stage 3: Pip install with --no-binary

Since, the os requirements have been met, let's find the library versions that work. That's another trial and error session that led me to truly appreciate the reasons why I should always pin library versions.

FROM os-base-libbzip2 as pip-base-no-binary

RUN pypy -m pip install \
    --verbose \
    gevent==21.8.0 \
    geventhttpclient==1.4.4 \
    pyzmq==22.2.1 \
    psutil==5.8.0 \
    cryptography==3.2.0 \
    --no-binary :all:

Stage 4: Pip install the rest

In this penultimate step (the final step was the ENTRYPOINT which has been omitted as it's deemed irrelevant to this writing) is to remove the libraries of step 3 above from your requirements.lock file and do one last thing that would have (most likely) worked if this was a typical python -m pip installation.

FROM pip-base-no-binary as app-base

ARG APPDIR=/app
WORKDIR $APPDIR/
COPY requirements.lock ./
# Application specifics have been omitted for readability

FROM requirements-no-binary as requirements-binary
RUN pypy -m pip install --requirement ./requirements.lock

Was this a painful read? Yes? Good. This took a fair bit of time to realise and I'm really glad I pushed through.

TL;DR

This is merely a thinking process and not necessarily a list of regimented steps in order of appearance. Should you find yourself in uncharted waters, expect some backwards and forwards while you're responding to the tidal stack-trace.

  1. Can a library be installed via pip? Yes? Everyone's happy!
  2. No? Check if an operating system dependency needs to be satisfied.
  3. Look in pkg.org to confirm your package exists. Yes? Lucky again!
  4. No? Look everywhere for a repo you can clone and build yourself. Got one? Brilliant. Now is the time to get a lottery ticket!
  5. Can't find a repo? Time to change direction. Let me know what you did.
  6. Yes? Make full use of pip's --no-binary option where needed.
  7. Remember to use multi stage builds
  8. Write READMEs to return the favour to all these developers that have gotten you out of tight spots out of principle for free.

And as always enjoy the ride!

武士は食わねど高楊子