-
-
Save gilbitron/36d48eb751875bebdf43da0a91c9faec to your computer and use it in GitHub Desktop.
<?php | |
# app/Http/Controllers/CaddyController.php | |
namespace App\Http\Controllers; | |
use App\Store; | |
use Illuminate\Http\Request; | |
class CaddyController extends Controller | |
{ | |
public function check(Request $request) | |
{ | |
$authorizedDomains = [ | |
'laravel.test', | |
'www.laravel.test', | |
// Add subdomains here | |
]; | |
if (in_array($request->query('domain'), $authorizedDomains)) { | |
return response('Domain Authorized'); | |
} | |
// Abort if there's no 200 response returned above | |
abort(503); | |
} | |
} |
# docker/Caddyfile | |
{ | |
on_demand_tls { | |
ask http://laravel.test/caddy-check | |
} | |
local_certs | |
} | |
:443 { | |
tls internal { | |
on_demand | |
} | |
reverse_proxy laravel.test { | |
header_up Host {host} | |
header_up X-Real-IP {remote} | |
header_up X-Forwarded-For {remote} | |
header_up X-Forwarded-Port {server_port} | |
header_up X-Forwarded-Proto {scheme} | |
health_timeout 5s | |
} | |
} |
services: | |
caddy: | |
image: caddy:latest | |
restart: unless-stopped | |
ports: | |
- '80:80' | |
- '443:443' | |
volumes: | |
- './docker/Caddyfile:/etc/caddy/Caddyfile' | |
- sailcaddy:/data | |
- sailcaddy:/config | |
networks: | |
- sail | |
# Remove "ports" from laravel.test service | |
volumes: | |
sailcaddy: | |
driver: local |
<?php | |
# routes/web.php | |
Route::get('/caddy-check', 'CaddyController@check'); |
thank you. I was able to solve it well.
However, I have a question.
If there are multiple projects, docker volume will be created for each project(ex: foo_sailcaddy, bar_sailcaddy), and multiple caddy root certificates will need to be registered.
Are there any good alternatives to work around this?
Hello and thank you for the great solution. I upvoted and commented on stackoverflow asking about HMR.
I want to say that I got all working after 16 hours with lots of trial/error/debugging/coffee and wine!
The solution works great for local development using Laravel Sail with different service names, totally different ports and even Laravel Mix having Webpack DevServer loading React Fast Refresh through HRM module on different ports.
I've been tricked by the CaddyController
as it executes only once for each domain/subdomain and then it caches the result, so I thought it was not running, but there is no problem with it.
Then I've swapped ports from app service (in my case "someapp.test") to caddy container, change domain names, fetched/convert and install the root.crt
/root.key
> root.p12
and the app got load with HTTPS and with no errors.
For the Laravel Mix Webpack Server HMR and anyone that wants to have it, I'm having a setup like this...
Inside a Mix config file set the hmrOptions
and devServer
options:
mix.options({
hmrOptions: {
host: 'localhost',
port: process.env.DEV_HOT_PORT,
},
});
mix.webpackConfig({
output: {
chunkFilename: `[name].[chunkhash].js`,
},
devServer: {
host: '0.0.0.0',
port: process.env.DEV_HOT_PORT,
},
});
and I'm using @pmmmwh/react-refresh-webpack-plugin
for binding React Refresh with the Webpack dev-server and with .env
settings:
APP_DOMAIN="someapp.test"
DEV_DOMAIN="dev.someapp.test"
DEV_HOT_PORT=12181
DEV_URL="//${DEV_DOMAIN}"
someapp
service inside docker-compose.yml
:
someapp.test:
build:
context: ./docker/8.0
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
image: sail-8.0/app
ports:
- '${DEV_HOT_PORT:-8080}:${DEV_HOT_PORT:-8080}'
environment:
WWWUSER: '${WWWUSER}'
LARAVEL_SAIL: 1
LANG: 'C.UTF-8'
TERM: 'xterm-256color'
volumes:
- '.:/var/www/html'
- '${LIBRARY_PATH:?LIBRARY_PATH__NOTSET}:/var/www/lib'
networks:
- someappnet
depends_on:
- mariadb
- mongodb
- redis
- meilisearch
caddy:
image: caddy:latest
restart: unless-stopped
ports:
- '${APP_PORT:-80}:80'
- '${APP_SECURE_PORT:-443}:443'
environment:
LARAVEL_SAIL: 1
HOST_DOMAIN: ${APP_DOMAIN}
APP_SECURE_PORT: '${APP_SECURE_PORT}'
DEV_HOT_PORT: '${DEV_HOT_PORT}'
volumes:
- './docker/caddy/Caddyfile:/etc/caddy/Caddyfile'
- './docker/caddy/certificates:/data/caddy/certificates/local'
- './docker/caddy/authorities:/data/caddy/pki/authorities/local'
- 'caddy:/data:cached'
- 'caddy:/config:cached'
networks:
- someappnet
depends_on:
- someapp.test
and inside config/app.php
:
// \Illuminate\Foundation\Mix.php#L35
'mix_hot_proxy_url' => env('DEV_URL', null),
and loading the assets using just mix
Blade directive which is a binding of Illuminate\Foundation\Mix.php
class:
<script src="{{ mix('/js/platform.js') }}" defer></script>
all the scripts will have a URL like https://dev.someapp.test/js/platform.js
and these will be parsed by Webpack dev-server HMR module.
The HMR port has to stay on the service container "someapp.test", because there is where the Webpack dev-server is running and we just need to proxy pass our dev.someapp.test
domain there.
And then inside the Caddyfile
, which we have replace :423
with https://
and inside define macthers for each service subdomain and reverse proxy them to their internal ports:
{
# debug
on_demand_tls {
ask http://{$HOST_DOMAIN}/~~local/caddy-check
}
local_certs
}
https:// {
tls internal {
on_demand
}
# Webpack HMR
@devhmr {
host dev.{$HOST_DOMAIN}
}
reverse_proxy @devhmr {$HOST_DOMAIN}:{$DEV_HOT_PORT} {
# https://caddy.community/t/context-cancelled-when-webpack-hmr-sends-updates-fix/9850
flush_interval -1
}
# PMA
@pma {
host pma.{$HOST_DOMAIN}
}
reverse_proxy @pma phpmyadmin:80
# Mongo Express
@mongo {
host mongoexpress.{$HOST_DOMAIN}
}
reverse_proxy @mongo mongoexpress:8081
# Mailhog
@mailhog {
host mailhog.{$HOST_DOMAIN}
}
reverse_proxy @mailhog mailhog:8025
# Meilisearch
@meilisearch {
host meilisearch.{$HOST_DOMAIN}
}
reverse_proxy @meilisearch meilisearch:7700
# Proxy everything else to Laravel someapp.test service
reverse_proxy {$HOST_DOMAIN}
}
Everything is working now localy on "someapp.test" with valid SSL and React Fast Refresh working through Laravel Mix.
Thanks again!
In case someone is falling over the same thing, I needed to add the namespace for the route in web.php
:
Route::get('/caddy-check', 'App\Http\Controllers\CaddyController@check');
Because I did not have any other web routes or namespace usage in the file.
Thanks for your help.
To enable a real SSL certified you should:
#
# Config that should be executed in the server
#
{
email your_valid@email.com
}
your_domail.com {
reverse_proxy laravel.test {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto {scheme}
health_timeout 5s
}
}
I got stuck to understand that in the "reverse_proxy" you MUST define the name of the service you want to reverse proxy. It is not the real domain!
I'm also getting ERR_SSL_PROTOCOL_ERROR (Chrome) or SSL_ERROR_INTERNAL_ERROR_ALERT (Firefox).
It would be nice to know what @dvlpr91 did to solve the issue.
Looking through the caddy logs, the only thing that stands out is:
Warning: "certutil" is not available, install "certutil" with "apt install libnss3-tools" or "yum install nss-tools" and try again.
Neither apt nor yum are in the caddy container.
The /caddy-check end point seems to be working. I copied in all 4 files with little to no changes.
I didn't attempt to trust the keys yet since it doesn't seem to be getting far enough anyway.
Any ideas?
Same problem here. I don't know how to proceed.
I installed a fresh sail project
curl -s "https://laravel.build/example-app" | bash
Add the files, removed the ports of the laravel application, added the namespace in web.php . Changed the Caddy Image Version to caddy:2.4.2 .
Ran
sail composer install && sail composer update && sail artisan migrate
Logs of caddy are:
example-app-caddy-1 | {"level":"info","ts":1647520369.6394668,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
example-app-caddy-1 | 2022/03/17 12:32:49 [WARNING] Unnecessary header_up ('X-Forwarded-Proto' field): the reverse proxy's default behavior is to pass headers to the upstream
example-app-caddy-1 | {"level":"warn","ts":1647520369.6403213,"msg":"input is not formatted with 'caddy fmt'","adapter":"caddyfile","file":"/etc/caddy/Caddyfile","line":2}
example-app-caddy-1 | {"level":"info","ts":1647520369.641571,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["localhost:2019","[::1]:2019","127.0.0.1:2019"]}
example-app-caddy-1 | {"level":"info","ts":1647520369.6419103,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc0000de0e0"}
example-app-caddy-1 | {"level":"info","ts":1647520369.6480498,"logger":"http","msg":"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}
example-app-caddy-1 | {"level":"info","ts":1647520369.6480796,"logger":"http","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
example-app-caddy-1 | {"level":"warn","ts":1647520369.6753442,"logger":"pki.ca.local","msg":"installing root certificate (you might be prompted for password)","path":"storage:pki/authorities/local/root.crt"}
example-app-caddy-1 | 2022/03/17 12:32:49 Warning: "certutil" is not available, install "certutil" with "apt install libnss3-tools" or "yum install nss-tools" and try again
example-app-caddy-1 | 2022/03/17 12:32:49 define JAVA_HOME environment variable to use the Java trust
example-app-caddy-1 | 2022/03/17 12:32:49 certificate installed properly in linux trusts
example-app-caddy-1 | {"level":"info","ts":1647520369.699206,"logger":"tls","msg":"cleaning storage unit","description":"FileStorage:/data/caddy"}
example-app-caddy-1 | {"level":"info","ts":1647520369.6992402,"logger":"tls","msg":"finished cleaning storage units"}
example-app-caddy-1 | {"level":"info","ts":1647520369.699266,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
example-app-caddy-1 | {"level":"info","ts":1647520369.6992774,"msg":"serving initial configuration"}
```shell DEV_URL="//${DEV_DOMAIN}"
Hi @clytras ,
I've been trying to set up the HMR based on your comment.
The https://dev.someapp.test/js/platform.js
return 502 for me.
Do you have any idea what I am doing wrong?
Many thanks
ERROR: for fible_caddy_1 Cannot start service caddy: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: rootfs_linux.go:76: mounting "/run/desktop/mnt/host/wsl/docker-desktop-bind-mounts/Ubuntu/91c064ef9216261a3f192079b2607b6ab73173626592a27b5f976cffbb10215e" to rootfs at "/etc/caddy/Caddyfile" caused: mount through procfd: not a directory: unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type
ERROR: for caddy Cannot start service caddy: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: rootfs_linux.go:76: mounting "/run/desktop/mnt/host/wsl/docker-desktop-bind-mounts/Ubuntu/91c064ef9216261a3f192079b2607b6ab73173626592a27b5f976cffbb10215e" to rootfs at "/etc/caddy/Caddyfile" caused: mount through procfd: not a directory: unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type
ERROR: Encountered errors while bringing up the project.
I run into this problem shown above. I saw in some previous comment that the Caddyfile is supposed to be placed in the docker directory in the root of the project. So I did that, but still having the same exact error. Now my Caddyfile has the content shown above as in the Github file in the directory ProjectName/docker/Caddyfile.
Am I missing something else?
for some reason i keep getting redirected to https://laravel.test:0
I have to remove header_up X-Forwarded-Port {server_port}
from the Caddyfile for it to work. why is that?
I've been trying to get this solution working for days. The setup is simple enough, but Caddy can't verify the domains because it creates a redirect loop. Caddy sends a request to http://laravel.test/caddy-check
, which it then redirects to https://laravel.test/caddy-check
causing the request to fail with the error following http redirects is not allowed
. Since there doesn't seem to be a way to exclude that single endpoint, the whole solution is a wash.
@Nilpo that sounds like you don't handle the caddy-check
route properly or it isn't configured properly.
I have this inside routes:
Route::get('caddy-check', [CaddyController::class, 'check']);
the CaddyController
looks like this:
class CaddyController extends Controller
{
public function check(Request $request)
{
$domain = $request->query('domain');
$domains = collect(config('app.domains'));
$domainsRegExp = regex_escape(collect($domains)->join('|'), '.');
if (preg_match("/$domainsRegExp/", $domain)) {
return response('Domain Authorized');
}
// Abort if there's no 200 response returned above
abort(503);
}
}
and inside Caddyfile
I have the on demand TLS like this (note that I'm only uing this with /~~local/
route prefix).
{
# debug
on_demand_tls {
ask http://{$HOST_DOMAIN}/~~local/caddy-check
}
local_certs
}
You should also check the logs and of course see what Caddy is doing by attaching the caddy container output using sail logs -f caddy
.
@Nilpo that sounds like you don't handle the
caddy-check
route properly or it isn't configured properly.I have this inside routes:
Route::get('caddy-check', [CaddyController::class, 'check']);
the
CaddyController
looks like this:class CaddyController extends Controller { public function check(Request $request) { $domain = $request->query('domain'); $domains = collect(config('app.domains')); $domainsRegExp = regex_escape(collect($domains)->join('|'), '.'); if (preg_match("/$domainsRegExp/", $domain)) { return response('Domain Authorized'); } // Abort if there's no 200 response returned above abort(503); } }
and inside
Caddyfile
I have the on demand TLS like this (note that I'm only uing this with/~~local/
route prefix).{ # debug on_demand_tls { ask http://{$HOST_DOMAIN}/~~local/caddy-check } local_certs }
You should also check the logs and of course see what Caddy is doing by attaching the caddy container output using
sail logs -f caddy
.
I'm using the exact code above. I posted the exact error from the logs. The configuration as posted creates a redirect loop.
It's not the handler, it never gets executed because Caddy won't process the redirect.
A call to http://laravel.test/caddy-check is handled by Caddy which transforms it to https but then refuses to follow it to complete the domain verification. So the request fails and Caddy sends the browser an SSL error.
@Nilpo Hi, it may be a late reply but I managed to solve the same error, here is what I did :
Inside CaddyController:
$authorizedDomains = [ 'localhost', // Add subdomains here ];
This way I can access via https://localhost
Then the error was because of the Caddyfile, as you said when this error occurs we don't even reach the controller.
Here is my Caddyfile:
{
on_demand_tls {
ask http://laravel.test/caddy-check
}
local_certs
}
:443 {
tls internal {
on_demand
}
reverse_proxy laravel.test {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto {scheme}
health_timeout 5s
}
}
Notice that laravel.test is the name of the reverse proxy service !
My on_demand_tls was set to : ask http://localhost/caddy-check, which caused the failure ...
Don't forget to delete and rebuild your container after modifications on the Caddyfile, otherwise it will keep the old one.
Hi @Maxime-Missichini Thanks for the reply. This still causes the same redirect loop. I've even tried modifying the authorized domains as follows, which accounts for every possibility.
$authorizedDomains = [
env('APP_SERVICE'),
'www.' . env('APP_SERVICE'),
'localhost',
];
But as I mentioned in my last comment, this has no effect because the Caddy handler never gets called. Caddy kills the request with an SSL error and never hands the request off to Laravel for routing.
For the record, APP_SERVICE and the Sail service name are both the same (laravel.test, localhost, or other) and the test domain is resolvable. The failure is with Caddy.
You can view my fork for more details.
The problems is ultimately that the ask URL cannot contain an https protocol, but Caddy refuses to complete the request if it doesn't.
I'm using Laravel Sail in Laravel 9 based on the PHP 8.1 image and Ubuntu 22.04
I'll create an empty project that reproduces this error and create a GitHub repo.
EDIT:
Here's the sample repo.
@Nilpo
try changing your APP_PORT env variable (or whatever that new variable is called now) to a different port like
APP_PORT=8080
referencing the docker-compose.yml
services:
laravel.test:
ports:
- '${APP_PORT:-80}:80'
Well, here's a weird one. I wonder if this is a Mac issue.
If you clone the sample repo I made (or start with a clean Laravel project), the first build doesn't work. But if you immediately force a rebuild with no changes, it works. (I'm starting with a clean Docker install. No locally downloaded images or containers. The strange thing is that the rebuild shouldn't actually change anything. The volumes are all the same too since there's no file changes.
git clone https://github.com/Nilpo/caddy-laravelsail
cd caddy-laravelsail && composer install
./vendor/bin/sail up -d
# opening https://caddy-laravelsail.test fails
./vendor/bin/sail up --build -d
# opening https://caddy-laravelsail.test succeeds
If the domains are going to be hard-coded into that controller anyway, why not put them in the Caddyfile?
# docker/Caddyfile
laravel.test, www.laravel.test {
tls internal
reverse_proxy laravel.test {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto {scheme}
health_timeout 5s
}
}
Is there any way to do it without creating code in the application? In theory development things should not have to be in the production project. Also, not everyone in the team necessarily has to use docker or sail. It causes me noise to create a route and a controller
If you're seeing
ERR_SSL_PROTOCOL_ERROR
it usually means Caddy hasn't been able to generate a certificate for the domain. A good place to start is checking the Caddy logs:This usually happens when something either isn't right with your Docker config or something has broken the app itself. For example, Caddy's request to the
CaddyController
has returned a 500 error. In this case, you can check your Laravel logs to see what's going. Some examples that I've run into includecomposer install
needing to be run,php artisan migrate
needing to be run, etc.