Let's talk SSL and local development; don't worry, I've been searching too.
There is an issue with developing locally, not just with Laravel
, where developers who are building saas products aren't able to get a clean
setup for SSL
in the browser. This makes setting things up like PWA
s or Google
sign-in buttons impossible if the certificate isn't valid.
Given the power of Docker
and Caddy
, the dream is real. You can absolutely use this for non-Laravel Sail
web applications as well.
The end result of implementing the code below should give you a couple of certificates (intermediate.crt, laravel.test.crt)
that you can install to your local system.
GitHub Gist: https://gist.github.com/adrianmejias/0997f2b8a20715428f594a4798e034f5
Directory Structure
- docker/
- caddy/
- authorities/ (intermediate.crt)
- certificates/
- laravel.test/ (laravel.test.crt)
- Caddyfile
- Dockerfile
- start-container
- docker-compose.yml
Files
docker-compose.yml
# ...
laravel.test:
# Comment or remove ports
# ports:
# - "${APP_PORT:-80}:80"
# ...
caddy:
build:
context: "./docker/caddy"
dockerfile: Dockerfile
args:
WWWGROUP: "${WWWGROUP}"
restart: unless-stopped
ports:
- "${APP_PORT:-80}:80"
- "${APP_SSL_PORT:-443}:443"
environment:
LARAVEL_SAIL: 1
HOST_DOMAIN: laravel.test
volumes:
- "./docker/caddy/Caddyfile:/etc/caddy/Caddyfile"
- ".:/srv:cache"
- "./docker/caddy/certificates:/data/caddy/certificates/local"
- "./docker/caddy/authorities:/data/caddy/pki/authorities/local"
- "sailcaddy:/data:cache"
- "sailcaddyconfig:/config:cache"
networks:
- sail
depends_on:
- laravel.test
# ...
volumes:
# ...
sailcaddy:
external: true
sailcaddyconfig:
driver: local
docker/caddy/Dockerfile
FROM caddy:alpine
LABEL maintainer="Adrian Mejias"
ARG WWWGROUP
ENV DEBIAN_FRONTEND noninteractive
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apk add --no-cache bash \
&& apk add --no-cache nss-tools \
&& rm -rf /var/cache/apk/*
RUN addgroup -S $WWWGROUP
RUN adduser -G $WWWGROUP -u 1337 -S sail
COPY start-container /usr/local/bin/start-container
RUN chmod +x /usr/local/bin/start-container
ENTRYPOINT ["start-container"]
docker/caddy/start-container
#!/usr/bin/env sh
if [ ! -z "$WWWUSER" ]; then
addgroup $WWWUSER sail
fi
if [ $# -gt 0 ]; then
# @todo find alpine equivilent of below
# exec gosu $WWWUSER "$@"
else
/usr/bin/caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
fi
docker/caddy/Caddyfile
{
admin off
# debug
on_demand_tls {
ask http://laravel.test/caddy
}
local_certs
}
:80 {
reverse_proxy laravel.test {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-Host {host}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port 443
# header_up X-Forwarded-Proto {scheme}
health_timeout 5s
}
}
:443 {
tls internal {
on_demand
}
reverse_proxy laravel.test {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-Host {host}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port 443
# header_up X-Forwarded-Proto {scheme}
health_timeout 5s
}
}
app/Http/Controllers/CaddyController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class CaddyController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
if (in_array($request->query('domain'), config('caddy.authorized'))) {
return response('Domain Authorized');
}
abort(503);
}
}
config/caddy.php
<?php
return [
/*
|--------------------------------------------------------------------------
| Authorized Domains
|--------------------------------------------------------------------------
|
| Domains that are authorized to be viewed through Caddy.
|
*/
'authorized' => [
'laravel.test',
// 'app.laravel.test',
],
];
app/Http/Middleware/TrustProxies.php
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array|string|null
*/
protected $proxies = '*'; // Add wildcard or specific domain(s)
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
routes/web.php
<?php
use App\Http\Controllers\CaddyController;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/caddy', CaddyController::class)->name('caddy');
// ...
Top comments (9)
Great post ! I've followed these steps but the certificate is not generated, only the authorities folder is populated. Can you please, look into it ? Also the start-container file produce and error when the
if
block is left empty, I've added anecho
there.So I spent the day with a colleague looking into this:
The reason there are no certificates generated and passed into your authorities folder is because the /caddy endpoint is not being successfully executed. This is essential for caddy to know which authorised domains to create certificates for.
If you tunnel into your caddy container, and curl for your application, you may observe that there is an error executing the file. Perhaps you have some authentication middleware preventing the /caddy endpoint from successfully executing.
We noticed that all Controller calls were being intercepted by middleware that demanded we logged in first.
Hey Adrian, this was an extremely helpful post and helped me go deeper into understanding docker. One major hiccup I am having is that my Storage facades don't seem to be working with Caddy. When I use https and echo storage_path(), it pulls up the ubuntu linux path to the file, whereas with using http, this the filepath to the directory on my computer. I am new to docker and couldn't locate the solution. Could this be an issue with the caddy configuration in the docker-compose?
Great Post!
I have a couple of questions:
What do we do with the resulting cert files? I also have keys, what do I do with those?
Is it a problem that instead of getting a laravel.test.cert, I got root.cert?
Edit:
With respect to 1., I believe I have to place the cert files into my keychain, I'm running on Mac.
With respect to 2, I started the CaddyServer in debug mode, tried to curl a request towards it and found that there was no certificate for laravel.test
{"level":"debug","ts":****************,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"laravel.test"}
This leads me to believe there is an issue with receiving root.cert instead of laravel.test.cert, I just don't know why my result is different to yours.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.