PureScript 0.14.2 update: a slight problem with Docker image

PureScript is a great language, and I can't find words good enough, to say "thank you" for all those, who made it (and all its tools) available for us. Ladies and gentlemen, you have been doing a great job!

Recently, a new version of PureScript, came out: 0.14.2. I decided to build my own Docker image for that version, as usual... and it ended with a little headache.

Let's start with the good news, though. As can be read in the annoucement, the libtinfo dependency has been dropped. That's good. So far, I've been using Node.js images, based on the current Debian ones, to install PureScript, Spago, and other packages. I don't remember the details, but libtinfo version 5 was required, while the version 6 was installed in the base image. Not a complex problem to solve: it was enough to either install the libtinfo library in the correct version, or (what is really interesting) to trick the system, by creation a symlink to the installed version of libtinfo, as if it was the older version. Anyway, in my Dockerfile, I was always installing the libtinfo library in the older version. And it was fine.

This time, I was happy to remove this single dependency, and watch the build process of Docker image, which... failed miserably.


A new problem: glibc

PureScript is written in Haskell. Its authors compile it for the Linux systems, and installation of npm package is all about downloading the pre-built version, validating it works, and setting all up. Nothing complex.

Not this time, tough. The problem was with the glibc version: the downloaded, pre-built binary version of PureScript, was compiled for glibc 2.29, and... the Node.js image, based on current Debian one, is using glibc 2.28.

I have checked it: all Node.js images, available on the hub.docker.com site, are based either on the current Debian image, with glibc 2.28, or on the old stable Debian image (probably with some older glibc version, I haven't even checked it), or on the Alpine Linux. And Alpine Linux doesn't use glibc at all - so the whole PureScript (and other tools) would have to be built for this platform.

Huston, we may have a problem here.

The possible solutions

There were only two possible solutions to this problem:

  1. Find a suitable image, with glibc 2.29 or newer, and install all needed software: Node.js, PureScript, Spago and any other tools. Downside: I was always using an image with Node.js already installed, so it would be more work to do on my side.
  2. Find a suitable, minimal image, and try to compile PureScript and Spago from sources.

While the second option would be more interesting, and probably also a better way to provide images for platforms other than Node.js (e.g. for Erlang, or C), it would be also much more time-consuming. PureScript is written in Haskell, so probably GHC would have to be installed, and stack, and who knows, what else.

Not to mention, that all the software, needed only for source compilation, would have to be removed from the result image.

And I'm really, really short on time lately.

So I decided to try the first option.

The first step: Node.js

PureScript is supposed to work on the Node.js platform (or, at least, it targets JavaScript, and then some JS tools can be used, which work on Node.js). So, the first step was to get a working Node.js platform and Glibc 2.29.

What I did was really simple:

  • I checked, how the official Node.js images, based on the current Debian, are made,
  • I checked the glibc version for the Ubuntu 20.04 (the latest LTS), and it was 2.31 (or something like this): yay!,
  • also, the size of Ubuntu 20.04 was quite small,
  • I tried to built the Node.js image, basing on the Ubuntu 20.04, and it succeeded!

So far, so good. Then, using the built image, I tried to install the newest PureScript 0.14.2 - and it also succeeded. Spago installation also went well. I was halfway to the success.

I slightly modified the Dockerfile (taken from the official Node.js dockerfiles), so the packages were installed once, and not twice. (I still don't understand, why some of them were installed twice: once for node and npm, and then again for yarn, removed every time after those installations.)

The second step: PureScript

Having such an image, all I did was adding installation of git, PureScript, Spago and Terser (which I use to minimize the JavaScript code, bundled by PureScript; Terser is much smaller in size than Parcel).

And the result was an image of just 472MB, in comparison to my previous images over 1GB! So, success on all fronts!

(The only question, which remains unanswered, is: would image be even smaller, when the Alpine Linux was used? Of course, it would require to built PureScript and Spago from source...)

The final Dockerfile

Here's the Dockerfile I used to build the image. So far, I only had time to check, whether it works for my project. Well, it works - at least I was able to compile it and to run tests. However, be warned: it is provided as it is, without any guarantees.

FROM ubuntu:20.04

RUN groupadd --gid 1000 node \
  && useradd --uid 1000 --gid node --shell /bin/bash --create-home node


RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \
    && case "${dpkgArch##*-}" in \
      amd64) ARCH='x64';; \
      ppc64el) ARCH='ppc64le';; \
      s390x) ARCH='s390x';; \
      arm64) ARCH='arm64';; \
      armhf) ARCH='armv7l';; \
      i386) ARCH='x86';; \
      *) echo "unsupported architecture"; exit 1 ;; \
    esac \
    && set -ex \
    # libatomic1 for arm
    && apt-get update && apt-get install -y ca-certificates curl wget gnupg dirmngr xz-utils libatomic1 --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && for key in \
      4ED778F539E3634C779C87C6D7062848A1AB005C \
      94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \
      74F12602B6F1C4E913FAA37AD3A89613643B6201 \
      71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \
      8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \
      C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
      C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C \
      DD8F2338BAE7501E3DD5AC78C273792F7D83545D \
      A48C2BEE680E841632CD4E44F07496B3EB3C1762 \
      108F52B48DB57BB0CC439B2997B01419BD92F80A \
      B9E2F5981AA6E0CD28160D9FF13993A75599653C \
    ; do \
      gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys "$key" || \
      gpg --batch --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys "$key" || \
      gpg --batch --keyserver hkp://pgp.mit.edu:80 --recv-keys "$key" ; \
    done \
    && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" \
    && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
    && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
    && grep " node-v$NODE_VERSION-linux-$ARCH.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
    && tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
    && rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
    && for key in \
    6A010C5166006599AA17F08146C2130DFD2497F5 \
  ; do \
    gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys "$key" || \
    gpg --batch --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys "$key" || \
    gpg --batch --keyserver hkp://pgp.mit.edu:80 --recv-keys "$key" ; \
  done \
    && curl -fsSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \
    && curl -fsSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz.asc" \
    && gpg --batch --verify yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz \
    && mkdir -p /opt \
    && tar -xzf yarn-v$YARN_VERSION.tar.gz -C /opt/ \
    && ln -s /opt/yarn-v$YARN_VERSION/bin/yarn /usr/local/bin/yarn \
    && ln -s /opt/yarn-v$YARN_VERSION/bin/yarnpkg /usr/local/bin/yarnpkg \
    && rm yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz \
    && apt-mark auto '.*' > /dev/null \
    && find /usr/local -type f -executable -exec ldd '{}' ';' \
      | awk '/=>/ { print $(NF-1) }' \
      | sort -u \
      | xargs -r dpkg-query --search \
      | cut -d: -f1 \
      | sort -u \
      | xargs -r apt-mark manual \
    && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
    && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
    # smoke tests
    && node --version \
    && npm --version \
    && yarn --version \
    # PureScript stuff
    && apt update \
    && apt install -y git \
    && npm install -g purescript@$PURESCRIPT_VERSION --unsafe-perm \
    && npm install -g spago@$SPAGO_VERSION --unsafe-perm \
    && npm install -g terser@$TERSER_VERSION \
    # smoke tests
    && purs --version \
    && spago --version \
    && terser --version

CMD [ "node" ]