Serve SPA nuxt from / and laravel api from /api

1. Caddy version (caddy version):

v2.2.0 h1:sMUFqTbVIRlmA8NkFnNt9l7s0e+0gw+7GPIrhty905A=

2. How I run Caddy:

locally for development

a. System environment:

$  uname -a
Linux pc10e748 5.4.0-48-generic #52-Ubuntu SMP Thu Sep 10 10:58:49 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$  cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.1 LTS"

b. Command:

sudo ./caddy start

d. My complete Caddyfile or JSON config:

*.wave-web.test {
        tls /home/hafiz/projects/wave/web/keys/wave-certificate.pem /home/hafiz/projects/wave/web/keys/wave-key.pem
        log {
                output file ./wave.log
                level warn
        }
        reverse_proxy localhost:3100
        encode gzip
        file_server

        php_fastcgi /api/* unix//run/php/php7.2-fpm.sock {
                root /home/hafiz/projects/wave/api/public
        }

3. The problem I’m having:

I would like to serve Nuxt ran with npm run dev at https://localhost:3100 that is accessible from https://abc.wave-web.test. And a Laravel api at /home/hafiz/projects/wave/api/public accessible starting with https://abc.wave-web.test/api

I have the following in nuxt.config.js to enable https. The location of the key and cert is the same as in the Caddyfile.

  server: {
    https: {
      key: fs.readFileSync(path.resolve(__dirname, 'keys/wave-key.pem')),
      cert: fs.readFileSync(path.resolve(__dirname, 'keys/wave-certificate.pem'))
    },
    host: 'wave-web.test',
    port: 3100
  }

I also have the following in /etc/hosts

127.0.0.1 abc.wave-web.test

4. Error messages and/or full log output:

I am getting a 502 error with blank page when I visit https://abc.wave-web.test

2020/10/13 22:49:59.742	INFO	autosaved config	{"file": "/root/.config/caddy/autosave.json"}
2020/10/13 22:49:59.743	INFO	admin.api	load complete
2020/10/13 22:49:59.743	INFO	tls.obtain	acquiring lock	{"identifier": "api.wave-web.test"}
2020/10/13 22:49:59.743	INFO	tls.obtain	lock acquired	{"identifier": "api.wave-web.test"}
2020/10/13 22:49:59.744	INFO	tls.issuance.acme	waiting on internal rate limiter	{"identifiers": ["api.wave-web.test"]}
2020/10/13 22:49:59.744	INFO	tls.issuance.acme	done waiting on internal rate limiter	{"identifiers": ["api.wave-web.test"]}
2020/10/13 22:49:59.747	INFO	tls.obtain	acquiring lock	{"identifier": "*.wave-web.test"}
2020/10/13 22:49:59.747	INFO	tls.obtain	lock acquired	{"identifier": "*.wave-web.test"}
2020/10/13 22:49:59.748	INFO	tls.issuance.acme	waiting on internal rate limiter	{"identifiers": ["*.wave-web.test"]}
2020/10/13 22:49:59.748	INFO	tls.issuance.acme	done waiting on internal rate limiter	{"identifiers": ["*.wave-web.test"]}
2020/10/13 22:50:00.240	INFO	admin	stopped previous server
2020/10/13 22:50:00.741	ERROR	tls.obtain	will retry	{"error": "[*.wave-web.test] Obtain: [*.wave-web.test] creating new order: request to https://acme-v02.api.letsencrypt.org/acme/new-order failed after 1 attempts: HTTP 400 urn:ietf:params:acme:error:rejectedIdentifier - Error creating new order :: Cannot issue for \"*.wave-web.test\": Domain name does not end with a valid public suffix (TLD) (ca=https://acme-v02.api.letsencrypt.org/directory)", "attempt": 1, "retrying_in": 60, "elapsed": 0.994353286, "max_duration": 2592000}
2020/10/13 22:50:00.791	ERROR	tls.obtain	will retry	{"error": "[api.wave-web.test] Obtain: [api.wave-web.test] creating new order: request to https://acme-v02.api.letsencrypt.org/acme/new-order failed after 1 attempts: HTTP 400 urn:ietf:params:acme:error:rejectedIdentifier - Error creating new order :: Cannot issue for \"api.wave-web.test\": Domain name does not end with a valid public suffix (TLD) (ca=https://acme-v02.api.letsencrypt.org/directory)", "attempt": 1, "retrying_in": 60, "elapsed": 1.047674146, "max_duration": 2592000}
2020/10/13 22:50:28.076	ERROR	http.log.error.log2	EOF	{"request": {"remote_addr": "127.0.0.1:58636", "proto": "HTTP/2.0", "method": "GET", "host": "abc.wave-web.test", "uri": "/", "headers": {"Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], "Accept-Language": ["en-US,en;q=0.5"], "Accept-Encoding": ["gzip, deflate, br"], "Upgrade-Insecure-Requests": ["1"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"], "Te": ["trailers"], "User-Agent": ["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "proto_mutual": true, "server_name": "abc.wave-web.test"}}, "duration": 0.000916714, "status": 502, "err_id": "d1iuhz9xp", "err_trace": "reverseproxy.(*Handler).ServeHTTP (reverseproxy.go:440)"}
2020/10/13 22:50:28.421	ERROR	http.log.error.log2	EOF	{"request": {"remote_addr": "127.0.0.1:58636", "proto": "HTTP/2.0", "method": "GET", "host": "abc.wave-web.test", "uri": "/favicon.ico", "headers": {"Te": ["trailers"], "User-Agent": ["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0"], "Accept": ["image/webp,*/*"], "Accept-Language": ["en-US,en;q=0.5"], "Accept-Encoding": ["gzip, deflate, br"], "Referer": ["https://abc.wave-web.test/"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "proto_mutual": true, "server_name": "abc.wave-web.test"}}, "duration": 0.000920695, "status": 502, "err_id": "j5yhhyh1z", "err_trace": "reverseproxy.(*Handler).ServeHTTP (reverseproxy.go:440)"}

Same with https://abc.wave-web.test/api/auth/signin

2020/10/13 23:49:37.339	ERROR	http.log.error.log2	EOF	{"request": {"remote_addr": "127.0.0.1:59646", "proto": "HTTP/1.1", "method": "POST", "host": "abc.wave-web.test", "uri": "/api/auth/signin", "headers": {"Postman-Token": ["b1014cbf-0228-438c-b0d5-e5395660f07f"], "Accept-Encoding": ["gzip, deflate, br"], "Origin": ["https://abc.wave-web.test"], "Content-Type": ["application/json"], "User-Agent": ["PostmanRuntime/7.26.5"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Content-Length": ["68"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "", "proto_mutual": true, "server_name": "abc.wave-web.test"}}, "duration": 0.002473961, "status": 502, "err_id": "95agsgn71", "err_trace": "reverseproxy.(*Handler).ServeHTTP (reverseproxy.go:440)"}

5. What I already tried:

I can access https://localhost:3100, but not https://abc.wave-web.test

 sudo ./caddy validate
2020/10/13 23:51:46.737	INFO	using adjacent Caddyfile
2020/10/13 23:51:46.739	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc000495d50"}
2020/10/13 23:51:46.739	WARN	tls	stapling OCSP	{"error": "no OCSP stapling for [wave-web.test]: no OCSP server specified in certificate"}
2020/10/13 23:51:46.739	INFO	http	enabling automatic HTTP->HTTPS redirects	{"server_name": "srv0"}
2020/10/13 23:51:46.739	INFO	http	server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server	{"server_name": "srv1", "http_port": 80}
2020/10/13 23:51:46.740	WARN	http	user server is listening on same interface as automatic HTTP->HTTPS redirects; user-configured routes might override these redirects	{"server_name": "srv1", "interface": "tcp/:80"}
2020/10/13 23:51:46.741	INFO	tls.cache.maintenance	stopped background certificate maintenance	{"cache": "0xc000495d50"}
Valid configuration

I think what you want is something more like this:

*.wave-web.test {
	tls cert.pem key.pem
	log {
		output file ./wave.log
		level warn
	}

	encode gzip

	handle /api* {
		root * /home/hafiz/projects/wave/api/public
		php_fastcgi unix//run/php/php7.2-fpm.sock
		file_server
	}

	reverse_proxy localhost:3100
}

The problem you were encountering is that the Caddyfile adapter will sort the directives based on a pre-determined order, which you can find here:

Specifically, php_fastcgi is sorted after reverse_proxy, so it will always take precedence, unless you override the order with route.

Instead, since the Laravel app is a single unit, we can use a handle directive to collect the directives relevant to that app together. The handle directive is ordered higher than reverse_proxy, so it will be matched first.

Requests that don’t match the /api* path matcher will fall through to the reverse_proxy.

Also note that file_server also needs to know what the site root, so using the root directive, rather than the root subdirective of php_fastcgi, sets the root variable for both of them, rather than just the one.