Smallest "speedtest" container


#1

When we are doing support here at resin, there are quite a few times when we need to debug devices, and the network can be especially problematic sometimes. There are some easy tests for network quality, such as running “ping” in the device and checking the latency and packet loss. This doesn’t tell how fast the network is, though, so I’ve thought it would be interesting to create a speedtest container that can be just pulled onto the device, and run from there (thus nothing is installed or left behind after the support agent is done). It would however be very good if that container would be the smallest possible. Speedtest is more important on slow networks, and downloading the tools should not be already stretching the device if possible. :slight_smile:

What came out of this small project is a series of attempts to make that smallest container (specifically for Raspberry Pi 3 here).

Attempt #1 - naive

One thing was pretty sure, that I’d like to work from the Alpine Linux, since that’s so much smaller than the more commonly used Debian one. So the first attempt is to just install speedtest-cli and see what’s the outcome.

FROM resin/raspberrypi3-alpine:3.6

RUN apk add --no-cache speedtest-cli

CMD speedtest

This ends up being a 93MB container / 31MB compressed. Definitely can do better.

Attempt #2 - remove cache

Next comes the realization, that Alpine actually installs a bunch of precompiled and cached Python binaries, that can be removed to shrink the image.

FROM resin/raspberrypi3-alpine:3.6

# Alpine installs a lot of pre-compiled and cached Python files,
# remove them for great good
RUN apk add --no-cache speedtest-cli && \
    find /usr/lib/ | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf

CMD speedtest

This is no 66.9MB container / 24MB compressed (a 23% save, nice, but still big!)

Attempt #3 - static compile and multistage build

The next step was complicating things a little bit, using cx-Freeze to compile and bundle the speedtest binary, thus won’t need any explicitly install Python on the system to use it. To make things simpler, I’ve also used Docker’s multi-stage builds, so less cleanup is necessary and the Dockerfile is hopefully more readable.

###
# Build image
###
FROM resin/raspberrypi3-alpine:3.6 as build

WORKDIR /usr/src/app

RUN apk add --no-cache build-base python3 python3-dev git

ENV CXFREEZE_VERSION=5.0.2
RUN pip3 install cx-Freeze==${CXFREEZE_VERSION}

ENV SPEEDTEST_VERSION=v1.0.6
RUN git clone https://github.com/sivel/speedtest-cli.git && \
    cd speedtest-cli && \
    git checkout -b build ${SPEEDTEST_VERSION} && \
    cxfreeze speedtest.py

###
# Deployed image
###
FROM resin/raspberrypi3-alpine:3.6

WORKDIR /usr/src/app

COPY --from=build /usr/src/app/speedtest-cli/dist/ ./

CMD ["./speedtest"]

It takes a few more steps than before, but should be still pretty straightforward. The result is a 49.5MB container / 18MB compressed (another 25% save, lovely!)

Attempt #4 - barebones base image

This attempt was based on realizing that while it’s really nice to use resin/raspberrypi3-alpine for general applications, it’s pretty small already, and has QEMU included to facilitate cross-compilation, we can get rid of all those nice things to save some more space.

That “back-to basics” resulted in using arm32v6/alpine, and still quite simple, except for some library-copying that was figured out by trial-and-error.

###
# Build image
###
FROM resin/raspberrypi3-alpine:3.6 as build

WORKDIR /usr/src/app

RUN apk add --no-cache build-base python3 python3-dev git

ENV CXFREEZE_VERSION=5.0.2
RUN pip3 install cx-Freeze==${CXFREEZE_VERSION}

ENV SPEEDTEST_VERSION=v1.0.6
RUN git clone https://github.com/sivel/speedtest-cli.git && \
    cd speedtest-cli && \
    git checkout -b build ${SPEEDTEST_VERSION} && \
    cxfreeze speedtest.py

###
# Deployed image
###
FROM arm32v6/alpine:3.6

WORKDIR /usr/src/app

# Required libraries
COPY --from=build /usr/bin/xmlwf /usr/bin/xmlwf
COPY --from=build /usr/lib/libexpat.so* /usr/lib/
COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib
# Copy precompiled binary
COPY --from=build /usr/src/app/speedtest-cli/dist/ ./

CMD ["./speedtest"]

Finally this will become a 13.9MB container / 7MB image - another 61% save in network transfer, or 75% altogether since the first, naive, simple version! :slight_smile:

One downside is that this is the first container in the list that cannot be run through QEMU, only on the appropriate platform, but shouldn’t be too bad (it can compiled on any platform properly set up, though:)

Further attempts and alternatives

I’m guessing this could be shrinked further. in a number of ways:

Compiled speedtest (e.g. Rust)

For example using a non-Python speed test, such as speedtest-rs in Rust. I was playing with that, so far close but no cigar, mainly because I was using a Alpine Linux image to deploy, and that uses musl (instead of gnu), and that’s not really first class toolchain in Rust just yet, needing some crazy cross-compile acrobatics that didn’t quite work out for me so far. There must be a way, just haven’t found it.

Busybox base image

The system could be probably cut down by using an even smaller base image, such as a busybox-based one. This would probably work well with the musl-static-compiled binaries the best, so one doesn’t need to spend ages trying to add runtime dependencies…

Just use curl anyways…

Or the smallest is just forgetting the speedtest tools anyways, and use good ol’ curl, to download a file and measure the speed. This has the downside of not testing to a nearby server, but to one particular one, thus will always report lower-or-equal to the max speed, but often it’s worth it. For example pick a file from https://www.thinkbroadband.com/download and run curl <URL> > /dev/null and read off the speed from the last column. This is the simplest solution - but where’s the fun in that :wink:


Would love to hear if you have any comments, or tricks to make even smaller speedtest container than I’ve put together here!


#2

This is pretty awesome, my speedtest app weighs in at 538mb right now, definitely will be borrowing from this to slim it down.

Something I’ve been mulling over after a discussion with Tim is figuring out how to get a rough speed test without consuming that much data (i.e. to mitigate the usage of cellular data) - that’s probably a good next step after slimming my image down…