Using Caddy and .localhost for Local HTTPS and Subdomain Support on Linux
Replacing puma-dev when migrating from macOS to Linux
Recently, I made the switch from my old MacBook to a new Framework 13 Laptop, taking a small step out of the Apple ecosystem. For my new machine, I chose to install Bluefin, an opinionated Linux distribution based on Fedora Silverblue, known for its use of containers and Homebrew. I was pleasantly surprised by how straightforward the initial setup was. However, I encountered a significant hurdle that’s common when moving from macOS to Linux: how to replicate the functionality of puma-dev.
Puma-dev is a fantastic tool for web development, providing local domains with HTTPS and subdomain support out of the box. It’s particularly useful for applications like my main project, Librario, which heavily relies on subdomains. While puma-dev works perfectly on macOS and theoretically supports Linux, I struggled to get it running properly on my new Linux setup. This led me to search for an alternative solution that would provide similar functionality in my new environment.
After some research and experimentation, I found an elegant solution using two key components:
.localhost
top-level domain (TLD), instead of the .test
TLD used by puma-dev.The switch to .localhost
is crucial because it’s treated as a special top-level domain which itself, including its subdomains, always resolves to 127.0.0.1.
This behavior simplifies our setup significantly.
Here’s how I set it up:
Thanks to Bluefin’s support for Homebrew (which felt familiar coming from macOS), installing Caddy was as simple as running:
brew install caddy
In my previous setup with puma-dev, I had the following configuration:
echo 5000 > ~/.puma-dev/library # enabled https://*.library.test
echo 9000 > ~/.puma-dev/s3.library # enabled https://s3.library.test
echo 9001 > ~/.puma-dev/admin.s3.library # enabled https://admin.s3.library.test
echo 8025 > ~/.puma-dev/mailcatcher.library # enabled https://mailcatcher.library.test
To replicate this functionality with Caddy, I created a Caddyfile
to handle my local development needs.
This configuration essentially translates my previous puma-dev setup to Caddy.
Here’s what it looks like:
# Specific subdomains
s3.library.localhost {
reverse_proxy localhost:9000
}
admin.s3.library.localhost {
reverse_proxy localhost:9001
}
mailcatcher.library.localhost {
reverse_proxy localhost:8025
}
# Default catch-all for *.library.localhost
*.library.localhost {
reverse_proxy localhost:5000
}
This configuration allows Caddy to act as a reverse proxy, forwarding requests to the appropriate local ports based on the subdomain.
Note that all domains end with .localhost
, which ensures they resolve to 127.0.0.1.
By default, non-root processes can’t bind to ports below 1024 on Linux, a security measure that differs from macOS.
To allow Caddy to use ports 80 and 443, I used the setcap
command:
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f $(which caddy))
The readlink -f
command is necessary here because which caddy
returns a symlink path.
The readlink -f
resolves this symlink to the actual executable path.
This is important because setcap
needs to modify the actual binary, not the symlink.
However, this also means that this command may need to be re-applied when caddy gets updated.
With the configuration in place, I started Caddy as a service using Homebrew:
brew services start caddy
It’s worth noting that when Caddy starts for the first time, it attempts to install its root certificate into the system’s local trust store(s). This automatic installation of the root certificate simplifies our HTTPS setup significantly.
To verify that everything was working correctly, I used curl:
curl https://foo.library.localhost
This command worked out of the box, indicating that Caddy was successfully generating and using a self-signed certificate, installed the root certificate on the system and that .localhost
was correctly resolving to 127.0.0.1.
While curl worked fine due to Caddy’s automatic certificate installation, I noticed that Firefox (installed as a Flatpak) didn’t trust the self-signed certificate generated by Caddy. This is because Flatpak applications run in a sandboxed environment with their own certificate store, separate from the system’s. As a result, the Flatpak version of Firefox doesn’t have access to the system’s trust store where Caddy’s self-signed certificate is recognized.
To resolve this, I needed to add Caddy’s root certificate to Firefox’s certificate store manually:
/home/linuxbrew/.linuxbrew/var/lib/caddy/pki/authorities/local/root.crt
In Firefox, I navigated to Settings > Privacy & Security > Certificates > View Certificates.
In the Certificate Manager, under the “Authorities” tab, I clicked “Import” and selected the Caddy root certificate.
After restarting Firefox, it now trusts the Caddy-generated certificates for my local domains.
With this setup, I now have a functioning local environment on Linux that supports HTTPS and subdomains, crucial for developing and testing my SaaS application.
The combination of Caddy and the .localhost
TLD provides a robust alternative to puma-dev on Linux systems.
While it required a bit more manual configuration than I was used to with puma-dev on macOS, this solution offers great flexibility and works seamlessly across different Linux distributions.
The use of .localhost
as the TLD simplifies DNS resolution, making the setup more straightforward and reliable.