Use alpine for phx.gen.release --docker

When generating a release with phx.gen.release --docker a debian version is used by default to avoid “DNS issues” on Alpine.

It seems like the mentioned issues have been resolved in Alpine 3.18:

Because Alpine images are much smaller, and as far as I’m aware have fewer vulnerabilities.
I suggest either switching to alpine by default or adding an option to generate Dockerfile that uses alpine as a base image (like phx.gen.release --docker --alpine).

If the community agrees, I can work on a pull request.

Thank you,
Anna

7 Likes

While DNS might have been a truely blocking issue I still wouldn’t call alpine a great option for a default. People will eventually add a NIF based dependency where runing musl has a high likelyhood of breaking things.

Personally I also don’t understand the obsession with image size given docker caching that stuff anyways.

3 Likes

There are many reasons to prefer smaller images:

  • Faster build times
  • Deployments in constrained environments
  • Smaller surface area for vulnerabilities, for example debian:12-slim vs alpine:latest (base images). This problem with debian docker images and phoenix was also mentioned here

Are you sure there are that many popular libraries that don’t work with musl? Could you provide some examples?

1 Like

I think that this:

musl’s dynamic loader loads libraries permanently for the lifetime of the process, until it exits or calls exec. dlclose is a no-op. This is very different from glibc’s approach of reference counting libraries and unloading them when the count reaches zero.

May be problematic WRT some NIF code.

But honestly I do not think that the difference is meaningful:

  • Glibc is often slightly faster than Musl
  • Difference in downloads time is IMHO negligible, especially when we take into account layers sharing in Docker
  • You would probably still would need to install Bash in the target container, which mean that the difference in raw base image sizes would be negligible

I’ve recently moved one of my real production apps from debian:12-slim to alpine the result was a 70% reduction in size (131MB → 37MB) (I don’t have bash in my container - why would I? phx.gen.release uses sh)

As far as I’m aware it doesn’t seem to be an issue for other projects in docker/kubernetes ecosystem.

Maybe my understanding is limited, but I don’t know any real examples of issues with that. Could you please provide some examples?

I originally agreed that it would be nice to have options in the generator to customize the variant. My work asks me to ship alpine images where possible, so it’s a frequent concern.

For example, the following could explicitly generate the current state/default:

mix phx.gen.release \
  --docker \
  --elixir-version='1.18.3' \
  --otp-version='27.3' \
  --variant=debian \
  --variant-version='bullseye-20250317-slim'

--variant could specify the OS variant to base your build/run on and --variant-version its version.

Then again, it seemed easy enough to tweak the default Dockerfile to support all three OS options. But with this solution, I’m back to not needing new options in the generator.

Only 2 meaningful changes. First toward the top:

...

ARG VARIANT=debian
ARG VARIANT_VERSION=bullseye-20250317-slim

ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-${VARIANT}-${VARIANT_VERSION}"
ARG RUNNER_IMAGE="${VARIANT}:${VARIANT_VERSION}"

FROM ${BUILDER_IMAGE} AS builder

ARG VARIANT

