How to Enable SSL for Local Laravel Sail Development using Caddy and Docker
Tags: laravel, ssl, docker, devops
Published: 2021-10-10 17:21:02

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

```yaml
# ...
  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

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

```sh
#!/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
<?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
<?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
<?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
<?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');

// ...
```