Skip to content

Container Hosting (Docker extension)

The Docker extension brings per-website container hosting to the panel — built on rootless Podman, isolated and with no Docker daemon by default. It is installed as an opt-in extension via the marketplace; the panel core stays untouched until it is enabled.

Architecture in one sentence

A container is a new website app-type (alongside PHP, Node.js, Python, Static), bound to exactly one website. It runs rootless as that website's Linux user, is reachable only through the website's own nginx domain proxy (port bound internally to 127.0.0.1), and is managed from a dedicated Containers sidebar page.

Installation & permission

  1. Enable the extension (marketplace) — installs the runtime only on the selected servers: Podman + the rootless helpers (uidmap, fuse-overlayfs, passt) and the Grype CVE scanner. This is what makes "no Docker daemon by default" true.
  2. Package permission — in the package the admin decides with Allow container hosting (perm_docker) whether a customer may use containers, plus limits: Containers, RAM per container (MB), CPUs per container, Disk per container (GB) (0 = unlimited).

Running a container

  1. Create a website of type "Container" (under Websites). It automatically gets an internal port and an nginx proxy rule: domain → 127.0.0.1:<port>.
  2. On the Containers page, deploy an image (image from an allowed registry, the container's internal port, optional environment variables). Then: start / stop / restart / remove, live status/health, and logs.

Security model (rootless, honest)

Every container runs with:

  • Rootless / user namespace per website (its own subuid/subgid range) — never host root.
  • --cap-drop=ALL plus a minimal allowlist (CHOWN, SETUID, SETGID, NET_BIND_SERVICE), --security-opt=no-new-privileges.
  • seccomp default-deny profile (Podman's default, additionally pinned explicitly).
  • Resource limits from the package (--memory, --cpus, --pids-limit).
  • Network: port published on 127.0.0.1 only (reachable only through the website's own domain proxy); host isolation via --no-map-gw (the container cannot reach host services through the gateway — SSRF / lateral-movement guard); the existing SMTP egress block (port 25) stops direct-to-MX spam.

Limits of the rootless model (no sugar-coating)

  • AppArmor is not applied to containers: rootless Podman cannot load an AppArmor profile (missing CAP_MAC_ADMIN). Kernel-level confinement is provided by seccomp + caps + user namespace.
  • Shared kernel: like everything on a single-kernel system, rootless containers share the kernel — a kernel privesc CVE affects all. Stronger isolation (gVisor/Sysbox) is a possible Phase-2 option with a performance cost.

CVE pull-gate (Grype)

Before starting, every image is scanned with Grype; the result (counts per severity) rides along on every deploy response. The panel setting container_cve_gate controls the behaviour:

  • off (default) — scan and show only, never block.
  • critical — block the deploy when critical CVEs are present.
  • fixable-critical — block only when critical CVEs with an available fix are present.

Admins/resellers can override a block with force; customers cannot. When an image carries critical CVEs, the container_vulnerability_detected notification fires to admin and customer.

The Security Advisor → CRA tab shows a "Container CVEs" card that re-scans every deployed container image with Grype and lists the per-website severity counts. The card only appears when the Docker extension is enabled.

Dependencies & later-added servers

Enabling the extension fans out to every active server and installs the runtime idempotently (Podman + rootless helpers + Grype + subordinate-id ranges). A server added after the extension was enabled does not get this fan-out — so the agent self-installs Podman on the first container deploy (and backfills subuid/subgid) instead of failing. The result: every server that ever hosts a container has the full runtime, whether it was present at enable-time or added later.

Backups & cleanup

  • The definition (image + env) is stored on the website → part of the panel/DB backup and pre-filled in the UI.
  • Container data (volumes/storage) lives under the website's home → captured by the website's normal file backup.
  • Cleanup: deleting the website (or the customer) cleanly stops+removes the container, disables the lingering session, and frees the subuid range — no orphaned containers.

Registry allowlist & image management

  • Curated registry allowlist — the extension setting container_registry_allowlist (comma-separated, empty = any) restricts which registries images may come from. A deploy from a registry not on the list is rejected (registry_not_allowed). Docker's implicit docker.io rule is applied (nginxdocker.io).
  • Image management — the container page lists the local images per website and offers "Clean images" to remove unused/dangling images, freeing disk that counts against the package quota.

Not yet included (Phase 2)

  • Consistent volume-snapshot backups + automatic re-deploy on restore (today: clean re-deploy from the stored definition; rootless storage doesn't restore 1:1 across machines).
  • Optional gVisor/Sysbox for stronger isolation.