Access to local https site using Synology DDNS + Synology reverse proxy

1. The problem I’m having:

Hi. I have the following devices in my local network:

  • router (192.168.1.1)
  • Synology NAS (192.168.1.3)
  • PC (192.168.1.20)

80/443 ports are forwarded from router to NAS. On NAS I have configured DDNS *.superdomain.synology.me and certificate for it (Let’s Encrypt, derived using Synology UI). On PC I have local site running in docker container with Caddy inside with server name https://myservice.localhost.

In Synology reverse proxy I created subdomain myservice.superdomain.synology.me and configure the following rules:

HTTPS myservice.superdomain.synology.me 443 -> HTTPS 192.168.1.20 443
HTTP myservice.superdomain.synology.me 80 -> HTTP 192.168.1.20 80

but after that myservice.*.synology.me return me internal 502 Synology error page. I tried different solutions, but no luck. I believe problem with missing correct reverse_proxy directive Caddyfile and probably TLS, because with absolutely same settings in Synology reverse proxy I can successfully open other local sites on my PC inside docker (which use Nginx inside) using myservice.**.synology.me. What is the proper Caddyfile settings to achieve that?

2. Error messages and/or full log output:

3. Caddy version: 2.7.6

4. How I installed and ran Caddy:

a. System environment:

Ubuntu 22.04, Docker 25.0.3, Docker Compose v2.24.5

b. Command:

SERVER_NAME=myservice.localhost docker compose up -d

c. Service/unit/compose file:

services:
  php:
    image: ${IMAGES_PREFIX:-}app-php
    restart: unless-stopped
    environment:
      SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
      MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
      MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
      TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}
      TRUSTED_HOSTS: ^${SERVER_NAME:-example\.com|localhost}|php$$
      # Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
      DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
      # Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
      MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
      MERCURE_PUBLIC_URL: https://${SERVER_NAME:-localhost}/.well-known/mercure
      MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
      # The two next lines can be removed after initial installation
      SYMFONY_VERSION: ${SYMFONY_VERSION:-}
      STABILITY: ${STABILITY:-stable}
    volumes:
      - caddy_data:/data
      - caddy_config:/config
    ports:
      # HTTP
      - target: 80
        published: ${HTTP_PORT:-80}
        protocol: tcp
      # HTTPS
      - target: 443
        published: ${HTTPS_PORT:-443}
        protocol: tcp
      # HTTP/3
      - target: 443
        published: ${HTTP3_PORT:-443}
        protocol: udp

# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###

volumes:
  caddy_data:
  caddy_config:
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###

d. My complete Caddy config:

{
	{$CADDY_GLOBAL_OPTIONS}

	frankenphp {
		{$FRANKENPHP_CONFIG}
	}

	# https://caddyserver.com/docs/caddyfile/directives#sorting-algorithm
	order mercure after encode
	order vulcain after reverse_proxy
	order php_server before file_server
}

{$CADDY_EXTRA_CONFIG}

{$SERVER_NAME:localhost} {
	log {
		# Redact the authorization query parameter that can be set by Mercure
		format filter {
			wrap console
			fields {
				uri query {
					replace authorization REDACTED
				}
			}
		}
	}

	root * /app/public
	encode zstd br gzip

	mercure {
		# Transport to use (default to Bolt)
		transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
		# Publisher JWT key
		publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
		# Subscriber JWT key
		subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
		# Allow anonymous subscribers (double-check that it's what you want)
		anonymous
		# Enable the subscription API (double-check that it's what you want)
		subscriptions
		# Extra directives
		{$MERCURE_EXTRA_DIRECTIVES}
	}

	vulcain

	{$CADDY_SERVER_EXTRA_DIRECTIVES}

	# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
	header ?Permissions-Policy "browsing-topics=()"

	php_server
}

5. Links to relevant resources:

I think you mean from router to PC? You need ports 80/443 forwarded to your server running Caddy.

Oh, you’re trying to make requests go router → synology → PC? Why?

I think you mean from router to PC? You need ports 80/443 forwarded to your server running Caddy.

no, to NAS. Reverse proxy on Synology also add other subdomains to different apps like Plex, HomeAssistant, etc running locally on NAS.

Oh, you’re trying to make requests go router → synology → PC? Why?

I need public access to my local app, which is under development now. I just want to use existing DDNS subdomain, which always point to my public IP. Of course I can use something like Ngrok, but I thought it would be easy to achieve using Caddy. Absolutely same approach works fine with other local sites on my PC handled by docker and nginx inside.

If you’re proxying through something else, you need to think about how TLS is managed. Is your proxy on Synology serving TLS? If so then don’t use TLS on your Caddy server, and only serve HTTP (e.g. use :80 as your site address).

Caddy usually can’t automate TLS issuance if it’s behind another proxy.

so there is no way to have both working https urls for 1 site:
local https://myservice.localhost
and public https://myservice.superdomain.synology.me
?

Yes it’s possible, but you’re saying that you want to proxy through another server, so unless that server is a TCP-layer proxy (doesn’t sound like it is), it must be terminating TLS. So Caddy itself would be serving HTTP behind it.

ok, let’s consider, that everything is ok with reverse proxy on Synology (I believe it’s ok, because I can open other local HTTPS sites using it). what should I change in Caddyfile to make both (local and public) HTTPS urls work?

Your site address might be something like:

http://myservice.superdomain.synology.me, https://myservice.localhost {

This would match HTTP requests for myservice.superdomain.synology.me (proxied from your other server) and set up local HTTPS for myservice.localhost.

that’s it? I just need to replace existing line

{$SERVER_NAME:localhost} {

which is actually

myservice.localhost, php:80 {

to

http://myservice.superdomain.synology.me, https://myservice.localhost, php:80 {

and change existing rule

HTTPS myservice.superdomain.synology.me 443 -> HTTPS 192.168.1.20 443

to

HTTPS myservice.superdomain.synology.me 443 -> HTTP 192.168.1.20 80

on Synology reverse proxy?

omg, it works! thanks! the only problem, that now I have the following error in chrome console:

(index):2 Mixed Content: The page at 'https://myservice.superdomain.synology.me/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://myservice.superdomain.synology.me/_wdt/677fc6'. This request has been blocked; the content must be served over HTTPS.

does it mean, that I should resolve it on app level or it can be fixed in Caddyfile?

Basically, your app is generating URLs with http://, probably because the X-Forwarded-Proto header Caddy is sending it is “wrong”.

You probably need to configure trusted_proxies so that the X-Forwarded-Proto header is passed through correctly.

it turned out that everything was fine with X-Forwarded-Proto header. problem was in app configuration, it doesn’t consider TRUSTED_PROXIES env variable defined in docker compose file.

1 Like

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