Skip to content

Commit b5ba1d1

Browse files
committed
Add Containerfile
Previously, the https://github.com/Icinga/docker-icinga2 repository was used to build the Docker images for Icinga 2. However, due to its various design flaws, the resulted images had limited usability and required a lot of manual tweaking to make something useful out of them. This commit now follows our new principles of building Docker images from the Icinga DB repository, and replaces the old separate repository with this one. It makes use of the newest Docker BuildKit features to build the images in a more efficient way, while also granting users full flexibility to easily extend or modify the images as they see fit without any issues.
1 parent 8776279 commit b5ba1d1

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed

Containerfile

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Icinga 2 Docker image | (c) 2025 Icinga GmbH | GPLv2+
2+
3+
FROM debian:bookworm-slim AS build-base
4+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
5+
6+
# Install all the necessary build dependencies for building Icinga 2 and the plugins.
7+
#
8+
# This stage includes the build dependencies for the plugins as well, so that they can share the same base
9+
# image, since Docker builds common stages only once [^1] even if they are used in multiple build stages.
10+
# This eliminates the need to have a separate base image for the plugins, that basically has kind of the
11+
# same dependencies as the Icinga 2 build stage (ok, not exactly the same, but some of them are shared).
12+
#
13+
# [^1]: https://docs.docker.com/build/building/best-practices/#create-reusable-stages
14+
RUN apt-get update && \
15+
apt-get install -y --no-install-{recommends,suggests} \
16+
autoconf \
17+
automake \
18+
bison \
19+
ccache \
20+
cmake \
21+
flex \
22+
g++ \
23+
git \
24+
libboost{,-{context,coroutine,date-time,filesystem,iostreams,program-options,regex,system,test,thread}}1.74-dev \
25+
libedit-dev \
26+
libmariadb-dev \
27+
libpq-dev \
28+
libssl-dev \
29+
libsystemd-dev \
30+
make && \
31+
rm -rf /var/lib/apt/lists/*
32+
33+
# Set the default working directory for subsequent commands of the next stages.
34+
WORKDIR /icinga2-build
35+
36+
FROM build-base AS build-plugins
37+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
38+
39+
# Install all the plugins that are not included in the monitoring-plugins package.
40+
ADD https://github.com/lausser/check_mssql_health.git#747af4c3c261790341da164b58d84db9c7fa5480 /check_mssql_health
41+
ADD https://github.com/lausser/check_nwc_health.git#a5295475c9bbd6df9fe7432347f7c5aba16b49df /check_nwc_health
42+
ADD https://github.com/bucardo/check_postgres.git#58de936fdfe4073413340cbd9061aa69099f1680 /check_postgres
43+
ADD https://github.com/matteocorti/check_ssl_cert.git#341b5813108fb2367ada81e866da989ea4fb29e7 /check_ssl_cert
44+
45+
WORKDIR /check_mssql_health
46+
RUN mkdir bin && \
47+
autoconf && \
48+
autoreconf && \
49+
./configure "--build=$(uname -m)-unknown-linux-gnu" --libexecdir=/usr/lib/nagios/plugins && \
50+
make && \
51+
make install DESTDIR="$(pwd)/bin"
52+
53+
WORKDIR /check_nwc_health
54+
RUN mkdir bin && \
55+
autoreconf && \
56+
./configure "--build=$(uname -m)-unknown-linux-gnu" --libexecdir=/usr/lib/nagios/plugins && \
57+
make && \
58+
make install DESTDIR="$(pwd)/bin"
59+
60+
WORKDIR /check_postgres
61+
RUN mkdir bin && \
62+
perl Makefile.PL INSTALLSITESCRIPT=/usr/lib/nagios/plugins && \
63+
make && \
64+
make install DESTDIR="$(pwd)/bin" && \
65+
# This is necessary because of this build error: cannot copy to non-directory: /var/lib/docker/.../merged/usr/local/man
66+
rm -rf bin/usr/local/man
67+
68+
FROM build-base AS build-icinga2
69+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
70+
71+
# To access the automated build arguments in the Dockerfile originated from the Docker BuildKit [^1],
72+
# we need to declare them here as build arguments. This is necessary because we want to use unique IDs
73+
# for the mount cache below for each platform to avoid conflicts between multi arch builds. Otherwise,
74+
# the build targets will invalidate the cache one another, leading to strange build errors.
75+
#
76+
# [^1]: https://docs.docker.com/reference/dockerfile/#automatic-platform-args-in-the-global-scope
77+
ARG TARGETPLATFORM
78+
79+
# Create the directory where the final Icinga 2 files will be installed.
80+
#
81+
# This directory will be used as the destination for the `make install` command below and will be
82+
# copied to the final image. Other than that, this directory will not be used for anything else.
83+
RUN mkdir /icinga2-install
84+
85+
# Mount the source code as a bind mount instead of copying it, so that we can use the cache effectively.
86+
# Additionally, add the ccache and CMake build directories as cache mounts to speed up rebuilds.
87+
RUN --mount=type=bind,source=.,target=/icinga2,readonly \
88+
--mount=type=cache,id=ccache-${TARGETPLATFORM},target=/root/.ccache \
89+
--mount=type=cache,id=icinga2-build-${TARGETPLATFORM},target=/icinga2-build \
90+
PATH="/usr/lib/ccache:$PATH" \
91+
cmake -S /icinga2 -B /icinga2-build \
92+
-DCMAKE_BUILD_TYPE=ReleaseWithDebInfo \
93+
# The command group name below is required for the prepare-dirs script to work, as it expects
94+
# the command group name, which by default is `icingacmd` to exist on the system. Since we
95+
# don't create the `icingacmd` command group in this image, we need to override it with icinga.
96+
-DICINGA2_COMMAND_GROUP=icinga \
97+
-DCMAKE_INSTALL_PREFIX=/usr \
98+
-DCMAKE_INSTALL_SYSCONFDIR=/data/etc \
99+
-DCMAKE_INSTALL_LOCALSTATEDIR=/data/var \
100+
-DICINGA2_SYSCONFIGFILE=/etc/sysconfig/icinga2 \
101+
-DICINGA2_RUNDIR=/run \
102+
# See https://github.com/Icinga/docker-icinga2/pull/103 for why we need to enable systemd support.
103+
-DUSE_SYSTEMD=ON \
104+
-DICINGA2_WITH_{COMPAT,LIVESTATUS}=OFF && \
105+
make -j$(nproc) && \
106+
CTEST_OUTPUT_ON_FAILURE=1 make test && \
107+
make install DESTDIR=/icinga2-install
108+
109+
RUN rm -rf /icinga2-install/etc/icinga2/features-enabled/mainlog.conf \
110+
/icinga2-install/usr/share/doc/icinga2/markdown && \
111+
strip -g /icinga2-install/usr/lib/*/icinga2/sbin/icinga2 && \
112+
strip -g /icinga2-install/usr/lib/nagios/plugins/check_nscp_api
113+
114+
# Prepare the final image with the necessary configuration files and runtime dependencies.
115+
FROM debian:bookworm-slim AS icinga2
116+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
117+
118+
# Install the necessary runtime dependencies for the Icinga 2 binary and the monitoring-plugins.
119+
RUN apt-get update && \
120+
DEBIAN_FRONTEND=noninteractive && \
121+
apt-get install -y --no-install-{recommends,suggests} \
122+
bc \
123+
ca-certificates \
124+
curl \
125+
dumb-init \
126+
file \
127+
libboost-{context,coroutine,date-time,filesystem,iostreams,program-options,regex,system,thread}1.74.0 \
128+
libcap2-bin \
129+
libedit2 \
130+
libldap-common \
131+
libmariadb3 \
132+
libmoosex-role-timer-perl \
133+
libpq5 \
134+
libssl3 \
135+
libsystemd0 \
136+
mailutils \
137+
monitoring-plugins \
138+
msmtp{,-mta} \
139+
openssh-client \
140+
openssl && \
141+
# Official Debian images automatically run `apt-get clean` after every install, so we don't need to do it here.
142+
rm -rf /var/lib/apt/lists/*
143+
144+
# Create the icinga user and group with a specific UID as recommended by Docker best practices.
145+
# The user has a home directory at /var/lib/icinga2, and if configured, that directory will also
146+
# be used to store the ".msmtprc" file created by the entrypoint script.
147+
RUN adduser \
148+
--system \
149+
--group \
150+
--home /var/lib/icinga2 \
151+
--disabled-login \
152+
--allow-bad-names \
153+
--no-create-home \
154+
--uid 5665 icinga
155+
156+
COPY --from=build-plugins /check_mssql_health/bin/ /
157+
COPY --from=build-plugins /check_nwc_health/bin/ /
158+
COPY --from=build-plugins /check_postgres/bin/ /
159+
COPY --from=build-plugins /check_ssl_cert/check_ssl_cert /usr/lib/nagios/plugins/check_ssl_cert
160+
161+
COPY --from=build-icinga2 /icinga2-install/ /
162+
163+
# Create for all Icinga 2 directories in /data a corresponding symlink in the root directory.
164+
# This is necessary because we want to maintain the compatibility with containers built with the
165+
# legacy Dockerfile, which expects the Icinga 2 directories to be in the root directory.
166+
RUN for dir in /etc/icinga2 /var/cache/icinga2 /var/lib/icinga2 /var/log/icinga2 /var/spool/icinga2; do \
167+
ln -vs "/data$dir" "$dir"; \
168+
done
169+
170+
# Run the prepare-dirs script to create non-existing directories and set the correct permissions for them.
171+
# It's invoked in the same way as in the systemd unit file in a Debian package, so this will ensure that
172+
# all the necessary directories are created with the correct permissions and ownership.
173+
RUN /usr/lib/icinga2/prepare-dirs /etc/sysconfig/icinga2
174+
175+
# Well, since the /data directory is intended to be used as a volume, we should also declare it as such.
176+
# This will allow users to mount their own directories or even specific files to the /data directory
177+
# without any issues. We've already filled the /data directory with the necessary configuration files,
178+
# so users can simply mount their own files or directories if they want to override the default ones and
179+
# they will be able to do so without any issues.
180+
VOLUME ["/data"]
181+
182+
COPY tools/container/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
183+
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
184+
ENTRYPOINT ["/usr/bin/dumb-init", "-c", "--", "/usr/local/bin/docker-entrypoint.sh"]
185+
186+
EXPOSE 5665
187+
USER icinga
188+
189+
CMD ["icinga2", "daemon"]

tools/container/docker-entrypoint.sh

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/bin/bash
2+
3+
set -eo pipefail
4+
5+
# Function to display messages with different severity levels
6+
# Usage: icinga2_log <severity> <message>
7+
icinga2_log() {
8+
local severity="$1"
9+
local message="$2"
10+
11+
local color=""
12+
local reset=""
13+
14+
# Check if we are running in a terminal that supports colors,
15+
# otherwise fallback to plain text output.
16+
if [ -t 2 ]; then
17+
reset="\033[0m"
18+
# Set color codes based on severity
19+
case "$severity" in
20+
"information")
21+
color="\033[1;32m" # Green
22+
;;
23+
"warning")
24+
color="\033[1;33m" # Yellow
25+
;;
26+
"error")
27+
color="\033[1;31m" # Red
28+
;;
29+
esac
30+
fi
31+
32+
# Print the message with the appropriate color and reset code to stderr
33+
echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] ${color}${severity}${reset}/DockerEntrypoint: ${message}" >&2
34+
}
35+
36+
# The entrypoint script expects at least one command to run.
37+
if [ $# -eq 0 ]; then
38+
icinga2_log "warning" "Icinga 2 Docker entrypoint script requires at least one command to run."
39+
exit 1
40+
fi
41+
42+
icinga2_log "information" "Icinga 2 Docker entrypoint script started."
43+
44+
ca="/var/lib/icinga2/certs/ca.crt"
45+
if [ ! -e "$ca" ]; then
46+
nodeSetup=("node" "setup")
47+
runNodeSetup=false
48+
49+
# The following loop looks for environment variables that start with ICINGA_ and applies some transformations
50+
# to the keys before processing them in one way or another. Their values are never modified or printed in
51+
# unintended ways. The key transformations have the following rules and are applied in the order they are listed:
52+
#
53+
# - Since it only processes environment variables that start with ICINGA_, it'll first strip that prefix.
54+
# It then passes the key through awk to convert it to lowercase e.g. ICINGA_CN becomes cn.
55+
# - For each key, that hits one of the cases below, it will be processed a bit differently. In the first match,
56+
# the environment variable is expected to be a boolean (1 or 0) and it only becomes part of the node setup
57+
# command if and only if its value is 1. In that case, underscores in the key are replaced with dashes and
58+
# passed as-is to the node setup command (e.g., ICINGA_ACCEPT_COMMANDS=1 becomes --accept-commands).
59+
# - For the second match, the key is expected to be a key-value pair that should be passed to the node setup
60+
# command. In this case, the key is transformed in the same way as above, i.e., underscores are replaced with
61+
# dashes and the value is passed as-is (e.g., ICINGA_CN=example.com becomes --cn example.com).
62+
# - For the third match, the trusted certificate is expected to be a PEM-encoded certificate that should be
63+
# written to a temporary file and passed to the node setup command.
64+
# - Lastly, the CA certificate is likewise expected to be a PEM-encoded certificate that should be written to
65+
# the expected location at /var/lib/icinga2/certs/ca.crt.
66+
#
67+
# When encountering an environment variable prefixed with ICINGA_ that we don't know how to handle, we log it
68+
# as an informational message and continue processing the next environment variable but it doesn't cause the
69+
# script to fail.
70+
while IFS='=' read -r k value; do
71+
# Strip the ICINGA_ prefix and convert the key to lowercase.
72+
key=$(echo "${k#ICINGA_}" | awk '{print tolower($0)}')
73+
74+
case "$key" in
75+
"accept_commands" | "accept_config" | "disable_confd" | "master")
76+
runNodeSetup=true
77+
if [ "$value" = "1" ]; then
78+
nodeSetup+=("--${key//_/-}")
79+
fi
80+
;;
81+
"cn" | "endpoint" | "global_zones" | "listen" | "parent_host" | "parent_zone" | "zone" | "ticket")
82+
runNodeSetup=true
83+
nodeSetup+=("--${key//_/-}" "$value")
84+
;;
85+
"trustedcert")
86+
icinga2_log "information" "Writing trusted certificate to temporary file."
87+
runNodeSetup=true
88+
trustedCertFile=$(mktemp /tmp/trusted.cert)
89+
echo "$value" > "$trustedCertFile"
90+
nodeSetup+=("--$key" "$trustedCertFile")
91+
chmod 0644 "$trustedCertFile"
92+
;;
93+
"cacert")
94+
icinga2_log "information" "Writing CA certificate to $ca."
95+
runNodeSetup=true
96+
echo "$value" > "$ca"
97+
chmod 0644 "$ca"
98+
;;
99+
*)
100+
# We don't know how to handle this environment variable, so log it and move on.
101+
icinga2_log "warning" "Ignoring unknown environment variable $k"
102+
;;
103+
esac
104+
done < <(env | grep -E '^ICINGA_')
105+
106+
if [ "$runNodeSetup" = true ]; then
107+
icinga2_log "information" "Running Icinga 2 node setup command..."
108+
109+
icinga2 "${nodeSetup[@]}"
110+
# If the node setup command wasn't successful, we shouldn't reach this point due to set -e.
111+
icinga2_log "information" "Node setup completed successfully."
112+
else
113+
icinga2_log "information" "No node setup required based on environment variables."
114+
fi
115+
fi
116+
117+
mSmtpRc="/var/lib/icinga2/.msmtprc"
118+
# This script should initialize the container's msmtp configuration but never overwrite an existing configuration file.
119+
# If the file already exists, it should not be modified, even if the MSMTPRC environment variable is set.
120+
if [ ! -e "$mSmtpRc" ] && [ -n "${MSMTPRC}" ]; then
121+
icinga2_log "information" "Configuring msmtp with the provided MSMTPRC environment variable."
122+
echo "$MSMTPRC" > "$mSmtpRc"
123+
chmod 0644 "$mSmtpRc"
124+
fi
125+
126+
icinga2_log "information" "Starting Icinga 2 daemon."
127+
128+
exec "$@"

0 commit comments

Comments
 (0)