Skip to content

Instantly share code, notes, and snippets.

@gilbitron
Created June 16, 2021 10:55
Show Gist options
  • Save gilbitron/36d48eb751875bebdf43da0a91c9faec to your computer and use it in GitHub Desktop.
Save gilbitron/36d48eb751875bebdf43da0a91c9faec to your computer and use it in GitHub Desktop.
Enabling HTTPS (SSL) for Laravel Sail using Caddy
<?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');
@domsii
Copy link

domsii commented Oct 20, 2021

Thanks ! But I ran in a Problem ...
The Caddyfile is a file in the "docker" subdirectory of the laravel project - correct ?
Or has the Caddyfile to be in a directory "Caddyfile" ?

I get an error :
ERROR: for test-project_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/30bced14b9bc1133143e6086f29c9ed8d5aa55c5aa4d4cd84a64037bbd9f28ef" 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/30bced14b9bc1133143e6086f29c9ed8d5aa55c5aa4d4cd84a64037bbd9f28ef" 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.

@gilbitron
Copy link
Author

The Caddyfile should be in a ./docker subdirectory yes. This line is how the file is mounted to the container:

volumes:
      - './docker/Caddyfile:/etc/caddy/Caddyfile'

@kimcee
Copy link

kimcee commented Oct 22, 2021

This all seems to work until the last two steps. Going to https://localdev.test doesn't work, chrome says "This site can’t provide a secure connection localdev.test sent an invalid response. ERR_SSL_PROTOCOL_ERROR". Any ideas?

UPDATE: Was able to get the root.crt into Keychain and set it to Always Trust. Still getting the same error when trying to access the site in chrome, not sure what I'm missing.

@CarterBland
Copy link

@kimcee ensure that you're pointing to port 443, I had the same issue @ https://localhost:80 but switching to https://localhost:443 resolved it.

@dvlpr91
Copy link

dvlpr91 commented Nov 22, 2021

This all seems to work until the last two steps. Going to https://localdev.test doesn't work, chrome says "This site can’t provide a secure connection localdev.test sent an invalid response. ERR_SSL_PROTOCOL_ERROR". Any ideas?

UPDATE: Was able to get the root.crt into Keychain and set it to Always Trust. Still getting the same error when trying to access the site in chrome, not sure what I'm missing.

So am I.

Have you changed the values ​​of 'laravel.test' or 'www.laravel.test' in 'authorizedDomains' and 'Caddyfile' to the values ​​you want? (ex: 'https://localdev.test')

@dvlpr91
Copy link

dvlpr91 commented Nov 22, 2021

// CaddyController.php

...
$authorizedDomains = [
  'foo.bar.test',
];
...
// Caddyfile

...
reverse_proxy foo.bar.test {
...

but.. not working...
I see the message 'ERR_SSL_PROTOCOL_ERROR' in Chrome.

@gilbitron
Copy link
Author

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:

sail logs -f caddy

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 include composer install needing to be run, php artisan migrate needing to be run, etc.

@dvlpr91
Copy link

dvlpr91 commented Nov 24, 2021

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?

@clytras
Copy link

clytras commented Dec 5, 2021

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!

@hubtraum
Copy link

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.

@anselmobattisti
Copy link

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!

@WebKudu
Copy link

WebKudu commented Mar 5, 2022

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?

@alexanderp99
Copy link

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"}

@szhorvath
Copy link

szhorvath commented May 30, 2022

```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

@VRGunnar
Copy link

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.
image_2022-06-10_182318091
Am I missing something else?

@franz-deleon
Copy link

franz-deleon commented Sep 22, 2022

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?

@Nilpo
Copy link

Nilpo commented Oct 28, 2022

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.

@clytras
Copy link

clytras commented Oct 29, 2022

@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
Copy link

Nilpo commented Oct 29, 2022

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

@Maxime-Missichini
Copy link

Maxime-Missichini commented Dec 2, 2022

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

@Nilpo
Copy link

Nilpo commented Dec 3, 2022

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.

https://github.com/Nilpo/caddy-laravelsail

@franz-deleon
Copy link

franz-deleon commented Dec 4, 2022

@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' 

@Nilpo
Copy link

Nilpo commented Dec 4, 2022

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

@ethanclevenger91
Copy link

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
    }
}

@mreduar
Copy link

mreduar commented Mar 23, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment