Configure Caddy behind Ngnix (https to https)

1. The problem I’m having:

Hi !
I’m trying to configure a Caddy server behind a ngnix acting as a reverse proxy (HTTPS to HTTPS).

I tried several configurations but I cannot figure out the correct one, I’m always getting SSL errors.

2. Error messages and/or full log output:

2023/06/09 01:10:01 [error] 448#448: *1 SSL_do_handshake() failed (SSL: error:0A000438:SSL routines::tlsv1 alert internal error:SSL alert number 80) while SSL handshaking to upstream, client:, server: staging.dolium.*, request: "GET / HTTP/2.0", upstream: "", host: ""
2023/06/09 01:10:01 [error] 448#448: *1 SSL_do_handshake() failed (SSL: error:0A000438:SSL routines::tlsv1 alert internal error:SSL alert number 80) while SSL handshaking to upstream, client:, server: staging.dolium.*, request: "GET /favicon.ico HTTP/2.0", upstream: "", host: "", referrer: ""

3. Caddy version: v2.6.4

4. How I installed and ran Caddy:

a. System environment:

Docker with Docker-Compose

b. Command:

# Build Caddy with the Mercure and Vulcain modules
# Temporary fix for
FROM caddy:2.7-builder-alpine AS app_caddy_builder

RUN xcaddy build v2.6.4 \
    --with \

# Caddy image
FROM caddy:2-alpine AS app_caddy

WORKDIR /srv/app

COPY --from=app_caddy_builder /usr/bin/caddy /usr/bin/caddy
COPY --from=app_php /srv/app/public public/
COPY docker/caddy/Caddyfile /etc/caddy/Caddyfile

c. Service/unit/compose file:

version: "3.4"

      - backend
    restart: unless-stopped
      - php_socket:/var/run/php
      - backend
      - php
      SERVER_NAME: ${}
    restart: unless-stopped
      - php_socket:/var/run/php
      - caddy_data:/data
      - caddy_config:/config


    name: proxy
    external: true

d. My complete Caddy config:

    # Debug



# Matches requests for HTML documents, for static files and for Next.js files,
# except for known API paths and paths with extensions handled by API Platform
@pwa expression `(
        header({'Accept': '*text/html*'})
        && !path(
            '/docs*', '/graphql*', '/bundles*', '/contexts*', '/_profiler*', '/_wdt*',
            '*.json*', '*.html', '*.csv', '*.yml', '*.yaml', '*.xml'
    || path('/favicon.ico', '/manifest.json', '/robots.txt', '/_next*', '/sitemap*')`

route {
    root * /srv/app/public
    mercure {
        # Transport to use (default to Bolt)
        transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
        # Publisher JWT key
        # Subscriber JWT key
        # Allow anonymous subscribers (double-check that it's what you want)
        # Enable the subscription API (double-check that it's what you want)
        # Extra directives

    # Add links to the API docs and to the Mercure Hub if not set explicitly (e.g. the PWA)
    header ?Link `</docs.jsonld>; rel="", </.well-known/mercure>; rel="mercure"`
    # Disable Topics tracking if not enabled explicitly:
    header ?Permissions-Policy "browsing-topics=()"

    # Comment the following line if you don't want Next.js to catch requests for HTML documents.
    # In this case, they will be handled by the PHP app.
    reverse_proxy @pwa http://{$PWA_UPSTREAM}

    php_fastcgi unix//var/run/php/php-fpm.sock
    encode zstd gzip

e. My complete NGNIX Reverse proxy config:

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name staging.dolium.*;
    include /config/nginx/ssl.conf;

    location / {
        include /config/nginx/proxy.conf;
        include /config/nginx/resolver.conf;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;

        set $upstream_app dolium-caddy-1;
        set $upstream_port 443;
        set $upstream_proto https;
        proxy_pass $upstream_proto://$upstream_app:$upstream_port;


5. Links to relevant resources:

Why do you need Nginx at all? Why not only use Caddy? It’ll be much simpler.


Thank you for your quick answer.
I have a lot of other applications that are proxied by the reverse proxy and I don’t want to have multiple entrypoints (just 1 reverse proxy to front every app).

It should not be hard to set up a Caddy behind a reverse proxy (I guess it’s a common architecture), the issue is probably just a matter of configuration.

P.S : For test purpose, I set up the Caddy server in front (with no reverse proxy) and it was working properly.

It can be complicated.

If you want Caddy to automate TLS, it essentially has to be in front, because otherwise the ACME TLS-ALPN challenge cannot work, and the ACME HTTP challenge often does not work if something in front redirects HTTP.

And it also complicates client IP handling, since you need to trust the IP of the upstream (i.e. trusted_proxies config).

Caddy can proxy those too.

Yeah, I was afraid of that.
So today I tried to set up Caddy to serve on http instead of https and I put it in front (so the reverse proxy out of the equation for now).

I just changed the SERVER_NAME variable like this


But I’m still getting a 400 error : Bad request

{"level":"error","ts":1686592693.9719913,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"","remote_port":"48236","proto":"HTTP/1.1","method":"GET","host":"","uri":"/docs","headers":{"Connection":["Keep-Alive"],"User-Agent":["Wget/1.21"],"Accept":["*/*"],"Accept-Encoding":["identity"]}},"user_id":"","duration":0.006384815,"size":811,"status":400,"resp_headers":{"Link":["</docs.jsonld>; rel=\"\""],"Status":["400 Bad Request"],"Content-Type":["text/html; charset=UTF-8"],"Cache-Control":["no-cache, private"],"Date":["Mon, 12 Jun 2023 17:58:13 GMT"],"Permissions-Policy":["browsing-topics=()"],"Server":["Caddy"]}}

Do you know if it’s possible to have more detailed logs ?

Yes, I suppose but I would rather not have to modify the existing infrastructure. :slightly_smiling_face:


Set this env var to debug to enable debug level logging.

The 400 status response is coming from your PHP app, not from Caddy itself. You should log requests in your PHP app to see what caused it to respond with a 400 status.

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