towerops/k8s/base-image
Graham McIntire 8f87d4bbab
feat: add custom minimal Debian base image build system
Created build system for minimal Debian 13 (Trixie) image with Erlang/OTP
and Elixir runtime pre-installed. This will speed up CI/CD builds by caching
the runtime environment.

Features:
- Multi-stage Dockerfile for minimal final image (~150-200 MB)
- Build script with automatic tagging (versioned, latest, dated)
- Update script for easy security patch rebuilds
- Makefile for common operations
- Comprehensive documentation (README + QUICKSTART)

Benefits:
- Faster CI/CD: Saves 30-60s per build (no apt-get install runtime deps)
- Smaller images: Only essential runtime packages included
- Security: Easy to rebuild weekly/monthly with latest patches
- Consistency: Same runtime across all environments

Usage:
  cd k8s/base-image
  make build    # Build the image
  make test     # Verify it works
  make push     # Push to registry

Then update k8s/Dockerfile to use the custom base image.

This replaces the previous Dockerfile.base approach with a more
comprehensive and maintainable build system.
2026-01-25 09:07:55 -06:00
..
.gitignore feat: add custom minimal Debian base image build system 2026-01-25 09:07:55 -06:00
build.sh feat: add custom minimal Debian base image build system 2026-01-25 09:07:55 -06:00
Dockerfile feat: add custom minimal Debian base image build system 2026-01-25 09:07:55 -06:00
Makefile feat: add custom minimal Debian base image build system 2026-01-25 09:07:55 -06:00
QUICKSTART.md feat: add custom minimal Debian base image build system 2026-01-25 09:07:55 -06:00
README.md feat: add custom minimal Debian base image build system 2026-01-25 09:07:55 -06:00
update.sh feat: add custom minimal Debian base image build system 2026-01-25 09:07:55 -06:00

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.

Why a Custom Base Image?

Building a custom base image provides several benefits:

  1. Faster CI/CD - Base image is cached, no need to reinstall Erlang/Elixir on every build
  2. Smaller Images - Only includes essential runtime dependencies
  3. Security - Regularly rebuild with latest security patches
  4. 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: Installs Erlang and Elixir from Erlang Solutions repository
  • Final stage: Minimal Debian with only runtime dependencies

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 Registry

make push

Pushes both versioned and :latest tags to GitLab Container Registry.

Image Tags

Each build creates three tags:

  1. Full version: 28.3-elixir-1.19.5-debian-trixie (pinned, immutable)
  2. Latest: latest (always points to most recent build)
  3. 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 support
  • libsctp1 - SCTP protocol (required by Erlang)
  • libncurses6 - Terminal handling
  • libstdc++6 - C++ standard library
  • ca-certificates - SSL certificate validation
  • locales - Locale support (en_US.UTF-8)
  • iputils-ping - Network diagnostics
  • iproute2 - 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 Erlang Solutions repository is accessible
wget -O- https://packages.erlang-solutions.com/ubuntu/erlang_solutions.asc

# 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):

  1. Check /etc/resolv.conf in running container
  2. Verify Kubernetes DNS is working
  3. 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 :inet and libcluster
  • Smaller package selection
  • Only ~50 MB smaller (not worth the issues)

See Also