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 }