
Hello Caddy, Goodbye Nginx.
Why setting up SSL is so hard? Can’t SSL be automatically setup for our servers?
I just can’t understand and remember all the Nginx config syntax and file locations. I need an easy way out, please … Save me! Help me!
Worry not …
Caddy is here to save your ass 🍑
Let’s start by installing Caddy in our server
Install Caddy on Amazon Linux using yum
This will work with Fedora, RedHat and CentOS without the epel-7-$(arch)
argument.
If you’re not logged into your root account, then you’ll need to use
sudo
yum -y install yum-plugin-copr
yum -y copr enable @caddy/caddy epel-7-$(arch)
yum -y install caddy
To install Caddy on Debian, Ubuntu, Raspbian:
Use your apt package manager to import the keys and install Caddy
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Universal Linux / MacOS installer
If nothing works out you can install it using the webi.sh installer on any Linux or Mac operating system with curl:
curl -sS https://webi.sh/caddy | sh
Installation complete
Here is what you’ll see at the time of installation:
$ sudo yum -y install caddy
Copr repo for caddy owned by @caddy 1.0 kB/s | 2.5 kB 00:02
Last metadata expiration check: 0:00:01 ago on Fri Aug 11 19:44:42 2023.
Dependencies resolved.
============================================================================================================================================================================
Package Architecture Version Repository Size
============================================================================================================================================================================
Installing:
caddy x86_64 2.7.3-1.el7 copr:copr.fedorainfracloud.org:group_caddy:caddy 24 M
Transaction Summary
============================================================================================================================================================================
Install 1 Package
Total download size: 24 M
Installed size: 60 M
Downloading Packages:
caddy-2.7.3-1.el7.x86_64.rpm 7.5 MB/s | 24 MB 00:03
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Total 7.5 MB/s | 24 MB 00:03
Copr repo for caddy owned by @caddy 14 kB/s | 994 B 00:00
Importing GPG key 0xD605147E:
Userid : "@caddy_caddy (None) <@caddy#caddy@copr.fedorahosted.org>"
Fingerprint: 4A76 F92F F6D4 0440 F8FC 4F36 C521 91B1 D605 147E
From : https://download.copr.fedorainfracloud.org/results/@caddy/caddy/pubkey.gpg
Key imported successfully
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Running scriptlet: caddy-2.7.3-1.el7.x86_64 1/1
Installing : caddy-2.7.3-1.el7.x86_64 1/1
Running scriptlet: caddy-2.7.3-1.el7.x86_64 1/1
Verifying : caddy-2.7.3-1.el7.x86_64 1/1
============================================================================================================================================================================
Installed:
caddy-2.7.3-1.el7.x86_64
Complete!
Testing the Caddy server
Simply run caddy run
it’ll start it’s admin API server.
❯ caddy run
2023/08/11 20:06:49.972 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//127.0.0.1:2019", "//localhost:2019", "//[::1]:2019"]}
2023/08/11 20:06:49.972 INFO serving initial configuration
Calling Admin APIs
If you simply do curl localhost:2019
, you’ll get a 404 Page Not Found.
❯ curl localhost:2019
404 page not found
That’s unsettling! But it is expected.
You can learn more about Caddy admin API here.
But just to show you a demo, we can stop the server by making a POST
request to the /stop
endpoint.
curl -X POST "http://localhost:2019/stop"
You’ll see Caddy saying “byeee!! 👋” on the terminal where you ran Caddy:
2023/08/11 20:18:39.553 INFO admin.api received request {"method": "POST", "host": "localhost:2019", "uri": "/stop", "remote_ip": "127.0.0.1", "remote_port": "56018", "headers": {"Accept":["*/*"],"User-Agent":["curl/8.2.1"]}}
2023/08/11 20:18:39.553 WARN admin.api exiting; byeee!! 👋
2023/08/11 20:18:39.553 INFO admin stopped previous server {"address": "localhost:2019"}
2023/08/11 20:18:39.553 INFO admin.api shutdown complete {"exit_code": 0}
And the server will be gracefully shut down.
Use Caddy files to serve requests
Simply create a text file in /etc/caddy
called Caddyfile (no extensions)
sudo vim Caddyfile
And write the config below which will simply respond with a text:
localhost
respond "Hello, Daddy!"
Now save and exit from your editor :wq
.
You can see all the available options of Caddyfile here.
Start Caddy server with config
Run caddy start
to start your Caddy server.
Soon enough you’ll be greeted with a nice error saying listen tcp :443: bind: permission denied
.
2023/08/11 20:25:31.047 INFO using adjacent Caddyfile
2023/08/11 20:25:31.049 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2023/08/11 20:25:31.049 INFO http server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {"server_name": "srv0", "https_port": 443}
2023/08/11 20:25:31.049 INFO http enabling automatic HTTP->HTTPS redirects {"server_name": "srv0"}
2023/08/11 20:25:31.049 INFO tls.cache.maintenance started background certificate maintenance {"cache": "0xc000460cb0"}
2023/08/11 20:25:31.050 INFO tls.cache.maintenance stopped background certificate maintenance {"cache": "0xc000460cb0"}
Error: loading initial config: loading new config: http app module: start: listening on :443: listen tcp :443: bind: permission denied
Error: caddy process exited with error: exit status 1
This error is telling us that Caddy doesn’t have the permission to run on port 80 and 443.
Fix TCP permission denied error
We allow Caddy to run on ports below 1000 like 80 and 443 using setcap
sudo setcap cap_net_bind_service=+ep $(which caddy)
If there is no error, you’re good to go.
If you get an error saying: Failed to set capabilities on file node (No such file or directory)
You may get this error if the Caddy path is symbolic link AKA symlink.
To fix this simply find out where Caddy was installed run which caddy
, it’ll give you the full path of the binary.
To check if there is symlink, run ls -la
[ec2-user@ip-172-31-12-247 ~]$ ls -la .local/bin/
total 8
drwxr-xr-x. 2 ec2-user ec2-user 31 Aug 11 19:12 .
drwxrwxr-x. 5 ec2-user ec2-user 41 Aug 11 19:12 ..
lrwxrwxrwx. 1 ec2-user ec2-user 48 Aug 11 19:12 caddy -> /home/ec2-user/.local/opt/caddy-v2.7.3/bin/caddy
-rwxr-xr-x. 1 ec2-user ec2-user 5274 Aug 11 19:12 webi
Now you can put the actual path of Caddy instead of $(which caddy)
.
Stop and Uninstall Nginx
Sometimes it can be hard to say goodbyes, but we do it anyways. Don’t we? Let’s say goodbye to our beloved Nginx.
sudo systemctl stop nginx
Remove it from your system using the package manager:
sudo yum remove nginx
Feel free to use sudo apt remove nginx
on Debian based distros like Ubuntu.
Press Y
and hit Enter!
Dependencies resolved.
============================================================================================================================================================================
Package Architecture Version Repository Size
============================================================================================================================================================================
Removing:
nginx x86_64 1:1.22.1-1.amzn2023.0.3 @amazonlinux 152 k
Removing unused dependencies:
generic-logos-httpd noarch 18.0.0-12.amzn2023.0.3 @amazonlinux 21 k
gperftools-libs x86_64 2.9.1-1.amzn2023.0.2 @amazonlinux 1.4 M
libunwind x86_64 1.4.0-5.amzn2023.0.2 @amazonlinux 165 k
nginx-core x86_64 1:1.22.1-1.amzn2023.0.3 @amazonlinux 1.6 M
nginx-filesystem noarch 1:1.22.1-1.amzn2023.0.3 @amazonlinux 0
nginx-mimetypes noarch 2.1.49-3.amzn2023.0.3 @amazonlinux 43 k
Transaction Summary
============================================================================================================================================================================
Remove 7 Packages
Freed space: 3.4 M
Is this ok [y/N]: y
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Running scriptlet: nginx-1:1.22.1-1.amzn2023.0.3.x86_64 1/7
Removed "/etc/systemd/system/multi-user.target.wants/nginx.service".
Erasing : nginx-1:1.22.1-1.amzn2023.0.3.x86_64 1/7
Running scriptlet: nginx-1:1.22.1-1.amzn2023.0.3.x86_64 1/7
Erasing : generic-logos-httpd-18.0.0-12.amzn2023.0.3.noarch 2/7
Erasing : nginx-core-1:1.22.1-1.amzn2023.0.3.x86_64 3/7
Erasing : nginx-mimetypes-2.1.49-3.amzn2023.0.3.noarch 4/7
Erasing : gperftools-libs-2.9.1-1.amzn2023.0.2.x86_64 5/7
Erasing : nginx-filesystem-1:1.22.1-1.amzn2023.0.3.noarch 6/7
Erasing : libunwind-1.4.0-5.amzn2023.0.2.x86_64 7/7
Running scriptlet: libunwind-1.4.0-5.amzn2023.0.2.x86_64 7/7
Verifying : generic-logos-httpd-18.0.0-12.amzn2023.0.3.noarch 1/7
Verifying : gperftools-libs-2.9.1-1.amzn2023.0.2.x86_64 2/7
Verifying : libunwind-1.4.0-5.amzn2023.0.2.x86_64 3/7
Verifying : nginx-1:1.22.1-1.amzn2023.0.3.x86_64 4/7
Verifying : nginx-core-1:1.22.1-1.amzn2023.0.3.x86_64 5/7
Verifying : nginx-filesystem-1:1.22.1-1.amzn2023.0.3.noarch 6/7
Verifying : nginx-mimetypes-2.1.49-3.amzn2023.0.3.noarch 7/7
============================================================================================================================================================================
Run caddy start
Once you give Caddy the port permissions and start it again, you’ll get to see:
Successfully started Caddy (pid=138022) - Caddy is running in the background
If you run curl localhost
you’ll get nothing.
Wondering why?
Let’s run curl
again, with -i
flag:
❯ curl localhost -i
HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://localhost/
Server: Caddy
Date: Fri, 11 Aug 2023 20:44:26 GMT
Content-Length: 0
We get to see that there is permanent redirect with status 308.
By default Caddy will redirect the requests to HTTPS version
If you want to follow the redirect just add --location
flag at the end of the curl command.
❯ curl localhost --location
Hello, Daddy!
Finally, after all the hard work. We see Daddy! ~ from Caddy

