# Find eligible builder and runner images on Docker Hub. We use Debian # instead of Alpine to avoid DNS resolution issues in production. # # https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=debian # https://hub.docker.com/_/debian?tab=tags # # This file is based on these images: # # - https://hub.docker.com/r/hexpm/elixir/tags - for the build image # - https://hub.docker.com/_/ubuntu - for the release image # - https://pkgs.org/ - resource for finding needed packages # - Ex: hexpm/elixir:1.19.5-erlang-28.0-ubuntu-noble-20260324 # ARG ELIXIR_VERSION=1.19.5 ARG OTP_VERSION=28.1.1 ARG UBUNTU_VERSION=noble-20260324 ARG RELEASE_NAME=elektrine ARG ELEKTRINE_RELEASE_MODULES=all ARG PRIMARY_DOMAIN=example.com ARG EMAIL_DOMAIN=example.com ARG SUPPORTED_DOMAINS=example.com ARG PROFILE_BASE_DOMAINS=example.com ARG NPM_VERSION=11.13.0 ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-ubuntu-${UBUNTU_VERSION}@sha256:1486c932fb7576bea9becaaa33da9946205161a2d16938a3351c455d59ce6c3a" ARG RUNNER_IMAGE="ubuntu:${UBUNTU_VERSION}@sha256:84e77dee7d1bc93fb029a45e3c6cb9d8aa4831ccfcc7103d36e876938d28895b" FROM ${BUILDER_IMAGE} AS builder ARG RELEASE_NAME ARG ELEKTRINE_RELEASE_MODULES ARG PRIMARY_DOMAIN ARG EMAIL_DOMAIN ARG SUPPORTED_DOMAINS ARG PROFILE_BASE_DOMAINS ARG NPM_VERSION # Retry helper function for flaky network operations SHELL ["/bin/bash", "-c"] # install build dependencies with retry RUN for i in 1 2 3 4 5; do \ if apt-get update -y && \ apt-get install -y build-essential git libvips-dev; then \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ break; \ fi; \ if [ "$i" = "5" ]; then \ echo "Retry $i failed, giving up." >&2; \ exit 1; \ fi; \ echo "Retry $i failed, waiting..."; \ sleep 5; \ done # prepare build dir WORKDIR /app # install hex + rebar with retry RUN for i in 1 2 3 4 5; do \ if mix local.hex --force && \ mix local.rebar --force; then \ break; \ fi; \ if [ "$i" = "5" ]; then \ echo "Retry $i failed, giving up." >&2; \ exit 1; \ fi; \ echo "Retry $i failed, waiting..."; \ sleep 5; \ done # set build ENV ENV MIX_ENV="prod" ENV ELEKTRINE_RELEASE_MODULES="${ELEKTRINE_RELEASE_MODULES}" ENV PRIMARY_DOMAIN="${PRIMARY_DOMAIN}" ENV EMAIL_DOMAIN="${EMAIL_DOMAIN}" ENV SUPPORTED_DOMAINS="${SUPPORTED_DOMAINS}" ENV PROFILE_BASE_DOMAINS="${PROFILE_BASE_DOMAINS}" ENV NPM_CONFIG_UPDATE_NOTIFIER=false # install mix dependencies COPY mix.lock ./ COPY release_builder/mix.exs release_builder/mix.exs COPY release_builder/module_selection.exs release_builder/module_selection.exs COPY release_builder/config/config.exs release_builder/config/config.exs COPY release_builder/config/runtime.exs release_builder/config/runtime.exs COPY apps/elektrine/mix.exs apps/elektrine/mix.exs COPY apps/arblarg/mix.exs apps/arblarg/mix.exs COPY apps/atomine/mix.exs apps/atomine/mix.exs COPY apps/kairo/mix.exs apps/kairo/mix.exs COPY apps/paige/mix.exs apps/paige/mix.exs COPY apps/elektrine_web/mix.exs apps/elektrine_web/mix.exs COPY apps/elektrine_email/mix.exs apps/elektrine_email/mix.exs COPY apps/elektrine_social/mix.exs apps/elektrine_social/mix.exs COPY apps/elektrine_dns/mix.exs apps/elektrine_dns/mix.exs COPY apps/elektrine_nerve/mix.exs apps/elektrine_nerve/mix.exs COPY apps/elektrine_vpn/mix.exs apps/elektrine_vpn/mix.exs COPY apps/elektrine_uptime/mix.exs apps/elektrine_uptime/mix.exs COPY config/config.exs config/prod.exs config/ WORKDIR /app/release_builder RUN for i in 1 2 3 4 5; do \ if mix deps.get --only $MIX_ENV; then \ break; \ fi; \ if [ "$i" = "5" ]; then \ echo "Retry $i failed, giving up." >&2; \ exit 1; \ fi; \ echo "Retry $i failed, waiting..."; \ sleep 5; \ done RUN mix deps.compile WORKDIR /app COPY apps apps COPY release_builder release_builder COPY config/runtime.exs config/runtime.exs COPY config/runtime config/runtime COPY --chmod=755 scripts/release/deploy_release.sh scripts/release/deploy_release.sh COPY scripts/lib scripts/lib # Install Node.js 20.x (required for Tailwind v4) RUN for i in 1 2 3 4 5; do \ if apt-get update -y && \ apt-get install -y ca-certificates curl gnupg && \ mkdir -p /etc/apt/keyrings && \ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ apt-get update -y && \ apt-get install -y nodejs; then \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ break; \ fi; \ if [ "$i" = "5" ]; then \ echo "Retry $i failed, giving up." >&2; \ exit 1; \ fi; \ echo "Retry $i failed, waiting..."; \ sleep 5; \ done # Update npm before installing asset dependencies. RUN for i in 1 2 3 4 5; do \ if npm install -g npm@"${NPM_VERSION}"; then \ break; \ fi; \ if [ "$i" = "5" ]; then \ echo "Retry $i failed, giving up." >&2; \ exit 1; \ fi; \ echo "Retry $i failed, waiting..."; \ sleep 5; \ done # Install npm dependencies for assets with retry (includes tailwindcss) RUN for i in 1 2 3 4 5; do \ if (cd apps/elektrine/assets && npm ci); then \ break; \ fi; \ if [ "$i" = "5" ]; then \ echo "Retry $i failed, giving up." >&2; \ exit 1; \ fi; \ echo "Retry $i failed, waiting..."; \ sleep 5; \ done # Install esbuild with retry (tailwind is now installed via npm) RUN for i in 1 2 3 4 5; do \ if (cd release_builder && mix esbuild.install); then \ break; \ fi; \ if [ "$i" = "5" ]; then \ echo "Retry $i failed, giving up." >&2; \ exit 1; \ fi; \ echo "Retry $i failed, waiting..."; \ sleep 5; \ done # Build the selected hoster release through release_builder/ RUN ./scripts/release/deploy_release.sh --release-name "${RELEASE_NAME}" --mix-env "${MIX_ENV}" # start a new build stage so that the final image will only contain # the compiled release and other runtime necessities FROM ${RUNNER_IMAGE} ARG RELEASE_NAME ARG ELEKTRINE_RELEASE_MODULES # Optional overrides: true|false|auto (auto derives from ELEKTRINE_RELEASE_MODULES). ARG INSTALL_TOR=auto ARG INSTALL_VPN_TOOLS=auto SHELL ["/bin/bash", "-c"] # Core runtime packages always. Tor / WireGuard / Shadowsocks only when the # release modules (or explicit build args) need them — keeps slim images smaller. RUN set -euo pipefail; \ modules="${ELEKTRINE_RELEASE_MODULES:-all}"; \ install_tor="${INSTALL_TOR:-auto}"; \ install_vpn="${INSTALL_VPN_TOOLS:-auto}"; \ if [ "$install_tor" = "auto" ]; then \ case ",${modules}," in \ *,all,*|*,tor,*) install_tor=true ;; \ *) install_tor=false ;; \ esac; \ fi; \ if [ "$install_vpn" = "auto" ]; then \ case ",${modules}," in \ *,all,*|*,vpn,*) install_vpn=true ;; \ *) install_vpn=false ;; \ esac; \ fi; \ pkgs="libstdc++6 openssl libncurses6 ca-certificates curl libvips42 gpg gnupg2 dirmngr"; \ if [ "$install_tor" = "true" ] || [ "$install_tor" = "1" ]; then \ pkgs="$pkgs tor"; \ fi; \ if [ "$install_vpn" = "true" ] || [ "$install_vpn" = "1" ]; then \ pkgs="$pkgs wireguard-tools iproute2 shadowsocks-libev"; \ fi; \ echo "Runner packages (modules=${modules} tor=${install_tor} vpn=${install_vpn}): $pkgs"; \ for i in 1 2 3 4 5; do \ if apt-get update -y && apt-get install -y $pkgs; then \ apt-get clean; \ rm -rf /var/lib/apt/lists/*; \ break; \ fi; \ if [ "$i" = "5" ]; then \ echo "Retry $i failed, giving up." >&2; \ exit 1; \ fi; \ echo "Retry $i failed, waiting..."; \ sleep 5; \ done RUN which gpg && gpg --version # Setup data directories (will be mounted as volume in production) RUN mkdir -p /data/tor/elektrine /data/tor/data /data/certs && \ chown -R nobody:nogroup /data # Tor config is harmless when tor is not installed; start.sh only launches tor # when the runtime role/profile needs it. COPY --chmod=644 deploy/onion/torrc /etc/tor/torrc # Use a built-in UTF-8 locale available on Ubuntu images ENV LANG=C.UTF-8 ENV LANGUAGE=C.UTF-8 ENV LC_ALL=C.UTF-8 WORKDIR "/app" RUN chown nobody /app # Copy startup scripts COPY --chmod=755 deploy/docker/start.sh /app/start.sh COPY --chmod=755 deploy/docker/docker-entrypoint.sh /app/docker-entrypoint.sh COPY --chmod=755 scripts/acme /app/scripts/acme # set runner ENV ENV MIX_ENV="prod" ENV PATH="/usr/bin:$PATH" ENV RELEASE_NAME="${RELEASE_NAME}" ENV ELEKTRINE_RELEASE_MODULES="${ELEKTRINE_RELEASE_MODULES}" # Only copy the final release from the build stage COPY --from=builder --chown=nobody:root /app/_deploy_release/${RELEASE_NAME} ./ # Expose ports for HTTP and HTTPS (non-privileged ports, fly.toml maps external 80/443) EXPOSE 8080 8443 CMD ["/app/docker-entrypoint.sh"]