DEV Community

Cover image for How to Enable SSL for Local Laravel Sail Development using Caddy and Docker
Adrian Mejias
Adrian Mejias

Posted on

How to Enable SSL for Local Laravel Sail Development using Caddy and Docker

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 PWAs 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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

config/caddy.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authorized Domains
    |--------------------------------------------------------------------------
    |
    | Domains that are authorized to be viewed through Caddy.
    |
    */

    'authorized' => [
        'laravel.test',
        // 'app.laravel.test',
    ],

];
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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');

// ...
Enter fullscreen mode Exit fullscreen mode

Top comments (9)

Collapse
 
mrdionjr profile image
Salomon Dion

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 an echo there.

Collapse
 
sonneybouy profile image
sonneybouy

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.

Collapse
 
sonneybouy profile image
sonneybouy • Edited

Great Post!

I have a couple of questions:

  1. What do we do with the resulting cert files? I also have keys, what do I do with those?

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

Collapse
 
richardbnelson profile image
richardbnelson

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?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.