Listen to Daddy Cool!
Install SSL certificate for your custom domain
Localhost is not something you can continue your life with, you and your user needs custom domain with SSL.
Make sure you have your domain pointed to your server IP.
Replace localhost
in the Caddyfile with your custom-domain.com
you can put your subdomains as well, like www
or app
.
Reload Caddy using caddy reload
command.
You’ll see a message saying “certificate obtained successfully”.
2023/08/11 19:32:01.414 INFO using adjacent Caddyfile
2023/08/11 19:32:01.415 WARN Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies {"adapter": "caddyfile", "file": "Caddyfile", "line": 4}
2023/08/11 19:32:01.416 INFO admin.api received request {"method": "POST", "host": "localhost:2019", "uri": "/load", "remote_ip": "127.0.0.1", "remote_port": "49072", "headers": {"Accept-Encoding":["gzip"],"Content-Length":["240"],"Content-Type":["application/json"],"Origin":["http://localhost:2019"],"User-Agent":["Go-http-client/1.1"]}}
2023/08/11 19:32:01.416 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2023/08/11 19:32:01.416 INFO http.auto_https server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {"server_name": "srv0", "https_port": 443}
2023/08/11 19:32:01.416 INFO http.auto_https enabling automatic HTTP->HTTPS redirects {"server_name": "srv0"}
2023/08/11 19:32:01.417 INFO http enabling HTTP/3 listener {"addr": ":443"}
2023/08/11 19:32:01.417 INFO http.log server running {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2023/08/11 19:32:01.417 INFO http.log server running {"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2023/08/11 19:32:01.417 INFO http enabling automatic TLS certificate management {"domains": ["store.curiosta.com"]}
2023/08/11 19:32:01.417 INFO http servers shutting down with eternal grace period
2023/08/11 19:32:01.418 INFO tls.obtain acquiring lock {"identifier": "store.curiosta.com"}
2023/08/11 19:32:01.418 INFO autosaved config (load with --resume flag) {"file": "/home/ec2-user/.config/caddy/autosave.json"}
2023/08/11 19:32:01.418 INFO admin.api load complete
[ec2-user@ip-172-31-12-247 ~]$ 2023/08/11 19:32:01.421 INFO tls.obtain lock acquired {"identifier": "store.curiosta.com"}
2023/08/11 19:32:01.421 INFO tls.obtain obtaining certificate {"identifier": "store.curiosta.com"}
2023/08/11 19:32:01.422 INFO admin stopped previous server {"address": "localhost:2019"}
2023/08/11 19:32:02.714 INFO http waiting on internal rate limiter {"identifiers": ["store.curiosta.com"], "ca": "https://acme-v02.api.letsencrypt.org/directory", "account": ""}
2023/08/11 19:32:02.715 INFO http done waiting on internal rate limiter {"identifiers": ["store.curiosta.com"], "ca": "https://acme-v02.api.letsencrypt.org/directory", "account": ""}
2023/08/11 19:32:03.264 INFO http.acme_client trying to solve challenge {"identifier": "store.curiosta.com", "challenge_type": "http-01", "ca": "https://acme-v02.api.letsencrypt.org/directory"}
2023/08/11 19:32:03.850 INFO http served key authentication {"identifier": "store.curiosta.com", "challenge": "http-01", "remote": "18.118.13.40:61778", "distributed": false}
2023/08/11 19:32:03.952 INFO http served key authentication {"identifier": "store.curiosta.com", "challenge": "http-01", "remote": "35.90.59.29:43326", "distributed": false}
2023/08/11 19:32:03.970 INFO http served key authentication {"identifier": "store.curiosta.com", "challenge": "http-01", "remote": "23.178.112.107:64588", "distributed": false}
2023/08/11 19:32:04.650 INFO http.acme_client authorization finalized {"identifier": "store.curiosta.com", "authz_status": "valid"}
2023/08/11 19:32:04.651 INFO http.acme_client validations succeeded; finalizing order {"order": "https://acme-v02.api.letsencrypt.org/acme/order/1253415916/200955216196"}
2023/08/11 19:32:05.952 INFO http.acme_client successfully downloaded available certificate chains {"count": 2, "first_url": "https://acme-v02.api.letsencrypt.org/acme/cert/03db8f52dbe2724b7686405505656f1f0bde"}
2023/08/11 19:32:05.952 INFO tls.obtain certificate obtained successfully {"identifier": "store.curiosta.com"}
2023/08/11 19:32:05.952 INFO tls.obtain releasing lock {"identifier": "store.curiosta.com"}
Visit your website from your browser or curl it to see if you’re seeing daddy or not.
If you have followed all the steps carefully you should see the Hello Daddy.
You don’t need to manually renew or do anything for SSL certificate.
Enable Caddy service to keep it running in background
You don’t want your server to die the moment you close your terminal, do you? No you don’t.
If you have installed Caddy using your system package manager, you should have caddy.service
installed in your system.
All you need to do it enable it:
sudo systemctl enable --now caddy
# to enable api service
sudo systemctl enable --now caddy-api
With this the Caddy service will be enabled and started immediately and automatically start on boot as well.
If you don’t have the service already, you can manually create by following this guide.
Now your Caddy server will keep running even when you sleep unless your server takes a nap too.
Use Caddy as reverse proxy
Most of us don’t just host static sites we love to use NodeJS, Python and Java to run our servers behind a proxy.
Caddy can definitely do that too.
Edit your Caddyfile with reverse_proxy
param and pass your application port.
example.com
reverse_proxy :9000 # Whatever your port is
Use Caddy for serving static files
S3 is a better choice but hey, we love EC2 and our VPS more than anything. Okay I won’t lecture you anymore, take this:
example.com
root * /home/me/mysite
file_server
Multiple paths for API and File server
We usually have a /api
for doing API calls and we serve static content like HTML on the root, you can do this by following the example below:
localhost
encode zstd gzip # enable compression
root * /home/me/mysite
file_server
reverse_proxy /api/* 127.0.0.1:9005
That’s all about Caddy?
No! There is more. Always has been.

You can learn more about Caddy from their website.
They have way more features like dynamic HTML templating, multiple server blocks, CLI guide and much more. I just couldn’t cover everything in this blog, but this should give you a kick-start or just enough guidance to keep your server running with HTTP/3 and free SSL.
I wish you all the best with your new Daddy Caddy ;)
Don’t forget to leave your thoughts in the comments.