# install build dependencies
RUN case "${VARIANT}" in \
    "debian" | "ubuntu") \
        apt-get update -y && apt-get install -y build-essential git \
        && apt-get clean && rm -f /var/lib/apt/lists/*_* \
    ;; \
    "alpine") \
        apk add --no-cache build-base git \
    ;; \
    *) \
        echo "Unsupported variant: ${VARIANT}" && exit 1 \
    ;; \
    esac

...

Here, we:

  • introduce the VARIANT build arg
  • switch DEBIAN_VERSION to VARIANT_VERSION
  • switch debian to ${VARIANT}
  • RUN a case statement to toggle OS package manager setup commands based on the value of VARIANT

Then toward the end of the file:

...

FROM ${RUNNER_IMAGE}

ARG VARIANT

RUN case "${VARIANT}" in \
    "debian" | "ubuntu") \
        apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
        && apt-get clean && rm -f /var/lib/apt/lists/*_* \
    ;; \
    "alpine") \
        apk add --no-cache libstdc++ openssl ncurses-dev musl-locales musl-locales-lang ca-certificates \
    ;; \
    *) \
        echo "Unsupported variant: ${VARIANT}" && exit 1 \
    ;; \
    esac

# Set the locale
ENV MUSL_LOCPATH="/usr/share/i18n/locales/musl"

RUN if [ "${VARIANT}" = "debian" ] || [ "${VARIANT}" = "ubuntu" ]; then \
        unset MUSL_LOCPATH && sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen ; \
    fi

ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

...

Here, we:

  • use the same case strategy to set up OS deps on the runner image
  • finish out alpine locale setup by assuming an alpine build and setting MUSL_LOCPATH
  • if not alpine, unset MUSL_LOCPATH and perform remaining locale setup command for ubuntu/debian

I’ve adapted the above solution from this post for setting up the locale in alpine. Tho this post directed me to musl-locales-lang over lang.

To get the various image types:

docker build \
  -t my-ubuntu-app \
  --build-arg VARIANT=ubuntu \
  --build-arg VARIANT_VERSION=focal-20241011 \
  .
docker build \
  -t my-debian-app \
  --build-arg VARIANT=debian \
  --build-arg VARIANT_VERSION=buster-20240612 \
  .
docker build \
  -t my-alpine-app \
  --build-arg VARIANT=alpine \
  --build-arg VARIANT_VERSION=3.21.3 \
  .

Available options for VARIANT and VARIANT_VERSION were found by browsing the available tags on Dockerhub.

Your mileage may vary, but this at least works for me on a freshly generated LiveView app using Elixir 1.18.3 on OTP 27.3.

2 Likes

I’d prefer a leaner interface without unnecessary options like --elixir-version and others, because you can easily change that in the Dockerfile itself. So something like mix phx.gen.release --docker --docker-base=debian

Don’t you think that’s an unnecessary complication? I think in the vast majority of cases there is no need to support multiple base images in one dockerfile. I think it’s better to generate simpler dockerfiles that support only one base image (which one will depend on --docker-base option)

I’d prefer a leaner interface without unnecessary options like --elixir-version and others, because you can easily change that in the Dockerfile itself. So something like mix phx.gen.release --docker --docker-base=debian

I can get behind that idea. The other flags felt like optional convenience flags for overriding the default versions written into the dockerfile, but there’s no real need for them, since you can override them at docker build time using the build args.

Don’t you think that’s an unnecessary complication? I think in the vast majority of cases there is no need to support multiple base images in one dockerfile. I think it’s better to generate simpler dockerfiles that support only one base image (which one will depend on --docker-base option)

I agree. The case statements, tho they will work here, are ugly to look at and are arguably error-prone to maintain over time. To be clear, I straight-up replace the debian default and its package management setup with alpine equivalents when it comes to production. A flag to generate it from the onset would be lovely. Either the Dockerfile itself supports multiple bases or the generator does; and I do prefer simpler dockerfiles.

I’m not sure it should be the default, but I do think a large portion of the community would appreciate a built-in option to generate an alpine based image, or even scratch if possible.

IMO, the lower CVE count out-of-the-box for container workloads is worth it being the default (with anyone running into NIF issues then swapping to debian or ubuntu when they run into such problems), but a flag would suffice.

1 Like

+1 for alpine support. While it’s not a big deal and I usually just copy/paste the Dockerfile from previous project - I do prefer to have a smaller image. Never had a problem with it in more than 5 years.

1 Like

Debian seems like a"good enough" default for me. It would introduce little barrier to new users running it in a Linux container. If we want to optimize further - why not choosing scratch or distroless? (Yeah I wanted it with Bazel - Elixir and Bazel (Google’s open-sourced monorepo build system) )

If there are strong reasons to use Alpine as the default, it might be better to make it the only option (as Debian currently is), rather than supporting multiple base images - which could be add extra maintenance overhead.

@ericmj I just noticed that hexpm/bob recently switched its Dockerfile base image from Alpine to Debian. Could you share what motivated the change? Was it related to GitHub Actions? Alpine => Debian and updates by ericmj · Pull Request #204 · hexpm/bob · GitHub

1 Like