diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a359070 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build base stage + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: base + load: true + tags: elixir-docker-template:base + + - name: Build dev stage + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: dev + load: true + tags: elixir-docker-template:dev + + - name: Build prod stage + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: prod + load: true + tags: elixir-docker-template:prod + + - name: Validate compose + run: docker compose config --quiet + + - name: Run Hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: docker/Dockerfile + ignore: DL3008 diff --git a/.gitignore b/.gitignore index e1369f5..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +0,0 @@ -mutagen.yml.lock diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..8f7e23e --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,2 @@ +ignored: + - DL3008 diff --git a/README.md b/README.md index 4133770..66194d8 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ help to miss some hurdles. - Docker based environment for Elixir / Erlang - `justfile` for easier handling of the stack +- Multi-stage Dockerfile for dev and production builds ## Prerequisites @@ -28,6 +29,65 @@ You need to have the following installed: - [Docker](https://docker.io) for running the code - [Just](https://just.systems) for running commands +## Docker Build + +The template uses a single `docker/Dockerfile` with multiple build stages: + +| Stage | Purpose | +|-------|---------| +| `base` | Base image with essential tools | +| `dev` | Development environment (default) | +| `prod` | Production build environment | + +### Build Commands + +```bash +# Development (via compose) +just start + +# Production +docker build --target prod -t myapp:prod . +``` + +The versions are defined in the `FROM` statement in the base stage (currently Elixir 1.19.5, Erlang 28.3, Debian trixie). + +### Optimized Production Build + +For a smaller production image, you can use a two-stage build pattern. Create a separate `Dockerfile.prod` based on the template's commented sections: + +```dockerfile +# Stage 1: Builder +FROM docker.io/hexpm/elixir:1.19.5-erlang-28.3-debian-trixie-20260202-slim AS builder + +WORKDIR /app +RUN mix local.hex --force && mix local.rebar --force + +COPY mix.exs mix.lock ./ +RUN mix deps.get --only prod +RUN mix deps.compile + +COPY config/ config/ +COPY lib/ lib/ +COPY priv/ priv/ +RUN mix compile + +COPY rel/ rel/ +RUN mix release + +# Stage 2: Runtime +FROM debian:trixie-20260202-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libstdc++6 openssl libncurses6 locales ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +ENV LANG=en_US.UTF-8 +WORKDIR /app + +COPY --from=builder /app/_build/prod/rel/your_app ./ +CMD ["./bin/server"] +``` + ## Usage To use the template either download the files and augment an existing project diff --git a/compose.yml b/compose.yml index 1e47d3b..adc5959 100644 --- a/compose.yml +++ b/compose.yml @@ -3,7 +3,8 @@ services: init: true build: context: . - dockerfile: docker/Dockerfile.dev + dockerfile: docker/Dockerfile + target: dev stdin_open: true tty: true volumes: @@ -34,9 +35,8 @@ services: # - default volumes: - # postgres-data: + # postgres-data: app: - # The lines below need to be removed when using mutagen driver: local driver_opts: o: bind diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..23e0253 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,92 @@ +# ============================================ +# Multi-stage Dockerfile for Elixir development +# +# Build stages: +# --target base : Base image with essential tools +# --target dev : Development environment (default) +# --target prod : Production build environment +# +# Examples: +# docker build --target dev -t myapp:dev . +# docker build --target prod -t myapp:prod . +# ============================================ + +# ============================================ +# Stage 1: Base +# Single source of truth for versions +# ============================================ +FROM docker.io/hexpm/elixir:1.19.5-erlang-28.3-debian-trixie-20260202-slim AS base + +RUN apt-get update -qq && \ + apt-get install -y --no-install-recommends \ + build-essential git curl make \ + # When using Postgres uncomment the following line + # libpq-dev postgresql-client\ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +RUN mix local.hex --force && mix local.rebar --force + +# ============================================ +# Stage 2: Dev +# Development environment with interactive shell +# ============================================ +FROM base AS dev + +RUN apt-get update -qq && \ + apt-get install -y --no-install-recommends \ + erlang-crypto inotify-tools \ + && rm -rf /var/lib/apt/lists/* + +COPY docker/app/run.sh /run.sh +RUN chmod +x /run.sh + +CMD ["/run.sh"] + +# ============================================ +# Stage 3: Prod +# Production build environment +# ============================================ +FROM base AS prod + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libstdc++6 openssl libncurses6 locales ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \ + locale-gen + +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 +ENV MIX_ENV=prod + +# To use for production: +# 1. Uncomment to install dependencies: +# COPY mix.exs mix.lock ./ +# RUN mix deps.get --only prod +# RUN mix deps.compile + +# 2. Uncomment if your project has a config directory: +# COPY config/config.exs config/prod.exs config/ + +# 3. Uncomment if your project has assets: +# RUN mix assets.setup +# COPY assets assets +# COPY priv priv +# COPY lib lib + +# 4. Uncomment to compile the application: +# RUN mix compile + +# 5. Uncomment to build assets for production: +# RUN mix assets.deploy + +# 6. Uncomment to build the release: +# COPY rel rel +# RUN mix release + +# 7. Update CMD to match your app: +# CMD ["/app/bin/server"] diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base deleted file mode 100644 index dcc59f0..0000000 --- a/docker/Dockerfile.base +++ /dev/null @@ -1,26 +0,0 @@ -# ============================================ -# Base Dockerfile -# Shared foundation for all environments -# This image contains only essential build tools -# ============================================ - -ARG ELIXIR_VERSION=1.19.5 -ARG OTP_VERSION=28.3 -ARG DEBIAN_VERSION=trixie-20260202-slim - -ARG BASE_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" - -FROM ${BASE_IMAGE} - -RUN apt-get update -qq && \ - apt-get install -y --no-install-recommends \ - build-essential \ - git \ - curl \ - make \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -RUN mix local.hex --force && \ - mix local.rebar --force diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev deleted file mode 100644 index c818ce0..0000000 --- a/docker/Dockerfile.dev +++ /dev/null @@ -1,33 +0,0 @@ -# ============================================ -# Development Dockerfile -# Extends the base image with dev-specific tools -# Use this for local development -# ============================================ - -ARG ELIXIR_VERSION=1.19.2 -ARG OTP_VERSION=28.1 -ARG DEBIAN_VERSION=trixie-20251103-slim - -ARG DEV_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" - -FROM ${DEV_IMAGE} - -RUN apt-get update -qq && \ - apt-get install -y --no-install-recommends \ - curl \ - git \ - make \ - erlang-crypto \ - inotify-tools \ - apt-transport-https \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -RUN mix local.hex --force && \ - mix local.rebar --force - -COPY docker/app/run.sh /run.sh -RUN chmod +x /run.sh - -CMD ["/run.sh"] diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod deleted file mode 100644 index 7fd5a66..0000000 --- a/docker/Dockerfile.prod +++ /dev/null @@ -1,93 +0,0 @@ -# ============================================ -# Production Dockerfile (OPTIONAL) -# Multi-stage build for production releases -# -# To use this: -# 1. Uncomment the COPY lines below as needed -# 2. Update the COPY paths for your project structure -# 3. Ensure config/config.exs and config/prod.exs exist -# 4. Uncomment mix assets.deploy if you have assets -# 5. Uncomment mix release if using releases -# 6. Update the final CMD to match your app name -# -# Then build with: docker build -f docker/Dockerfile.prod . -# ============================================ - -ARG ELIXIR_VERSION=1.19.2 -ARG OTP_VERSION=28.1 -ARG DEBIAN_VERSION=trixie-20251103-slim - -ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" -ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}" - -# ============================================ -# Builder Stage -# ============================================ -FROM ${BUILDER_IMAGE} AS builder - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - build-essential \ - git \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -RUN mix local.hex --force && \ - mix local.rebar --force - -ENV MIX_ENV="prod" - -# Uncomment to install dependencies: -# COPY mix.exs mix.lock ./ -# RUN mix deps.get --only $MIX_ENV -# RUN mix deps.compile - -# Uncomment if your project has a config directory: -# COPY config/config.exs config/prod.exs config/ - -# Uncomment if your project has assets: -# RUN mix assets.setup -# COPY assets assets -# COPY priv priv -# COPY lib lib - -# Uncomment to compile the application: -# RUN mix compile - -# Uncomment to build assets for production: -# RUN mix assets.deploy - -# Uncomment to build the release: -# COPY rel rel -# RUN mix release - -# ============================================ -# Runtime Stage -# ============================================ -FROM ${RUNNER_IMAGE} AS runtime - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - libstdc++6 \ - openssl \ - libncurses6 \ - locales \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \ - locale-gen - -ENV LANG=en_US.UTF-8 -ENV LANGUAGE=en_US:en -ENV LC_ALL=en_US.UTF-8 - -WORKDIR /app - -ENV MIX_ENV="prod" - -# Uncomment to copy the release from builder: -# COPY --from=builder --chown=root:root /app/_build/prod/rel/your_app_name ./ - -# CMD ["/app/bin/server"]