Comparing procedural Docker build scripts with declarative RPM package definitions to show how proper separation of concerns leads to more maintainable, reusable container builds.
← Back to Main PageWhen compiling software from source in containers, you can choose between procedural build scripts or declarative package definitions. Let's compare both approaches using a container that needs OpenSSL and Apache httpd.
# Multi-stage Dockerfile for OpenSSL + Apache httpd
FROM registry.fedoraproject.org/fedora:43 AS builder
# Install build dependencies
RUN dnf install -y \
gcc \
make \
perl \
perl-IPC-Cmd \
wget \
tar \
zlib-devel \
pcre-devel \
apr-devel \
apr-util-devel \
expat-devel \
&& dnf clean all
# Build OpenSSL
WORKDIR /usr/src
RUN wget https://www.openssl.org/source/openssl-3.4.0.tar.gz \
&& tar xzf openssl-3.4.0.tar.gz \
&& cd openssl-3.4.0 \
&& ./config \
--prefix=/opt/openssl \
--openssldir=/opt/openssl/ssl \
shared \
zlib \
&& make -j$(nproc) \
&& make install
# Build Apache httpd
WORKDIR /usr/src
RUN wget https://dlcdn.apache.org/httpd/httpd-2.4.62.tar.gz \
&& tar xzf httpd-2.4.62.tar.gz \
&& cd httpd-2.4.62 \
&& ./configure \
--prefix=/opt/httpd \
--enable-ssl \
--with-ssl=/opt/openssl \
--enable-so \
--enable-deflate \
--enable-http2 \
--with-mpm=event \
&& make -j$(nproc) \
&& make install
# Production stage - minimal image
FROM registry.fedoraproject.org/fedora:43
# Install runtime dependencies only
RUN dnf install -y \
zlib \
pcre \
apr \
apr-util \
expat \
&& dnf clean all
# Copy built software from builder
COPY --from=builder /opt/openssl /opt/openssl
COPY --from=builder /opt/httpd /opt/httpd
# Set up environment
ENV PATH="/opt/httpd/bin:${PATH}"
ENV LD_LIBRARY_PATH="/opt/openssl/lib64:/opt/httpd/lib"
# Configure httpd
RUN sed -i \
-e 's/^#ServerName.*/ServerName localhost/' \
-e 's/^Listen 80/Listen 8080/' \
/opt/httpd/conf/httpd.conf
EXPOSE 8080
CMD ["/opt/httpd/bin/httpd", "-D", "FOREGROUND"]
# build-assist.yaml
base: fedora-43-x86_64
build:
- type: dist-git
url: https://src.fedoraproject.org/rpms/
packages:
- openssl:release-3.4
- httpd:release-2.4
install:
type: container
packages:
- openssl
- httpd
Your application now needs PHP in addition to OpenSSL and Apache httpd. Let's compare how much work this requires in each approach.
# Add to builder stage
RUN dnf install -y \
libxml2-devel \
sqlite-devel \
libcurl-devel \
oniguruma-devel \
# ... many more dependencies
# Build PHP
WORKDIR /usr/src
RUN wget https://www.php.net/distributions/php-8.3.14.tar.gz \
&& tar xzf php-8.3.14.tar.gz \
&& cd php-8.3.14 \
&& ./configure \
--prefix=/opt/php \
--with-openssl=/opt/openssl \
--with-apxs2=/opt/httpd/bin/apxs \
--enable-mbstring \
--with-curl \
--with-pdo-mysql \
# ... many more configure flags \
&& make -j$(nproc) \
&& make install
# Add to production stage
RUN dnf install -y \
libxml2 \
sqlite-libs \
libcurl \
oniguruma \
# ... corresponding runtime deps
COPY --from=builder /opt/php /opt/php
# Update environment variables
ENV PATH="/opt/php/bin:${PATH}"
ENV LD_LIBRARY_PATH="/opt/php/lib:${LD_LIBRARY_PATH}"
# Configure Apache to load PHP module
RUN echo "LoadModule php_module modules/libphp.so" \
>> /opt/httpd/conf/httpd.conf
Each component requires researching dependencies, download locations, configure flags, and integration steps. The Dockerfile grows linearly with complexity.
# build-assist.yaml
base: fedora-43-x86_64
build:
- type: dist-git
url: https://src.fedoraproject.org/rpms/
packages:
- openssl:release-3.4
- httpd:release-2.4
- php:release-8.3 # ← Only change needed
install:
type: container
packages:
- openssl
- httpd
- php
The RPM spec for PHP already defines all dependencies, build flags, and Apache integration. Just add it to the list. The configuration stays simple regardless of how many components you add.
The difference becomes even more pronounced over time.
Traditional Docker: When OpenSSL 3.5.0 or httpd 2.4.63 is released:
RPM Build Assist: When packages are updated in dist-git:
Traditional Docker:
RPM Build Assist:
When you run rpm-build-assist with the build-assist.yaml configuration:
The traditional Dockerfile manually implements a multi-stage build to separate build and runtime dependencies. With RPM build assist, this is automatic:
The traditional Docker approach isn't just longer—it's fundamentally less maintainable and reusable. RPM-based builds provide the structure and tooling needed for sustainable, production-grade container images.