Docker Compose - Caddy - localhost: Wildcard Subdomain (*.localhost) reverse_proxy to https://localhost

1. Caddy version (caddy version):

v2.4.6 h1:HGkGICFGvyrodcqOOclHKfvJC0qTU7vny/7FhYp9hNw=

2. How I run Caddy:

By cloning the following repository and following the README instructions:

a. System environment:

Docker Compose

b. Command:

In case Github link is not enough:

git clone git@github.com:dunglas/symfony-docker.git sf-docker
cd sf-docker
docker-compose build --pull --no-cache
docker-compose up

Go to https://localhost

c. Service/unit/compose file:

docker-compose.yaml

version: "3.4"

services:
  php:
    build:
      context: .
      target: symfony_php
      args:
        SYMFONY_VERSION: ${SYMFONY_VERSION:-}
        SKELETON: ${SKELETON:-symfony/skeleton}
        STABILITY: ${STABILITY:-stable}
    restart: unless-stopped
    volumes:
      - php_socket:/var/run/php
    healthcheck:
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 30s
    environment:
      # Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
      DATABASE_URL: postgresql://${POSTGRES_USER:-symfony}:${POSTGRES_PASSWORD:-ChangeMe}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-13}
      # Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
      MERCURE_URL: ${CADDY_MERCURE_URL:-http://caddy/.well-known/mercure}
      MERCURE_PUBLIC_URL: https://${SERVER_NAME:-localhost}/.well-known/mercure
      MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeMe!}

  caddy:
    build:
      context: .
      target: symfony_caddy
    depends_on:
      - php
    environment:
      SERVER_NAME: ${SERVER_NAME:-localhost, caddy:80}
      MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeMe!}
      MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeMe!}
    restart: unless-stopped
    volumes:
      - php_socket:/var/run/php
      - 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 ###

###> doctrine/doctrine-bundle ###
  database:
    image: postgres:${POSTGRES_VERSION:-13}-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-app}
      # You should definitely change the password in production
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ChangeMe}
      POSTGRES_USER: ${POSTGRES_USER:-symfony}
    volumes:
      - db-data:/var/lib/postgresql/data:rw
      # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
      # - ./docker/db/data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###

  pgadmin:
        container_name: pgadmin_container
        image: dpage/pgadmin4
        links:
            - database
        depends_on: 
            - database
        environment:
            PGADMIN_DEFAULT_EMAIL: '${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}'
            PGADMIN_DEFAULT_PASSWORD: '${PGADMIN_DEFAULT_PASSWORD:-adminstsapi}'
        volumes:
            - 'pgadmin:/root/.pgadmin'
        ports:
            - '${PGADMIN_PORT:-5050}:80'
        restart: unless-stopped

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

###> doctrine/doctrine-bundle ###
  db-data:
###< doctrine/doctrine-bundle ###
  pgadmin:

d. My complete Caddyfile or JSON config:

{
    # Debug
    {$DEBUG}
    # HTTP/3 support
    servers {
        protocol {
            experimental_http3
        }
    }
}

{$SERVER_NAME}

log

route {
    root * /srv/app/public
    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
    push
    php_fastcgi unix//var/run/php/php-fpm.sock
    encode zstd gzip
    file_server
}

3. The problem I’m having:

I am trying to point all subdomains (*.localhost) to localhost, not as a redirect but I will be extracting the subdomain in my Symfony app. I was able to do this outside of Docker by running Caddy via Homebrew and the Symfony app via php local dev server with the following in the Caddyfile:

*.localhost {
    reverse_proxy localhost:8000
}

And setting a default URL in config/packages/dev/framework.yaml:

framework:
  router:
    default_uri: 'https://localhost'

How can I achieve the same thing with a docker-ized app? Many thanks in advance!

4. Error messages and/or full log output:

{
   "level":"error",
   "ts":1640819019.8104544,
   "logger":"http.log.error",
   "msg":"dial tcp: lookup localhost,: no such host",
   "request":{
      "remote_addr":"172.23.0.1:59338",
      "proto":"HTTP/2.0",
      "method":"GET",
      "host":"hello.localhost",
      "uri":"/hello",
      "headers":{
         "Sec-Ch-Ua-Platform":[
            "\"macOS\""
         ],
         "Pragma":[
            "no-cache"
         ],
         "Cache-Control":[
            "no-cache"
         ],
         "Dnt":[
            "1"
         ],
         "Upgrade-Insecure-Requests":[
            "1"
         ],
         "Sec-Fetch-Dest":[
            "document"
         ],
         "Sec-Ch-Ua-Mobile":[
            "?0"
         ],
         "Accept-Language":[
            "en-GB,en;q=0.9,en-US;q=0.8"
         ],
         "User-Agent":[
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 Edg/96.0.1054.62"
         ],
         "Sec-Fetch-Site":[
            "none"
         ],
         "Sec-Fetch-Mode":[
            "navigate"
         ],
         "Sec-Fetch-User":[
            "?1"
         ],
         "Sec-Ch-Ua":[
            "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"96\", \"Microsoft Edge\";v=\"96\""
         ],
         "Accept":[
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
         ],
         "Accept-Encoding":[
            "gzip, deflate, br"
         ]
      },
      "tls":{
         "resumed":false,
         "version":772,
         "cipher_suite":4865,
         "proto":"h2",
         "proto_mutual":true,
         "server_name":"hello.localhost"
      }
   },
   "duration":0.000286095,
   "status":502,
   "err_id":"cgczcyd19",
   "err_trace":"reverseproxy.statusError (reverseproxy.go:886)"
}

5. What I already tried:

Caddyfile:

{
    # Debug
    {$DEBUG}
    # HTTP/3 support
    servers {
        protocol {
            experimental_http3
        }
    }
}

# wildcard subdomain
*.{$SERVER_NAME} {
    reverse_proxy {$SERVER_NAME}
}

# {$SERVER_NAME}

log

route {
    root * /srv/app/public
    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
    push
    php_fastcgi unix//var/run/php/php-fpm.sock
    encode zstd gzip
    file_server
}

6. Links to relevant resources:

This will cause an invalid config, because you’re mixing use of a site with a block, with a site without a block. See the Caddyfile Concepts doc page to understand.

Basically, the Symfony Caddyfile uses the “single site” syntax where it doesn’t use a block (i.e. { }) so that directives can be written at the left gutter instead of tabbed in once. But this only works if there’s only one site. Otherwise, parsing will fail.

This error happens because you used a comma in the SERVER_NAME environment variable (i.e. ${SERVER_NAME:-localhost, caddy:80} in your docker-compose.yml), which isn’t supported by reverse_proxy. Don’t mix usage of the environment variable, it’s specifically formatted to work as a site address, but not as reverse_proxy upstreams. Make another env var for that instead, or just hard-code it.

1 Like

Hi Francis,

Thanks for your response! That was very helpful. I will have a deeper dive into Caddy itself as clearly I am just guessing how to configure it. As for the env variable, you was right. Assigning it to a separate env variable did the trick.

Thanks again!

1 Like

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