Confusing behaviour when starting a server on the admin port
With this:
% cat Caddyfile
http://localhost:2019 {
file_server browse
}
% caddy run
2025/06/08 15:17:36.279 INFO using adjacent Caddyfile
2025/06/08 15:17:36.279 INFO adapted config to JSON {"adapter": "caddyfile"}
2025/06/08 15:17:36.279 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2025/06/08 15:17:36.280 INFO tls.cache.maintenance started background certificate maintenance {"cache": "0xc00037da80"}
2025/06/08 15:17:36.280 WARN http HTTP/2 skipped because it requires TLS {"network": "tcp", "addr": ":2019"}
2025/06/08 15:17:36.280 WARN http HTTP/3 skipped because it requires TLS {"network": "tcp", "addr": ":2019"}
2025/06/08 15:17:36.280 INFO http.log server running {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2025/06/08 15:17:36.280 INFO autosaved config (load with --resume flag) {"file": "/home/martin/.config/caddy/autosave.json"}
2025/06/08 15:17:36.280 INFO serving initial configuration
2025/06/08 15:17:36.281 INFO tls storage cleaning happened too recently; skipping for now {"storage": "FileStorage:/home/martin/.local/share/caddy", "instance": "cf308ad4-1b49-40a8-aca4-dd95b219400e", "try_again": "2025/06/09 15:17:36.281", "try_again_in": 86399.99999964}
2025/06/08 15:17:36.281 INFO tls finished cleaning storage units
2025/06/08 15:17:39.664 INFO admin.api received request {"method": "GET", "host": "localhost:2019", "uri": "/", "remote_ip": "127.0.0.1", "remote_port": "53406", "headers": {"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept-Language":["en-IE,en;q=0.5"],"Connection":["keep-alive"],"Priority":["u=0, i"],"Sec-Fetch-Dest":["document"],"Sec-Fetch-Mode":["navigate"],"Sec-Fetch-Site":["none"],"Sec-Fetch-User":["?1"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0"]}}
Going to http://localhost:2019 gives a 404.
The reason is that localhost:2019 is also used as an "admin port" by default. Simply using another port or admin off fixes it.
This is kind of obvious in hindsight, but not being familiar with Caddy this took me quite some time to figure out. I just saw port 2019 referenced somewhere and figured "well, that's a free port on my system, as good as any other".
Trying to use the admin port should probably give an error, or at least a big warning.
Ha, yeah I can see why that's confusing.
We can probably add a warning in the logs.
Hi, it's my first time contributing to this project — would it be okay if I work on this issue?
I took a look at the Validate() method in caddy/modules/caddyhttp/app.go, and I believe a viable solution would be to add the current admin address to the lnAddrs map with a srvName like "admin". That way, port conflicts such as reusing :2019 can be detected earlier with a proper error message.
Would that be an acceptable approach? If so, could you advise how to access the current admin configuration (status and bind address) from within this method?
That wouldn't work because admin is higher level than the caddyhttp app, caddyhttp does not know and should not know about admin.
It might need to be something more like a function near caddy.Listen() (i.e. listeners.go) which checks if a given port is available to use, so caddyhttp could call it to ask "can I use this port" during Validate. Something like CanListen(port int), since admin.go is in the same package it can reject using the admin port. Maybe too simplistic, I dunno, only spent 2 minutes thinking about it.
Thanks for the feedback!
I'll proceed by forking the repository and creating a dedicated branch for this issue. My plan is to:
- Follow TDD: Start by writing a test that demonstrates the port conflict.
- Implement CanListen: Create the
CanListenfunction as recommended. - Integrate: Use this function to validate the port during Caddy's configuration loading.
I'll post here again if I encounter any specific questions or need clarification on implementation details. Once I have a functional branch, I'll open a draft PR to get early feedback.
Does this approach align with the project's best practices, or is there anything else I should be aware of before proceeding?
Hi there!
I've opened a draft Pull Request (PR) related to this issue to kick off a discussion about Admin API port conflict validation.
The PR description details the proposed solution and my key architectural questions regarding the best way forward, especially concerning:
- How to robustly compare
NetworkAddressobjects. - How to dynamically get the Admin API's configured address.
- Whether the proposed approach aligns with Caddy's design principles.
I'd really appreciate it if you could take a look and provide some guidance on the direction.
Thanks!
Hi @francislavoie / @mholt ,
I'm new to Caddy and I was looking at this issue. I could finally sent a PR #7116. Please kindly review and let me know if any changes have to be made.
Thank you!
Hi, can i work on this?
Hi, can i work on this?
Can you please share your implementation plan?
Hi! I'm reproducing this issue and can confirm the problem exists, but it's actually even worse than described in the original report. My test setup:
Environment: Windows, latest master branch. Config: Same as original issue.
What I observed:
Server starts without any errors: INFO admin admin endpoint started {"address": "localhost:2019"} INFO http.log server running {"name": "srv0"}
But Admin API is completely broken: curl http://localhost:2019/config/ returns: 404 Not Found
HTTP server works instead: curl http://localhost:2019/ returns: HTML file browser (works fine)
The real problem: Admin API becomes completely inaccessible when HTTP server uses the same port.
Proposed solution:
Add validation in replaceLocalAdminServer() that runs after the admin address is fully resolved (including CADDY_ADMIN env var) but before any servers start. The validation will:
- Parse HTTP app configuration to get all server listen addresses
- Use
NetworkAddress.overlaps()method to detect conflicts (handles port ranges, localhost variants, wildcards). I will add this method. - Fail fast with clear error message identifying the conflicting server
Example error output: Error: admin port conflict: HTTP server 'srv0' listener address 'localhost:2019' conflicts with admin API address 'localhost:2019'
I'll add tests covering various scenarios: localhost vs 127.0.0.1, wildcards, port ranges, IPv6, environment overrides, etc.
Would appreciate feedback on this approach before implementation!