5.21. 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-apipull the publishedghcr.io/cobbler/cobblerdimage; nothing builds locally.compose.dev.yml– development. Identical apart fromcobblerd/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:
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:
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.
5.21.1. Images
docker/images/cobblerd/Dockerfile builds a single shared, openSUSE-Leap-16.0-based runtime image for both roles:
A builder stage builds the
cobblerRPM from source (mirroringdocker/rpms/opensuse_leap/openSUSE_Leap16.dockerfile’s build environment), and the final stage installs only the basecobblerpackage – explicitly excluding thecobbler-apache2/cobbler-nginxsubpackages (the webserver-integration packages split out of the base package) and thecobbler-tests/cobbler-tests-containerspackages. The resulting image genuinely has no Apache/httpd installed.It declares volumes for
/etc/cobbler,/srv/www/cobbler,/var/lib/cobblerand/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.ymlinstead setscommand:per service: thecobblerdservice runscobblerd -F, while thehttp-apiservice overrides this withcommand: ["gunicorn", "cobbler.services:application", "--bind", "0.0.0.0:8000"]. Round 1’s rationale for a separately-minimized API image (avoiding pulling incobbler.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 thehttp-apirole, so both roles simply share this one image.distro_signatures.jsonand/var/lib/cobbler/miscare 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/cobblerstill gets this content on firstdocker 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.
5.21.2. 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"withhash_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"anddhcp.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.
5.21.3. Required volumes
cobblerd needs read-write access to:
/etc/cobbler–settings.yamland 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 followswebdir, whatever it is set to./srv/tftpboot– the TFTP root. This followstftpboot_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.
5.21.4. 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 theCOBBLER_XMLRPC_BIND_ADDRESSenvironment variable) controls what addresscobblerdbinds its XML-RPC server to. The Python-level default is loopback-only, which a separatehttp-apicontainer could never reach; the compose stack sets this to"0.0.0.0"oncobblerdso thehttp-apicontainer can dial in over the shared Docker network.xmlrpc_host(default"127.0.0.1", overridable via theCOBBLER_XMLRPC_HOSTenvironment variable) controls what host/address thehttp-apiapplication (cobbler/services/svc.pyandcobbler/services/files.py) dials to reachcobblerd. The compose stack sets this to"cobblerd"(the Compose service name) onhttp-api.
Both are ordinary entries under the top level of settings.yaml (see settings.yaml); 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.
5.21.5. 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:
cobblerdis matched onPathPrefix(`/cobbler_api`), but its raw XML-RPC server (a plainSimpleXMLRPCRequestHandler, seecobbler/remote.py) only ever answers on/(and/RPC2) –cobbler-api-stripstrips the/cobbler_apiprefix before the request reaches it.http-apiis matched onPathPrefix(`/cblr`) || PathPrefix(`/httpboot`) || PathPrefix(`/images`)(Traefik v3’s matchers take exactly one argument each – the v2 multi-valuePathPrefixsyntax was removed, hence the||). Gunicorn expects the/cblr/svcprefix already stripped (historically done by Apache’sProxyPass), but/httpboot//imagesmust arrive un-stripped – socobbler-svc-striponly strips/cblr/svc, chained onto the router viamiddlewares=cobbler-svc-strip.web(see Web UI below) matches the catch-allPathPrefix(`/`)at the lowest priority in the stack, so it only ever receives requests the two more specific routers above don’t match. Thecobbler-apirouter 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.
5.21.6. 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 thehttp-apicontainer’s/cblr/svc/treeroute, instead of copying it intowebdir/distro_mirror.modules.tftpd.module: "managers.dynamic_tftp"skips writing boot files intotftpboot_locationentirely; something else (for example the separately-maintained cobbler-tftp daemon, enabled by default viadocker/compose/tftp.yml, included by bothcompose.ymlandcompose.dev.yml) has to actually answer TFTP (UDP/69) requests by calling back intocobblerd’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.
5.21.7. 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/<distro_name>/... 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:
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 thecobblerdandhttp-apicontainers. Content placed anywherehttp-apidoesn’t also have mounted – for example under/var/lib/cobbler, which is a volume onlycobblerdhas – looks fine whencobblerdresolves the distro’s metadata over XML-RPC, buthttp-apistill 404s when it tries to actually open the file, since the path simply doesn’t exist inside its own container.Set the distro’s
source_tree_pathto that same path, as seen inside the containers (/srv/distro-sources/<something>, not the host-side path you used to place the files there):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)
Verify:
curl http://localhost/cblr/svc/tree/example_distro/should return a directory listing instead of404 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.
5.21.8. 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 whethercobblerdis running inside a container (seecobbler/modules/process_management/detection.py’sis_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 containerizedcobblerdpicks 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 optionaldockerPython extra (pip install cobbler[docker]) and mounting the host’s Docker socket into thecobblerdcontainer. 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=<value>, where <value> 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:
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.
5.21.9. 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.
5.21.10. 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.ymlsupplies this content inline via a Composeconfigs:entry (no separate tracked file at the repository root):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 thewebservice anywhere else, edit thecontent:block indocker/compose/web.yml(or supply a Compose override file overriding just this config’scontent:) to your deployment’s real, externally-reachable hostname, e.g.: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 withAccess-Control-Allow-Origin(and related) headers, which its rawSimpleXMLRPCRequestHandler(cobbler/remote.py) never sends on its own.compose.yml/compose.dev.ymlhandle this with a Traefik middleware chained onto the existingcobbler-apirouter, rather than a code change:- "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"
accessControlAllowHeadersmatters as much as the origin/methods headers above: cobblerd’s XML-RPC endpoint requiresContent-Type: text/xml, which is not a CORS-“simple” content type, so browsers send a preflightOPTIONSrequest (Access-Control-Request-Headers: content-type) before the real one. Without a matchingAccess-Control-Allow-Headersresponse, that preflight fails and the browser blocks the real request outright, even though the origin/method headers look correct.If you front
/cobbler_apiwith 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 thoughcobblerditself 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.
5.21.11. 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. Gettingcobblerd’s rendered DHCP/DNS configuration into the sidecar container in the first place is solved for thedhcp/dnssidecars, via the directory+symlink volume-sharing mechanism described above (verified end-to-end); it remains genuinely unsolved only for the illustrativednsmasqexample, 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
cobblerdandhttp-apiinto separate containers (COBBLER_XMLRPC_BIND_ADDRESS/COBBLER_XMLRPC_HOST) have environment variable overrides. Every othersettings.yamlkey 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/dnssidecar images (see above) set up/etc/dhcpd.conf//etc/named.confas symlinks into the sharedcobbler-dhcp-config/cobbler-dns-configvolumes;cobblerd’s image sets up the identical symlinks on its side. If a sidecar starts beforecobblerdhas ever run acobbler syncwithmanage_dhcp/manage_dnsenabled, that symlink is dangling and the sidecar’sdhcpd/namedprocess exits immediately.restart: unless-stoppedon those services (already set indocker/compose/dhcp.yml/dns.ymland their.dev.ymlvariants) mitigates this: the sidecar keeps retrying and comes up cleanly as soon as the first sync writes real config into the shared volume. The illustrativednsmasqexample 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
cobblerdruntime image plus thecobbler-dhcp/cobbler-dnssidecar 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.
5.21.12. See also
compose.yml/compose.dev.ymlanddocker/compose/*.yml– the reference Compose stack itself.docker/images/cobblerd/Dockerfile– the single shared runtime image for both thecobblerdandhttp-apiroles.cobbler-web – the Cobbler Web UI project itself, served by this stack’s optional
webservice.settings.yaml for the full list of
settings.yamlkeys.DHCP Management and DNS management for the DHCP/DNS managers that
process_managementrestarts.The TFTP Directory for background on TFTP directory materialization and
dynamic_tftp.