This explains how I setup Caddy with a Reverse Proxy to two Laravel backends on a Linux VPS. Use a reverse proxy like this is handy when your backends are running inside a Docker container, or on other hosts in the network.
Check out this setup for a simpler setup without a Reverse Proxy.
Warning
The configs below are modified slightly from my production environment (where they worked) and I haven't tested them since I changed 'em. Feel free to submit fixes and/or improvements as you find them.
- A VPS with:
- PHP with FastCGI (
sudo apt install php8.1-fpm
) - MySQL (
sudo apt install mysql-server
) - The neccessary PHP extensions for Laravel (
sudo apt install php-intl php-gd php-zip php-bcmath php-ctype php-curl php-dom php-fileinfo php-mbstring php-pdo php-tokenizer php-xml php-mysql
) - See this article for a full setup guide for Ubuntu 22.04
- PHP with FastCGI (
- Caddy Server 2
This configures Caddy through it's API. You first customize the .caddy.json
files below to your situation. Then you'll send them to Caddy which will configure itself with them.
By default Caddy uses Caddyfiles. Anything sent to it's API isn't automatically saved or restored.
For that reason I have installed and configured Caddy to use the API for configurations by running the caddy-api
service.
You can setup the caddy-api
service by running these commands once after installing Caddy
sudo systemctl disable --now caddy
sudo systemctl enable --now caddy-api
The caddy-api
service stores the API config even if the VPS is restarted (the default caddy service does not save API modifications, since it relies on caddy files).
I have cloned a repository to 2 locations on the server:
- Production repo:
/var/repos/myapp
- Staging repo:
/var/repos/myapp-staging
Download these files to a directory on your server:
base.caddy.json
production.caddy.json
staging.caddy.json
This is a reverse proxy config that has Caddy listen to port 443. It forwards requests to internal subroutes to the laravel backends on ports 3001 and 3009.
Activate this config by sending it to the API with:
curl -X POST localhost:2019/config/ -H "Content-Type: application/json" -v -d @./base.caddy.json
As you can see the reverse proxy must set the Host
HTTP Header to {http.reverse_proxy.upstream.hostport}
. This ensures the response is handled correctly by Laravel.
The transport
object in the reverse proxy handle contains "tls": {}
to enable internal HTTPS between the proxy and backend.
Warning
If you use Laravel you will have to configure trusted proxies.
The following should work for most situations:
- Go to
App\Http\Middleware\TrustProxies.php
in your Laravel project - Change the line for the
$proxies
property to:
protected $proxies = '*';
After these steps Laravel will take the true base URL from HTTP_X_FORWARDED_HOST
.
This config sets up a Laravel backend for production on the host myapp.example
.
Change routes.match.host
in the base.caddy.json
to change how your app is reached by users.
Activate this config by sending it to the API with:
curl -X POST localhost:2019/config/apps/http/servers/production -H "Content-Type: application/json" -d @./production.caddy.json
The key automatic_https.disable_redirects
is set to true, so port 80
stays clear. Otherwise Caddy would expose a service that upgrades HTTP to HTTPS. Since we know that we will always use HTTPS internally, it makes no sense to expose that port. Furthermore it would conflict with the port 80 that we want to expose from the proxy to the outside world for upgrading the connection.
PHP is served using the FastCGI configuration that comes with Caddy. It's configured here to use php8.1-fpm over a unix socket. Ensure that you installed it with sudo apt install php8.1-fpm
.
By default FastCGI will not trust any proxies. For this reason all local IP variants have been added in the config with "trusted_proxies":["192.168.0.0/16","172.16.0.0/12","10.0.0.0/8","127.0.0.1/8","fd00::/8","::1"]
. Without this you may find that all URL's on your site redirect to the internal host (e.g: https://localhost:3009
)
Warning
This example runs it on localhost, but realistically this would run on a separate machine or in a docker container. Be aware of the risk if your firewall allows external access to the port. If people can reach the app through it's port (3001), they might also be able to setup a malicious proxy server. You should limit access to the service, so that only the proxy can reach it over the local network.
This config works similarly to the production config above, except an additional authentication handler locks the route behind HTTP Basic Auth (so you must loging with username test and a password of your choosing).
You must set the password to a valid hash. To generate this hash use this caddy command:
caddy hash-password --plaintext YOURPASSWORD
Next replace PASSWORDHASHHERE
in the staging.caddy.json
config with the hash generated by the above command.
Finally activate this config by sending it to the API with:
curl -X POST localhost:2019/config/apps/http/servers/staging -H "Content-Type: application/json" -d @./staging.caddy.json