ARG ALPINE_VERSION="3.20.6" ARG ERLANG_VERSION="27.3.4.1" # curl -fsSL https://github.com/erlang/otp/releases/download/OTP-27.3.4.1/otp_src_27.3.4.1.tar.gz | sha256 ARG ERLANG_DOWNLOAD_SHA256="2672f0c52b9ff39695b9c8f99cd1846ed9e47e21cd5b045ccdd08719a3019652" ARG ELIXIR_VERSION="1.18.4" # curl -fsSL https://github.com/elixir-lang/elixir/archive/refs/tags/v1.18.4.tar.gz | sha256 ARG ELIXIR_DOWNLOAD_SHA256="8e136c0a92160cdad8daa74560e0e9c6810486bd232fbce1709d40fcc426b5e0" FROM alpine:${ALPINE_VERSION} AS base # Important! Update this no-op ENV variable when this Dockerfile # is updated with the current date. It will force refresh of all # of the base images and things like `apk add` won't be using # old cached versions when the Dockerfile is built. ENV REFRESHED_AT=2023-10-05 \ LANG=C.UTF-8 \ HOME=/app/ \ TERM=xterm # Add tagged repos as well as the edge repo so that we can selectively install edge packages ARG ALPINE_VERSION RUN set -xe \ && ALPINE_MINOR_VERSION=$(echo ${ALPINE_VERSION} | cut -d'.' -f1,2) \ && echo "@main http://dl-cdn.alpinelinux.org/alpine/v${ALPINE_MINOR_VERSION}/main" >> /etc/apk/repositories \ && echo "@community http://dl-cdn.alpinelinux.org/alpine/v${ALPINE_MINOR_VERSION}/community" >> /etc/apk/repositories \ && echo "@edge http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories RUN set -xe \ # Upgrade Alpine and base packages && apk --no-cache --update-cache --available upgrade \ # Install bash, Erlang/OTP and Elixir runtime dependencies && apk add --no-cache --update-cache \ bash \ libstdc++ \ ca-certificates \ ncurses \ openssl \ pcre \ unixodbc \ zlib \ # Update ca certificates && update-ca-certificates --fresh FROM base AS build_erlang # Install bash and Erlang/OTP deps RUN set -xe \ && apk add --no-cache --update-cache --virtual .fetch-deps \ curl \ libgcc \ lksctp-tools \ zlib-dev # Install Erlang/OTP build deps RUN set -xe \ && apk add --no-cache --virtual .build-deps \ dpkg-dev \ dpkg \ gcc \ g++ \ libc-dev \ linux-headers \ make \ autoconf \ ncurses-dev \ openssl-dev \ unixodbc-dev \ lksctp-tools-dev \ tar # Download OTP ARG ERLANG_VERSION ARG ERLANG_DOWNLOAD_SHA256 WORKDIR /tmp/erlang-build RUN set -xe \ && curl -fSL -o otp-src.tar.gz "https://github.com/erlang/otp/releases/download/OTP-${ERLANG_VERSION}/otp_src_${ERLANG_VERSION}.tar.gz" \ && tar -xzf otp-src.tar.gz -C /tmp/erlang-build --strip-components=1 \ # && sha256sum otp-src.tar.gz && exit 1 \ && echo "${ERLANG_DOWNLOAD_SHA256} otp-src.tar.gz" | sha256sum -c - # Configure & Build RUN set -xe \ && export ERL_TOP=/tmp/erlang-build \ && export CPPFLAGS="-D_BSD_SOURCE $CPPFLAGS" \ && export gnuArch="$(dpkg-architecture --query DEB_HOST_GNU_TYPE)" \ && ./configure \ --build="$gnuArch" \ --prefix=/usr/local \ --sysconfdir=/etc \ --mandir=/usr/share/man \ --infodir=/usr/share/info \ --without-javac \ --without-jinterface \ --without-wx \ --without-debugger \ --without-observer \ --without-cosEvent \ --without-cosEventDomain \ --without-cosFileTransfer \ --without-cosNotification \ --without-cosProperty \ --without-cosTime \ --without-cosTransactions \ --without-et \ --without-gs \ --without-ic \ --without-megaco \ --without-orber \ --without-percept \ --without-odbc \ --without-typer \ --enable-threads \ --enable-shared-zlib \ --enable-dynamic-ssl-lib \ --enable-ssl=dynamic-ssl-lib \ $(if [[ "${TARGET}" != *"amd64"* ]]; then echo "--disable-jit"; fi) \ && $( \ if [[ "${TARGETARCH}" == *"amd64"* ]]; \ then export CFLAGS="-g -O2 -fstack-clash-protection -fcf-protection=full"; \ else export CFLAGS="-g -O2 -fstack-clash-protection"; fi \ ) \ && make -j$(getconf _NPROCESSORS_ONLN) # Install to temporary location, strip the install, install runtime deps and copy to the final location RUN set -xe \ && make DESTDIR=/tmp install \ && cd /tmp && rm -rf /tmp/erlang-build \ && find /tmp/usr/local -regex '/tmp/usr/local/lib/erlang/\(lib/\|erts-\).*/\(man\|doc\|obj\|c_src\|emacs\|info\|examples\)' | xargs rm -rf \ && find /tmp/usr/local -name src | xargs -r find | grep -v '\.hrl$' | xargs rm -v || true \ && find /tmp/usr/local -name src | xargs -r find | xargs rmdir -vp || true \ # Strip install to reduce size && scanelf --nobanner -E ET_EXEC -BF '%F' --recursive /tmp/usr/local | xargs -r strip --strip-all \ && scanelf --nobanner -E ET_DYN -BF '%F' --recursive /tmp/usr/local | xargs -r strip --strip-unneeded \ && runDeps="$( \ scanelf --needed --nobanner --format '%n#p' --recursive /tmp/usr/local \ | tr ',' '\n' \ | sort -u \ | awk 'system("[ -e /tmp/usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ )" \ && ln -s /tmp/usr/local/lib/erlang /usr/local/lib/erlang \ && /tmp/usr/local/bin/erl -eval "beam_lib:strip_release('/tmp/usr/local/lib/erlang/lib')" -s init stop > /dev/null \ && (/usr/bin/strip /tmp/usr/local/lib/erlang/erts-*/bin/* || true) \ && apk add --no-cache --virtual .erlang-runtime-deps $runDeps lksctp-tools ca-certificates # Cleanup after Erlang install RUN set -xe \ && apk del .fetch-deps .build-deps \ && rm -rf /var/cache/apk/* WORKDIR ${HOME} CMD ["erl"] FROM base AS build_elixir # Install Elixir build deps RUN set -xe \ && apk add --no-cache --virtual .build-deps \ make \ curl \ tar \ git # Download Elixir ARG ELIXIR_VERSION ARG ELIXIR_DOWNLOAD_SHA256 WORKDIR /tmp/elixir-build RUN set -xe \ && curl -fSL -o elixir-src.tar.gz "https://github.com/elixir-lang/elixir/archive/refs/tags/v${ELIXIR_VERSION}.tar.gz" \ && mkdir -p /tmp/usr/local/src/elixir \ && tar -xzC /tmp/usr/local/src/elixir --strip-components=1 -f elixir-src.tar.gz \ # && sha256sum elixir-src.tar.gz && exit 1 \ && echo "${ELIXIR_DOWNLOAD_SHA256} elixir-src.tar.gz" | sha256sum -c - \ && rm elixir-src.tar.gz COPY --from=build_erlang /tmp/usr/local /usr/local # Compile Elixir RUN set -xe \ && cd /tmp/usr/local/src/elixir \ && make DESTDIR=/tmp install clean \ && find /tmp/usr/local/src/elixir/ -type f -not -regex "/tmp/usr/local/src/elixir/lib/[^\/]*/lib.*" -exec rm -rf {} + \ && find /tmp/usr/local/src/elixir/ -type d -depth -empty -delete \ && rm -rf /tmp/elixir-build \ && apk del .build-deps # Cleanup apk cache RUN rm -rf /var/cache/apk/* WORKDIR ${HOME} CMD ["iex"] FROM base AS elixir WORKDIR ${HOME} # Copy Erlang/OTP and Elixir installations COPY --from=build_erlang /tmp/usr/local /usr/local COPY --from=build_elixir /tmp/usr/local /usr/local # Install hex + rebar RUN set -xe \ && mix local.hex --force \ && mix local.rebar --force CMD ["bash"] FROM elixir AS compiler WORKDIR /app # Install build deps RUN apk add --update --no-cache \ make \ git \ nodejs \ npm \ build-base # Add pnpm RUN npm i -g pnpm # Copy only the files needed to fetch the dependencies, # to leverage Docker layer cache for them COPY mix.exs mix.lock ./ COPY apps/domain/mix.exs ./apps/domain/mix.exs COPY apps/web/mix.exs ./apps/web/mix.exs COPY apps/api/mix.exs ./apps/api/mix.exs COPY config config # Fetch and compile the dependencies ARG MIX_ENV="prod" RUN mix deps.get --only ${MIX_ENV} RUN mix deps.compile --skip-umbrella-children # Copy the files needed to fetch asset deps COPY apps/web/assets/package.json ./apps/web/assets/ COPY apps/web/assets/pnpm-lock.yaml ./apps/web/assets/ # Install npm deps and assets pipeline RUN cd apps/web \ && mix assets.setup # Tailwind needs assets and app directories to look for used classes, # so we can't optimize further at this stage COPY apps apps # Install pipeline and compile assets for Web app RUN cd apps/web \ && mix assets.deploy # Copy the rest of the application files and compile them # mix doesn't know when GIT_SHA changes, so --force is needed to avoid # pulling in a cached version. ARG GIT_SHA RUN mix compile --force FROM elixir AS builder # Install build deps RUN apk add --update --no-cache \ git WORKDIR /app # Copy the compiled dependencies from the previous step # leveraging the possible layer cache COPY --from=compiler /app /app COPY rel rel ARG APPLICATION_NAME ARG MIX_ENV="prod" ARG GIT_SHA RUN mix release ${APPLICATION_NAME} RUN mix sentry.package_source_code # start a new build stage so that the final image will only contain # the compiled release and other runtime necessities FROM base AS runtime RUN set -xe \ # Install Firezone runtime deps && apk add --no-cache --update-cache \ curl \ jq \ tini # Create default user and home directory, set owner to default RUN set -xe \ && mkdir -p /app \ && adduser -s /bin/sh -u 1001 -G root -h /app -S -D default \ && chown -R 1001:0 /app WORKDIR /app ARG APPLICATION_NAME ENV APPLICATION_NAME=$APPLICATION_NAME # Only copy the final release from the build stage ARG MIX_ENV="prod" COPY --from=builder /app/_build/${MIX_ENV}/rel/${APPLICATION_NAME} ./ # Allow the default user to write to the priv directory of some libraries RUN chmod -R ugo+rw /app/lib/tzdata-*/priv # Change user to "default" to limit runtime privileges USER default # This is critical when you run this container in containers where # running process would get a PID 1. # # BEAM is usually not started by itself but via some shell script (eg. the one generated by # Elixir 1.9 releases or Distillery) and this script is not designed to become an init script # for a Docker container and it DOES NOT reap zombie processes. # # So whenever a child process runs and terminates inside the same container it would result in memory and # PID leak to a point where host VM would get unresponsive. # # A good example why you would start a process within container is the `ping` command for liveness probes. # It starts a VM to issue an RPC command to a live node and then terminates, but would never be reaped. # # Tini would become an entrypoin script and would take care of zombie reaping no matter how you start the VM. ENTRYPOINT ["/sbin/tini", "--"] CMD ["bin/server"]