This is more of a proof of concept rather than a fully working project. This tries to replicate the caddy JSON API structure to work through chainable PHP classes.
At the moment there is only a tiny subset of commands available from Caddy 2.0 that covered my currently needed use case.
composer require mattvb91/caddy-php
A basic example of a http server with a static response:
$caddy = new Caddy();
$caddy->addApp(
(new Http())->addServer(
'server1', (new Http\Server())->addRoute(
(new Route())->addHandle(
new StaticResponse('Hello world', 200)
)
))
);
$caddy->load();
This will result in the following Caddy config:
{
"admin": {
"disabled": false,
"listen": ":2019"
},
"apps": {
"http": {
"servers": {
"server1": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"body": "Hello world",
"status_code": 200
}
]
}
]
}
}
}
}
}
curl -v localhost
-----
< HTTP/1.1 200 OK
< Server: Caddy
Hello world
If you are managing hostnames dynamically (in a database) and can't build out the config with a list of existing hostnames because you need to manage them at runtime you can do the following:
The important part in this example is the host_group_name
identifier which is later
used to add / remove domains to this host.
$caddy = new Caddy();
$caddy->addApp(
(new Http())->addServer(
'server1', (new Http\Server())->addRoute(
(new Route())->addHandle(
new StaticResponse('host test', 200)
)->addMatch((new Host('host_group_name'))
->setHosts(['localhost'])
)
)->addRoute((new Route())
->addHandle(new StaticResponse('Not found', 404))
->addMatch((new Host('notFound'))
->setHosts(['*.localhost'])
)
))
);
$caddy->load();
Now later on in a script or event on your system you can get your caddy configuration object and post a new domain to it under that route:
$caddy->addHostname('host_group_name', 'new.localhost')
$caddy->addHostname('host_group_name', 'another.localhost')
curl -v new.localhost
> GET / HTTP/1.1
> Host: new.localhost
>
< HTTP/1.1 200 OK
curl -v another.localhost
> GET / HTTP/1.1
> Host: another.localhost
>
< HTTP/1.1 200 OK
$caddy->syncHosts('host_group_name'); //Sync from caddy current hostname list
$caddy->removeHostname('host_group_name', 'new.localhost');
$caddy->removeHostname('host_group_name', 'another.localhost');
curl -v new.localhost
> GET / HTTP/1.1
> Host: new.localhost
>
< HTTP/1.1 404 Not Found
curl -v another.localhost
> GET / HTTP/1.1
> Host: another.localhost
>
< HTTP/1.1 404 Not Found
Let's take a case where you want to have a Node frontend and a PHP backend taking requests on the /api/*
route.
In this case the example breaks down to 2 reverse proxy's with a route matcher to filter the /api/*
to the PHP
upstream.
This assumes the 3 hosts (Caddy, Node, PHP) are all docker containers and accessible by container name within the same docker network, so you may have to adjust your hostnames as required.
use mattvb91\CaddyPhp\Caddy;
use mattvb91\CaddyPhp\Config\Apps\Http;
use mattvb91\CaddyPhp\Config\Apps\Http\Server;
use mattvb91\CaddyPhp\Config\Apps\Http\Server\Route;
use mattvb91\CaddyPhp\Config\Apps\Http\Server\Routes\Handle\ReverseProxy;
use mattvb91\CaddyPhp\Config\Apps\Http\Server\Routes\Handle\ReverseProxy\Transport\FastCGI;
use mattvb91\CaddyPhp\Config\Apps\Http\Server\Routes\Handle\ReverseProxy\Upstream;
use mattvb91\CaddyPhp\Config\Apps\Http\Server\Routes\Handle\Subroute;
use mattvb91\CaddyPhp\Config\Apps\Http\Server\Routes\Match\Host;
use mattvb91\CaddyPhp\Config\Apps\Http\Server\Routes\Match\Path;
$apiReverseProxy = (new ReverseProxy())
->addUpstream((new Upstream())
->setDial('laravel-api:9000')
)->addTransport((new FastCGI())
->setRoot('/app/public/index.php')
->setSplitPath([''])
);
$apiMatchPath = (new Path())
->setPaths([
'/api/*',
]);
$backendAPIRoute = (new Route())
->addHandle($apiReverseProxy)
->addMatch($apiMatchPath);
$route = new Route();
$route->addHandle((new Subroute())
->addRoute($backendAPIRoute)
->addRoute((new Route())
->addHandle((new ReverseProxy())
->addUpstream((new Upstream())
->setDial('nextjs:3000')
)
)
)
)->addMatch((new Host())
->setHosts([
'localhost',
])
)->setTerminal(true);
$caddy = new Caddy();
$caddy->addApp((new Http())
->addServer('myplatform', (new Server())
->addRoute($route)
)
);
$caddy->load();
This will post the following caddy config:
{
"admin": {
"disabled": false,
"listen": ":2019"
},
"apps": {
"http": {
"servers": {
"myplatform": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"protocol": "fastcgi",
"root": "/app/public/index.php",
"split_path": [
""
]
},
"upstreams": [
{
"dial": "laravel-api:9000"
}
]
}
],
"match": [
{
"path": [
"/api/*"
]
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "nextjs:3000"
}
]
}
]
}
]
}
],
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
}
}
}
curl -v localhost
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=utf-8
< Server: Caddy
< X-Powered-By: Next.js
< Transfer-Encoding: chunked
<
<!DOCTYPE html><html>....
curl -v localhost/api/testroute
< HTTP/1.1 200 OK
< Content-Type: application/json
< Server: Caddy
< X-Powered-By: PHP/8.1.7
<
{"status":200}
Take a look in the tests for more examples.