Skip to content

Instantly share code, notes, and snippets.

@luttje
Last active December 16, 2023 08:35
Show Gist options
  • Save luttje/689290ad45dc564cd2d3270b0bbac381 to your computer and use it in GitHub Desktop.
Save luttje/689290ad45dc564cd2d3270b0bbac381 to your computer and use it in GitHub Desktop.
Caddy Server 2 - Laravel Project Setup with Reverse Proxy and Staging environment

Caddy Server 2 - Reverse Proxy to 2 Laravel Backends

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.

Requirements

  • 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
  • Caddy Server 2

Step-by-step

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.

1. Caddy Server Setup

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).

2. Setup the Laravel backend repositories

I have cloned a repository to 2 locations on the server:

  • Production repo: /var/repos/myapp
  • Staging repo: /var/repos/myapp-staging

3. Download the caddy config json files from this gist

Download these files to a directory on your server:

  • base.caddy.json
  • production.caddy.json
  • staging.caddy.json

4. base.caddy.json Reverse Proxy Config

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:

  1. Go to App\Http\Middleware\TrustProxies.php in your Laravel project
  2. 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.

5. production.caddy.json Production Config

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.

6. staging.caddy.json Staging Config

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
{
"apps": {
"http": {
"servers": {
"entry_proxy": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"staging.myapp.example"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"Host": [
"{http.reverse_proxy.upstream.hostport}"
]
}
}
},
"transport": {
"protocol": "http",
"tls": {}
},
"upstreams": [
{
"dial": "localhost:3009"
}
]
}
]
}
]
}
]
},
{
"match": [
{
"host": [
"myapp.example"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"Host": [
"{http.reverse_proxy.upstream.hostport}"
]
}
}
},
"transport": {
"protocol": "http",
"tls": {}
},
"upstreams": [
{
"dial": "localhost:3001"
}
]
}
]
}
]
}
]
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"localhost"
],
"issuers": [
{
"module": "internal"
}
]
}
]
}
}
}
}
{
"listen": [
":3001"
],
"automatic_https": {
"disable_redirects": true
},
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/var/repos/myapp/public"
},
{
"encodings": {
"gzip": {}
},
"handler": "encode",
"prefer": [
"gzip"
]
}
]
},
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"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"],
"upstreams": [
{
"dial": "unix//run/php/php8.1-fpm.sock"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
},
{
"handle": [
{
"handler": "file_server"
}
]
}
]
}
]
}
]
}
{
"listen": [
":3009"
],
"automatic_https": {
"disable_redirects": true
},
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/var/repos/myapp-staging/public"
},
{
"encodings": {
"gzip": {}
},
"handler": "encode",
"prefer": [
"gzip"
]
},
{
"handler": "authentication",
"providers": {
"http_basic": {
"accounts": [
{
"password": "PASSWORDHASHHERE",
"username": "test"
}
],
"hash": {
"algorithm": "bcrypt"
},
"hash_cache": {}
}
}
}
]
},
{
"handle": [
{
"handler": "static_response",
"headers": {
"Location": [
"{http.request.orig_uri.path}/"
]
},
"status_code": 308
}
],
"match": [
{
"file": {
"try_files": [
"{http.request.uri.path}/index.php"
]
},
"not": [
{
"path": [
"*/"
]
}
]
}
]
},
{
"handle": [
{
"handler": "rewrite",
"uri": "{http.matchers.file.relative}"
}
],
"match": [
{
"file": {
"split_path": [
".php"
],
"try_files": [
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
]
}
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"split_path": [
".php"
]
},
"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"],
"upstreams": [
{
"dial": "unix//run/php/php8.1-fpm.sock"
}
]
}
],
"match": [
{
"path": [
"*.php"
]
}
]
},
{
"handle": [
{
"handler": "file_server"
}
]
}
]
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment