Tangible Bytes

A Web Developer’s Blog

Laravel Https in Dev With Docker Compose

I need to work on some cross site javascript and so I need a trusted TLS cert installed an running in my dev system.

I was already running via FrankenPHP and Docker Compose

I used (mkcert)[github.com/FiloSottile/mkcert] and customised the Caddyfile that FrankenPHP uses

Background

Read FrankenPHP and Docker Compose for more details

I have an entry in my /etc/hosts file so that locally laravel.test resolves to 127.0.0.1 and docker compose maps port 8000 on that container to localhost

Within docker laravel.test is the name of the container running laravel - other containers within the docker network resolve the name to the docker assigned IP.

So in both cases https://laravel.test:8000/ will map to the laravel container.

Mkcert

For me the easiest option was to clone the repo at https://github.com/FiloSottile/mkcert

Then I ran go build which generated the executable I needed - other install options are available

./mkcert -install created and installed a personal root Certificate that my browser can use to trust my dev certificates

./mkcert laravel.test creates laravel.test.pem" and laravel.test.pem which I copy to a ./certs/ directory in my Laravel app

I had .gitignore’d the certs directory as these only work for mea and each dev can create their own.

Docker Compose

FrankenPHP is based on caddy

I modified my startup command in docker-compose.yaml to specify a custom Caddyfile

entrypoint: php artisan octane:frankenphp --max-requests=1 --caddyfile=/app/Caddyfile

Caddyfile

I based this on the default stub at vendor/laravel/octane/src/Commands/stubs/Caddyfile

 1{ 
 2	admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT}
 3
 4	frankenphp {
 5		worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
 6	}
 7}
 8
 9https://:8000 {
10	tls /app/certs/laravel.test.pem /app/certs/laravel.test-key.pem
11	log {
12		level {$CADDY_SERVER_LOG_LEVEL}
13
14		# Redact the authorization query parameter that can be set by Mercure...
15		format filter {
16			wrap {$CADDY_SERVER_LOGGER}
17			fields {
18				uri query {
19					replace authorization REDACTED
20				}
21			}
22		}
23	}
24
25	route {
26		root * "{$APP_PUBLIC_PATH}"
27		encode zstd br gzip
28
29		# Mercure configuration is injected here...
30		{$CADDY_SERVER_EXTRA_DIRECTIVES}
31
32        reverse_proxy /node/* node:5173
33
34
35		php_server {
36			index frankenphp-worker.php
37			try_files {path} frankenphp-worker.php
38			# Required for the public/storage/ directory...
39			resolve_root_symlink
40		}
41	}
42}

I put in https://:8000 in place of {$CADDY_SERVER_SERVER_NAME} in the stub - this makes frankenPHP run https mode and on the port I want

I use a high numbered port so that the server does not need any special privileges

I specified the location of my certs in the tls line

I use vitejs which runs it’s own server with hot-reloaded javascript - this runs on a different container and in order to get this javascript on the same https server I reverse proxied it

reverse_proxy /node/* node:5173

ViteJS

In order to reverse proxy my viteJS code in dev mode I used a prefix /node/

Now I need to tell vitejs to use that prefix - but only in dev mode.

During build it places the files in the normal place - and those static files can be served by caddy/FrankenPHP directly

I also updated the origins to use https

 1import { sentryVitePlugin } from "@sentry/vite-plugin";
 2import { defineConfig } from "vite";
 3import laravel from "laravel-vite-plugin";
 4import react from "@vitejs/plugin-react";
 5
 6export default defineConfig(({ command, mode }) => {
 7    const isDev = command === "serve";
 8    return {
 9        base: isDev ? "/node/" : undefined,
10        plugins: [
11            laravel({
12                input: "resources/js/app.jsx",
13                refresh: true,
14            }),
15            react(),
16            sentryVitePlugin({
17                org: "myorg",
18                project: "cms-browser",
19            }),
20        ],
21
22        // deprecations in Sass not supported in bootstrap
23        //
24        // https://github.com/twbs/bootstrap/issues/40962
25        // https://github.com/twbs/bootstrap/issues/40849
26        css: {
27            preprocessorOptions: {
28                scss: {
29                    silenceDeprecations: [
30                        "mixed-decls",
31                        "color-functions",
32                        "global-builtin",
33                        "import",
34                        "legacy-js-api",
35                    ],
36                },
37            },
38        },
39        build: {
40            sourcemap: true,
41        },
42        test: {
43            environment: "jsdom",
44            setupFiles: ["./resources/js/tests/setup.js"],
45            globals: true,
46            resetMocks: true,
47            coverage: {
48                enabled: true,
49                include: "resources/js",
50                reporter: ["html", "lcov"],
51                reportsDirectory: "public/vitest-coverage/",
52            },
53            open: false,
54        },
55        server: {
56            host: "0.0.0.0",
57            origin: "https://laravel.test:8000",
58            cors: {
59                origin: "https://laravel.test:8000",
60                methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
61                preflightContinue: false,
62                optionsSuccessStatus: 204,
63            },
64            watch: {
65                ignored: ["/app/vendor", "/app/public", "/app/storage"],
66            },
67        },
68        resolve: {
69            alias: {
70                "@": "/resources/js",
71            },
72        },
73    };
74});

At this point it all worked - but I had failing Dusk tests

Dusk Tests

My Dusk test files - run selenium tests in a chrome browser.

This browser is inside a docker container and doesn’t know about my personal cert

I needed to modify the DuskTestCase.php file to ignore-certificate-errors and allow-insecure-localhost

This allowed my tests to run again

 1    protected function driver(): RemoteWebDriver
 2    {
 3        $options = (new ChromeOptions)->addArguments(collect([
 4            $this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
 5            '--disable-search-engine-choice-screen',
 6            '--ignore-certificate-errors',
 7            '--allow-insecure-localhost',
 8        ])->unless($this->hasHeadlessDisabled(), function (Collection $items) {
 9            return $items->merge([
10                '--disable-gpu',
11                '--headless=new',
12            ]);
13        })->all());
14
15        return RemoteWebDriver::create(
16            $_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515',
17            DesiredCapabilities::chrome()->setCapability(
18                ChromeOptions::CAPABILITY,
19                $options
20            )
21        );
22    }