Skip to content

Conversation

@i5okie
Copy link

@i5okie i5okie commented Nov 19, 2025

Add rootless Docker images for enhanced security and Kubernetes/OpenShift compatibility

Purpose

This PR adds rootless variant of the Caddy Docker images that run as a non-root user (UID 1001), making them suitable for security-constrained environments like Kubernetes and OpenShift, while remaining a drop-in replacement for the alpine images.

Why rootless?

Security best practices: Running containers as root is discouraged in production environments. Non-root containers provide defense-in-depth by limiting the impact of potential container breakouts or exploits.

Kubernetes/OpenShift requirements: Many Kubernetes clusters enforce Pod Security Standards that prohibit root containers. OpenShift, in particular, assigns arbitrary UIDs to containers by default and requires image to support this pattern.

Port restrictions: Non-root users cannot bind to privileged ports (< 1024). Using ports 80 and 443 in Kubernetes is problematic anyway since:

  • Services and Ingress controllers handle external traffic routing
  • Internal container ports are mapped through service definitions
  • Non-privileged ports (8080, 8443) are the standard convention

What's included

New image variant:

  • caddy:rootless - Rootless runtime image (ports 8080, 8443, 2019)

Key features:

  • Runs as UID 1001 with GID 0 (root group) for OpenShift compatibility
  • Automatically uses ports 8080/8443 via environment variables
  • Drop-in replacement for the standard image - just change the tag
  • All directories writable by group 0 for arbitrary UID support
  • Uses the standard Caddyfile with automatic port substitution

Implementation details:
The rootless templates are based on the standard alpine templates with these modifications:

  • Removed setcap capability (not needed for non-privileged ports)
  • Added non-root user creation with adduser -D -u 1001 -g 0
  • Set proper ownership (chown 1001:0) and group permissions (chmod g+w) on all Caddy directories
  • Downloads the standard Caddyfile and uses sed to replace :80 with :{$CADDY_HTTP_PORT:8080}
  • Sets CADDY_HTTP_PORT=8080 and CADDY_HTTPS_PORT=8443 environment variables
  • Changed exposed ports from 80/443 to 8080/8443
  • Added USER 1001 directive to run as non-root
  • Users can mount custom Caddyfiles that reference these env vars or hardcode ports

Testing

Built and tested locally - serves the welcome page on port 8080 as expected. The image runs without root privileges and properly serves static content.

@i5okie
Copy link
Author

i5okie commented Nov 24, 2025

Is anyone available to take a look at this and offer feedback? There are several issues discussing the need for a rootless image.

Our team is working on several Hyperledger and OpenWallet Foundation projects utilizing Caddy server as a reverse proxy.
I've discovered that most of our deployments use older Caddy images and are working properly in a government maintained OpenShift platform. I'm working on creating helm charts for some of these projects. When attempting to use the latest caddy docker images, the deployments fail due to strict SCC rules in OpenShift. When developing helm charts which are meant to be used by the wider community, and be compatible with the government platform, using an initContainer to copy the binary into an arbitrary directory to bypass the issues, is not ideal. It does not look like a professional and robust solution to the problem. This PR should solve this issue. Not just for our projects, but for the wider community overall. And hopefully boost Caddy Server's use across the community :).

@francislavoie
Copy link
Member

Sorry for the wait. Thanks for this, I think it's the way we should go (separate tagged variant for rootless), rather than try to force the main one to support rootless (which is what prior discussion has always suggested).

There's still some things I think won't work well though, like the pki app trying to install its own root cert into /etc/ssl/certs which would require root. So that means if trying to use the rootless Caddy instance as an ACME server for mTLS, it wouldn't work well since it wouldn't trust connections made to servers which use a cert signed by Caddy's internal CA.

I want to get @hairyhenderson's thoughts on this before moving ahead with it though.

@hairyhenderson
Copy link
Contributor

Overall I'm supportive of this - @francislavoie and I have chatted about this and I'll let him make a few comments and get this merged. Thanks for your patience, @i5okie!

