Server an external site on a subpath

1. The problem I’m having:

Note: On this question, I have anonymized all URLs and etc,

Context:

My customer uses a caddy 2.6.3 on his server (https://mygooddomain.com.br), and wants a specific URL /pages/ and every subpath of that url to transparently display to the user the content of an external website (https://sample-site.deno.dev) ( it’s a new CMS he wants to try and migrate the site little by little)

I managed to create a configuration that works only partially and doesn’t suit all cases by using:

	handle /pages* {
		redir /pages /pages/ permanent
		uri strip_prefix /pages/
		reverse_proxy * https://sample-site.deno.dev {
			header_up Host {http.reverse_proxy.upstream.hostport}
			header_up X-Forwarded-Proto {scheme}
		}
	}

Problem:

The previous configuration works correctly for https://mygooddomain.com.br/pages/ but does not work for https://mygooddomain.com.br/pages/my-subpath and all subpaths of /pages/

Is there any way to configure this?

That is, I want the routes to look something like this:

https://mygooddomain.com.br → Caddy redirects the request to production-a:5000 or production-b:5000

https://mygooddomain.com.br/pages/?q=querystring → Caddy shows/redirects-changing-the-url the request to https://sample-site.deno.dev/?q=querystring

https://mygooddomain.com.br/pages/my-subpath?q=querystring → Caddy shows/redirects-without-changing-the-url the request to https://sample-site.deno.dev/my-subpath?q=querystring

https://mygooddomain.com.br/pages/my-subpath/?q=querystring → Caddy shows/redirects-without-changing-the-url the request to https://sample-site.deno.dev/my-subpath/?q=querystring

https://mygooddomain.com.br/pages/my-subpath/subsubpath?q=querystring → Caddy shows/redirects-without-changing-the-url the request to https://sample-site.deno.dev/my-subpath/subsubpath?q=querystring

2. Error messages and/or full log output:

Caddy 2.6.3 generate two log lines:

Note: I had to send a link to gist because the this forum site refused to accept the JSON content even though it was inside a ``` code block

3. Caddy version:

2.6.3

4. How I installed and ran Caddy:

official docker caddy:2.6.3-alpine image

a. System environment:

Ubuntu 22.04
Docker version 23.0.3, build 3e7cbfd
Docker Compose version v2.17.2

b. Command:

PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

c. Service/unit/compose file:


d. My complete Caddy config:

# See https://caddyserver.com/docs

# Email for Let's Encrypt expiration notices
{
	email {$TLS_EMAIL}
	# Production directory
	# comment the bellow line to allow Caddy uses letsencrypt
	# or fallback to ZeroSSL
	# acme_ca https://acme-v02.api.letsencrypt.org/directory

	# Staging directory - certificates will be invalid
	# use this to testing and avoid the rate-limit
	# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

(security_headers) {
	header * {
		# enable HSTS
		# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#strict-transport-security-hsts
		Strict-Transport-Security "max-age=3600; includeSubDomains; preload"

		# disable clients from sniffing the media type
		# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options
		X-Content-Type-Options "nosniff"

		# clickjacking protection
		# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-frame-options
		X-Frame-Options "DENY"

		# xss protection
		# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-xss-protection
		X-XSS-Protection "1; mode=block"

		# Remove -Server header, which is an information leak
		# Remove Caddy from Headers
		-Server

		# keep referrer data off of HTTP connections
		# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#referrer-policy
		Referrer-Policy strict-origin-when-cross-origin
	}
}

# "www" redirect to "non-www" version
www.mygooddomain.com.br {
	import security_headers
	redir https://mygooddomain.com.br{uri}
}


www.staging.mygooddomain.com.br {
	import security_headers
	redir https://staging.mygooddomain.com.br{uri}
}

(shared_config) {
	import security_headers
	# Dynamically compress response with gzip when it makes sense.
	# This setting is ignored for precompressed files.
	encode zstd gzip

	# Logs:
	log {
		output stdout
	}

	@health_path {
		method GET
		path /health
	}
	handle @health_path {
		respond "Webserver ok" 200
	}
	@other_paths {
		# controls domains or path that
		# the basicauth should not be enforced
		not path /health
		not host mygooddomain.com.br
	}

	basicauth @other_paths {
		# https://caddyserver.com/docs/caddyfile/directives/basicauth
		# ./caddy_linux_amd64 hash-password --plaintext "<REPLACE_BY_YOUR_PASSWORD>"
		# DO NOT USE THE HASH BELOW. IT'S A RANDOM GENERATED USED ONLY FOR THIS SAMPLE
		mygooddomain $2a$14$UgRcULU9Hgv3f2QfRXV2qOUpq3.WxWs4vKlYetIaiQc3j.CezzkPO
	}

	tls {
		# uncomment force generates a self-signed TLS certificate
		# issuer internal

		# generated 2023-01-28, Mozilla Guideline v5.6, Caddy 2.1.2, intermediate configuration
		# https://ssl-config.mozilla.org/#server=caddy&version=2.1.2&config=intermediate&guideline=5.6
		# note that Caddy automatically configures safe TLS settings
		protocols tls1.2 tls1.3
		ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
	}
}

mygooddomain.com.br, staging.mygooddomain.com.br {
	import shared_config

	@production {
		host mygooddomain.com.br
	}

	@staging {
		host staging.mygooddomain.com.br
	}

	handle /pages* {
		redir /pages /pages/ permanent
		uri strip_prefix /pages/
		reverse_proxy * https://sample-site.deno.dev {
			header_up Host {http.reverse_proxy.upstream.hostport}
			header_up X-Forwarded-Proto {scheme}
		}
	}

	# Serve static files
	handle /static/* {
		uri strip_prefix /static
		header {
			Cache-Control "public, max-age=2592000, must-revalidate"
			defer
		}

		file_server @production {
			# STATIC_ROOT for production
			root /data/mygooddomain.com.br/static
			# Staticfiles are pre-compressed in `start.sh`
			precompressed br gzip
		}
		file_server @staging {
			# STATIC_ROOT for production
			root /data/staging.mygooddomain.com.br/static
			# Staticfiles are pre-compressed in `start.sh`
			precompressed br gzip
		}
	}

	# Serve media files
	handle /media/* {
		uri strip_prefix /media
		header {
			Cache-Control "private, max-age=2592000, must-revalidate"
			defer
		}
		file_server @production {
			# MEDIA_ROOT for production
			root /data/mygooddomain.com.br/media
		}
		file_server @staging {
			# MEDIA_ROOT for production
			root /data/staging.mygooddomain.com.br/media
		}
	}

	# Serve production app
	handle @production {
		reverse_proxy production-a:5000 production-b:5000 {
			health_headers {
				Host localhost
			}
			health_uri /health/app
			health_interval 1s
			health_timeout 5s
			health_status 200
			fail_duration 5s
			lb_policy round_robin
			header_up Host {host}
			header_up X-Real-IP {remote}
		}
	}
	# Serve staging app
	handle @staging {
		reverse_proxy staging-a:5000 staging-b:5000 {
			health_headers {
				Host localhost
			}
			health_uri /health/app
			health_interval 1s
			health_timeout 5s
			health_status 200
			fail_duration 5s
			lb_policy round_robin
			header_up Host {host}
			header_up X-Real-IP {remote}
		}
	}
}

5. Links to relevant resources:

See this article:

I’d strongly recommend using a subdomain instead of subpath. Much simpler, avoids these problems.

Thanks. I will try to convince my customer to use a subdomain.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.