.. _docker-deployment:
*****************************
Container Deployment (Docker)
*****************************
Cobbler can be run as a set of separate containers instead of a single traditional host install: a ``cobblerd``
container (the daemon -- XML-RPC API, sync, templating, ...) and an ``http-api`` container (Gunicorn serving
``cobbler.services:application``, i.e. ``/cblr/svc``, ``/httpboot`` and ``/images``), fronted by a reverse proxy for
HTTP routing. Both containers run from the *same* image, built from the ``cobbler`` RPM; which role a given
container plays is selected purely by its ``command:`` override in the Compose file, not by anything baked into the
image itself. An optional ``web`` container can additionally be enabled to serve the actual Cobbler Web UI -- see
`Web UI`_ below; despite the similar name, it is unrelated to ``http-api`` (which was itself named ``web`` in an
earlier revision of this stack, before being renamed to free up the name for the real UI).
A reference/example Compose stack implementing this is shipped at the repository root as two core files, both
built around the single shared runtime image at ``docker/images/cobblerd/Dockerfile``:
* ``compose.yml`` -- **production**, the default a normal user reaches for. ``cobblerd``/``http-api`` pull the
published ``ghcr.io/cobbler/cobblerd`` image; nothing builds locally.
* ``compose.dev.yml`` -- **development**. Identical apart from ``cobblerd``/``http-api``, which build from
source instead of pulling.
Optional services (the ``web`` UI, ``cobbler-tftp``, and the DHCP/DNS sidecars) are not defined inline in either
core file -- they live in their own files under ``docker/compose/`` and are pulled in via Compose's ``include:``
directive, so enabling one is a matter of uncommenting its ``include:`` entry in whichever core file you're using.
These files are deliberately minimally commented; this page is the source of truth for the reasoning behind
every volume, label and environment variable.
Two mechanical details of ``include:`` matter if you're editing these files: Compose errors if the same
top-level ``networks:``/``volumes:``/``configs:`` name is declared in more than one file across the whole
include graph, which is why they're declared exactly once, in ``docker/compose/base.yml``; and a relative path
*inside* an included file (e.g. a ``build.context``) resolves relative to that file's own directory, not the
root file's -- ``docker/compose/dhcp.dev.yml``/``dns.dev.yml``'s ``build.context: ../..`` is relative to
``docker/compose/``, resolving to the repository root.
This is *not* the same thing as ``docker/develop`` (the interactive development container), ``docker/tests`` (the
package-build/test harness), or ``docker/compose.yml`` (the RPM/DEB package-build/test harness, an unrelated file
despite the similar name -- note it lives under ``docker/``, not at the repository root). Those exist to build and
test Cobbler itself; the files described on this page exist to *run* Cobbler.
Quick start:
.. code-block:: shell
docker compose -f compose.yml up -d
curl http://localhost/cobbler_api # -> routed to cobblerd's XML-RPC server
curl http://localhost/httpboot/ # -> routed to http-api's Gunicorn app
curl http://localhost/ # -> routed to the web UI (if enabled)
Building from source instead (development) is the same, using ``compose.dev.yml`` instead:
.. code-block:: shell
docker compose -f compose.dev.yml up --build -d
Neither file sets a fixed Compose project name -- Compose derives one from the containing directory by
default, so both would otherwise collide on the same "cobbler" project name. If you need ``compose.yml`` and
``compose.dev.yml`` running side by side from the same checkout, pass distinct ``-p``/``COMPOSE_PROJECT_NAME``
values explicitly.
Images
######
``docker/images/cobblerd/Dockerfile`` builds a single shared, openSUSE-Leap-16.0-based runtime image for both roles:
* A builder stage builds the ``cobbler`` RPM from source (mirroring
``docker/rpms/opensuse_leap/openSUSE_Leap16.dockerfile``'s build environment), and the final stage installs only
the base ``cobbler`` package -- explicitly excluding the ``cobbler-apache2``/``cobbler-nginx`` subpackages (the
webserver-integration packages split out of the base package) and the ``cobbler-tests``/
``cobbler-tests-containers`` packages. The resulting image genuinely has no Apache/httpd installed.
* It declares volumes for ``/etc/cobbler``, ``/srv/www/cobbler``, ``/var/lib/cobbler`` and ``/srv/tftpboot``, and
exposes both the XML-RPC port (``25151``) and the HTTP/XML-RPC API's Gunicorn port (``8000``).
* The image ships no default ``ENTRYPOINT``/``CMD``. ``compose.yml``/``compose.dev.yml`` instead sets ``command:``
per service: the ``cobblerd`` service runs ``cobblerd -F``, while the ``http-api`` service overrides this with
``command: ["gunicorn", "cobbler.services:application", "--bind", "0.0.0.0:8000"]``. Round 1's rationale for a
separately-minimized API image (avoiding pulling in ``cobbler.api``/items/collections/modules just to run
Gunicorn) no longer applies once installation is RPM-package-based rather than pip-based: there is no lighter
subset of the package to install for just the ``http-api`` role, so both roles simply share this one image.
* ``distro_signatures.json`` and ``/var/lib/cobbler/misc`` are seeded into the image directly by the RPM's own
install step (``cobblerd setup``, run as part of building the package) -- the Dockerfile itself does no manual
seeding. A fresh named-volume mount over ``/var/lib/cobbler`` still gets this content on first
``docker compose up`` (Docker seeds an empty named volume from the image's content at that mount point).
``compose.yml`` never builds this image -- it only pulls ``ghcr.io/cobbler/cobblerd:latest``. To build it yourself,
either run ``docker build -f docker/images/cobblerd/Dockerfile -t cobbler-cobblerd .`` from the repository root
directly, or use ``compose.dev.yml``, which builds it as part of ``docker compose -f compose.dev.yml up --build``.
Settings overrides
###################
``docker/compose/base.yml`` (included by both ``compose.yml`` and ``compose.dev.yml``) injects a single shared
``settings.yaml`` (via a Compose ``config:``) into both ``cobblerd`` and ``http-api``. It only lists the keys that
differ from ``cobbler.settings.Settings``'s built-in
Python defaults (see ``cobbler/settings/__init__.py``); every other setting keeps its normal default. This works
because ``Settings.from_dict()`` (called from ``cobbler.api.CobblerAPI``'s settings generation) starts from a
fully-populated ``Settings()`` object and overlays only the keys present in the file on top of it.
.. important::
``modules`` is given in full in that file, not as a partial override. ``from_dict()`` replaces each top-level key
*wholesale* rather than deep-merging nested dicts, so a partial ``modules: {httpd: ..., tftpd: ...}`` override
would silently blank out the ``authentication``/``authorization``/``dns``/``dhcp``/``process_management``/
``serializers`` choices instead of leaving them at their defaults. Any edit to one ``modules`` sub-key must repeat
the rest of the block.
The ``modules`` block sets:
* ``authentication.module: "authentication.configfile"`` with ``hash_algorithm: "sha3_512"`` -- the standard
file-backed authentication backend, unchanged from Cobbler's own default choice of backend but pinned to a modern
hash algorithm.
* ``authorization.module: "authorization.allowall"`` -- the permissive default; tighten this for a real deployment.
* ``dns.module: "managers.bind"`` and ``dhcp.module: "managers.isc"`` -- the traditional BIND/ISC-dhcpd managers,
unchanged from Cobbler's own defaults.
* ``httpd``/``tftpd`` -- see `Dynamic HTTP and TFTP serving`_ below.
* ``process_management`` -- see `Process management and DHCP/DNS sidecar containers`_ below.
* ``serializers.module: "serializers.file"`` -- the default flat-file storage backend, unchanged.
One setting is deliberately **not** overridden: ``server`` (defaults to ``127.0.0.1``) is the hostname/IP PXE
clients use to reach Cobbler for templates/files. Set it to this stack's externally-reachable address (e.g. the
reverse proxy host's IP/DNS name) -- this is environment-specific and out of scope for the container-to-container
wiring described on this page.
Required volumes
#################
``cobblerd`` needs read-write access to:
* ``/etc/cobbler`` -- ``settings.yaml`` and other runtime configuration (``users.digest``/``users.conf``, etc).
* ``/var/lib/cobbler`` -- collections (distros/profiles/systems/...), triggers state, the signatures cache.
* ``/srv/www/cobbler`` -- webdir (templates, misc, images, ...). This follows ``webdir``, whatever it is set to.
* ``/srv/tftpboot`` -- the TFTP root. This follows ``tftpboot_location``, whatever it is set to.
``http-api`` only ever needs read-only access to the webdir and TFTP-root content (so its ``/cblr/svc``, ``/httpboot``
and ``/images`` routes have something to read), plus a read-only copy of ``/etc/cobbler/settings.yaml`` for the
``xmlrpc_port``/``xmlrpc_host`` values it needs to reach ``cobblerd``.
The shipped ``docker/images/cobblerd/Dockerfile`` image follows the openSUSE packaging convention of rooting these
under ``/srv`` (``/srv/www/cobbler``, ``/srv/tftpboot``), which does **not** match Cobbler's own Python-level
defaults (``/var/www/cobbler``, ``/var/lib/tftpboot``). ``docker/compose/base.yml`` overrides ``webdir`` and
``tftpboot_location`` in its injected ``settings.yaml`` to match the image; if you build your own image with
different paths, override these two settings accordingly.
On an SELinux-enforcing host (e.g. openSUSE/Fedora/RHEL, the same distro family these images target), the named
volumes shared between ``cobblerd`` (read-write) and ``http-api`` (read-only) are mounted with the ``:z`` suffix so
Docker relabels them with a shared SELinux content label. Without it, the two containers' concurrent,
differing-mode mounts of the same volume can race during labeling and leave ``cobblerd`` unable to stat/create its
own directories, failing startup with a plain ``PermissionError``. This is a no-op (and harmless) on hosts where
SELinux isn't enforcing.
XML-RPC host/bind settings
###########################
Splitting the daemon and the HTTP/XML-RPC API application into separate containers only works because of two
settings added for this purpose:
* ``xmlrpc_bind_address`` (default ``"127.0.0.1"``, overridable via the ``COBBLER_XMLRPC_BIND_ADDRESS`` environment
variable) controls what address ``cobblerd`` binds its XML-RPC server to. The Python-level default is
loopback-only, which a separate ``http-api`` container could never reach; the compose stack sets this to ``"0.0.0.0"``
on ``cobblerd`` so the ``http-api`` container can dial in over the shared Docker network.
* ``xmlrpc_host`` (default ``"127.0.0.1"``, overridable via the ``COBBLER_XMLRPC_HOST`` environment variable)
controls what host/address the ``http-api`` application (``cobbler/services/svc.py`` and ``cobbler/services/files.py``)
dials to reach ``cobblerd``. The compose stack sets this to ``"cobblerd"`` (the Compose service name) on ``http-api``.
Both are ordinary entries under the top level of ``settings.yaml`` (see :ref:`settings-ref`); the environment
variables only exist so the *same* ``settings.yaml`` can be shared, read-only, between both containers while each
one still gets the value it individually needs.
``cobblerd`` is never published to the host directly in the reference stack -- only Traefik's port 80 is; binding
``xmlrpc_bind_address`` to ``0.0.0.0`` is only safe to the extent that nothing outside the Docker network can reach
that port.
Traefik and routing
#####################
``docker/compose/base.yml`` runs `Traefik `_ as the stack's single entry point on
host port 80, using Traefik's Docker provider (``--providers.docker=true``, with
``--providers.docker.exposedbydefault=false`` so a container only gets routed once it carries an explicit
``traefik.enable=true`` label). Traefik auto-detects each labeled container's address on the shared ``cobbler``
bridge network -- every service has exactly one network attached, so no ``traefik.docker.network`` label is
needed to disambiguate.
Each backend's Traefik labels declare a router rule and, where needed, a ``stripprefix`` middleware:
* ``cobblerd`` is matched on ``PathPrefix(`/cobbler_api`)``, but its raw XML-RPC server (a plain
``SimpleXMLRPCRequestHandler``, see ``cobbler/remote.py``) only ever answers on ``/`` (and ``/RPC2``) --
``cobbler-api-strip`` strips the ``/cobbler_api`` prefix before the request reaches it.
* ``http-api`` is matched on ``PathPrefix(`/cblr`) || PathPrefix(`/httpboot`) || PathPrefix(`/images`)`` (Traefik
v3's matchers take exactly one argument each -- the v2 multi-value ``PathPrefix`` syntax was removed, hence
the ``||``). Gunicorn expects the ``/cblr/svc`` prefix already stripped (historically done by Apache's
``ProxyPass``), but ``/httpboot``/``/images`` must arrive un-stripped -- so ``cobbler-svc-strip`` only strips
``/cblr/svc``, chained onto the router via ``middlewares=cobbler-svc-strip``.
* ``web`` (see `Web UI`_ below) matches the catch-all ``PathPrefix(`/`)`` at the lowest priority in the stack, so
it only ever receives requests the two more specific routers above don't match. The ``cobbler-api`` router also
carries a CORS middleware for this service's benefit -- see `Web UI`_ for why.
Traefik needs its own read-only mount of the host's Docker socket to watch for labeled containers -- the same
class of trust boundary as ``process_management.docker``'s socket mount discussed above: even read-only, it
grants Traefik root-equivalent control over the **entire host Docker daemon**. This is accepted here as a
deliberate trade-off for local, label-based service discovery.
On an SELinux-enforcing host, the Docker socket's own SELinux context (typically ``container_var_run_t``) is
not one a bind-mounted volume can be transparently relabeled to (unlike the named volumes described above,
which use the ``:z`` suffix for that) -- SELinux denies Traefik's container access to the socket outright,
surfacing as a plain "permission denied" error talking to the socket rather than an SELinux-specific one.
``docker/compose/base.yml`` sets ``security_opt: [label:disable]`` on the ``traefik`` service to work around
this, turning off SELinux label confinement for that one container instead of relabeling the socket itself.
The reference stack pins ``traefik:v3.6``. Older ``v3.3`` was found, while testing this stack, to negotiate a
stale Docker API version against some newer Docker Engine releases, failing with "client version ... is too
old" errors when talking to the socket; ``v3.6`` does not have this problem.
Dynamic HTTP and TFTP serving
##############################
By default Cobbler copies imported distro trees into ``webdir``/``distro_mirror`` and materializes boot files under
``tftpboot_location``. In a container deployment it is usually preferable to serve content directly from wherever
it already lives instead of duplicating it into a (potentially large) writable volume:
* ``modules.httpd.module: "managers.dynamic_httpd"`` serves a distro's source tree on demand, straight from its
original location, through the ``http-api`` container's ``/cblr/svc/tree`` route, instead of copying it into
``webdir``/``distro_mirror``.
* ``modules.tftpd.module: "managers.dynamic_tftp"`` skips writing boot files into ``tftpboot_location`` entirely;
something else (for example the separately-maintained
`cobbler-tftp `_ daemon, enabled by default via ``docker/compose/tftp.yml``,
included by both ``compose.yml`` and ``compose.dev.yml``) has to actually answer TFTP (UDP/69) requests by calling
back into ``cobblerd``'s XML-RPC API.
``docker/compose/base.yml`` selects both of these by default in its injected settings, and ``compose.yml``/
``compose.dev.yml`` bind-mount a read-only ``distro-sources`` directory into both ``cobblerd`` and ``http-api``
for this purpose, so a large distro tree can be served without ever being copied into a Docker volume.
.. warning::
The traditional copying managers (``managers.in_httpd``/``managers.in_tftpd``) are **not** supported by this
reference stack, even though ``cobblerd`` would happily copy content into ``webdir``/``distro_mirror`` if you
selected them and pointed ``webdir``/``tftpboot_location`` at a writable volume. The problem is on the serving
side: Apache historically served that copied webdir content back out at ``/cblr`` (the non-``/svc`` paths),
``/cobbler`` and ``/cobbler_track``, but the Gunicorn ``http-api`` application only implements ``/cblr/svc``,
``/httpboot`` and ``/images`` (see ``cobbler/services/__init__.py``), and ``compose.yml``/``compose.dev.yml``'s
Traefik router for ``http-api`` only matches ``/cblr``, ``/httpboot``, ``/images`` -- there is no route at all for
``/cobbler`` or ``/cobbler_track``. Content copied into the volume by ``in_httpd``/``in_tftpd`` would therefore
sit there unreachable over HTTP in this reference stack. Using either manager requires adding your own Traefik
route(s) and a way to actually serve that content, neither of which this plan provides -- stick with
``managers.dynamic_httpd``/``managers.dynamic_tftp`` (the defaults above) unless you build that yourself.
Manually-created distros (bypassing ``cobbler import``)
##########################################################
Everything above happens automatically only inside ``cobbler import``: that is the only code path that sets a
distro's ``source_tree_path`` for you. If you create ``Distro``/``Profile`` objects yourself -- for example
directly through the XML-RPC API, without ever calling ``cobbler import`` -- ``source_tree_path`` stays empty
unless you set it yourself, and ``/cblr/svc/tree//...`` returns ``404 Not Found`` for that distro even
though ``managers.dynamic_httpd`` is selected.
To make ``/cblr/svc/tree`` work for a manually-created distro:
1. Place the distro's extracted tree (or a mount of it) somewhere under the host directory bind-mounted at
``/srv/distro-sources`` (``${COBBLER_DISTRO_SOURCE_DIR:-./distro-sources}``, see `Settings overrides`_ above).
This is the only location that resolves to the *same* path inside both the ``cobblerd`` and ``http-api``
containers. Content placed anywhere ``http-api`` doesn't also have mounted -- for example under
``/var/lib/cobbler``, which is a volume only ``cobblerd`` has -- looks fine when ``cobblerd`` resolves the
distro's metadata over XML-RPC, but ``http-api`` still 404s when it tries to actually open the file, since the
path simply doesn't exist inside its own container.
2. Set the distro's ``source_tree_path`` to that same path, as seen *inside the containers*
(``/srv/distro-sources/``, not the host-side path you used to place the files there):
.. code-block:: python
import xmlrpc.client
remote = xmlrpc.client.Server("http://localhost/cobbler_api", allow_none=True)
token = remote.login("cobbler", "cobbler")
did = remote.get_distro_handle("example_distro")
remote.modify_distro(did, ["source_tree_path"], "/srv/distro-sources/example_distro", token)
remote.save_distro(did, True, True, "bypass", token)
3. Verify: ``curl http://localhost/cblr/svc/tree/example_distro/`` should return a directory listing instead of
``404 Not Found``.
.. note:: ``http-api`` caches the distro-name-to-``source_tree_path`` lookup for up to 30 seconds (see
``CACHE_TTL_SECONDS`` in ``cobbler/services/files.py``) to avoid an XML-RPC round trip per served file.
A ``source_tree_path`` you just set may not be picked up immediately -- no container restart is needed,
just retry after the cache entry expires.
Process management and DHCP/DNS sidecar containers
#####################################################
Managing DHCP/DNS as local processes (systemd services, or processes supervised by supervisord) does not make sense
once ``cobblerd`` runs in its own minimal container with no DHCP/DNS daemon inside it. The
``modules.process_management.module`` setting controls how Cobbler restarts these services after a
``cobbler sync``:
* ``"auto"`` (the shipped default) -- auto-detects whether ``cobblerd`` is running inside a container (see
``cobbler/modules/process_management/detection.py``'s ``is_containerized()``) and resolves to
``"process_management.docker"`` if so, or ``"process_management.service"`` otherwise. Outside a container this
reproduces today's traditional behavior byte-for-byte, so a containerized ``cobblerd`` picks up the Docker
backend with no explicit override needed, while a non-containerized install keeps working unchanged.
* ``"process_management.service"`` -- today's behavior, unchanged: restart a local systemd/supervisord-managed
process. Explicitly setting this always wins, even inside a container -- ``"auto"`` is the only value affected
by container detection.
* ``"process_management.docker"`` -- an opt-in alternative that restarts a DHCP/DNS **sidecar container** instead
of a local process. It requires the optional ``docker`` Python extra (``pip install cobbler[docker]``) and
mounting the host's Docker socket into the ``cobblerd`` container. Like ``"process_management.service"``, an
explicit setting here is never overridden by container detection.
When ``process_management.docker`` is selected, ``cobblerd`` (see ``cobbler/modules/process_management/docker.py``)
picks the container to restart purely by Docker label: it looks for exactly one running container carrying the
label ``cobbler.io/managed-service=``, where ```` is one of ``dhcp``, ``dns`` or ``dnsmasq``. The
mapping from Cobbler's internal service names (``dhcpd``, ``dhcpd4``, ``dhcpd6``, ``named``, ``dnsmasq``) to those
label values is configurable via ``modules.process_management.docker_service_labels``, whose default is:
.. code-block:: yaml
modules:
process_management:
module: "auto"
docker_socket_path: "/var/run/docker.sock"
docker_service_labels:
dhcpd: "dhcp"
dhcpd4: "dhcp"
dhcpd6: "dhcp"
named: "dns"
dnsmasq: "dnsmasq"
``modules.process_management.docker_socket_path`` (default ``/var/run/docker.sock``) is the path, inside the
``cobblerd`` container, of the Docker Engine API Unix socket to connect to. The Docker Engine API connection is
always local-only (a Unix socket) -- this module never connects to a remote or TLS-secured Docker host, by design.
If zero containers or more than one container carry the expected label, this is treated as a hard error (logged and
reported as a non-zero return from the restart call), never a silent no-op and never a guess at which container to
restart. There is no retry loop and no fallback.
``compose.yml``/``compose.dev.yml`` ship a commented-out example of mounting the socket into ``cobblerd``, plus
commented-out ``include:`` entries pulling in ``docker/compose/dhcp.yml``/``dns.yml``/``dnsmasq.yml``
(``dhcp.dev.yml``/``dns.dev.yml`` in ``compose.dev.yml``, which build from source instead of pulling) -- sidecar
service definitions carrying the matching labels. Read the comments in those files before enabling any of it, in
particular the trust-boundary warning below. The ``dhcp``/``dns`` sidecars' config-sharing volumes (described in
the next paragraph) are a real, working mechanism, verified end-to-end with an actual cross-container config
write/read test -- not illustrative placeholders. The ``dnsmasq`` example remains illustrative only (see
`Known limitations and non-goals`_ for the one genuine remaining caveat with the ``dhcp``/``dns`` sidecars).
The ``cobbler-dhcp-config``/``cobbler-dns-config`` volumes that ``cobblerd`` and the sidecars would share are
mounted at plain directories (``/etc/cobbler-dhcp``, ``/etc/cobbler-dns``), not directly at ``/etc/dhcpd.conf`` or
``/etc/named.conf``. Both images instead create those config files as symlinks into the mounted directory
(``cobbler/utils/dhcpconf_location()``/``namedconf_location()`` still resolve to the plain path; ``open()`` follows
the symlink transparently). This sidesteps Docker's fragile, version-dependent behavior for seeding a named volume
mounted directly onto a path that is a plain file in the image -- confirmed outright broken (container creation
fails unconditionally) on at least one real Docker Engine build, regardless of whether that file is empty or has
real content. Mounting onto a directory instead is the unambiguous, universally-supported case.
``named.conf`` alone isn't enough for the ``dns`` sidecar to actually serve anything, though: ``bind.py`` also
renders the zone files it references to ``bind_zonefile_path`` (``/var/lib/named`` by default), a directory
separate from ``/etc/cobbler-dns``. The ``cobbler-dns-zones`` volume, mounted at ``/var/lib/named`` on both
``cobblerd`` (read-write) and the ``dns`` sidecar (read-only), carries that zone data across -- the same
plain-directory-volume approach as ``cobbler-dns-config``, without needing a symlink trick since
``/var/lib/named`` is already a directory in both images, not a single file. ``docker/compose/base.yml`` also
pins ``bind_zonefile_path: "/var/lib/named"`` explicitly in the ``cobbler-settings`` config so it can't silently
drift out of sync with this mount path.
The ``dhcp`` sidecar additionally runs with ``network_mode: host`` instead of joining the ``cobbler`` bridge
network like everything else in the stack -- ISC ``dhcpd`` needs real L2 broadcast visibility to serve DHCP
clients, which a NAT'd bridge network does not provide. The ``dns`` sidecar has no such requirement and stays
on the ``cobbler`` network.
.. warning::
Mounting the Docker socket into ``cobblerd`` -- even read-only -- grants ``cobblerd`` root-equivalent control
over the **entire host Docker daemon**, not just the ``dhcp``/``dns``/``dnsmasq``-labeled sibling containers it
is meant to restart. Anyone who can reach ``cobblerd``'s XML-RPC API (or exploit it) can, via this socket, create
a new privileged container, bind-mount arbitrary host paths into it, or inspect/stop/remove *any* container on
the host. Mounting it read-only only prevents the socket file itself from being replaced or deleted -- it does
not restrict which Docker Engine API calls can be made over it. This is accepted here only as a deliberate
trade-off for local-only sidecar management; do not extend it to a remote or TLS-secured Docker endpoint.
Health checks
#############
The ``http-api`` container exposes a ``/healthz`` endpoint (``cobbler/services/files.py``'s ``healthz_application``)
that performs a real XML-RPC ``ping()`` round trip against ``cobblerd`` and returns ``200 OK`` if it succeeds or
``503 Service Unavailable`` otherwise. The ``cobblerd`` container's own health check performs an equivalent XML-RPC
round trip directly against its own port. Since both roles now share one image, ``docker/images/cobblerd/Dockerfile``
only declares a ``HEALTHCHECK`` suited to the ``cobblerd`` role (a plain TCP connect to the XML-RPC port);
``compose.yml``/``compose.dev.yml``'s ``http-api`` service overrides this with its own ``healthcheck:`` hitting
``/healthz`` instead, since a bare TCP connect to the Gunicorn port would only prove Gunicorn is listening, not that
it can actually reach ``cobblerd``. Both are used for ``depends_on: condition: service_healthy``.
``/healthz`` is **not** routed externally by Traefik in the reference stack -- only ``/cblr``, ``/httpboot``,
``/images`` (routed to ``http-api``) and ``/cobbler_api`` (routed to ``cobblerd``) are. The health check is
container-internal only: it is consumed by Docker's own ``HEALTHCHECK``/``depends_on`` machinery, not reachable
from outside the Compose network.
Web UI
######
``docker/compose/web.yml`` -- included by both ``compose.yml`` and ``compose.dev.yml`` -- ships an optional
``web`` service running the actual `Cobbler Web UI `_ -- a
separately-maintained Angular application, published as ``ghcr.io/cobbler/cobbler-web`` on GHCR and
built/versioned independently of the ``cobbler`` package itself. It is **not** the same thing as this stack's
``http-api`` service: an earlier revision of this reference stack named the Gunicorn HTTP/XML-RPC-API container
``web``, and that service was renamed to ``http-api`` specifically to free up the ``web`` name for this real UI.
If you are looking for the code that serves ``/cblr/svc``, ``/httpboot`` and ``/images``, that is ``http-api``,
not ``web``.
Unlike ``http-api``, the ``web`` UI does no server-side proxying at all: it is a static Angular application served by
``nginx-unprivileged`` on port ``8080``, and every XML-RPC call it makes to Cobbler is issued directly from the end
user's *browser* to ``/cobbler_api``. Two consequences follow from that:
* **The UI needs to be told where the API is.** It reads ``/cobbler_api``'s URL at container start from a
runtime-mounted ``/config/app-config.json`` (the image's own entrypoint script copies it into the webroot -- there
is no build-time environment variable for this). ``docker/compose/web.yml`` supplies this content inline via a
Compose ``configs:`` entry (no separate tracked file at the repository root):
.. code-block:: yaml
configs:
cobbler-web-app-config:
content: |
{"cobblerUrls": ["http://localhost/cobbler_api"]}
services:
web:
configs:
- source: cobbler-web-app-config
target: /config/app-config.json
The shipped ``"http://localhost/cobbler_api"`` value is a *local-development-only* default: it only resolves
correctly when the browser and the Docker host are the same machine. Before using the ``web`` service anywhere
else, edit the ``content:`` block in ``docker/compose/web.yml`` (or supply a Compose override file overriding
just this config's ``content:``) to your deployment's real, externally-reachable hostname, e.g.:
.. code-block:: yaml
content: |
{"cobblerUrls": ["https://cobbler.example.org/cobbler_api"]}
* **The API needs CORS headers.** Because the browser -- not a server-side process -- is the one making the
cross-origin request, ``cobblerd``'s XML-RPC endpoint must answer with ``Access-Control-Allow-Origin`` (and related)
headers, which its raw ``SimpleXMLRPCRequestHandler`` (``cobbler/remote.py``) never sends on its own.
``compose.yml``/``compose.dev.yml`` handle this with a Traefik middleware chained onto the existing
``cobbler-api`` router, rather than a code change:
.. code-block:: yaml
- "traefik.http.middlewares.cobbler-api-cors.headers.accessControlAllowOriginList=*"
- "traefik.http.middlewares.cobbler-api-cors.headers.accessControlAllowMethods=GET,POST,OPTIONS"
- "traefik.http.middlewares.cobbler-api-cors.headers.accessControlAllowHeaders=content-type"
- "traefik.http.middlewares.cobbler-api-cors.headers.accessControlMaxAge=100"
- "traefik.http.routers.cobbler-api.middlewares=cobbler-api-strip,cobbler-api-cors"
``accessControlAllowHeaders`` matters as much as the origin/methods headers above: cobblerd's XML-RPC endpoint
requires ``Content-Type: text/xml``, which is not a CORS-"simple" content type, so browsers send a preflight
``OPTIONS`` request (``Access-Control-Request-Headers: content-type``) before the real one. Without a matching
``Access-Control-Allow-Headers`` response, that preflight fails and the browser blocks the real request outright,
even though the origin/method headers look correct.
If you front ``/cobbler_api`` with your own reverse proxy instead of this reference stack's Traefik, you need to
replicate the equivalent CORS headers yourself, or the UI's browser-side requests will be blocked by the browser's
own CORS enforcement even though ``cobblerd`` itself answered the request successfully.
The ``web`` service's Traefik router matches the catch-all root path (``PathPrefix(`/`)``) at the lowest priority in
the stack (``priority=1``), so it only ever receives requests that ``cobbler-api``'s and ``cobbler-http-api``'s more
specific routes (``/cobbler_api``, ``/cblr``, ``/httpboot``, ``/images``) don't match.
.. note::
As of this writing, the ``cobbler-web`` project's ``v1.0.0`` release may not yet be fully compatible with this
version of Cobbler (4.0.0) -- the two projects are versioned and released independently. The compose wiring,
Traefik routing and CORS headers described above are independent of that and can be verified purely at the HTTP
level even if the UI application itself does not yet fully work end-to-end against a given Cobbler version.
Known limitations and non-goals
#################################
This deployment model is deliberately scoped. Before relying on it, be aware of the following:
* **Sidecar restart is not sidecar management.** `GH #3138
`_ (remote sibling-service management) is only *partially*
closed by ``process_management.docker``: restarting a DHCP/DNS sidecar container by label works, but broader
remote-service management -- pushing configuration changes to a sidecar, scaling it, or managing it through a
non-Docker orchestrator -- is still open. Getting ``cobblerd``'s rendered DHCP/DNS configuration into the sidecar
container in the first place **is** solved for the ``dhcp``/``dns`` sidecars, via the directory+symlink
volume-sharing mechanism described above (verified end-to-end); it remains genuinely unsolved only for the
illustrative ``dnsmasq`` example, whose config paths (``dnsmasq_settings_file``/``-hosts_file``/``-ethers_file``)
are not covered by any volume in the reference stack.
* **One container per service, local Docker only.** The label-based restart mechanism requires exactly one running
container per service label and a local-only Docker daemon. There is no support for multiple replicas of the same
service, no support for a remote or TLS-secured Docker API, and no Kubernetes support. A Kubernetes "pod"
selection mechanism would need a different, Kubernetes-native implementation -- it is not something this Docker
Engine API-based module can grow into.
* **Partial environment-variable configuration.** `GH #3137
`_ (full environment-variable-driven configuration) is only
partially covered: only the XML-RPC host/bind-address settings needed to split ``cobblerd`` and ``http-api`` into
separate containers (``COBBLER_XMLRPC_BIND_ADDRESS``/``COBBLER_XMLRPC_HOST``) have environment variable
overrides. Every other ``settings.yaml`` key must still be set through the settings file itself.
* **Kubernetes is out of scope.** Nothing on this page or in the referenced Dockerfile/Compose file has been
designed, tested, or is intended for Kubernetes. Running this image under Kubernetes is not supported.
* **DHCP/DNS sidecars need cobblerd to have rendered real config at least once.** The ``dhcp``/``dns`` sidecar
images (see above) set up ``/etc/dhcpd.conf``/``/etc/named.conf`` as symlinks into the shared
``cobbler-dhcp-config``/``cobbler-dns-config`` volumes; ``cobblerd``'s image sets up the identical symlinks on its
side. If a sidecar starts before ``cobblerd`` has ever run a ``cobbler sync`` with ``manage_dhcp``/``manage_dns``
enabled, that symlink is dangling and the sidecar's ``dhcpd``/``named`` process exits immediately. ``restart:
unless-stopped`` on those services (already set in ``docker/compose/dhcp.yml``/``dns.yml`` and their
``.dev.yml`` variants) mitigates this: the sidecar keeps retrying and comes up cleanly as soon as the first sync
writes real config into the shared volume. The illustrative ``dnsmasq`` example remains a genuine placeholder --
no image is built for it in this repository, and its config paths are not covered by any volume in the
reference stack.
* **Publishing is GHCR-only for now.** `.github/workflows/docker-publish.yml` publishes all three images it
builds -- the shared ``cobblerd`` runtime image plus the ``cobbler-dhcp``/``cobbler-dns`` sidecar images -- to
GHCR (``ghcr.io``). Cobbler's existing packages and CI test image are built and published through openSUSE's
Open Build Service (OBS) instead; building/publishing these container images through OBS as well is out of
scope for now.
See also
########
* ``compose.yml``/``compose.dev.yml`` and ``docker/compose/*.yml`` -- the reference Compose stack itself.
* ``docker/images/cobblerd/Dockerfile`` -- the single shared runtime image for both the ``cobblerd`` and
``http-api`` roles.
* `cobbler-web `_ -- the Cobbler Web UI project itself, served by this
stack's optional ``web`` service.
* :ref:`settings-ref` for the full list of ``settings.yaml`` keys.
* :ref:`dhcp-management` and :ref:`dns-management` for the DHCP/DNS managers that ``process_management`` restarts.
* :ref:`tftp-directory` for background on TFTP directory materialization and ``dynamic_tftp``.