Caddy: Difference between revisions
imported>Onny Add example for PHP Fastcgi |
|||
(31 intermediate revisions by 10 users not shown) | |||
Line 1: | Line 1: | ||
[https://caddyserver.com/ Caddy] is | [https://caddyserver.com/ Caddy] is an efficient, HTTP/2 capable web server that can serve static and dynamic web pages. | ||
It can also be a reverse proxy to serve multiple web services under one server. Its main features are its simple config setup and automatic HTTPS: It will automatically request and renew a LetsEncrypt certificate so that users of your service get a Browser-trusted and secure connection. | |||
== | == Setup == | ||
To try out Caddy add the following minimal example to your [[NixOS modules | NixOS module]]: | |||
{ | <syntaxhighlight lang="nix"> | ||
services.caddy = { | { | ||
# ... | |||
services.caddy = { | |||
http://localhost | enable = true; | ||
virtualHosts."localhost".extraConfig = '' | |||
respond "Hello, world!" | |||
''; | |||
}; | |||
} | |||
</syntaxhighlight> | |||
This snippet will let Caddy respond on <code>http://localhost</code> and <code>https://localhost</code> with a dummy text "Hello world!". When no port is mentioned on virtualhost like just <code>localhost</code> instead of <code>localhost:8080</code>, Caddy listens on <code>80</code> and <code>443</code> by default and redirects requests from port 80 (unsecured) to 443 (secured). | |||
==== Check http connection ==== | |||
</ | You can use <code>curl</code> to test the http connections: | ||
== | |||
<syntaxhighlight lang="bash"> | |||
$ curl localhost -i -L -k | |||
HTTP/1.1 308 Permanent Redirect | |||
Location: https://localhost/ | |||
.. | |||
HTTP/2 200 | |||
alt-svc: h3=":443"; ma=2592000 | |||
content-type: text/plain; charset=utf-8 | |||
... | |||
Hello, world! | |||
</syntaxhighlight> | |||
Here you can see that Caddy automatically redirects from an unsecure http://localhost to a secure https://localhost call. | |||
For local addresses like "localhost" Caddy always generates and uses a self-signed certificate, which curl correctly doesn't trust; use the <code>-k</code> flag to ignore that. | |||
==== Check http(s) connection ==== | |||
When virtualhost and "real" host aren't the same it gets complicated with HTTPS, so the following curl command works: | |||
<syntaxhighlight lang="bash"> | |||
$ curl --connect-to <virtualhost>:443:<realhost>:443 https://<virtualhost> -k | |||
Hello, world! | |||
</syntaxhighlight> | |||
Curl will set <code>Host</code> header and TLS <code>SNI</code> in the request to <code><virtualhost></code> as desired by Caddy, but will make the actual request against the <code><realhost></code>, e.g. a load-balancer or ingress-controller. | |||
Alternatively with http and automatic redirects to https you can extend that call: | |||
<syntaxhighlight lang="bash"> | |||
$ curl --connect-to <virtualhost>:80:<realhost>:80 --connect-to <virtualhost>:443:<realhost>:443 https://<virtualhost> -k -L | |||
Hello, world! | |||
</syntaxhighlight> | |||
* [https://curl.se/docs/manpage.html#--connect-to curl connect-to documentation] | |||
* [https://www.claudiokuenzler.com/blog/693/curious-case-of-curl-ssl-tls-sni-http-host-header Curl on HTTPS, SNI, Host] | |||
* [https://github.com/caddyserver/caddy/issues/2656#issuecomment-1627342466 curl to Caddy over HTTPS] | |||
== Typical configurations == | |||
=== SSL === | === SSL === | ||
Line 28: | Line 69: | ||
Caddy will automatically try to acquire SSL certificates for the specified domain, in this example <code>example.org</code>. This requires you to configure the DNS records of your domain correctly, which should point to the address of your Caddy server. The [[firewall]] ports <code>80</code> and <code>443</code> needs to be opened. | Caddy will automatically try to acquire SSL certificates for the specified domain, in this example <code>example.org</code>. This requires you to configure the DNS records of your domain correctly, which should point to the address of your Caddy server. The [[firewall]] ports <code>80</code> and <code>443</code> needs to be opened. | ||
<syntaxhighlight lang="nix"> | |||
caddy = { | services.caddy = { | ||
enable = true; | enable = true; | ||
virtualHosts."example.org".extraConfig = '' | virtualHosts."example.org".extraConfig = '' | ||
Line 41: | Line 82: | ||
} | } | ||
''; | ''; | ||
}; | }; | ||
</ | networking.firewall.allowedTCPPorts = [ 80 443]; | ||
</syntaxhighlight> | |||
=== Reverse proxy === | === Reverse proxy === | ||
The following snippet creates a reverse proxy for the domain <code>example.org</code>, redirecting all requests to <code><nowiki>http://10.25.40.6</nowiki></code> | The following snippet creates a reverse proxy for the domain <code>example.org</code>, redirecting all requests to <code><nowiki>http://10.25.40.6</nowiki></code> | ||
<syntaxhighlight lang="nix"> | |||
caddy = { | services.caddy = { | ||
enable = true; | enable = true; | ||
virtualHosts."example.org".extraConfig = '' | virtualHosts."example.org".extraConfig = '' | ||
reverse_proxy http://10.25.40.6 | reverse_proxy http://10.25.40.6 | ||
''; | ''; | ||
}; | virtualHosts."another.example.org".extraConfig = '' | ||
</ | reverse_proxy unix//run/gunicorn.sock | ||
''; | |||
}; | |||
</syntaxhighlight>In case you would like to forward the real client IP of the request to the backend, add following headers<syntaxhighlight lang="nix"> | |||
services.caddy = { | |||
virtualHosts."example.org".extraConfig = '' | |||
reverse_proxy http://10.25.40.6 { | |||
header_down X-Real-IP {http.request.remote} | |||
header_down X-Forwarded-For {http.request.remote} | |||
} | |||
''; | |||
}; | |||
</syntaxhighlight>Fur further reverse proxy configuration, see [https://caddyserver.com/docs/quick-starts/reverse-proxy upstream documentation]. | |||
=== Redirect === | === Redirect === | ||
Permanent redirect of <code>example.org</code> and <code>old.example.org</code> to <code>www.example.org</code> | |||
<syntaxhighlight lang="nix> | <syntaxhighlight lang="nix"> | ||
caddy | services.caddy = { | ||
enable = true; | |||
virtualHosts."example.org" = { | |||
extraConfig = '' | |||
redir https://www.example.org{uri} permanent | |||
''; | |||
serverAliases = [ "old.example.org" ]; | |||
}; | }; | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 71: | Line 128: | ||
=== PHP FastCGI === | === PHP FastCGI === | ||
Serving a PHP application in <code>/var/www</code> on | Serving a PHP application in <code>/var/www</code> on http://localhost . | ||
<syntaxhighlight lang="nix> | <syntaxhighlight lang="nix"> | ||
services.caddy = { | services.caddy = { | ||
enable = true; | enable = true; | ||
Line 87: | Line 144: | ||
You'll need a [[Phpfpm|PHP-FPM]] socket listening on Unix socket path <code>/var/run/phpfpm/localhost.sock</code>. | You'll need a [[Phpfpm|PHP-FPM]] socket listening on Unix socket path <code>/var/run/phpfpm/localhost.sock</code>. | ||
=== Passing environment variable secrets/configuring acme_dns === | |||
To prevent any secrets from being put in the nix store (any NixOS setting that writes a config in the Nix store will expose any secret in it), you can use the following setting<syntaxhighlight lang="nixos"> | |||
services.caddy = { | |||
enable = true; | |||
globalConfig = '' | |||
acme_dns PROVIDER { | |||
api_key {$APIKEY} | |||
api_secret_key {$APISECRETKEY} | |||
} | |||
''; | |||
}; | |||
systemd.services.caddy.serviceConfig.EnvironmentFile = ["/path/to/envfile"]; | |||
</syntaxhighlight>And then at '''/path/to/envfile''':<syntaxhighlight> | |||
APIKEY=YOURKEY | |||
APISECRETKEY=OTHERKEY | |||
</syntaxhighlight> | |||
=== Adding plug-ins === | |||
There are many Issues/PR's about allowing Caddy to be built with plug-ins. Until then, you can use this workaround:<syntaxhighlight lang="nixos"> | |||
services.caddy = { | |||
enable = true; | |||
package = (pkgs.callPackage "${builtins.fetchurl https://raw.githubusercontent.com/jpds/nixpkgs/a33b02fa9d664f31dadc8a874eb1a5dbaa9f4ecf/pkgs/servers/caddy/default.nix}" { | |||
externalPlugins = [ | |||
{ name = "caddy-dns/porkbun"; repo = "github.com/caddy-dns/porkbun"; version = "4267f6797bf6543d7b20cdc8578a31764face4cf"; } | |||
# Set version to target repository commit hash | |||
]; | |||
vendorHash = ""; # Add this as explained in https://github.com/NixOS/nixpkgs/pull/259275#issuecomment-1763478985 | |||
}); | |||
globalConfig = '' | |||
... | |||
''; | |||
} | |||
</syntaxhighlight> | |||
== Debugging == | |||
=== Check used ports === | |||
To check if Caddy is running and listening as configured you can run <code>netstat</code>: | |||
<syntaxhighlight lang="bash"> | |||
$ netstat -tulpn | |||
Active Internet connections (only servers) | |||
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name | |||
tcp 0 0 127.0.0.1:2019 0.0.0.0:* LISTEN 1202/caddy | |||
tcp6 0 0 :::80 :::* LISTEN 1202/caddy | |||
tcp6 0 0 :::443 :::* LISTEN 1202/caddy | |||
udp6 0 0 :::443 :::* 1202/caddy | |||
</syntaxhighlight> | |||
The tcp (ipv4) socket port 2019 is Caddy's management endpoint, for when you want manage its config via web REST calls instead of Nix (ignore). | |||
The tcp6 (an ipv6 socket that also listens on ipv4) socket on port 80 (HTTP) and 443 (HTTPS) indicate that our virtualhost config was used. | |||
=== Virtualhost and real host not identical === | |||
When you connect to Caddy must ensure that the "Host" header matches the virtualhost entry of Caddy. For example, when testing locally a config like | |||
<syntaxhighlight lang="nix"> | |||
services.caddy = { | |||
enable = true; | |||
virtualHosts."example.org".extraConfig = '' | |||
respond "Hello, world!" | |||
''; | |||
}; | |||
</syntaxhighlight> | |||
you must send the request against "localhost" and manually override the host header to "example.org": | |||
<syntaxhighlight lang="bash"> | |||
$ curl localhost -i -H "Host: example.org" | |||
HTTP/1.1 308 Permanent Redirect | |||
Connection: close | |||
Location: https://example.org/ | |||
Server: Caddy | |||
... | |||
</syntaxhighlight> | |||
Above you also see the redirect from http://localhost to https://example.org; Caddy always redirects from the unsecure to the secure port of your virtualhost. | |||
If the response is empty, try setting a port number like 80 and/or try a local TLS security certificate instead of global LetsEncrypt: | |||
<syntaxhighlight lang="nix"> | |||
services.caddy = { | |||
enable = true; | |||
virtualHosts."example.org:80".extraConfig = '' | |||
respond "Hello, world!" | |||
tls internal | |||
''; | |||
}; | |||
</syntaxhighlight> | |||
With "tls internal" Caddy will generate a local certificate, which is good when testing locally and/or you don't have internet access (e.g. inside a nixos-container). | |||
* [https://caddyserver.com/docs/caddyfile/directives/tls Caddy TLS settings documentation] | |||
== See also == | == See also == | ||
* [https://search.nixos.org/options?query=services.caddy Available NixOS service options] | |||
* [https://caddyserver.com/docs/ Official Caddy documentation] | * [https://caddyserver.com/docs/ Official Caddy documentation] | ||
* [https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/web-servers/caddy/default.nix NixOS service definition] | |||
[[Category:Applications]] | [[Category:Applications]] | ||
[[Category: | [[Category:Server]] | ||
[[Category:Networking]] |
Latest revision as of 13:43, 11 September 2024
Caddy is an efficient, HTTP/2 capable web server that can serve static and dynamic web pages. It can also be a reverse proxy to serve multiple web services under one server. Its main features are its simple config setup and automatic HTTPS: It will automatically request and renew a LetsEncrypt certificate so that users of your service get a Browser-trusted and secure connection.
Setup
To try out Caddy add the following minimal example to your NixOS module:
{
# ...
services.caddy = {
enable = true;
virtualHosts."localhost".extraConfig = ''
respond "Hello, world!"
'';
};
}
This snippet will let Caddy respond on http://localhost
and https://localhost
with a dummy text "Hello world!". When no port is mentioned on virtualhost like just localhost
instead of localhost:8080
, Caddy listens on 80
and 443
by default and redirects requests from port 80 (unsecured) to 443 (secured).
Check http connection
You can use curl
to test the http connections:
$ curl localhost -i -L -k
HTTP/1.1 308 Permanent Redirect
Location: https://localhost/
..
HTTP/2 200
alt-svc: h3=":443"; ma=2592000
content-type: text/plain; charset=utf-8
...
Hello, world!
Here you can see that Caddy automatically redirects from an unsecure http://localhost to a secure https://localhost call.
For local addresses like "localhost" Caddy always generates and uses a self-signed certificate, which curl correctly doesn't trust; use the -k
flag to ignore that.
Check http(s) connection
When virtualhost and "real" host aren't the same it gets complicated with HTTPS, so the following curl command works:
$ curl --connect-to <virtualhost>:443:<realhost>:443 https://<virtualhost> -k
Hello, world!
Curl will set Host
header and TLS SNI
in the request to <virtualhost>
as desired by Caddy, but will make the actual request against the <realhost>
, e.g. a load-balancer or ingress-controller.
Alternatively with http and automatic redirects to https you can extend that call:
$ curl --connect-to <virtualhost>:80:<realhost>:80 --connect-to <virtualhost>:443:<realhost>:443 https://<virtualhost> -k -L
Hello, world!
Typical configurations
SSL
Caddy will automatically try to acquire SSL certificates for the specified domain, in this example example.org
. This requires you to configure the DNS records of your domain correctly, which should point to the address of your Caddy server. The firewall ports 80
and 443
needs to be opened.
services.caddy = {
enable = true;
virtualHosts."example.org".extraConfig = ''
encode gzip
file_server
root * ${
pkgs.runCommand "testdir" {} ''
mkdir "$out"
echo hello world > "$out/example.html"
''
}
'';
};
networking.firewall.allowedTCPPorts = [ 80 443];
Reverse proxy
The following snippet creates a reverse proxy for the domain example.org
, redirecting all requests to http://10.25.40.6
services.caddy = {
enable = true;
virtualHosts."example.org".extraConfig = ''
reverse_proxy http://10.25.40.6
'';
virtualHosts."another.example.org".extraConfig = ''
reverse_proxy unix//run/gunicorn.sock
'';
};
In case you would like to forward the real client IP of the request to the backend, add following headers
services.caddy = {
virtualHosts."example.org".extraConfig = ''
reverse_proxy http://10.25.40.6 {
header_down X-Real-IP {http.request.remote}
header_down X-Forwarded-For {http.request.remote}
}
'';
};
Fur further reverse proxy configuration, see upstream documentation.
Redirect
Permanent redirect of example.org
and old.example.org
to www.example.org
services.caddy = {
enable = true;
virtualHosts."example.org" = {
extraConfig = ''
redir https://www.example.org{uri} permanent
'';
serverAliases = [ "old.example.org" ];
};
PHP FastCGI
Serving a PHP application in /var/www
on http://localhost .
services.caddy = {
enable = true;
virtualHosts."http://localhost" = {
extraConfig = ''
root * /var/www
file_server
php_fastcgi unix/var/run/phpfpm/localhost.sock
'';
};
};
You'll need a PHP-FPM socket listening on Unix socket path /var/run/phpfpm/localhost.sock
.
Passing environment variable secrets/configuring acme_dns
To prevent any secrets from being put in the nix store (any NixOS setting that writes a config in the Nix store will expose any secret in it), you can use the following setting
services.caddy = {
enable = true;
globalConfig = ''
acme_dns PROVIDER {
api_key {$APIKEY}
api_secret_key {$APISECRETKEY}
}
'';
};
systemd.services.caddy.serviceConfig.EnvironmentFile = ["/path/to/envfile"];
And then at /path/to/envfile:
APIKEY=YOURKEY
APISECRETKEY=OTHERKEY
Adding plug-ins
There are many Issues/PR's about allowing Caddy to be built with plug-ins. Until then, you can use this workaround:
services.caddy = {
enable = true;
package = (pkgs.callPackage "${builtins.fetchurl https://raw.githubusercontent.com/jpds/nixpkgs/a33b02fa9d664f31dadc8a874eb1a5dbaa9f4ecf/pkgs/servers/caddy/default.nix}" {
externalPlugins = [
{ name = "caddy-dns/porkbun"; repo = "github.com/caddy-dns/porkbun"; version = "4267f6797bf6543d7b20cdc8578a31764face4cf"; }
# Set version to target repository commit hash
];
vendorHash = ""; # Add this as explained in https://github.com/NixOS/nixpkgs/pull/259275#issuecomment-1763478985
});
globalConfig = ''
...
'';
}
Debugging
Check used ports
To check if Caddy is running and listening as configured you can run netstat
:
$ netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:2019 0.0.0.0:* LISTEN 1202/caddy
tcp6 0 0 :::80 :::* LISTEN 1202/caddy
tcp6 0 0 :::443 :::* LISTEN 1202/caddy
udp6 0 0 :::443 :::* 1202/caddy
The tcp (ipv4) socket port 2019 is Caddy's management endpoint, for when you want manage its config via web REST calls instead of Nix (ignore). The tcp6 (an ipv6 socket that also listens on ipv4) socket on port 80 (HTTP) and 443 (HTTPS) indicate that our virtualhost config was used.
Virtualhost and real host not identical
When you connect to Caddy must ensure that the "Host" header matches the virtualhost entry of Caddy. For example, when testing locally a config like
services.caddy = {
enable = true;
virtualHosts."example.org".extraConfig = ''
respond "Hello, world!"
'';
};
you must send the request against "localhost" and manually override the host header to "example.org":
$ curl localhost -i -H "Host: example.org"
HTTP/1.1 308 Permanent Redirect
Connection: close
Location: https://example.org/
Server: Caddy
...
Above you also see the redirect from http://localhost to https://example.org; Caddy always redirects from the unsecure to the secure port of your virtualhost.
If the response is empty, try setting a port number like 80 and/or try a local TLS security certificate instead of global LetsEncrypt:
services.caddy = {
enable = true;
virtualHosts."example.org:80".extraConfig = ''
respond "Hello, world!"
tls internal
'';
};
With "tls internal" Caddy will generate a local certificate, which is good when testing locally and/or you don't have internet access (e.g. inside a nixos-container).