@i5okie i5okie changed the title feat: add rootless images feat: add rootless image Jan 13, 2026
@i5okie i5okie force-pushed the feat/add-rootless-images branch from caf8dc1 to d9685cb Compare January 15, 2026 18:50
Copy link
Member

@francislavoie francislavoie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks good to me. We'll need to generate the 2.11 ones as well (but I can do that after merge, that's fine)

Just as a last sanity check, @tianon do you have any objections to us adding another image variant for rootless usage? It's tricky with a webserver cause low ports as well as filesystem permissions, so I think this is the best of both worlds instead of trying to support both root and rootless in one image.

i5okie and others added 7 commits January 16, 2026 07:24
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
@francislavoie francislavoie force-pushed the feat/add-rootless-images branch from d9685cb to 7e724d4 Compare January 16, 2026 12:26
@francislavoie
Copy link
Member

I rebased on master, added 2.11, and added curl as a dep, and normalized the ENV syntax

@tianon
Copy link

tianon commented Jan 23, 2026

My knee-jerk reaction is definitely that this is weird (both it being a whole separate variant and it being a complete copy not just a few configuration/metadata tweaks) 😅

However, I clearly need to dig in to the details more to make sure I understand how we ended up here so I can provide more meaningful thoughts 🙇 ❤️

@tianon
Copy link

tianon commented Jan 24, 2026

Heh, moby/moby#8460 -- this issue haunts me forever 😂

The privileged ports problem in Docker itself was fixed in moby/moby#41030, way back in Docker 20.10 😅

See also kubernetes/kubernetes#102612 for where Kubernetes has spent a lot of time discussing this same change.

More relevantly, containerd enabled this by default in containerd/containerd#9348, which is containerd 2.0+ (and I believe that applies transitively to Kubernetes deployments using containerd also).

Regarding the filesystem access, saying that users who want to run as non-root need to deal with filesystem permissions is really reasonable, but pre-seeding with directories at mode 1777 is extremely reasonable, and I think mostly solves the problem in a way that's reasonably safe (as I noted over in #287 (comment) 😅).

Just to illustrate, here's an example of me running caddy:latest as UID 1234 and GID 5678 without really doing anything extra (and adding --security-opt no-new-privileges to make sure we're completely locked down and can't possibly be hitting setcap or anything):

$ docker run -it --rm --pull=always --user 1234:5678 --security-opt no-new-privileges --tmpfs /data:mode=1777 --tmpfs /config:mode=1777 caddy
latest: Pulling from library/caddy
Digest: sha256:2adb640cdc0ce1d8870887c30af1e21edfb3cdfd8433431b2a15f40119a7d654
Status: Image is up to date for caddy:latest
2026/01/23 23:43:19.569	INFO	maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
2026/01/23 23:43:19.569	INFO	GOMEMLIMIT is updated	{"package": "github.com/KimMachineGun/automemlimit/memlimit", "GOMEMLIMIT": 60335316172, "previous": 9223372036854775807}
2026/01/23 23:43:19.569	INFO	using config from file	{"file": "/etc/caddy/Caddyfile"}
2026/01/23 23:43:19.570	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2026/01/23 23:43:19.570	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//[::1]:2019", "//127.0.0.1:2019", "//localhost:2019"]}
2026/01/23 23:43:19.570	WARN	http.auto_https	server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server	{"server_name": "srv0", "http_port": 80}
2026/01/23 23:43:19.571	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/01/23 23:43:19.571	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/01/23 23:43:19.571	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2026/01/23 23:43:19.571	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc0003f5480"}
2026/01/23 23:43:19.571	INFO	autosaved config (load with --resume flag)	{"file": "/config/caddy/autosave.json"}
2026/01/23 23:43:19.571	INFO	serving initial configuration
2026/01/23 23:43:19.571	INFO	tls	cleaning storage unit	{"storage": "FileStorage:/data/caddy"}
2026/01/23 23:43:19.571	INFO	tls	finished cleaning storage units

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants