| .. | ||
| .gitignore | ||
| build.sh | ||
| Dockerfile | ||
| Makefile | ||
| QUICKSTART.md | ||
| README.md | ||
| update.sh | ||
Minimal Debian Base Image for Elixir/Phoenix
This directory contains scripts to build a minimal Debian 13 (Trixie) base image with Erlang/OTP and Elixir runtime pre-installed.
Container Engine Support
The build scripts automatically detect and use either Podman or Docker, preferring Podman if both are installed. No configuration needed - just install your preferred container engine and the scripts will use it.
Why a Custom Base Image?
Building a custom base image provides several benefits:
- Faster CI/CD - Base image is cached, no need to reinstall Erlang/Elixir on every build
- Smaller Images - Only includes essential runtime dependencies
- Security - Regularly rebuild with latest security patches
- Consistency - Same runtime environment across dev/staging/production
Quick Start
# Build the image
make build
# Test it works
make test
# Push to registry
make push
Usage
Building the Base Image
# Using Makefile (recommended)
make build
# Or using the script directly
chmod +x build.sh
./build.sh
This builds a multi-stage Docker image with:
- Builder stage: Uses official hexpm/elixir image with pre-built Erlang/Elixir
- Final stage: Minimal Debian with only runtime dependencies copied from builder
Updating System Packages
Run this weekly/monthly to get latest security updates:
# Using Makefile
make update
# Or using the script
./update.sh
This rebuilds the image with --no-cache --pull to ensure all packages are updated.
Testing the Image
make test
Verifies:
- Erlang/OTP is installed and working
- Elixir is installed and working
- Locale is configured correctly
Pushing to Registries
# Login to registries (one-time setup)
make login # GitLab Container Registry
make login-dockerhub # Docker Hub
# Push to both GitLab and Docker Hub
make push
Automatically pushes to both:
- GitLab Container Registry:
registry.gitlab.com/towerops/towerops/elixir-runtime - Docker Hub:
docker.io/gmcintire/elixir-runtime
Both versioned and :latest tags are pushed to each registry.
Image Tags
Each build creates three tags:
- Full version:
28.3-elixir-1.19.5-debian-trixie(pinned, immutable) - Latest:
latest(always points to most recent build) - Dated:
28.3-elixir-1.19.5-debian-trixie-2026-01-25(for rollback)
Using the Base Image
Update your main k8s/Dockerfile:
# Before
ARG RUNNER_IMAGE="docker.io/debian:trixie-20251229-slim"
# After
ARG RUNNER_IMAGE="registry.gitlab.com/towerops/towerops/elixir-runtime:28.3-elixir-1.19.5-debian-trixie"
Then remove the runtime installation steps (already in base image):
# Remove these lines - they're now in the base image
# RUN apt-get update && apt-get install -y erlang elixir ...
# RUN locale-gen ...
Image Size
The final image is approximately 150-200 MB (vs ~120 MB for plain Debian).
This is significantly smaller than:
- Official Elixir images (~400-500 MB)
- Debian with manually installed Erlang/Elixir (~300 MB)
What's Included
Erlang/OTP Runtime
- Latest Erlang/OTP from Erlang Solutions repository
- All essential runtime libraries (ssl, crypto, etc.)
- No build tools or dev dependencies
Elixir Runtime
- Latest Elixir release
- Mix build tool
- IEx interactive shell
System Packages (Minimal Set)
libssl3- SSL/TLS supportlibsctp1- SCTP protocol (required by Erlang)libncurses6- Terminal handlinglibstdc++6- C++ standard libraryca-certificates- SSL certificate validationlocales- Locale support (en_US.UTF-8)iputils-ping- Network diagnosticsiproute2- Network utilities
Configuration
Environment variables in build.sh:
REGISTRY=registry.gitlab.com/towerops/towerops
IMAGE_NAME=elixir-runtime
ERLANG_VERSION=28.3
ELIXIR_VERSION=1.19.5
DEBIAN_VERSION=trixie-20251229-slim
Override during build:
ERLANG_VERSION=29.0 ELIXIR_VERSION=1.20.0 make build
Maintenance Schedule
Recommended rebuild schedule:
- Weekly: Security updates (
make update) - Monthly: Review Erlang/Elixir versions, upgrade if needed
- As needed: When CVEs are announced for included packages
CI/CD Integration
Add to GitLab CI to rebuild weekly:
rebuild-base-image:
stage: build
only:
- schedules # Run on schedule
script:
- cd k8s/base-image
- make build
- make push
Troubleshooting
Image fails to build
# Check if hexpm images are accessible
docker pull hexpm/elixir:1.19.5-erlang-28.3-debian-trixie-20251229-slim
# Try building with verbose output
docker build --progress=plain --no-cache -f Dockerfile .
Application fails with missing dependencies
Add missing package to Dockerfile:
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
your-missing-package \
&& rm -rf /var/lib/apt/lists/*
Then rebuild: make build
DNS resolution issues (unlikely with Debian)
If you experience DNS issues (should not happen with Debian, only Alpine):
- Check
/etc/resolv.confin running container - Verify Kubernetes DNS is working
- Consider switching to Debian (already using it)
Comparison: Debian vs Alpine
We use Debian instead of Alpine because:
✅ Debian Advantages:
- Glibc (standard C library) - better compatibility
- No DNS resolution issues with Erlang
- Larger package ecosystem
- Better tested with Erlang/Elixir
❌ Alpine Disadvantages:
- Musl libc - can cause subtle bugs with Erlang NIFs
- DNS resolution issues with
:inetand libcluster - Smaller package selection
- Only ~50 MB smaller (not worth the issues)