HTTPS for Docker Containers with Caddy and Tailscale

1. The problem I’m having:

I’m trying to get Caddy to use TLS certificates from Tailscale to enable HTTPS for my Docker containers. Originally, I wanted to reverse proxy services into their own subdomains, but upon realizing this wasn’t possible with Tailscale MagicDNS, I switched to subfolders and ran into “the subfolder problem.” However, Jellyfin worked and I successfully created a WebDAV server backend, so I decided to keep the working parts of the configuration. Since none of the other containers worked (although HTTPS was enabled, they delivered blank pages), I decided to stick with port numbers for containers that actually need HTTPS (such as Actual, which breaks without it.) For some reason, I can’t get it to work despite attempting multiple different configurations. From what I understand, this is supposed to be automatic, but I’m not sure how to get it working. I have Tailscale installed globally on the machine and I’m using a pre-built Docker image of Caddy bundled with mholt’s WebDAV plugin.

I’m very new to self-hosting and server management, so any guidance would be much appreciated.

2. Error messages and/or full log output:

$ wget https://orangepi.pancake-enigmatic.ts.net:5006
--2025-01-08 03:55:10--  https://orangepi.pancake-enigmatic.ts.net:5006/
Resolving orangepi.pancake-enigmatic.ts.net (orangepi.pancake-enigmatic.ts.net)... 100.109.35.122
Connecting to orangepi.pancake-enigmatic.ts.net (orangepi.pancake-enigmatic.ts.net)|100.109.35.122|:5006... connected.
GnuTLS: An unexpected TLS packet was received.
Unable to establish SSL connection.
$ docker logs caddy
{"level":"info","ts":1736310310.1800172,"msg":"using config from file","file":"/etc/caddy/Caddyfile"}
{"level":"info","ts":1736310310.1872396,"msg":"adapted config to JSON","adapter":"caddyfile"}
{"level":"info","ts":1736310310.1952615,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//[::1]:2019","//127.0.0.1:2019","//localhost:2019"]}
{"level":"info","ts":1736310310.1959658,"logger":"http.auto_https","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
{"level":"info","ts":1736310310.1959867,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0x4000218e80"}
{"level":"info","ts":1736310310.1960385,"logger":"http.auto_https","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
{"level":"info","ts":1736310310.2001424,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
{"level":"info","ts":1736310310.2008374,"msg":"failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB). See https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes for details."}
{"level":"info","ts":1736310310.201608,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
{"level":"warn","ts":1736310310.201685,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":5006"}
{"level":"warn","ts":1736310310.2017977,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":5006"}
{"level":"info","ts":1736310310.2018197,"logger":"http.log","msg":"server running","name":"srv1","protocols":["h1","h2","h3"]}
{"level":"warn","ts":1736310310.2018487,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":80"}
{"level":"warn","ts":1736310310.20194,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":80"}
{"level":"info","ts":1736310310.2019649,"logger":"http.log","msg":"server running","name":"remaining_auto_https_redirects","protocols":["h1","h2","h3"]}
{"level":"info","ts":1736310310.2026048,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1736310310.2026432,"msg":"serving initial configuration"}
{"level":"info","ts":1736310310.213812,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/data/caddy","instance":"3dd2ea30-3c7f-4e39-857d-0e118c5c7dda","try_again":1736396710.213804,"try_again_in":86399.999996918}
{"level":"info","ts":1736310310.2142305,"logger":"tls","msg":"finished cleaning storage units"}

3. Caddy version:

v2.9.0

4. How I installed and ran Caddy:

Docker compose. I also use Portainer for management.

a. System environment:

Caddy is installed as a Docker container on my Orange Pi Zero 3 running DietPi. It has its own Docker network that all containers share.

b. Command:

$ docker compose up -d

c. Compose file:

services:
  caddy:
    image: juvenn/caddy-dav:2.9.0
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - /mnt/external/siyuan-data:/mnt/external/siyuan-data # Siyuan notes
      - /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock:ro # Tailscale
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped
    networks:
      - caddy_network

networks:
  caddy_network:
    driver: bridge

volumes:
  caddy_data:
  caddy_config:

d. My complete Caddy config:

{
  order webdav before file_server
}

orangepi.pancake-enigmatic.ts.net {

  handle_path /jellyfin/* {
    reverse_proxy jellyfin:8096
  }

  handle_path /webdav/* {
    root * /mnt/external/siyuan-data
    basicauth {
      admin HASHED_PASSWORD
    }
    file_server browse
    webdav  
  }
}

:5006 {
    reverse_proxy actual:5006
  }

5. Links to relevant resources:

Generally, a blank page indicates a problem with the port specified to reverse proxy. That could mean that the port for the application is not designed to work as HTTP access, which Caddy then makes accessible as HTTPS.

What exact application is Actual?

Actual is a budget management software that uses port 5006; I’ve set it up to use the same port internally and for user access. To be clear, the blank page behavior only occurs when I attempt to use subfolders with applications that apparently do not support it (the subfolder problem). The issue I encounter when I try to use ports instead and turn a URL like http://orangepi.pancake-enigmatic.ts.net:5006 into https://orangepi.pancake-enigmatic.ts.net:5006 is a full-blown error message in the browser. The terminal responds with:

$ wget https://orangepi.pancake-enigmatic.ts.net:5006
--2025-01-12 21:23:15--  https://orangepi.pancake-enigmatic.ts.net:5006/
Resolving orangepi.pancake-enigmatic.ts.net (orangepi.pancake-enigmatic.ts.net)... 100.109.35.122
Connecting to orangepi.pancake-enigmatic.ts.net (orangepi.pancake-enigmatic.ts.net)|100.109.35.122|:5006... connected.
GnuTLS: An unexpected TLS packet was received.
Unable to establish SSL connection.

and:

$ curl https://orangepi.pancake-enigmatic.ts.net:5006
curl: (35) OpenSSL/3.0.15: error:0A00010B:SSL routines::wrong version number

I vaguely understand what the problem is, but can’t figure out how to resolve it. This happens with every port I try, so it is not exclusive to 5006. I feel I must be making a basic mistake, but I don’t know what it is.

I’m prefacing this with my experience being little compared to many on this forum.

It looks to me like you’re trying to access both Actual and Jellyfin from the same subdomain. If you gave Actual a proper subdomain, my assumption is the issue would be resolved. Did you already try that?

Edit: I didn’t know much about TailScale and still don’t, but I see the issue with subdomains in that regard. I think you need to add the Actual reverse proxy to the orangepi.pancake-enigmatic.ts.net block. So it should look like this.

  order webdav before file_server
}

orangepi.pancake-enigmatic.ts.net {

  handle_path /jellyfin/* {
    reverse_proxy jellyfin:8096
  }

  handle_path /webdav/* {
    root * /mnt/external/siyuan-data
    basicauth {
      admin HASHED_PASSWORD
    }
    file_server browse
    webdav  
  }

  handle_path /actual/* { 
    reverse_proxy actual:5006 
  }
}

Does an Actual path exist?

Forgive me if I’m misunderstanding, but isn’t this solution attempting to create a subpath/subfolder for Actual? As I mentioned, attempting to reverse proxy applications into dedicated subfolders is only successful with Jellyfin (ostensibly because Jellyfin devs anticipated some people would try to use subfolders instead of subdomains).

Regardless, based on Tailscale’s documentation, Caddy is supposed to automatically fetch Tailscale’s certificates without needing any configuration at all. I don’t understand what I’m doing wrong, because this obviously is not my experience.

The crux of my issue is: Caddy can fetch these certificates and upgrade the URLs to HTTPS, but only if they’re being reverse proxied into subfolders. This is fine for services like Jellyfin, but for every other application (including my WebDAV server), this leads to a blank page. In the case of my WebDAV server, I’ve just treated it as a bandaid solution because the backend working is more important than my actually being able to browse files. However, I want to resolve the underlying problem of “I need to upgrade some of these HTTP Tailscale URLs to HTTPS.” I feel I must be missing something very obvious/straightforward because all the documentation suggests this is supposed to be quite simple.

I’ve considered exploring something like DuckDNS to try and work around all this, but before I invest the time and energy, I want to know if there is a simple way I can use Caddy to turn http://orangepi.pancake-enigmatic.ts.net:5006 into https://orangepi.pancake-enigmatic.ts.net:5006.

I have tried your suggestion previously, which returned the same error of curl: (35) OpenSSL/3.0.15: error:0A00010B:SSL routines::wrong version number. I tried it again just to make doubly sure, but yes, it still returns this error and returns a length of 0 despite the 200 OK HTTP response. I have tried various configurations (using handle instead of handle_path, attempting subdomains, subpaths, moving blocks out of the subdomain block and then back in, etc., but no success).

Thank you for your suggestion, however. I’m grateful for any clarity or potential solutions – this problem has been driving me a little insane for weeks now.

Yeah, this is out of my realm. Sorry I can’t help more. I’m hoping someone else sees this and can provide more direction.