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.ymlproduction, the default a normal user reaches for. cobblerd/http-api pull the published ghcr.io/cobbler/cobblerd image; nothing builds locally.

  • compose.dev.ymldevelopment. 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:

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 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.

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" 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.

5.21.3. Required volumes

cobblerd needs read-write access to:

  • /etc/cobblersettings.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.

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 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 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:

  • 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.

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 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.

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 importsource_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:

  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/<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)
    
  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.

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 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=<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.yml supplies this content inline via a Compose configs: 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 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.:

    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:

    - "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.

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. 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.

5.21.12. 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.

  • settings.yaml for the full list of settings.yaml keys.

  • DHCP Management and DNS management for the DHCP/DNS managers that process_management restarts.

  • The TFTP Directory for background on TFTP directory materialization and dynamic_tftp.