duffyduckandClaude Opus 5 5861989289 Restore im neu erstellten Container: fehlende Systemteile nachziehen
"omd restore" brach ab mit

  ERROR: apache user www-data must be member of group cmk

Die Ursache liegt nicht im Backup. Beim Anlegen einer Site traegt
"omd create" den Apache-Benutzer in die Gruppe der Site ein - das steht
in /etc/group, und das liegt im Container, nicht im Bind-Mount. Wird der
Container neu erstellt, ist /etc/group wieder der Stand aus dem Abbild,
waehrend die Site unveraendert weiterlebt. Der offizielle Entrypoint
erkennt das und legt Gruppe und Benutzer neu an, nimmt www-data dabei
aber nicht in die Gruppe auf. Im Betrieb faellt es nicht auf: die Site
laeuft, die Oberflaeche antwortet. Erst user_verify beim Restore prueft
es.

Direkt dahinter der zweite Halt:

  ERROR: fstab entry for /omd/sites/cmk/tmp does not exist

omd laesst einen fehlenden /etc/fstab ausdruecklich durchgehen ("seen in
some containers") - das Abbild liefert aber eine Datei mit einer
Kommentarzeile, also greift die Ausnahme nicht.

Beides am Abbild checkmk/check-mk-raw:2.4.0-latest nachgestellt: Site im
Bind-Mount anlegen, Container verwerfen, neuen Container mit demselben
Mount starten - die Gruppenzugehoerigkeit ist weg und der Restore bricht
mit genau dieser Meldung ab. Nach "gpasswd -a www-data cmk" und dem
fstab-Eintrag laeuft er durch.

container_fix_site_system stellt beides her, vor jedem restore und
import (do_restore ist die gemeinsame Stelle) und nach jedem "up", damit
auch ein von Hand aufgerufenes omd nicht darueber stolpert. Der
Apache-Benutzer und das Kommando zum Eintragen kommen aus
share/omd/distro.info - derselben Quelle, aus der omd sie liest, statt
"www-data" fest zu verdrahten. Passt schon alles, bleibt die Funktion
still.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 21:00:59 +02:00
2026-08-20 10:33:59 +02:00
2026-08-20 10:33:59 +02:00

Checkmk in a container – Docker (Debian) & Podman (RHEL) with NGINX Proxy Manager

Deutsche Fassung: README.md

A complete, production-ready container installation of Checkmk with NGINX Proxy Manager for HTTPS termination (Let's Encrypt or self-signed), including tooling for backup, restore and the migration of existing sites from a bare-metal installation – without having to re-register the agents.

Note on language: the scripts and their menus are in German. Literal program output and menu screenshots in this document are therefore shown unchanged; the surrounding text explains what they mean.


Table of contents

  1. Overview & architecture
  2. Directory layout
  3. Requirements & ports
  4. Installation on Debian/Ubuntu (Docker)
  5. Installation on RHEL/Rocky/Alma/Fedora (Podman)
  6. Starting automatically after a host reboot (Podman)
  7. Configuration (.env)
  8. Obtaining the Checkmk image (Raw, Enterprise & co.)
  9. First start
  10. Setting up HTTPS – Let's Encrypt or self-signed
  11. Configuring the Proxy Manager locally (terminal browser)
  12. Moving the admin interface to a different port
  13. Firewall
  14. Management: cmk-manage.sh (CLI and ncurses interface)
  15. Backup
  16. Restore
  17. Migrating a bare-metal installation (migrate_creator.sh)
  18. Several sites in one container
  19. Upgrading Checkmk
  20. Troubleshooting
  21. Security notes
  22. Uninstalling

1. Overview & architecture

                    Internet / LAN
                          |
                     :80  :443  :81
                          |
        +-----------------------------------+
        |  NGINX Proxy Manager (container)  |   network_mode: host
        |  - Let's Encrypt OR self-signed   |
        |  - web interface on port 81       |
        +-----------------------------------+
                          |  http://127.0.0.1:5000
                          v
        +-----------------------------------+
        |  Checkmk / OMD (container)        |   network_mode: host
        |  :5000  web GUI (Apache)          |
        |  :8000  agent receiver            |   <-- agents (cmk-agent-ctl)
        |  :6557  Livestatus (optional)     |
        +-----------------------------------+
                          |
             bind mount: ./data/checkmk/sites  ->  /omd/sites

Deliberate design decisions

Topic Implementation Rationale
Checkmk on the host network network_mode: host, no ports: entries agents, SNMP traps, ICMP and the agent receiver work without NAT; the source IP of the monitored hosts is preserved
NGINX Proxy Manager also on the host network network_mode: host reaches the Checkmk backend directly via 127.0.0.1:5000; no bridge network, no host-gateway tricks – behaves identically under Docker and Podman
No named volumes bind mounts into ./data/ only all data sits visibly in the project directory, easy to back up, copy and inspect
One compose file for both docker compose and podman-compose identical operation on Debian and RHEL
:Z on the bind mounts SELinux relabelling required on RHEL; a no-op on Debian/Docker (not an error)

2. Directory layout

.
├── README.md                        # German documentation
├── README_en.md                     # this file
├── docker-compose.yml               # for Docker AND Podman
├── .env.example                     # template -> .env
├── install.sh                       # installs Docker (Debian) / Podman (RHEL)
│
├── scripts/
│   ├── cmk-manage.sh                # stack, backup, restore, import + ncurses GUI
│   ├── migrate_creator.sh           # -> copy this to the SOURCE SERVER
│   └── lib/
│       └── common.sh                # shared functions + TUI helpers
│
├── containers/
│   └── browser/
│       └── Containerfile            # tools image: text browsers, curl, jq, openssl
│
├── systemd/
│   ├── checkmk-stack.service.example # compose-based unit (autostart)
│   └── quadlet/
│       ├── checkmk.container         # Podman Quadlet (alternative)
│       └── nginx-proxy-manager.container
│
├── data/                            # ALL payload data (bind mounts)
│   ├── checkmk/sites/               # -> /omd/sites   (complete OMD sites)
│   └── npm/
│       ├── data/                    # -> /data            (NPM config/DB)
│       ├── letsencrypt/             # -> /etc/letsencrypt (certificates)
│       └── custom-certs/            # self-signed certificates ready to upload
│
├── backups/                         # backups and migration bundles (*.tar.gz)
└── images/                          # downloaded Checkmk image tarballs

3. Requirements & ports

  • 64-bit Linux, at least 2 CPU cores and 4 GB RAM (recommended: 4 cores, 8 GB)
  • Disk space: rule of thumb 1 GB per 100 monitored hosts and year, plus backups
  • root privileges
  • Outbound internet access to download the images

Ports in use (host network!)

Port Service Reachable from
80/tcp NGINX Proxy Manager – HTTP, Let's Encrypt challenge internet/LAN
443/tcp NGINX Proxy Manager – HTTPS internet/LAN
81/tcp NGINX Proxy Manager – admin interface (default; changeable via npm-port) admin network only!
5000/tcp Checkmk Apache (backend) localhost only – do not expose. Exception: with the Proxy Manager turned off (12.4), open it for the external proxy's IP only
8000/tcp Checkmk agent receiver (TLS-registered agents) monitored hosts
6557/tcp Livestatus (only with CMK_LIVESTATUS_TCP=on) distributed sites

Important: because both containers run on the host network, ports 80, 443 and 81 must not already be taken on the host (e.g. by an installed Apache/NGINX). Check with ss -tlnp | grep -E ':(80|81|443|5000)\b'.


4. Installation on Debian/Ubuntu (Docker)

# put the project where it should live
sudo mkdir -p /opt/checkmk-stack
sudo cp -r ./* ./.env.example /opt/checkmk-stack/
cd /opt/checkmk-stack

# install Docker CE + compose plugin, prepare the project
sudo ./install.sh --with-systemd

install.sh does the following:

  1. adds the Docker repository from download.docker.com (GPG key + sources list)
  2. installs docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin, docker-compose-plugin
  3. installs dialog, openssl, tar, curl (for the ncurses interface)
  4. runs systemctl enable --now docker
  5. creates the directories under data/ and backups/ and generates .env from .env.example
  6. optionally sets up the systemd unit checkmk-stack.service

Then:

sudo $EDITOR /opt/checkmk-stack/.env     # at least CMK_PASSWORD and CMK_SITE_ID

# choose the image (Raw needs no login, Enterprise see chapter 8)
sudo ./scripts/cmk-manage.sh image pull --edition raw --version 2.3.0-latest

sudo ./scripts/cmk-manage.sh up
sudo ./scripts/cmk-manage.sh status

Under Docker, restart: unless-stopped already brings the containers back after a reboot (the Docker daemon starts them). The systemd unit is still worthwhile because it additionally starts all sites (see chapter 18).


5. Installation on RHEL/Rocky/Alma/Fedora (Podman)

Docker is not available on Red Hat systems – Podman is used instead. The compose file is the same one.

Step 0 on RHEL: check the subscription

An unregistered RHEL installs no packages at all – neither podman nor git, dialog or tar. dnf merely reports "This system is not registered with an entitlement server". This is the most common reason why install.sh does not get through on a fresh RHEL.

# is the system registered?
sudo subscription-manager status      # -> "Overall Status: Current" = fine
sudo dnf repolist                     # -> must not be empty

# if not: create a free account at https://developers.redhat.com
# (Developer Subscription for Individuals, up to 16 systems) and then:
sudo subscription-manager register --username <user>

RHEL 9 uses Simple Content Access, so subscription-manager attach is no longer required. Without an account it also works from the installation DVD or by switching to Rocky/Alma – all three routes are covered in detail in section 5.1.

Rocky Linux, AlmaLinux, CentOS Stream and Fedora need none of this – just read on.

sudo mkdir -p /opt/checkmk-stack
sudo cp -r ./* ./.env.example /opt/checkmk-stack/
cd /opt/checkmk-stack

sudo ./install.sh --with-systemd

install.sh installs:

  1. first checks whether a usable repository exists at all, and otherwise names the three routes from section 5.1
  2. podman, tar, openssl
  3. dialog for the ncurses interface – if that fails, newt (whiptail) is used instead; the scripts support either
  4. podman-compose – attempted in this order: distribution repository → EPEL → pip3 install podman-compose
  5. sets up the systemd unit (always with Podman, see the next chapter)

Done manually, that is:

sudo dnf install -y podman podman-compose tar openssl
sudo dnf install -y dialog || sudo dnf install -y newt   # dialog: AppStream, newt: BaseOS

# if podman-compose is not in the repository - add EPEL:
sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
sudo dnf install -y podman-compose

EPEL requires a working subscription. The epel-release package only configures the EPEL source; its dependencies and everything installed from it afterwards still come from Red Hat's BaseOS/AppStream. On an unregistered system even the command above therefore fails – EPEL is not a substitute for registering.

If podman-compose stays out of reach because of this, it is no obstacle: with Quadlet (chapter 6, option B) the stack runs entirely without compose, straight through systemd.

Starting up:

sudo $EDITOR /opt/checkmk-stack/.env

# obtain the image - for Enterprise with credentials, see chapter 8
sudo ./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \
     --user <user> --password <password>

sudo ./scripts/cmk-manage.sh up          # automatically uses podman-compose
sudo ./scripts/cmk-manage.sh status

On RHEL, do read chapter 8.2: short image names and podman import instead of podman load are the two causes of "image not known".

5.1 Unregistered RHEL: where the packages come from

A freshly installed Red Hat Enterprise Linux without a subscription hands out nothing at all:

$ sudo dnf install -y podman
This system is not registered with an entitlement server.
You can use "rhc" or "subscription-manager" to register.
Error: There are no enabled repositories in "/etc/yum.repos.d".

This affects every package – podman, git, dialog, tar. There are three ways out, and install.sh now detects the situation and names them.

Only RHEL itself is affected. Rocky Linux, AlmaLinux, CentOS Stream and Fedora need no registration – dnf works there straight away.

Red Hat hands out the Developer Subscription for Individuals free of charge. It covers up to 16 systems, physical or virtual, and explicitly allows small production use alongside development and testing. It is granted to the individual, not to a company.

  1. Create a free account at https://developers.redhat.com
  2. Register the server:
sudo subscription-manager register --username <user>
# you will be prompted for the password; alternatively --password <password>

RHEL 9 uses Simple Content Access – the repositories are enabled right away, a subscription-manager attach is no longer necessary.

  1. Verify:
sudo subscription-manager status      # -> Overall Status: Current
sudo dnf repolist                     # -> rhel-9-for-x86_64-baseos-rpms etc.
sudo dnf install -y git podman

If individual repositories are missing:

sudo subscription-manager repos --list-enabled
sudo subscription-manager repos --enable=rhel-9-for-x86_64-appstream-rpms

With a corporate account and an activation key, use this instead:

sudo subscription-manager register --org <org-id> --activationkey <key>

Route 2 – installation DVD as a local repository (no account, offline)

Works without any registration and without internet access. You need the DVD ISO (rhel-9.x-x86_64-dvd.iso, several GB) – the small boot.iso contains no packages.

# mount the ISO permanently
sudo mkdir -p /mnt/rhel
sudo mount -o loop,ro /path/rhel-9.8-x86_64-dvd.iso /mnt/rhel

# mount it automatically at boot
echo '/path/rhel-9.8-x86_64-dvd.iso /mnt/rhel iso9660 loop,ro,nofail 0 0' \
  | sudo tee -a /etc/fstab

# create the repository file
sudo tee /etc/yum.repos.d/rhel-local.repo >/dev/null <<'REPO'
[rhel-local-baseos]
name=RHEL local - BaseOS
baseurl=file:///mnt/rhel/BaseOS
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

[rhel-local-appstream]
name=RHEL local - AppStream
baseurl=file:///mnt/rhel/AppStream
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
REPO

sudo dnf clean all
sudo dnf repolist
sudo dnf install -y podman tar openssl dialog git

Limitation: the local repository holds exactly the state of the ISO – there are no security updates. For a permanently running monitoring server that is not acceptable; as a stopgap until registration is in place it is fine.

Route 3 – switch to Rocky Linux or AlmaLinux

Both are binary-compatible rebuilds of RHEL, permanently free and without registration. This project runs there unchanged – install.sh recognises them as part of the RHEL family.

# Rocky Linux
sudo dnf install -y git
sudo git clone https://github.com/rocky-linux/rocky-tools.git
sudo bash rocky-tools/migrate2rocky/migrate2rocky.sh -r

# AlmaLinux
curl -O https://raw.githubusercontent.com/AlmaLinux/almalinux-deploy/master/almalinux-deploy.sh
sudo bash almalinux-deploy.sh

Take a backup or a snapshot first. The conversion replaces the base packages on a running system.

What this project actually needs

Package Purpose Repository
podman container runtime AppStream
tar backups and migration bundles BaseOS
openssl self-signed certificates BaseOS
dialog ncurses interface AppStream
newt (whiptail) substitute if dialog is missing BaseOS
curl image download BaseOS
podman-compose compose support EPEL or pip3
git only needed to clone this project AppStream

dialog is optional: if it is missing, the scripts automatically use whiptail from the newt package, which lives in BaseOS. install.sh tries exactly this order and therefore no longer aborts.

git is not strictly required – the project can just as well be transferred as a ZIP file or via scp.

EPEL does not help here: the EPEL repository depends on Red Hat's base repositories. Without a subscription or a local repository you cannot even install epel-release – and therefore no podman-compose. In that case the Quadlet route from chapter 6 is the answer: it needs no compose at all.

5.2 SELinux

The bind mounts carry :Z in docker-compose.yml, so Podman automatically applies the container_file_t context. Should access still fail:

sudo semanage fcontext -a -t container_file_t "/opt/checkmk-stack/data(/.*)?"
sudo restorecon -Rv /opt/checkmk-stack/data
sudo ausearch -m avc -ts recent          # inspect AVC messages

6. Starting automatically after a host reboot (Podman)

This is the most important difference from Docker: Podman has no daemon. restart: unless-stopped in the compose file therefore does not survive a reboot. Autostart has to go through systemd. There are two proven ways.

Created and enabled automatically by ./install.sh --with-systemd. Manually:

sudo tee /etc/systemd/system/checkmk-stack.service >/dev/null <<'UNIT'
[Unit]
Description=Checkmk + NGINX Proxy Manager (container stack)
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/checkmk-stack
EnvironmentFile=/opt/checkmk-stack/.env
TimeoutStartSec=0
ExecStart=/bin/sh -c '/usr/bin/podman-compose up -d'
ExecStartPost=/opt/checkmk-stack/scripts/cmk-manage.sh start-all-sites
ExecStop=/bin/sh -c '/usr/bin/podman-compose down'

[Install]
WantedBy=multi-user.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now checkmk-stack.service
sudo systemctl status checkmk-stack.service

Verify that it survives a reboot:

sudo systemctl is-enabled checkmk-stack     # -> enabled
sudo reboot
# after the restart:
sudo podman ps
sudo /opt/checkmk-stack/scripts/cmk-manage.sh status

ExecStartPost calls start-all-sites. This is necessary because the Checkmk entrypoint only starts the site named in CMK_SITE_ID – with several sites the remaining ones would stay stopped.

Option B – Podman Quadlet (Podman 4.4 and newer, no compose)

Quadlet turns simple description files into real systemd units. Advantages: no podman-compose required, clean dependencies, native Podman.

sudo ./install.sh --quadlet

Using the values from .env, this writes to /etc/containers/systemd/:

  • checkmk.container
  • nginx-proxy-manager.container

Then:

sudo systemctl daemon-reload
sudo systemctl start checkmk nginx-proxy-manager
sudo systemctl status checkmk

WantedBy=multi-user.target default.target makes the units start automatically at boot – a separate systemctl enable is neither needed nor possible with Quadlet, because the units are generated.

Templates live in systemd/quadlet/.

Option C – rootless Podman (optional, more effort)

sudo ./install.sh --rootless
sudo loginctl enable-linger checkmk           # service account
# as a user unit:
mkdir -p ~/.config/systemd/user
cp systemd/checkmk-stack.service.example ~/.config/systemd/user/checkmk-stack.service
systemctl --user daemon-reload
systemctl --user enable --now checkmk-stack

--rootless sets net.ipv4.ip_unprivileged_port_start=80 so that ports 80/443 may be bound without root. loginctl enable-linger makes user units start after a reboot even when nobody is logged in.

Note: in rootless mode the UIDs of the site users are mapped through user namespaces. Files under data/checkmk/sites then belong to subordinate UIDs (e.g. 165536). That is normal, but it makes working with the files from the host awkward. For migrations, rootful operation is recommended.


7. Configuration (.env)

cp .env.example .env
chmod 600 .env
$EDITOR .env

The most important settings:

Variable Meaning
CMK_RUNTIME empty = auto-detect, otherwise docker or podman
CMK_IMAGE the image actually used, always fully qualified – see chapter 8
CMK_EDITION, CMK_VERSION inputs for the image downloader only
CMK_IMAGE_SOURCE auto/registry/download – where the image is fetched from
CMK_REGISTRY_USER/-PASSWORD login for registry.checkmk.com (Enterprise)
CMK_DOWNLOAD_USER/-PASSWORD login for the Checkmk download portal
CMK_SITE_ID the site the container creates and starts
CMK_PASSWORD initial password for user cmkadmin (only on creation)
CMK_LIVESTATUS_TCP on opens port 6557 for distributed monitoring
MAIL_RELAY_HOST SMTP relay for notifications
TZ time zone, e.g. Europe/Berlin
NPM_ENABLED on/off – should the Proxy Manager container run at all? See 12.4
NPM_ADMIN_PORT port of the Proxy Manager interface (default 81) – change only via npm-port or the menu
NPM_ADMIN_URL start page of the terminal browser; empty = derived from NPM_ADMIN_PORT
BACKUP_DIR location for manual backups (default ./backups); scheduled jobs have their own paths
BACKUP_AUTO on/off – should the backup jobs run via cron?
CERT_DOMAIN, CERT_DAYS defaults for self-signed certificates
FW_ROLE single/central/remote – role in distributed monitoring, drives the firewall check
FW_EC_SYSLOG, FW_EC_SNMPTRAP, FW_EC_TCP Event Console: syslog (514), SNMP traps (162/udp), status over TCP (6558)
FW_AGENT does a Checkmk agent run on this host in pull mode? (6556)
FW_EXTRA_PORTS additional ports for the check, e.g. 3000/tcp=Grafana

7.1 Who reads .env – and why does it exist alongside the script?

.env is the only configuration file. There is deliberately no second configuration inside the management script:

                    .env  (single source of truth)
                      |
      +---------------+----------------+------------------+
      |               |                |                  |
docker-compose    systemd unit    cmk-manage.sh      migrate_creator.sh
(variable         (EnvironmentFile)  (source)         (independent, runs on
 substitution)                                         the source server)
  • Read by compose, by the systemd unit and by the script.
  • Written by the script – either specifically (image … sets CMK_IMAGE, import --set-default-site sets CMK_SITE_ID, npm-port sets NPM_ADMIN_PORT) or through the settings menu / config set (see chapter 14). An editor is no longer necessary, but remains available as a menu entry.
  • The script itself only holds fallback values for the case that a key is missing from .env (in scripts/lib/common.sh, function load_env). It never overwrites a value that is set.

In short: .env says what runs, the script does something with it. Nothing has to be maintained twice.


8. Obtaining the Checkmk image (Raw, Enterprise & co.)

8.1 Which edition lives where?

This is the most common pitfall: the Enterprise edition is not on Docker Hub. The checkmk namespace on Docker Hub contains only:

Edition (CMK_EDITION) Source Login
raw (up to 2.4) docker.io/checkmk/check-mk-raw no
community (2.5 and later, successor of raw) docker.io/checkmk/check-mk-community no
cloud docker.io/checkmk/check-mk-cloud no
managed (up to 2.4) docker.io/checkmk/check-mk-managed no
pro (2.5 and later, successor of enterprise) docker.io/checkmk/check-mk-pro no
ultimate, ultimatemt (2.5 and later) docker.io/checkmk/check-mk-ultimate[mt] no
enterprise (2.0 – 2.4) registry.checkmk.com/enterprise/check-mk-enterprise or an image tarball from the customer portal yes
free (historic, up to 2.1) docker.io/checkmk/check-mk-free no

With Checkmk 2.5 the editions were renamed; those images are publicly available on Docker Hub again. So if you run 2.5 or newer, simply use pro. For 2.2/2.3/2.4 Enterprise the only routes are registry.checkmk.com or the tarball.

Beware of the names

The rename is the second big pitfall. checkmk.com only shows the new names, while files and repositories keep the old ones up to 2.4. For 2.3.0p9 specifically – verified against the version archive:

Choice on checkmk.com --edition here actual file name
Community raw check-mk-raw-docker-2.3.0p9.tar.gz
Pro enterprise check-mk-enterprise-docker-2.3.0p9.tar.gz
Ultimate cloud check-mk-cloud-docker-2.3.0p9.tar.gz
Ultimate MT managed check-mk-managed-docker-2.3.0p9.tar.gz

Picking "Pro" on the website and deriving check-mk-pro-docker-2.3.0p9.tar.gz from it yields an HTTP 404 – that file does not exist for 2.3.

The script takes care of this: it reads the real file name from the version archive and additionally tries both naming variants. In the interface the edition list shows both names.

Freely downloadable does not mean licence-free – the licence key is entered inside Checkmk afterwards.

8.2 Two Podman pitfalls (the ones that actually bite)

1. Short image names. Podman has no default registry. Short names such as checkmk/check-mk-enterprise:2.3.0p23 are resolved through unqualified-search-registries in /etc/containers/registries.conf – and on RHEL registry.access.redhat.com is listed first. Podman therefore looks for the image at Red Hat, does not find it and reports image not known, or asks interactively. That is why .env and docker-compose.yml consistently use the fully qualified name:

CMK_IMAGE=registry.checkmk.com/enterprise/check-mk-enterprise:2.3.0p23

Inspect the current resolution order:

grep -A3 unqualified-search-registries /etc/containers/registries.conf

2. import instead of load. A downloaded image tarball is installed with load:

Command Expects Result
podman import file.tar.gz a filesystem archive (rootfs) an image without entrypoint, environment or tag → unusable, "image unknown"
podman load -i file.tar.gz an image archive (docker save/OCI) a complete image including metadata and tag ✔

Getting rid of a broken image created by import:

podman images | grep '<none>'
podman rmi <image-id>

cmk-manage.sh image load checks the archive for manifest.json or oci-layout beforehand and rejects a wrong archive with exactly this hint.

8.3 Storing credentials in .env

CMK_EDITION=enterprise
CMK_VERSION=2.3.0p23

# route 1 - private registry
CMK_REGISTRY=registry.checkmk.com
CMK_REGISTRY_USER=<customer-portal-user>
CMK_REGISTRY_PASSWORD=<password>

# route 2 - tarball from the download portal
CMK_DOWNLOAD_BASEURL=https://download.checkmk.com/checkmk
CMK_DOWNLOAD_USER=<user>
CMK_DOWNLOAD_PASSWORD=<password>

The credentials come from the Checkmk customer portal (portal.checkmk.com). .env should therefore be chmod 600 – install.sh takes care of that.

8.4 Testing the credentials first

Rather than having a multi-gigabyte download fail on a wrong login, both can be checked in seconds:

./scripts/cmk-manage.sh image test --edition enterprise --version 2.3.0p23

Nothing is downloaded and no login is stored. What gets checked:

Check What it answers
registry reachable network, proxy, DNS
login accepted are the username and password correct?
access to the repository does the account have the required entitlement (subscription)?
download portal is the image tarball there and is the login accepted?

Example with correct credentials from .env:

Registry registry.checkmk.com
  [  OK  ] erreichbar (HTTP 401)
  [  OK  ] Anmeldung akzeptiert
  [  OK  ] Zugriff auf 'enterprise/check-mk-enterprise' vorhanden

Downloadportal download.checkmk.com
  [  OK  ] Datei erreichbar (HTTP 200)
           https://download.checkmk.com/checkmk/2.3.0p23/check-mk-enterprise-2.3.0p23.docker.tar.gz

[  OK  ] Zugangsweg nutzbar.

With a wrong password:

Registry registry.checkmk.com
  [  OK  ] erreichbar (HTTP 401)
  [FEHLER] Anmeldung abgelehnt (HTTP 401)
           Benutzername/Kennwort des Checkmk-Kundenportals pruefen

(erreichbar = reachable, Anmeldung akzeptiert = login accepted, Anmeldung abgelehnt = login rejected, Zugangsweg nutzbar = a route works.)

Useful distinctions the test makes:

  • Login accepted, but no access to the repository – the credentials are correct, but the subscription lacks the entitlement for this edition.
  • Login accepted, but HTTP 404 on the download portal – username and password are right, only the version or file name does not match. Give the exact version (no -latest) or set CMK_DOWNLOAD_URL.
  • For the freely available editions (raw, pro, cloud, …) the test reports "public repository – no login required" and still verifies that the repository can actually be reached.

Options:

# supply credentials once without writing them to .env
./scripts/cmk-manage.sh image test --edition enterprise --version 2.3.0p23 \
     --user <user> --password <password>

# check only one route
./scripts/cmk-manage.sh image test --only registry
./scripts/cmk-manage.sh image test --only download

The credentials are passed to curl via curl --config - on standard input and therefore do not appear in the process list.

Passwords with special characters: this used to go through a temporary .netrc file. Its format separates fields by whitespace, so a password containing spaces, # or quotes arrived silently mangled and the login failed for no apparent reason. Measured against a test server: with a password containing a space, #, " and $, the new route returns HTTP 200 and the old one HTTP 401. If curl is missing, the registry check falls back to podman login / docker login – into a throwaway file as well, so the system's stored login is left untouched.

In the ncurses interface the test can be reached from two places:

  • main menu → image → "Zugangsdaten prüfen"
  • Einstellungen → "Checkmk-Image und Zugangsdaten" → ">> Zugangsdaten jetzt prüfen" – right where the username and password are entered

After changing a credential field the menu also offers to run the test straight away.

8.5 Listing the available versions

Instead of guessing version numbers:

./scripts/cmk-manage.sh image versions --edition enterprise --filter 2.3.0p

Up to three sources are queried:

Source --source Credentials
version archive on checkmk.com web no
registry (Docker Hub or registry.checkmk.com) registry only for Enterprise
download portal (directory listing) download yes

The registry and the download portal do not carry the same thing. The registry usually keeps only the most recent patch levels – for 2.4.0 roughly p31 upwards, plus 2.4.0-latest and the daily builds. Older levels have rolled out of it. The download portal, by contrast, is an archive and still has them.

This matters precisely when a specific version is required – during a migration, because omd restore demands exactly the version of the source site (chapter 17.4). For those cases the tarball is the right route, not the registry. image why points this out by itself when a tag is missing from the registry but the version exists in the archive.

Without --source all three are tried – so even without stored credentials you get a usable list:

Versionsarchiv  https://checkmk.com/download/archive
  (oeffentlich, keine Zugangsdaten noetig)
  2.3.0p50
  2.3.0p49
  2.3.0p48
  ...

Uebernehmen
  aus der Registry :  ./scripts/cmk-manage.sh image pull     --edition enterprise --version <version>
  als Tarball      :  ./scripts/cmk-manage.sh image download --edition enterprise --version <version>
  nur eintragen    :  ./scripts/cmk-manage.sh config set CMK_VERSION <version>

Further options: --limit N (default 25), --all, --filter TEXT and --bare for the plain list without any framing – handy for scripting:

./scripts/cmk-manage.sh image versions --source web --filter 2.3.0p --bare | head -1

In the interface there is image → "Verfügbare Versionen auflisten"; and when fetching an image the version no longer has to be typed at all – a selection list appears instead.

8.6 Fetching the image – one command, three routes

Once edition and version are set, this is enough:

sudo ./scripts/cmk-manage.sh image get

Where it fetches from is controlled by CMK_IMAGE_SOURCE:

Value Effect
auto (default) registry first, then the download portal
registry registry only
download tarball from the download portal only

Deviate once with image get --source download.

auto is the sensible default because registries only keep the most recent patch levels – an older version needed for a migration then only exists in the download portal. The switch happens automatically, without asking.

up fetches automatically. If the image is missing locally and edition and version are set, up obtains it first through this route – otherwise compose attempts a plain pull, which must fail for a version that only exists in the archive:

[ INFO ] Das Image ist lokal nicht vorhanden - hole es (auto) ...
[ INFO ] Bezugsweg 'auto': zuerst die Registry ...
[ WARN ] Aus der Registry nicht zu bekommen - versuche das Downloadportal.

up --no-fetch suppresses it.

In the interface this is under image → "Image holen". Only the source is asked for there – the configured one is marked. To change edition or version, pick "Etwas anderes waehlen" or the wizard directly.

The individual steps

In the interface this is a wizard: image → "Image beschaffen" walks through edition, version and source in one go, then sets CMK_EDITION, CMK_VERSION and CMK_IMAGE, and does not ask again for credentials already stored in .env. It only asks if they are missing for the chosen route – and offers to save them. The same wizard is also available under Einstellungen → Checkmk-Image und Zugangsdaten → ">> Image beschaffen".

On the command line the individual steps remain:

a) From a registry (recommended)

For the Enterprise edition this is exactly the following two steps – the script performs them in one call and writes the result into .env:

podman login registry.checkmk.com          # or docker login
podman pull registry.checkmk.com/enterprise/check-mk-enterprise:2.3.0p9
# freely available, no login
sudo ./scripts/cmk-manage.sh image pull --edition raw --version 2.3.0-latest

# Enterprise with a login to registry.checkmk.com
sudo ./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \
     --user <user> --password <password>

b) Image tarball from the download portal (with credentials)

sudo ./scripts/cmk-manage.sh image download --edition enterprise --version 2.3.0p23 \
     --user <user> --password <password>

The downloader tries the usual file names under <CMK_DOWNLOAD_BASEURL>/<version>/ (check-mk-enterprise-<version>.docker.tar.gz and check-mk-enterprise-docker-<version>.tar.gz). Should Checkmk have changed the scheme, the full URL can be given directly:

sudo ./scripts/cmk-manage.sh image download \
     --url 'https://download.checkmk.com/checkmk/2.3.0p23/check-mk-enterprise-2.3.0p23.docker.tar.gz' \
     --user <user> --password <password>

The credentials are handed to curl via curl --config - and therefore do not appear in the process list.

c) Loading a file you already downloaded

If you fetched the file from the customer portal with a browser:

sudo ./scripts/cmk-manage.sh image load --file /root/check-mk-enterprise-2.3.0p23.docker.tar.gz

In all three cases the script determines the actual image name (for load typically docker.io/checkmk/check-mk-enterprise:2.4.0p9) and writes it to CMK_IMAGE in .env. Afterwards:

sudo ./scripts/cmk-manage.sh up

8.7 CMK_IMAGE versus CMK_EDITION/CMK_VERSION

These three values have different jobs, and confusing them is a common mistake:

Value Who reads it What for
CMK_IMAGE compose and the systemd units the image that actually starts
CMK_EDITION only cmk-manage.sh image … input for the downloader
CMK_VERSION only cmk-manage.sh image … input for the downloader

CMK_IMAGE can not be derived from edition and version – depending on how you obtained it, the very same version looks completely different:

docker.io/checkmk/check-mk-raw:2.3.0-latest                 (Docker Hub)
registry.checkmk.com/enterprise/check-mk-enterprise:2.3.0p9 (registry)
docker.io/checkmk/check-mk-enterprise:2.4.0p9               (loaded from a tarball)

The name after image download is surprising. The tarball carries the unqualified name checkmk/check-mk-enterprise:<version> internally, and Podman expands that to docker.io/ when loading – even though the image does not exist there. That is correct and harmless: the image is local and up uses it. It just could not be pulled under that name. Should it ever go missing, image get fetches it from the download portal again – not via podman pull.

So CMK_IMAGE records what actually applies. It is written by image pull, image download, image load and image use – you never have to type anything there by hand. In the interface the three values are therefore not editable individually: the wizard sets them together. On the command line they remain individually settable (config set …).

Recording it without downloading anything. The reference can be built from edition and version – no download required:

./scripts/cmk-manage.sh image use --edition enterprise --version 2.4.0p9
# -> CMK_IMAGE=registry.checkmk.com/enterprise/check-mk-enterprise:2.4.0p9
#    CMK_EDITION=enterprise
#    CMK_VERSION=2.4.0p9

The image itself is fetched by the runtime on the next up. In the wizard this route is called "Nur eintragen – geholt wird beim naechsten Start". If the registry needs a login (Enterprise), that has to have happened once – either via the "Registry holen" route or with podman login registry.checkmk.com.

All four wizard routes set CMK_IMAGE – they only differ in whether something is fetched right away. The three values therefore stay consistent by themselves, and the mismatch warning no longer appears in normal operation.

On the command line, image pull and image download take --remember to store the edition and version in .env as well:

./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0p9 --remember

Consequence: changing the edition or version does not start a different release. Only image pull (or download/load) writes the result to CMK_IMAGE, and only up recreates the container.

To stop these from drifting apart unnoticed, the script compares the three values – in image status, in status, and in the settings menu right after the edition or version is changed:

[ WARN ] CMK_IMAGE passt nicht zu den eingestellten Vorgaben.

  verwendet wird : docker.io/checkmk/check-mk-raw:2.3.0-latest
  Edition        : eingestellt "enterprise", im Image steckt "raw"
...
    ./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0-latest
    ./scripts/cmk-manage.sh up

("CMK_IMAGE does not match the configured defaults / in use / configured … but the image contains …".) Images loaded from a tarball (localhost/…) count as consistent as long as name and tag match.

If fetching fails, CMK_IMAGE stays unchanged – deliberately, because the old image keeps running. The wizard then says so explicitly ("Image wurde NICHT geholt") instead of merely pointing at the mismatch. It still leaves a state in which edition and version claim something different from the image in use. There are two ways out, both in the menu underneath the three values:

Route Direction Effect
Assistent starten (wizard) – walks through edition, version and source
Image aus den Vorgaben setzen (set from defaults) edition/version → image builds the reference and records it without fetching anything; the menu entry shows what it will be
Vorgaben aus dem Image ableiten (derive defaults) image → edition/version writes edition and version back from CMK_IMAGE; nothing about the running setup changes

On the command line the second one is:

./scripts/cmk-manage.sh image use "$CMK_IMAGE" --remember

8.8 Finding the registry path

"Login Succeeded" does not yet mean the path is right. If the pull reports unable to copy from source docker://…, the repository does not exist under that name, or the subscription does not cover it:

./scripts/cmk-manage.sh image repos --edition enterprise

This first queries the catalog (/v2/_catalog) – which many registries block – and then probes the usual paths:

Uebliche Pfade fuer "raw" durchprobiert
  STATUS TAGS     PFAD
  200 = nutzbar, 404 = gibt es nicht, 401/403 = kein Zugriff
  401    -        raw/check-mk-raw  (kein Zugriff - oder gibt es nicht)
  200    2394     checkmk/check-mk-raw
  ...
[  OK  ] Nutzbar: docker.io/checkmk/check-mk-raw

If an image pull against a private registry fails, this probe runs automatically and shows what is actually there.

401 does not necessarily mean "it exists". Many registries – Docker Hub among them – answer 401 for non-existent repositories too, so as not to reveal whether they exist. Only 200 = usable and 404 = does not exist are reliable.

If no path works, the tarball route remains. For Enterprise it is the documented one anyway, and it works independently of the registry:

./scripts/cmk-manage.sh image download --edition enterprise --version 2.4.0p9

8.9 Checking what is in effect

./scripts/cmk-manage.sh image status     # configured image + local images
./scripts/cmk-manage.sh image list       # local Checkmk images only
./scripts/cmk-manage.sh image use localhost/checkmk/check-mk-enterprise:2.3.0p23

image status explicitly warns when CMK_IMAGE is a short name without a registry.

The ncurses interface offers the same workflow under the menu entry "Checkmk-Image beschaffen/auswählen" (pick edition → version → credentials → fetch).


9. First start

sudo ./scripts/cmk-manage.sh up
sudo ./scripts/cmk-manage.sh status

Output (example, German as printed by the tool):

Container
  checkmk                  läuft
  nginx-proxy-manager      läuft

Sites im Container
  SITE                 VERSION            STATUS
  cmk                  2.3.0p23.cre       läuft

Zugang
  Checkmk (direkt)        : http://10.0.0.5:5000/cmk/
  Checkmk (über Proxy)    : https://<FQDN>/cmk/
  Agent-Receiver          : 10.0.0.5:8000
  NGINX Proxy Manager     : http://10.0.0.5:81/

(läuft = running, gestoppt = stopped, Zugang = access, Erstlogin = initial login.) The port of the admin interface follows NPM_ADMIN_PORT from .env; the output always shows the value actually configured.

Logging in to Checkmk: user cmkadmin, password from CMK_PASSWORD.


10. Setting up HTTPS – Let's Encrypt or self-signed

10.1 The administrator account

The former default credentials admin@example.com / changeme no longer exist as of version 2.15. The container only creates the first account if INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD are set at the first start – otherwise the browser shows a setup wizard. Verified in backend/setup.js of v2.15.1 and against a running container.

That is why the values live in .env and are passed through by docker-compose.yml:

NPM_ADMIN_EMAIL=admin@example.com
NPM_ADMIN_PASSWORD=a-proper-password

Set both before the first start. The account then exists right away – and with it the REST interface used by chapter 10.2.

If the stack has already run and the account was created by hand through the wizard, the variables no longer take effect (the database is no longer empty). Simply enter the credentials you chose – the script logs in with those.

When the login is missing

If the credentials are absent, the script tells you what to do depending on the situation – it checks whether data/npm/data/database.sqlite already exists:

Situation Route
no database yet config set NPM_ADMIN_EMAIL … and …_PASSWORD …, then up – the container creates the account itself
database present create the account in the browser (cmk-manage.sh browser), then enter the same credentials with config set

To start over cleanly – in the interface under Einstellungen → NGINX Proxy Manager → ">> Datenbank zuruecksetzen", or from the Proxy-Hosts menu, or on the command line:

sudo ./scripts/cmk-manage.sh npm-reset

This moves the database aside (renamed, not deleted) so the container recreates the account from NPM_ADMIN_EMAIL/NPM_ADMIN_PASSWORD on the next up. Proxy hosts, redirections and access lists are gone – the certificates remain. The command shows exactly that beforehand and asks for confirmation. The proxy hosts can be recreated with proxy add in a single call.

10.2 Creating a proxy host – without a browser

The web interface is painful in a terminal browser, and forms with file uploads do not work there at all. Since the Proxy Manager runs locally anyway, its interface is the more reliable route:

# publish Checkmk at checkmk.example.com with a self-signed certificate
sudo ./scripts/cmk-manage.sh proxy add --domain checkmk.example.com --cert self

# or with Let's Encrypt (port 80 must be reachable from the internet)
sudo ./scripts/cmk-manage.sh proxy add --domain checkmk.example.com --cert letsencrypt

# forwarding to a different port instead of 5000
sudo ./scripts/cmk-manage.sh proxy add --domain checkmk.example.com --forward-port 8080

In one go the command: creates the certificate (or requests it from Let's Encrypt), uploads it to the Proxy Manager, creates the proxy host, forces HTTPS, enables HTTP/2 and websockets, and sets the recommended advanced_config block.

Command Effect
proxy status check the login, count hosts and certificates
proxy list existing proxy hosts
proxy certs existing certificates with expiry date
proxy add … create a proxy host (--update replaces an existing one)
proxy show --domain X details including additional paths
proxy del --id N delete a proxy host
proxy edit --id N … change an existing proxy host – only the fields you name
proxy default --domain X enable HTTPS on the bare IP (mirrors X into a default server)
proxy default --off remove that mirror again

Options for proxy add:

Option Default Meaning
--domain FQDN PROXY_DOMAIN public name
--forward-host H 127.0.0.1 forwarding target
--forward-port P 5000 target port – freely selectable, not fixed to Checkmk
--scheme http|https http scheme towards the target
--cert self|letsencrypt|none self certificate
--update – replace an existing entry
--advanced TEXT see below custom nginx block
--location /P=HOST:PORT – another service under a path, repeatable – see 10.3
--no-force-ssl, --no-http2, --no-websockets, --no-block-exploits – switches

The advanced_config block

Without --advanced, proxy add sets this block – the same four lines you would otherwise type into the Advanced tab by hand:

client_max_body_size 0;
proxy_read_timeout 300s;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

What each is for:

Line Reason
client_max_body_size 0 removes the size limit – large agent uploads and bakery packages fail otherwise
proxy_read_timeout 300s long-running Checkmk views would otherwise time out
X-Forwarded-Proto without it Checkmk generates http:// links behind the HTTPS proxy
X-Forwarded-For without it the Checkmk access log only shows the proxy address instead of the real client IP

--advanced replaces the block (it does not append):

sudo ./scripts/cmk-manage.sh proxy add --domain checkmk.example.com \
     --advanced 'client_max_body_size 0;
proxy_read_timeout 600s;'

--advanced '' leaves the field empty. The wizard in the interface always uses the default above; if you need something else, use the command line or adjust the entry in the Proxy Manager afterwards.

In the interface the same flow is under proxy → "Proxy-Host anlegen" (name → target port → certificate) and under Einstellungen → NGINX Proxy Manager → ">> Proxy-Hosts anlegen/verwalten".

A self-signed certificate is reused on repeated runs instead of being created anew each time – otherwise dozens of entries with the same name pile up in the Proxy Manager.

10.3 Several services behind one port (paths instead of names)

The typical situation: only one port is reachable from outside, but the server runs several interfaces – Checkmk, Grafana, maybe something else. Instead of needing a separate name per service, they can be told apart by path:

https://10.0.0.5/cmk/      ->  Checkmk   (127.0.0.1:5000)
https://10.0.0.5/grafana   ->  Grafana   (127.0.0.1:3000)
https://10.0.0.5/other     ->  another service

This is exactly what the Proxy Manager is for. In one call:

sudo ./scripts/cmk-manage.sh proxy add \
     --domain 10.0.0.5 \
     --forward-port 5000 \
     --cert self \
     --location /grafana=127.0.0.1:3000 \
     --location /other=127.0.0.1:8080
  • --domain may be an IP address – a DNS name is not required. (The interface accepts anything except a handful of special characters.)
  • --forward-port is the target for everything not covered by a path – Checkmk here.
  • --location can be given any number of times. Syntax /path=host:port, optionally with a scheme (https://host:port) and with a path prefixed at the target (/alt=host:3000/base).

Inspect the result with:

./scripts/cmk-manage.sh proxy show --domain 10.0.0.5

In the interface the wizard asks about the certificate, whether further services should live under the same name, and then takes path and target in turn until you leave a field empty.

The path list is absolute. proxy add --update replaces the entry including all paths. So when adding a path later, pass the existing ones again – proxy show tells you which those are.

What the services themselves need to know

A service behind a sub-path otherwise generates links to / that lead nowhere. Checkmk is unaffected – it lives under /<site>/ anyway. Others need a setting:

Grafana (/etc/grafana/grafana.ini or GF_* environment variables):

[server]
domain = 10.0.0.5
root_url = %(protocol)s://%(domain)s/grafana/
serve_from_sub_path = true

As a container:

environment:
  GF_SERVER_ROOT_URL: https://10.0.0.5/grafana/
  GF_SERVER_SERVE_FROM_SUB_PATH: "true"

Without it Grafana loads but looks for its JavaScript under https://10.0.0.5/public/... instead of /grafana/public/... – the page stays blank. The same applies to any other service: it has to know its base path.

Why the Proxy Manager at all

All containers run on the host network, so the services listen directly on their ports (Checkmk 5000, Grafana 3000, …). You expose none of them – only 443. Behind it the Proxy Manager distributes by path and handles HTTPS termination with one certificate for all services.

         Internet / LAN
               |
             :443  (the only open port)
               |
     NGINX Proxy Manager
        |        |        |
   /cmk/     /grafana   /other
     |          |          |
  :5000      :3000      :8080     (localhost only)

10.4 If you do want the web interface

  1. open http://<server>:81 – or whichever port is set in NPM_ADMIN_PORT (see chapter 12); if that port is blocked on the network, use the terminal browser from chapter 11
  2. log in with NPM_ADMIN_EMAIL / NPM_ADMIN_PASSWORD

10.5 Creating a proxy host in the browser

Hosts → Proxy Hosts → Add Proxy Host

Field Value
Domain Names checkmk.example.com (or your internal name)
Scheme http
Forward Hostname / IP 127.0.0.1
Forward Port 5000
Block Common Exploits on
Websockets Support on (for the live data in the interface)

Tab Advanced (recommended, so Checkmk sees the real client IP and large agent uploads work):

client_max_body_size 0;
proxy_read_timeout 300s;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

10.6 Option A – Let's Encrypt (publicly reachable server)

Prerequisites: the DNS name resolves publicly to this server and port 80 is reachable from the internet (HTTP-01 challenge).

In the proxy host, tab SSL:

  • SSL Certificate → Request a new SSL Certificate
  • Force SSL on
  • HTTP/2 Support on
  • HSTS Enabled as required
  • enter an e-mail address, accept the terms → Save

Renewal is handled by the Proxy Manager automatically; the certificates live in the bind mount data/npm/letsencrypt/ and are therefore kept inside the project directory.

For an internal DNS zone without public access, use the DNS challenge (SSL → Use a DNS Challenge → select provider, store the API token).

10.7 Option B – self-signed certificate (internal environment)

Creating one:

sudo ./scripts/cmk-manage.sh cert \
     --domain checkmk.intern.example \
     --alt 10.0.0.5 --alt checkmk \
     --days 3650

Result in data/npm/custom-certs/checkmk.intern.example/:

  • privkey.pem – private key
  • fullchain.pem – certificate (with SANs for every name/IP given)

Installing it:

  1. SSL Certificates → Add SSL Certificate → Custom
  2. give it a name, upload privkey.pem as Certificate Key and fullchain.pem as Certificate
  3. select this certificate in the proxy host under SSL, enable Force SSL and HTTP/2

For Checkmk agents and browsers to trust the certificate, fullchain.pem must be installed as a trusted certificate on the clients (Windows: computer certificates → Trusted Root Certification Authorities; Linux: /usr/local/share/ca-certificates/ + update-ca-certificates). For plain agent communication over port 8000 this is not needed – there Checkmk uses its own site-internal CA.

The ncurses interface offers the same workflow under "Selbstsigniertes Zertifikat erzeugen".

10.8 HTTPS on the bare IP

Opening the server as https://172.0.250.82/cmk/ ends in ERR_SSL_UNRECOGNIZED_NAME_ALERT in the browser – while http:// on the very same address works. This is not a certificate problem.

The reason is in TLS: the browser tells the server which name it wants to talk to (SNI) while the connection is set up. RFC 6066 does not allow IP addresses there, so for an IP the browser sends nothing at all. nginx then cannot select the matching server block by name and falls back to its default server. With NPM that is this block from /etc/nginx/conf.d/default.conf:

server {
	listen 443 ssl;
	server_name localhost;
	ssl_reject_handshake on;      # <- exactly what the browser reports
	return 444;
}

Over HTTP it does not show, because there nginx picks the server block by the Host header – and that carries the IP.

The remedy is a default server of your own that accepts those nameless calls:

sudo ./scripts/cmk-manage.sh proxy default --domain checkmk.example.local --restart

The command reads the named proxy host through the API and mirrors it – target, certificate and all additional paths – into a block carrying default_server, written to the custom configuration. Calls by name are untouched: nginx still selects by SNI first, and the default server only applies when no name matches. None of the blocks NPM generates sets default_server itself, so there is no conflict over it.

Command Effect
proxy default shows whether it is set up, and which proxy host it mirrors
proxy default --domain X mirror proxy host X
proxy default --off remove the mirror
--restart restart the Proxy Manager right away

In the GUI this lives under Proxy-Hosts → HTTPS über die blanke IP (ohne Namen) ermöglichen.

Two things to keep in mind:

  • The certificate must carry the IP as a subject alternative name, otherwise the browser reports a name mismatch (clickable, but ugly). cmk-manage.sh cert and proxy add --cert self add the host's IP by themselves; with a Let's Encrypt certificate this is impossible by design – there are no certificates for IP addresses. The command checks this and says so.
  • The custom configuration lives in the Proxy Manager's data directory, which belongs to the container. So run it with sudo.

The clean alternative is still a name: a DNS record or one line in the clients' hosts file is enough for the browser to send SNI, and the proxy host applies as usual.

10.9 Changing a proxy host

proxy add --update rewrites the entry completely – using it to change just a port silently loses the paths and the advanced configuration. That is what proxy edit is for: you name only what should change, everything else is left alone.

./scripts/cmk-manage.sh proxy edit --id 3 --forward-port 5001
./scripts/cmk-manage.sh proxy edit --domain checkmk.example.local --cert self
./scripts/cmk-manage.sh proxy edit --id 3 --location /grafana=127.0.0.1:3000
./scripts/cmk-manage.sh proxy edit --id 3 --del-location /grafana
./scripts/cmk-manage.sh proxy edit --id 3          # just look

The entry is selected with --id or --domain.

Option Changes
--rename NAME the domain name
--forward-host HOST, --forward-port PORT the forwarding target
--scheme http|https how the proxy talks to the target
--cert self|letsencrypt|none|ID the certificate
--advanced 'TEXT' the advanced nginx configuration (\n separates lines)
--force-ssl, --http2, --websockets, --block-exploits each on/off
--enable, --disable switch the entry on or off
--location /path=host:port add or replace a path
--del-location /path remove a path

Two things to know:

  • The API only knows paths as a whole. --location and --del-location therefore read the current list, change the one entry and write the list back. If a replaced path had an advanced configuration of its own, you are told beforehand.
  • The Proxy Manager quietly corrects some things: without a certificate it turns http2_support and ssl_forced off by itself. proxy edit compares what was stored afterwards and reports a difference – otherwise you would look for the fault in the wrong place later.

The GUI has the same under Proxy-Hosts → Proxy-Host bearbeiten; there the fields come prefilled with the current values and the switches are offered as a checklist.

If the default server for IP access is set up (chapter 10.8), it mirrors an earlier state. Rewrite it after content changes – proxy edit reminds you.


11. Configuring the Proxy Manager locally (terminal browser)

When port 81 cannot be reached from the network, the NGINX Proxy Manager's admin interface has to be operated on the machine itself. For that, cmk-manage.sh browser starts another container on the host network – which reaches 127.0.0.1:81 just like the Proxy Manager itself.

sudo ./scripts/cmk-manage.sh browser

This is deliberately not a service in docker-compose.yml: a browser is an interactive program with a terminal, not a background service. It is started with run --rm -it and disappears without a trace when you quit.

11.1 Why not lynx

The Proxy Manager's admin interface is a JavaScript application. Classic text browsers cannot render it – you get a blank page. There are therefore two engines that render a full browser inside the terminal:

Engine Based on JavaScript Proxy Manager usable
browsh Firefox yes yes – default, maintained
carbonyl Chromium yes yes – nicer rendering, last released 2023
links (links2) text browser no no
w3m text browser no no
lynx text browser no no
shell Bash + curl/jq/openssl – for diagnostics and the API

The text browsers are included anyway because they are handy for quick checks. If you pick one, the script warns explicitly.

11.2 Usage

# default: browsh on http://127.0.0.1:81
sudo ./scripts/cmk-manage.sh browser

# a different engine or a different page
sudo ./scripts/cmk-manage.sh browser --engine carbonyl
sudo ./scripts/cmk-manage.sh browser --engine links --url http://127.0.0.1:5000/cmk/

# shell with network tools on the host network
sudo ./scripts/cmk-manage.sh browser --engine shell

Key bindings in browsh:

Key Action
Ctrl+L address bar
Ctrl+T new tab
Ctrl+W close tab
Ctrl+Q quit

The first time a text-browser or shell engine is used, the script builds a small tools image (Debian slim with links2, w3m, lynx, curl, jq, openssl, roughly 170 MB). Build it in advance – this works without a terminal, e.g. from an installation script:

sudo ./scripts/cmk-manage.sh browser --build-only

browsh and carbonyl, by contrast, are pulled as ready-made images; their names are in .env (BROWSH_IMAGE, CARBONYL_IMAGE) and are fully qualified so that Podman does not look for them at registry.access.redhat.com.

In the ncurses interface everything lives under "Terminal-Browser (Proxy Manager lokal bedienen)".

11.3 Blank screen: the start page does not answer

If everything stays black after Starting Browsh … / Waiting for Firefox to connect…, it is almost always because nothing answers on the start page – not because of the browser. A terminal browser shows no error page in that case, just nothing. You can tell that Browsh itself is running because Ctrl+Q still returns you to the menu.

The script therefore checks the start page beforehand and names the cause:

[ WARN ] Auf http://127.0.0.1:81 antwortet nichts.
Ein Terminal-Browser zeigt dann nur eine schwarze Seite.

Das ist die Oberflaeche des NGINX Proxy Managers.
Er ist abgeschaltet (NPM_ENABLED=off):
    ./scripts/cmk-manage.sh npm-enable on

Trotzdem starten? [j/N]

("nothing answers on …", "a terminal browser then shows only a black page", "it is switched off", "start anyway?") The cases it distinguishes:

Situation Message
Proxy Manager switched off points to npm-enable on
container missing or stopped points to up
container runs but does not answer points to logs and npm-port
Checkmk interface (port 5000) dead points to up or sites
external address points to the host's network access

--no-check skips the check. In the interface the same diagnosis appears as a dialog offering to start anyway or to pick a different start page from the menu.

11.4 Limitation: file upload

A terminal browser cannot open the operating system's file chooser. For Let's Encrypt that does not matter (only form fields are filled in), but uploading a self-signed certificate (SSL Certificates → Custom) does not work reliably this way.

For exactly this case, use the Proxy Manager's REST interface. Create the certificate as described in chapter 10.7, then:

NPM=http://127.0.0.1:81
CERTDIR=./data/npm/custom-certs/checkmk.intern.example

# 1) log in and obtain a token
TOKEN=$(curl -s -X POST "$NPM/api/tokens" \
  -H 'Content-Type: application/json' \
  -d '{"identity":"admin@example.com","secret":"<password>"}' | jq -r .token)

# 2) create the certificate entry (provider "other" = your own certificate)
CERT_ID=$(curl -s -X POST "$NPM/api/nginx/certificates" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"provider":"other","nice_name":"checkmk-selfsigned","domain_names":["checkmk.intern.example"]}' \
  | jq -r .id)

# 3) upload key and certificate
curl -s -X POST "$NPM/api/nginx/certificates/$CERT_ID/upload" \
  -H "Authorization: Bearer $TOKEN" \
  -F "certificate=@$CERTDIR/fullchain.pem" \
  -F "certificate_key=@$CERTDIR/privkey.pem"

# 4) create the proxy host for Checkmk
curl -s -X POST "$NPM/api/nginx/proxy-hosts" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"domain_names\":[\"checkmk.intern.example\"],
       \"forward_scheme\":\"http\",
       \"forward_host\":\"127.0.0.1\",
       \"forward_port\":5000,
       \"certificate_id\":$CERT_ID,
       \"ssl_forced\":true,
       \"http2_support\":true,
       \"block_exploits\":true,
       \"allow_websocket_upgrade\":true,
       \"advanced_config\":\"client_max_body_size 0;\",
       \"enabled\":true,
       \"meta\":{}}"

curl and jq are part of the tools image in case the host lacks them:

sudo ./scripts/cmk-manage.sh browser --engine shell

On the very first login the Proxy Manager forces you to change the e-mail address and password. That is easiest in the browser (cmk-manage.sh browser); afterwards the API works with the new credentials.

11.5 Alternative: SSH tunnel

If SSH access to the machine is possible, a tunnel is the most convenient route – you then use the interface in your usual browser:

ssh -N -L 8081:127.0.0.1:81 user@checkmk-server
# then locally:  http://127.0.0.1:8081

This changes nothing about the firewall: port 81 stays closed from outside, access runs through the existing SSH connection.


12. Moving the admin interface to a different port

Inside the image, the NGINX Proxy Manager's web interface listens on a hard-coded port 81 (listen 81 default; in /etc/nginx/conf.d/production.conf). No environment variable for this exists in the published 2.x versions – NPM_ADMIN_PORT is already prepared in the development branch, but is not part of version 2.15.1. And because both containers run on the host network, the port cannot be moved by port mapping either.

So if port 81 is blocked on your network:

sudo ./scripts/cmk-manage.sh npm-port 10443

12.1 What happens

Within its http{} block, nginx includes an official extension point:

include /data/nginx/custom/http.conf;

/data is our bind mount ./data/npm/data. The command places an additional server there which listens on the desired port and forwards to the interface on 127.0.0.1:81:

./data/npm/data/nginx/custom/http.conf

# >>> cmk-manage.sh: Admin-Port - nicht von Hand aendern >>>
server {
    listen 10443;
    listen [::]:10443;
    server_name _;
    client_max_body_size 0;
    location / {
        proxy_pass http://127.0.0.1:81;
        ...
    }
}
# <<< cmk-manage.sh: Admin-Port <<<

Only the region between the two markers is managed – your own additions to the same file are left untouched. The script then restarts the Proxy Manager container so nginx reads the file, and writes NPM_ADMIN_PORT and NPM_ADMIN_URL into .env. The terminal browser from chapter 11 then starts on the new port automatically.

12.2 Important: port 81 stays alive locally

The built-in server on port 81 cannot be switched off this way – it keeps listening on all addresses. That is harmless as long as the firewall blocks it, and that is exactly the recommendation:

# firewalld
sudo firewall-cmd --permanent --add-port=10443/tcp
sudo firewall-cmd --permanent --remove-port=81/tcp 2>/dev/null
sudo firewall-cmd --reload

# ufw
sudo ufw allow 10443/tcp
sudo ufw delete allow 81/tcp 2>/dev/null

12.3 Inspecting and reverting

./scripts/cmk-manage.sh npm-port          # show the current state
sudo ./scripts/cmk-manage.sh npm-port 81  # back to the default

With 81 the managed block is removed again; if the file is empty afterwards it is deleted entirely. Only the configuration from the image then applies.

The command rejects ports already taken by the stack (80, 443, 5000, 8000, 6557), validates the numeric range, and warns if another service is already listening on the port you want.

As soon as an NPM release ships NPM_ADMIN_PORT, this detour becomes unnecessary – the environment variable will be enough. Until then, the extension point is the route that needs no change to the image.

12.4 Turning the Proxy Manager off entirely (external NGINX)

If HTTPS termination is already handled elsewhere – for instance by an NGINX on an upstream system – the container is not needed at all. There is a switch for that:

sudo ./scripts/cmk-manage.sh npm-enable off    # turn off
sudo ./scripts/cmk-manage.sh npm-enable on     # turn back on
./scripts/cmk-manage.sh npm-enable             # show the current state

In the interface it is under Einstellungen → NGINX Proxy Manager → "Proxy Manager betreiben"; in .env it is called NPM_ENABLED.

What "off" means: the container is stopped and removed, and is not created by up in the first place – compose is only told about the Checkmk service. So no image is pulled and no container is built:

NPM_ENABLED=off  ->  docker compose up -d checkmk
NPM_ENABLED=on   ->  docker compose up -d checkmk nginx-proxy-manager

The data stays. Configuration, database and certificates live in the bind mounts ./data/npm/data and ./data/npm/letsencrypt, not inside the container. Before turning it off, the command shows both with their sizes; when you turn it back on, the existing configuration and certificates are picked up unchanged.

What to do afterwards: the external proxy forwards to port 5000 of this host. That port was meant for localhost only so far and now has to be reachable for the proxy – as narrowly as possible:

# firewalld
sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=<proxy-ip> port port=5000 protocol=tcp accept'
sudo firewall-cmd --reload

# ufw
sudo ufw allow from <proxy-ip> to any port 5000 proto tcp

In the external NGINX:

location / {
    proxy_pass http://<checkmk-host>:5000;
    proxy_set_header Host               $host;
    proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto  $scheme;
    client_max_body_size 0;
    proxy_read_timeout 300s;
}

npm-enable off prints the matching commands and this example configuration for you.

The agent receiver is unaffected. Agents talk to port 8000 of this host directly, not through the proxy – so it must stay reachable for the monitored systems.

With Quadlet (chapter 6, option B) install.sh --quadlet removes the nginx-proxy-manager.container unit while the switch is off. If you turn it on later, simply run sudo ./install.sh --quadlet again.


13. Firewall

RHEL and Rocky ship firewalld enabled, and it normally allows SSH only. A stack that runs perfectly is still unreachable from outside – agents cannot get through and the browser shows nothing. The manager therefore checks this for you:

./scripts/cmk-manage.sh firewall                 # report
./scripts/cmk-manage.sh firewall open            # only print the commands
sudo ./scripts/cmk-manage.sh firewall open --apply
sudo ./scripts/cmk-manage.sh firewall open --apply --source 10.0.0.0/24

firewalld, ufw, nftables and iptables are detected in that order; the first active one wins. For firewalld all active zones are evaluated, not just the default zone: if two interfaces sit in different zones, a port counts as open as soon as one of them allows it. Besides individual ports, service names (http, https, ssh) and rich rules are taken into account.

Which ports are needed depends on the configuration. Ports are handled as number/protocol throughout – syslog and SNMP traps use UDP, and a TCP rule does nothing for them.

Always:

Port Purpose Needed
80/tcp HTTP, Let's Encrypt challenge only with NPM_ENABLED=on
443/tcp HTTPS for Checkmk and other services only with NPM_ENABLED=on
NPM_ADMIN_PORT/tcp (81) Proxy Manager admin UI administration network only
5000/tcp Checkmk directly only with NPM_ENABLED=off, keep closed otherwise
8000/tcp agent receiver (registered agents) always

Distributed monitoring – driven by FW_ROLE:

Port Purpose Needed
6557/tcp Livestatus – the central site queries this one with FW_ROLE=remote, otherwise only with CMK_LIVESTATUS_TCP=on
443/tcp configuration sync and automation API of the central site with FW_ROLE=remote

The central site (FW_ROLE=central) opens the connections itself and needs nothing extra inbound – but outbound 6557/tcp and 443/tcp to the remote sites must be allowed. The report points this out, as well as the most common trap: FW_ROLE=remote together with CMK_LIVESTATUS_TCP=off – the port is open but nothing listens on it.

Event Console and agent – each has its own switch, because not every setup needs them:

Port Purpose Switch
514/udp, 514/tcp accept syslog messages FW_EC_SYSLOG=off|udp|tcp|beide
162/udp accept SNMP traps FW_EC_SNMPTRAP=on
6558/tcp Event Console status and events over TCP FW_EC_TCP=on
6556/tcp Checkmk agent of this host (pull mode) FW_AGENT=on

Anything else – Grafana, NSCA, whatever else lives on the machine – goes into FW_EXTRA_PORTS:

./scripts/cmk-manage.sh config set FW_EXTRA_PORTS "3000/tcp=Grafana,5667/tcp=NSCA"

The report flags the reverse case too: if 5000 is open while the Proxy Manager is running, it reads "open – should be closed". When something is missing, firewall exits with status 1, so you can evaluate it in your own scripts.

Port ranges in existing rules are resolved: if you allowed 6556-6558/tcp (firewalld, nftables) or 6556:6558/tcp (ufw, iptables), all three ports are reported as open. With ufw a rule without a protocol – ufw allow 514 – counts for TCP and UDP, which is exactly how ufw creates it.

--source restricts every generated rule to a source network. That makes sense for 8000, 6557 and 514 (only the monitored hosts or the central site), but not for port 80: Let's Encrypt probes the domain from the internet, and a restriction makes issuance fail. The report points this out.

Closing them again

firewall open only knows one direction. Turn the Event Console off again or drop an entry from FW_EXTRA_PORTS, and the rule stays – the port is open with nothing listening behind it. Hence the opposite direction:

./scripts/cmk-manage.sh firewall close            # only print the commands
sudo ./scripts/cmk-manage.sh firewall close --apply

Only what is open, belongs to this setup and is not currently needed is offered. That covers the ports from the table above plus everything firewall open --apply opened itself – recorded in firewall-opened.conf, so a deleted FW_EXTRA_PORTS entry is not forgotten.

Three safeguards are built in:

SSH stays open port 22, the port sshd actually listens on (from sshd -T), and the port of the running session (SSH_CONNECTION)
Foreign rules stay if you have 3306 open for your database, you keep it – only what this setup knows about is closed
Collective rules stay a rule like 80,443/tcp covers several ports; it is reported instead of deleted

With ufw the rules are deliberately not deleted by number: those shift on the first deletion, and the next call would hit the wrong rule. Deletion goes by rule specification instead (ufw delete allow 514/udp), with nftables by handle, and with iptables using the same specification it was added with.

The GUI has this under Einstellungen → Firewall → Nicht mehr benötigte Ports schließen.

Docker and ufw: published container ports normally bypass ufw through the DOCKER-USER chain. Not here – both containers run in the host network, so the host rules apply unchanged.

The GUI offers the same report under Firewall prüfen, including a prompt to set the missing rules right away. The switches live under Einstellungen → Firewall: was offen sein muss, which also holds two actions:

Menu entry what it does
>> Firewall jetzt pruefen the same report as firewall check
>> Firewall-Regeln uebernehmen (Ports oeffnen) asks for a source network first, then shows the commands that will run verbatim, and applies them after confirmation

The order is deliberate: the source network changes the commands, so it is asked before they are shown – what you see in the window is what runs. Without root the call is repeated through sudo. Afterwards the GUI shows the state that resulted, so you do not have to guess whether it worked.

If an older .env does not have the FW_* keys yet, the menu shows <leer> – the defaults apply then (single, everything off). As soon as a value is set, the key is written to .env.

By hand

firewalld (RHEL/Rocky/Alma):

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-port=8000/tcp          # agent receiver
sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/24 port port=81 protocol=tcp accept'

# distributed monitoring: Livestatus for the central site only
sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.5/32 port port=6557 protocol=tcp accept'

# Event Console: syslog and SNMP traps (UDP!)
sudo firewall-cmd --permanent --add-service=syslog          # 514/udp
sudo firewall-cmd --permanent --add-service=snmptrap        # 162/udp
sudo firewall-cmd --permanent --add-port=6558/tcp           # status over TCP

sudo firewall-cmd --reload

# With a different admin port (e.g. 10443) open that number instead -
# "cmk-manage.sh npm-port <port>" prints the matching command for you.

ufw (Debian/Ubuntu):

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8000/tcp
sudo ufw allow from 10.0.0.0/24 to any port 81 proto tcp
sudo ufw allow from 10.0.0.5 to any port 6557 proto tcp     # Livestatus for the central site
sudo ufw allow 514/udp                                      # Event Console: syslog
sudo ufw allow 162/udp                                      # Event Console: SNMP traps
sudo ufw enable

Port 5000 stays closed – access goes exclusively through the proxy. Open port 81 to the administration network only.


14. Management: cmk-manage.sh

The script is both a command-line tool and an ncurses interface.

14.1 ncurses interface (Midnight Commander style)

sudo ./scripts/cmk-manage.sh gui

The menu labels are German; the command table in 14.2 lists the same actions in English.

 ┌──────────────── Checkmk Stack Manager ─────────────────┐
 │ Projekt : /opt/checkmk-stack                           │
 │ Runtime : podman     Standard-Site: cmk                │
 │ Backups : /opt/checkmk-stack/backups                   │
 │                                                        │
 │   status    Status von Containern und Sites            │
 │   stack     Stack steuern (Start/Stop/Logs/Update)     │
 │   backup    Backup erstellen / planen                  │
 │   restore   Backup wiederherstellen                    │
 │   import    Site von Direktinstallation importieren    │
 │   manage    Backups verwalten (Liste/Details/Löschen)  │
 │   sites     Sites im Container verwalten               │
 │   image     Checkmk-Image beschaffen/auswählen         │
 │   proxy     Proxy-Hosts anlegen/verwalten (HTTPS)      │
 │   browser   Terminal-Browser (Proxy Manager lokal)     │
 │   cert      Selbstsigniertes Zertifikat erzeugen       │
 │   firewall  Firewall prüfen (benötigte Ports)          │
 │   config    Einstellungen (.env) - menügeführt         │
 │   shell     Root-Shell im Checkmk-Container            │
 │   quit      Beenden                                    │
 │                  <  OK  >   <Cancel>                   │
 └────────────────────────────────────────────────────────┘

Operation: arrow keys, space toggles entries in selection lists (e.g. several sites for a backup), Enter confirms, ESC/Cancel goes back. The window size adapts to the terminal size, the length of the text and the number of entries – in a larger terminal more entries are visible at once. This holds for every window, migrate_creator.sh included: all dialogs go through three shared entry points in scripts/lib/common.sh which compute the size themselves.

14.4 Borders show up as q, x, l, m (PuTTY)

If the interface looks like this –

lqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqk
x  Projekt : /opt/checkmk-stack         x

– then the server lacks a UTF-8 locale. Without one, dialog and whiptail draw the borders through the old VT100 switch ESC(0. Terminals in UTF-8 mode – PuTTY by default – ignore that switch and show the raw letters instead.

Measured: with LC_ALL=C, whiptail emits one ESC(0 switch and 91 raw q characters; with a UTF-8 locale, none at all.

The script fixes this in two ways.

1. UTF-8 locale. When starting the interface it sets LC_CTYPE to the first available UTF-8 locale (C.UTF-8, en_US.UTF-8, …) – only LC_CTYPE, so sorting and number formats stay untouched. If LC_ALL is set to a non-UTF-8 value it is removed, since it would otherwise override LC_CTYPE. 2. Unicode instead of VT100. That alone is not enough: ncurses uses the VT100 switch even with a UTF-8 locale as long as the terminfo offers it – which it does for TERM=xterm. So NCURSES_NO_UTF8_ACS=1 is set as well, making ncurses draw real Unicode box-drawing characters. Measured with dialog under LANG=C.UTF-8 and TERM=xterm:

ESC(0 UTF-8 box bytes raw q
without the switch 38 0 182
with the switch 0 220 0

whiptail does not need this – with a UTF-8 locale it draws Unicode by itself. For the opposite case – a terminal that handles ESC(0 but not UTF-8 – CMK_TUI_ACS=1 restores the old behaviour.

Verify with:

./scripts/cmk-manage.sh selftest      # section "Zeichensatz fuer die Oberflaeche"

If the check reports that no UTF-8 locale exists, the language pack is missing:

sudo dnf install -y glibc-langpack-en     # RHEL/Rocky/Alma
sudo apt-get install -y locales           # Debian/Ubuntu

PuTTY settings

So that the other end matches:

PuTTY section Setting
Window → Translation Remote character set: UTF-8
Window → Translation Handling of line drawing characters: Use Unicode line drawing code points
Connection → Data Terminal-type string: xterm-256color (the default is just xterm)
Window → Appearance a font with box-drawing glyphs, e.g. Consolas or DejaVu Sans Mono

Save the session under Session → Saved Sessions to keep these settings.

If it still looks wrong: look instead of guessing

./scripts/cmk-manage.sh tui-check

This prints two test lines:

  • Line A – real UTF-8 box-drawing characters (┌──┐)
  • Line B – the old VT100 switch ESC(0 with the letters lqk

The cause can be read off them:

Observation Meaning
A lines, B lines everything is fine
A lines, B shows lqqqk the terminal ignores the VT100 switch – the PuTTY case, see the settings above
A shows gibberish the terminal is not in UTF-8 mode
both show letters the font has no box-drawing glyphs

The command names the matching remedy for each observation.

Installing dialog instead of whiptail is the most effective move on this side – it handles the character set more reliably and is the only one that can draw plain ASCII borders:

sudo dnf install -y dialog        # RHEL/Rocky/Alma
sudo apt-get install -y dialog    # Debian/Ubuntu

The script prefers dialog as soon as it is present. After that, plain borders can be forced if needed:

CMK_TUI_ASCII=1 ./scripts/cmk-manage.sh gui

Without dialog the script says so explicitly – whiptail has no such option.

14.5 Self-check

./scripts/cmk-manage.sh selftest

Checks the scripts for the pitfalls that turned out to be recurring here:

Check Why
syntax of all four scripts typos surface immediately
dialogs go through the shared entry points otherwise a new window would get a fixed size again
no hard-coded window sizes same reason
no [ … ] && VAR=… returns 1 when the condition is false and aborts the script under set -e – the cause of a silent abort three times
settings table complete every key has six fields and appears in .env.example

Worth running after adding your own menu entries. dialog is preferred; if only whiptail is present, that is used instead. A file chooser with directory navigation is built in.

14.2 Command line

./scripts/cmk-manage.sh --help
Command Effect
up [--no-fetch] / down / restart / pull control the stack; up fetches a missing image itself
status containers, sites, access addresses
logs [-f] container logs
sites list the sites in the container
site-start SITE / site-stop SITE control a single site
start-all-sites start all sites (invoked by systemd)
backup … backup as a .tar.gz bundle
list / verify / prune --keep N manage backups
job list|add|set|del|run|auto scheduled backups (own target path per job)
restore … restore a backup
import … import a migration bundle
image get fetch the image via the configured route
image status|list|versions|repos|why|test|pull|download|load|use obtain and select the Checkmk image, and explain failures
browser [--engine …] terminal browser in a container – operate the Proxy Manager locally
config list|get|set|edit show and change settings in .env
npm-port [PORT] set/show the port of the Proxy Manager interface
npm-enable [on|off] turn the Proxy Manager on/off (data is preserved)
npm-reset move the database aside so the account is recreated
proxy status|list|certs|add|del create and manage proxy hosts through the API
cert … create a self-signed certificate
firewall [check|open|close] check, open, and close again what is no longer needed (firewalld, ufw, nftables, iptables)
shell root shell in the Checkmk container
omd … any omd command inside the container

Global options: --env-file FILE, -y/--yes (no prompts, e.g. for cron jobs), -v/--verbose.

14.3 Changing settings – without an editor

Every value in .env can be maintained through the menu. In the interface: Einstellungen (.env) – menügeführt. The values are grouped by area:

 ┌──────────────────── Einstellungen ─────────────────────┐
 │ Alle Werte der Datei .env - direkt hier änderbar.      │
 │ Datei: /opt/checkmk-stack/.env                         │
 │                                                        │
 │   runtime   Runtime und Zeitzone                       │
 │   image     Checkmk-Image und Zugangsdaten             │
 │   checkmk   Checkmk-Site und Container                 │
 │   npm       NGINX Proxy Manager                        │
 │   browser   Terminal-Browser                           │
 │   paths     Verzeichnisse                              │
 │   cert      Selbstsignierte Zertifikate                │
 │   show      Alle Werte auflisten                       │
 │   editor    Datei .env im Editor öffnen                │
 │   back      zurück                                     │
 └────────────────────────────────────────────────────────┘

(runtime and time zone / Checkmk image and credentials / Checkmk site and containers / NGINX Proxy Manager / terminal browser / directories / self-signed certificates / list all values / open .env in an editor / back.)

Each entry shows its current value and, when confirmed, opens the matching input: free text, number, selection list or password field (passwords appear as ******** in the overview and stay unchanged if you leave the field empty). The editor remains available as a menu entry.

The same values from the command line:

./scripts/cmk-manage.sh config list                  # everything with current values
./scripts/cmk-manage.sh config list --category npm   # one area only
./scripts/cmk-manage.sh config get CMK_SITE_ID
./scripts/cmk-manage.sh config set CMK_SITE_ID prod
./scripts/cmk-manage.sh config set NPM_ADMIN_PORT 10443

Validation happens when setting the value, not at the next start:

Key Check
NPM_ADMIN_PORT number 1–65535, not 80/443/5000/8000/6557, warning if in use
CMK_SITE_ID letters, digits and _ only; at most 16 characters
CMK_IMAGE, NPM_IMAGE, … must be fully qualified (the Podman pitfall)
CMK_RUNTIME must be installed on this system
CERT_DAYS and others numeric fields
URL fields must start with http:// or https://

Values containing spaces are quoted automatically so that .env remains readable by compose, systemd and the shell. When you change NPM_ADMIN_PORT, the script updates the additional nginx file along with it (see chapter 12). For values that only take effect once the containers are recreated, the menu says so explicitly.


15. Backup

Backups are created with omd backup and packed into a bundle:

cmkbackup_<site>_<version>_<timestamp>.tar.gz
└── cmkbundle/
    ├── MANIFEST              site, version, source, checksum …
    ├── INFO.txt              human-readable summary
    ├── site.tar.gz           the actual omd backup
    └── site.tar.gz.sha256

This way the restore/import tool always knows which site and which Checkmk version the archive contains, and can verify its integrity.

# a single site
sudo ./scripts/cmk-manage.sh backup --site cmk

# all sites, without log files, with a label
sudo ./scripts/cmk-manage.sh backup --all --no-logs --label vor-update

# most consistent backup: the site is stopped briefly
sudo ./scripts/cmk-manage.sh backup --site cmk --stop

# without performance data (considerably smaller)
sudo ./scripts/cmk-manage.sh backup --site cmk --no-rrds --dir /mnt/nas/checkmk

Listing and verifying:

./scripts/cmk-manage.sh list
./scripts/cmk-manage.sh verify --file backups/cmkbackup_cmk_2.3.0p23.cre_20260820-030000.tar.gz
./scripts/cmk-manage.sh prune --keep 7

15.1 Scheduled backups (backup jobs)

Manual backups always go to BACKUP_DIR from .env. Scheduled jobs, by contrast, bring their own target directory – so you can back up to a NAS share daily and somewhere else weekly.

In the menu: backup → "Backup erstellen / planen" (create / schedule)

 ┌──────────── Backup erstellen / planen ─────────────────┐
 │ Manuelle Backups gehen immer nach:                     │
 │   /opt/checkmk-stack/backups   (Einstellung BACKUP_DIR)│
 │                                                        │
 │ Geplante Jobs haben ihr eigenes Zielverzeichnis.       │
 │ Automatische Ausfuehrung: aus                          │
 │                                                        │
 │   manuell   Backup jetzt erstellen                     │
 │   jobs      Backup-Jobs verwalten                      │
 │   auto      Automatische Ausfuehrung ein/aus           │
 │   status    Zeitplan und Jobs anzeigen                 │
 │   jetzt     Einen Job sofort ausfuehren                │
 │   back      zurueck                                    │
 └────────────────────────────────────────────────────────┘

(create now / manage jobs / automatic execution on-off / show schedule / run a job now / back.) The same jobs are also reachable from Einstellungen → "Backup-Jobs".

Creating a job

In the menu a dialog walks through sites, schedule (daily / weekly / monthly / free cron expression), target directory, retention and options. The same with parameters:

# daily at 02:15, all sites, without logs, keep 14 backups
sudo ./scripts/cmk-manage.sh job add --name nachts \
     --cron "15 2 * * *" --no-logs --keep 14

# Sundays at 03:30, two sites only, to the NAS, without RRDs, keep 8
sudo ./scripts/cmk-manage.sh job add --name nas-woche \
     --cron "30 3 * * 0" --sites prod,test \
     --dir /mnt/nas/checkmk --no-rrds --no-logs --keep 8

All commands:

Command Effect
job list show the configured jobs
job status schedule, cron file and switch
job add --name N --cron "…" create a job
job set NAME … modify a job (fields not given stay as they are)
job del NAME delete a job (the backups remain)
job enable|disable NAME turn a job on/off
job run NAME run a job right now
job run-all run every active job
job auto on|off turn automatic execution on/off

Options for job add / job set:

Option Meaning
--sites ALL|a,b sites to back up (default ALL)
--cron "15 2 * * *" schedule, five cron fields
--dir DIR this job's target directory (default BACKUP_DIR)
--keep N how many backups stay in the target (0 = all)
--no-rrds, --no-logs, --stop as for backup
--disabled create it but do not schedule it yet

Turning automation on and off

sudo ./scripts/cmk-manage.sh job auto on     # schedule
sudo ./scripts/cmk-manage.sh job auto off    # remove the schedule

on writes /etc/cron.d/checkmk-backup from all active jobs – one line per job with its own schedule:

15 2 * * * root /opt/checkmk-stack/scripts/cmk-manage.sh -y job run nachts    >>/var/log/cmk-backup.log 2>&1
30 3 * * 0 root /opt/checkmk-stack/scripts/cmk-manage.sh -y job run nas-woche >>/var/log/cmk-backup.log 2>&1

This needs root. Without root the command prints the lines so you can add them yourself. off removes the file again; the jobs themselves are kept. The switch is stored as BACKUP_AUTO in .env.

After each run a job prunes its own target directory down to --keep – a separate prune entry is not needed.

Where the jobs live

In backup-jobs.conf in the project directory, one line per job:

name|sites|cron|directory|options|keep|enabled
nachts|ALL|15 2 * * *|/opt/checkmk-stack/backups|--no-logs|14|on
nas-woche|prod,test|30 3 * * 0|/mnt/nas/checkmk|--no-rrds --no-logs|8|on

The file is deliberately readable and diffable, but does not belong in the repository (it is in .gitignore because it contains system-specific paths).


16. Restore

# simplest case: the site no longer exists
sudo ./scripts/cmk-manage.sh restore --file backups/cmkbackup_cmk_2.3.0p23.cre_20260820-030000.tar.gz

# overwrite an existing site (terminating running processes)
sudo ./scripts/cmk-manage.sh restore --file backups/… --reuse --kill

# restore under a different name (e.g. for a test)
sudo ./scripts/cmk-manage.sh restore --file backups/… --as-site test

What the script does:

  1. checks the archive structure and the SHA256 checksum
  2. checks whether the Checkmk version of the backup exists in the container
  3. prompts / requires --reuse for an existing site, stops the site
  4. copies the archive into the container and runs omd restore
  5. turns off TMPFS for the site (not usable inside the container)
  6. starts the site and prints the next steps

--as-site changes the site name – registered agents would then point nowhere and would have to be re-registered. So do not use it for a migration.

A raw omd backup archive (without the bundle wrapper) is accepted as well; the site name is then read from the archive.

16.1 Why a restore failed after recreating the container

ERROR: apache user www-data must be member of group cmk

When a site is created, omd create adds the apache user to the site's group. That membership lives in /etc/group – inside the container, not in the bind mount. Recreate the container (every up with a changed image, a podman rm, an update) and /etc/group is back to the state from the image, while the site in the bind mount lives on unchanged.

The official entrypoint detects this case and recreates group and user – but does not add www-data to the group:

if ! getent group "$CMK_SITE_ID" >/dev/null; then
    groupadd -g 1000 "$CMK_SITE_ID"
fi
if ! getent passwd "$CMK_SITE_ID" >/dev/null; then
    useradd -u 1000 ... -G omd -s /bin/bash "$CMK_SITE_ID"
fi

In daily operation this goes unnoticed: the site runs, the GUI answers, the agent receiver accepts data. Only omd restore --reuse checks it (user_verify in omdlib/users_and_groups.py) and aborts.

Right behind it waits the second obstacle:

ERROR: fstab entry for /omd/sites/cmk/tmp does not exist

omd explicitly tolerates a missing /etc/fstab ("In case there is no fstab (seen in some containers) assume everything is OK") – but the image ships a file containing a comment line. So /etc/fstab exists, the exception does not apply, and the entry is found missing.

Both are state of the container, not of the data. cmk-manage.sh therefore restores them before every restore and import – and, for the same reason, after every up, so a hand-written omd call does not trip over it:

[ INFO ] Apache-Benutzer www-data -> cmk in die Gruppe aufgenommen
         (nach dem Neuerstellen des Containers verloren gegangen).
[ INFO ] fstab-Eintrag fuer /omd/sites/cmk/tmp ergaenzt - ohne ihn
         verweigert 'omd restore' den Dienst.

The apache user and the command to add it are read from /omd/versions/default/share/omd/distro.info – the same source omd itself uses. By hand it would be:

docker exec checkmk gpasswd -a www-data cmk
docker exec checkmk sh -c 'echo "tmpfs /omd/sites/cmk/tmp tmpfs noauto,user,mode=751,uid=cmk,gid=cmk 0 0" >> /etc/fstab'

17. Migrating a bare-metal installation (migrate_creator.sh)

Goal: take over an existing Checkmk installation on hardware or a VM into the container – without touching the agents.

17.1 Why the agents keep working

An omd backup contains the complete site including etc/ssl (the site's own CA and the certificates of the registered agents), etc/check_mk (rules, hosts, passwords) and the monitoring history. If the site is restored under the same name and the server is reachable at the same IP address, then

  • push/pull agents (cmk-agent-ctl) find their registration again,
  • the TLS connection to the agent receiver on port 8000 stays valid,
  • all URLs (https://<ip>/<site>/check_mk/…) remain correct,
  • performance data and history are preserved.

The prerequisite is therefore that the target system takes over the source server's IP (or that the DNS name is repointed) – which is the plan anyway.

17.2 Step 1 – copy the script to the source server

scp scripts/migrate_creator.sh root@old-server:/root/
ssh root@old-server 'chmod +x /root/migrate_creator.sh'

The script is self-contained – on the source server it only needs bash, tar and omd (plus dialog or whiptail for the interface).

17.3 Step 2 – create the bundle on the source server

Show the existing sites:

sudo ./migrate_creator.sh --list
SITE                 VERSION            STATUS       GROESSE
prod                 2.3.0p23.cre       laeuft       4.7 GiB
test                 2.3.0p23.cre       gestoppt     318.2 MiB

(GROESSE = size, laeuft = running, gestoppt = stopped.)

With the interface (select sites with the space bar):

sudo ./migrate_creator.sh --gui

The dialog asks in turn for: sites → options (stop the site, skip RRDs/logs) → target directory → label → optional transfer via scp.

With parameters:

# one site, stopping it during the backup (most consistent variant)
sudo ./migrate_creator.sh --site prod --stop --out /var/tmp

# all sites, without logs, transferred straight to the target system
sudo ./migrate_creator.sh --all --no-logs \
     --scp root@10.0.0.5:/opt/checkmk-stack/backups/

Result:

/var/tmp/cmkmigrate_prod_2.3.0p23.cre_20260820-101500.tar.gz

Besides the omd backup, the bundle contains a MANIFEST with the site name, Checkmk version, source hostname, source IP addresses, source operating system and SHA256 checksum, plus a siteinfo/ directory with omd config show, omd status and the IP configuration of the old system for later reference.

17.4 Step 3 – bring the target system to the right version

omd restore can only restore a site if exactly its Checkmk version is installed on the target system. In the container, the version comes from the image – so the image tag has to match:

tar -xzOf backups/cmkmigrate_prod_*.tar.gz cmkbundle/MANIFEST | grep CMK_VERSION
# CMK_VERSION=2.3.0p23.cre   ->  version 2.3.0p23, suffix .cre = Raw edition

# fetch the matching image and have it written to CMK_IMAGE
sudo ./scripts/cmk-manage.sh image pull --edition raw --version 2.3.0p23
sudo ./scripts/cmk-manage.sh up

For an Enterprise source (.cee) accordingly:

sudo ./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \
     --user <user> --password <password>
# or, if the registry is unreachable:
sudo ./scripts/cmk-manage.sh image download --edition enterprise --version 2.3.0p23 \
     --user <user> --password <password>

The import script checks this itself and aborts with a corresponding message if the version is missing.

Mind the edition: the version suffix reveals it – .cre = Raw, .cee = Enterprise, .cce = Cloud, .cme = Managed. The import script reads it from the manifest and, on failure, names the matching image pull command directly. Enterprise images are not on Docker Hub – see chapter 8.

17.5 Step 4 – import the bundle

With the interface:

sudo ./scripts/cmk-manage.sh gui      # -> "import"

With parameters:

sudo ./scripts/cmk-manage.sh import \
     --file backups/cmkmigrate_prod_2.3.0p23.cre_20260820-101500.tar.gz \
     --set-default-site

The script

  1. recognises the migration bundle and shows the source host and source IP,
  2. warns if this host's IP differs from the source IP (the agents would then have to be adjusted),
  3. verifies checksum, version and edition,
  4. prompts if the site already exists (--reuse overwrites),
  5. runs omd restore under the unchanged site name,
  6. optionally sets CMK_SITE_ID in .env (--set-default-site) so the container starts the site automatically after a reboot as well,
  7. starts the site and prints the next steps.

17.6 Step 5 – switch over and verify

# 1) shut down the old server or stop its site (no double monitoring!)
ssh root@old-server 'omd stop prod'

# 2) move the target system to the IP/DNS name of the old system
#    (take over the static IP or repoint the DNS record)

# 3) recreate the containers if CMK_SITE_ID was changed
sudo ./scripts/cmk-manage.sh up
sudo ./scripts/cmk-manage.sh status

# 4) check inside Checkmk
sudo ./scripts/cmk-manage.sh omd su prod
   OMD[prod]:~$ cmk -O                # activate the configuration
   OMD[prod]:~$ omd status
   OMD[prod]:~$ exit

Then check in the interface:

  • Monitor → All hosts – are the agents reporting again?
  • With TLS agents: the service "Check_MK Agent" must not report registration errors.
  • An agent can be tested from the monitored host with cmk-agent-ctl status – the site prod must appear there with the target server and "Connection: TLS".

17.7 Migration checklist

  • migrate_creator.sh copied to the source server
  • bundle created (preferably with --stop)
  • bundle copied to the target system (backups/)
  • matching image fetched (image pull/image download), CMK_IMAGE set, stack recreated
  • import --set-default-site executed, site running
  • old site stopped / old server shut down
  • IP or DNS switched to the target system
  • ports 80/443/8000 open in the firewall
  • proxy host set up in the NGINX Proxy Manager with a certificate
  • agents reporting, cmk -O executed
  • backup of the new system set up (cron)

18. Several sites in one container

At startup the Checkmk entrypoint only creates – and only starts – the site from CMK_SITE_ID. Further sites (e.g. several imported ones) exist in the bind mount but are not brought up automatically after a restart.

Hence:

sudo ./scripts/cmk-manage.sh start-all-sites

This command is executed automatically by the systemd unit (ExecStartPost) or the Quadlet unit after the container starts, and starts every site present.

Recommendation: for cleanly separated environments, run one project directory with its own .env per site. Note that because both containers run on the host network, only one Checkmk container per host can occupy ports 5000/8000 – for genuinely multiple instances use separate hosts or a bridge-network variant.


19. Upgrading Checkmk

Because /omd/sites is a bind mount, the data survives replacing the image. The Checkmk software itself lives in the image under /omd/versions.

# 1) ALWAYS back up first
sudo ./scripts/cmk-manage.sh backup --all --label vor-update

# 2) fetch the new image (writes CMK_IMAGE into .env automatically)
sudo ./scripts/cmk-manage.sh image pull --edition raw --version 2.3.0p28

# 3) recreate the containers
sudo ./scripts/cmk-manage.sh up

# 4) update the site (interactive, confirm conflicts)
sudo ./scripts/cmk-manage.sh omd stop cmk
sudo ./scripts/cmk-manage.sh omd update cmk
sudo ./scripts/cmk-manage.sh omd start cmk

For a major version jump (e.g. 2.2 → 2.3) be sure to read the Checkmk werks / release notes beforehand.


20. Troubleshooting

Symptom Cause / solution
ERR_SSL_UNRECOGNIZED_NAME_ALERT when opening https://<IP>/ browsers send no SNI for an IP, nginx falls back to its default server and rejects. cmk-manage.sh proxy default --domain <name> – see chapter 10.8
omd restore: apache user www-data must be member of group <site> the container was recreated while the site lives in the bind mount. cmk-manage.sh repairs this on restore and on up – see chapter 16.1
omd restore: fstab entry for /omd/sites/…/tmp does not exist same cause, same fix
Port 80/443 already in use a web server is already running on the host: ss -tlnp | grep ':80' → systemctl disable --now apache2 nginx httpd
Containers do not run after a reboot (Podman) systemd unit missing: ./install.sh --with-systemd or --quadlet, then systemctl is-enabled checkmk-stack
omd restore: version is not installed fetch the matching image: cmk-manage.sh image pull --edition <ed> --version <v>, then up
Import aborts with an edition warning an Enterprise backup needs an Enterprise image, see chapter 8
manifest unknown on pull The repository is there, the tag is not. cmk-manage.sh image why lists the tags that do exist in that branch
repository name unknown / Login Succeeded but the pull fails The repository path is wrong. cmk-manage.sh image repos --edition enterprise shows what the registry offers; otherwise use the tarball
image not known / Podman searches registry.access.redhat.com short image name. Set CMK_IMAGE fully qualified: cmk-manage.sh image use docker.io/… or registry.checkmk.com/enterprise/…
checkmk/check-mk-enterprise not found on Docker Hub correct – Enterprise is only on registry.checkmk.com or available as a tarball; from 2.5 the edition is called pro and is public on Docker Hub
Tarball installed with podman import, image "unknown" import produces an image without metadata. Remove the broken image with podman rmi <id> and use cmk-manage.sh image load --file … (which uses load)
Download of the image tarball fails (401/404) check the customer portal credentials, give the exact version (no -latest), if necessary image download --url '<full URL>'
Proxy returns 502 Bad Gateway the Checkmk container is not running, or the forwarding does not point to 127.0.0.1:5000: ./scripts/cmk-manage.sh status, curl -I http://127.0.0.1:5000/
Let's Encrypt fails port 80 not reachable from the internet, or wrong DNS; alternative: DNS challenge or a self-signed certificate
Agents do not report after the migration site name changed (--as-site), IP not taken over, or port 8000 blocked; check on the agent: cmk-agent-ctl status
Permission denied on data/ under RHEL SELinux: restorecon -Rv /opt/checkmk-stack/data, check :Z on the mounts
podman-compose not found enable EPEL or switch to Quadlet (./install.sh --quadlet)
This system is not registered with an entitlement server RHEL without a subscription – dnf then supplies no packages at all. Register, mount the installation DVD as a repository, or switch to Rocky/Alma: section 5.1
epel-release cannot be installed EPEL depends on Red Hat's base repositories and is no substitute for registering. Without a subscription, the Quadlet route without podman-compose gets you there: chapter 6, option B
Proxy Manager container no longer starts the switch is off: cmk-manage.sh npm-enable on. Check with cmk-manage.sh npm-enable
Admin interface unreachable from the LAN port 81 blocked. Move it to a free port: cmk-manage.sh npm-port 10443 and open that in the firewall
New port does not answer after npm-port the Proxy Manager was not restarted: cmk-manage.sh restart; check the file data/npm/data/nginx/custom/http.conf
cmk-manage.sh aborts with "CMK_RUNTIME … ist nicht installiert" it now only warns and detects the runtime itself; set it cleanly with config set CMK_RUNTIME auto
Browser stays black, "Waiting for Firefox to connect…" The start page does not answer – usually the Proxy Manager is not running. The script now reports this beforehand, see chapter 11.3
Proxy Manager interface blank / only text fragments a text browser without JavaScript was used. Use cmk-manage.sh browser --engine browsh (or carbonyl)
Self-signed certificate cannot be uploaded in the terminal browser there are no file dialogs there – upload via the API, see chapter 11.4
browser reports "braucht ein echtes Terminal" (needs a real terminal) called from a pipe/script. Call it on the console or over SSH; only the build runs headless, with --build-only
A menu entry drops out to the shell with an error fixed: missing containers, sites or backups are now reported in a dialog and the interface stays open
job auto on reports missing permissions /etc/cron.d/ needs root: call it with sudo, or add the printed lines yourself
The Proxy Manager asks for a new account on first use normal since 2.15. Set NPM_ADMIN_EMAIL/NPM_ADMIN_PASSWORD before the first start; if the account already exists, enter its credentials there
A JSON error appears for proxy hosts in the terminal browser the interface is too heavy for it. Create proxy hosts with cmk-manage.sh proxy add instead (chapter 10.2)
The TUI does not start install dialog: apt-get install -y dialog or dnf install -y dialog
Site does not start, tmp errors ./scripts/cmk-manage.sh omd config <site> set TMPFS off, then start the site

Useful commands:

./scripts/cmk-manage.sh logs -f                  # container logs
./scripts/cmk-manage.sh omd status cmk           # services of the site
./scripts/cmk-manage.sh shell                    # root shell in the container
docker exec -it checkmk omd su cmk               # or podman exec …

21. Security notes

  • Change CMK_PASSWORD in the interface right after installation and remove it from .env (it is only evaluated when the site is created); chmod 600 .env is already set.
  • Set NPM_ADMIN_PASSWORD to your own password before the first start – the Proxy Manager's administrator account is created from it.
  • Make port 81 (admin interface) reachable from the administration network only, and never expose port 5000.
  • Backups contain all passwords and certificates of the site – store them encrypted or restrict access tightly (chmod 700 backups).
  • Schedule ./scripts/cmk-manage.sh pull + up regularly for image security updates.
  • Host networking means the container shares the host's network stack. There is no additional network isolation – protection comes from the host firewall.

22. Uninstalling

sudo ./scripts/cmk-manage.sh down

# systemd
sudo systemctl disable --now checkmk-stack.service
sudo rm -f /etc/systemd/system/checkmk-stack.service
# or Quadlet
sudo rm -f /etc/containers/systemd/checkmk.container \
           /etc/containers/systemd/nginx-proxy-manager.container
sudo systemctl daemon-reload

# remove the images (names from "cmk-manage.sh image list")
docker image rm docker.io/checkmk/check-mk-raw:2.3.0-latest docker.io/jc21/nginx-proxy-manager:2
# podman image rm …

# remove a stored registry login if present
podman logout registry.checkmk.com   # or docker logout

# data (CAUTION: this deletes the entire monitoring setup!)
sudo rm -rf /opt/checkmk-stack/data

Licence / notes

This project contains only configuration and helper scripts. Checkmk (Raw Edition, GPLv2) and NGINX Proxy Manager (MIT) are subject to the licence terms of their respective vendors.

S
Description
No description provided
Readme
8.1 MiB
Languages
Shell 99.7%
Dockerfile 0.3%