Alpine Based Docker Images Make a Difference in Real World Apps

We all know that Alpine based Docker images are smaller, but how much does it matter for non-trivial applications that need libraries compiled?

Quick Jump: What's Considered a Real World Application? | How Does Alpine Compare to Debian Jessie (Slim)? | Phase 1 Dockerfile Comparison | Phase 2 Alpine Based Dockerfile | Is Alpine Worth It?

I develop a lot of Python and Ruby based applications, and my Dockerfile typically pulls in from the official Python or Ruby images on the Docker Hub.

Up until recently, you only had 3 choices as a base OS. You could choose to use Jessie, Wheezy or Slim. However, now you can also choose Alpine as a base.

I imagine most people used the Slim variant, and then installed things like build-essential in their Dockerfile if they needed it to compile libraries that their app’s packages use.

In this article you’re going to see how Alpine compares to Debian Jessie (Slim) and also learn how to optimize the Alpine version even more.

What’s Considered a Real World Application?

I just released Build a SAAS App With Flask which is a course that gives you the confidence to develop web apps with Flask.

It currently has around 20 top level Python packages and thousands of lines of code. It is a real application that connects to multiple databases and a few package dependencies require libraries to be compiled to function.

I’m a huge backer of Docker, not in a financial sense but I truly believe it’s a great tool so naturally I use Docker in the course.

How Does Alpine Compare to Debian Jessie (Slim)?

python:2.7-slim as a base for my Dockerfile produces a 467.4MB image. python:2.7-alpine as a base for my Dockerfile produces a 309.1MB image.

That’s a huge win. The final image is ~33% smaller and all I did was switch the base image and then spent 5 minutes learning how to use Alpine’s package manager.

That’s only the beginning too because we can shrink it down by an additional ~65% by introducing a few tricks into our Dockerfile .

Phase 1 Dockerfile Comparison

Phase 1 is a direct comparison of Debian Jessie (Slim) vs Alpine with no additional tricks to minimize the final image size.

The Slim Version

FROM python:2.7-slim MAINTAINER Nick Janetakis <nick.janetakis@gmail.com> RUN apt-get update && apt-get install -qq -y \ build-essential libpq-dev libffi-dev --no-install-recommends ENV INSTALL_PATH /bsawf RUN mkdir -p $INSTALL_PATH WORKDIR $INSTALL_PATH COPY requirements.txt requirements.txt RUN pip install -r requirements.txt COPY . . CMD gunicorn -b 0.0.0.0:8000 --access-logfile - "bsawf.app:create()"

The Alpine Version

FROM python:2.7-alpine MAINTAINER Nick Janetakis <nick.janetakis@gmail.com> RUN apk update && apk add build-base postgresql-dev libffi-dev ENV INSTALL_PATH /bsawf RUN mkdir -p $INSTALL_PATH WORKDIR $INSTALL_PATH COPY requirements.txt requirements.txt RUN pip install -r requirements.txt COPY . . CMD gunicorn -b 0.0.0.0:8000 --access-logfile - "bsawf.app:create()"

In this case, I need libffi-dev to compile a bcrypt package dependency and libpq-dev is for postgresql.

Phase 2 Alpine Based Dockerfile

At the moment, the Alpine version is 33% smaller but what happens when someone with more knowledge on Alpine than myself looks at it for a few minutes?

That’s what happened today when @n_copa reached out to me on Twitter:

@docker @alpinelinux @nickjanetakis you can make it even smaller by removing build time dependencies in same RUN https://t.co/Wahd9J5L1X — Natanael Copa (@n_copa) April 15, 2016

He gisted a new version of the Dockerfile :

FROM python:2.7-alpine MAINTAINER Nick Janetakis <nick.janetakis@gmail.com> ENV INSTALL_PATH /bsawf RUN mkdir -p $INSTALL_PATH WORKDIR $INSTALL_PATH COPY requirements.txt requirements.txt RUN apk add --no-cache --virtual .build-deps \ build-base postgresql-dev libffi-dev \ && pip install -r requirements.txt \ && find /usr/local \ \( -type d -a -name test -o -name tests \) \ -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \ -exec rm -rf '{}' + \ && runDeps = " $( \ scanelf --needed --nobanner --recursive /usr/local \ | awk '{ gsub(/,/, "

so:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u \ ) " \ && apk add --virtual .rundeps $runDeps \ && apk del .build-deps COPY . . CMD gunicorn -b 0.0.0.0:8000 --access-logfile - "bsawf.app:create()"

The above Dockerfile dropped the final image size from 309.1MB to 117.3MB.

That’s ~65% smaller than the original Alpine image. It’s not fair to compare it against the Debian Jessie (Slim) version because we’re not cleaning up our compile-time dependencies there, but it’s a heck of a lot smaller as is.

What’s the Catch?

The biggest downside is every time you change a package in your application, it will have to run through the entire apk add command which means all of the system dependencies will be re-installed.

This is going to add a few minutes to your build times, but remember – this increase only happens when you change your app’s package dependencies.

For typical code changes which is what 99% of your changes will be, the build time will be nearly identical with both versions.

Is it worth the occasional build time increases to ship around a 117MB image instead of one that’s over 300MB? That’s for you to decide, but I think it is.

Is Alpine Worth It?

I’m very happy with Alpine and will certainly be updating all of my projects to use it.

You can also expect the official Docker Hub versions of many images to support it in the near future. Redis already has an Alpine version and it’s only ~16MB.