Caddy + Tailscale + cloudflare DNS + owndomain

1. The problem I’m having:

I am trying to restrict access to portainer using my domain but only within tailscale network so that sensitive service like this is not exposed outside of my network. But since I have setup authentik oauth, i want to use it with domain name instead of ipaddress:port.

2. Error messages and/or full log output:

When I access from outside of my LAN network, although with tailscale connected and active, I am getting response denied message that I have setup to show for access outside of my network. So looks like caddy is not recognizing tailscale network access or I have incorrectly setup the remote_ip config. I am not sure if I have set it up correctly, but even though I am connected in my tailscale net, I still cant access portainer using the domain name. It works using IP, so the tailscale net is active.



3. Caddy version: v2.10.0

4. How I installed and ran Caddy:

version: "3.8"

services:
  caddy:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: caddy
    restart: unless-stopped
    env_file:
      - .env
    environment:
      -  CLOUDFLARE_API_TOKEN=${CADDY_CLOUDFLARE_API_TOKEN}  # Replace with your actual token (**don't commit this file to version control!**)
      -  CLOUDFLARE_EMAIL=${CADDY_CLOUDFLARE_EMAIL}
      -  ACME_AGREE=true
    ports:
      - "${COMPOSE_PORT_HTTPS:-443}:443"
      - "${COMPOSE_PORT_HTTP:-80}:80"
      - 2019:2019
    volumes:
      - /volume1/docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - /volume1/docker/caddy/data:/data
      - /volume1/docker/caddy/config:/config
      - /volume1/docker/caddy:/etc/caddy
      - /volume1/docker/caddy/logs:/var/log/caddy/
      - /volume1/docker/caddy/index.html:/usr/share/caddy/index.html
            
    command: caddy run --config /etc/caddy/Caddyfile
    networks:
      - caddyNet

networks:
  caddyNet:
    external: true



a. System environment:

Ugreen NAS running UGOS (based on debian), Docker container. Tailscale is installed on the NAS and acts as exit node (192.168.3.198 is the DNS setup on tailscale admin console). Adguard can see tailscale client as it is running in host mode for DNS level ad blocking. All client pcs are connected to the same tailnet.

b. Command:

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

c. Service/unit/compose file:

version: "3.8"

services:
  caddy:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: caddy
    restart: unless-stopped
    env_file:
      - .env
    environment:
      -  CLOUDFLARE_API_TOKEN=${CADDY_CLOUDFLARE_API_TOKEN}  # Replace with your actual token (**don't commit this file to version control!**)
      -  CLOUDFLARE_EMAIL=${CADDY_CLOUDFLARE_EMAIL}
      -  ACME_AGREE=true
    ports:
      - "${COMPOSE_PORT_HTTPS:-443}:443"
      - "${COMPOSE_PORT_HTTP:-80}:80"
      - 2019:2019
    volumes:
      - /volume1/docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - /volume1/docker/caddy/data:/data
      - /volume1/docker/caddy/config:/config
      - /volume1/docker/caddy:/etc/caddy
      - /volume1/docker/caddy/logs:/var/log/caddy/
      - /volume1/docker/caddy/index.html:/usr/share/caddy/index.html
            
    command: caddy run --config /etc/caddy/Caddyfile
    networks:
      - caddyNet

networks:
  caddyNet:
    external: true



d. My complete Caddy config:

{
    admin 0.0.0.0:2019
    email {env.CLOUDFLARE_EMAIL}
    log {
        output file /var/log/caddy/access.log {
            roll_size 10MB
            roll_keep 10
            roll_keep_for 336h
        }
        format json
    }
    acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    acme_ca https://acme-v02.api.letsencrypt.org/directory
    servers {
        trusted_proxies cloudflare {
            interval 12h
            timeout 15s
        }
    }
    dynamic_dns {
        provider cloudflare {env.CLOUDFLARE_API_TOKEN}
        domains {
            mydomain.com
        }
        check_interval 5m
    }
}

(authentikv2) { 
    reverse_proxy /outpost.goauthentik.io/* http://192.168.3.198:9300 {
        header_up X-Real-IP {remote}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Proto {scheme}
    }
    
    route {
        forward_auth http://192.168.3.198:9300 {
            uri /outpost.goauthentik.io/auth/caddy
            copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
            trusted_proxies private_ranges
            transport http {
                dial_timeout 10s
                response_header_timeout 30s
            }
        }
    }
}

(at_ugnas) {
    reverse_proxy 192.168.3.198:{args[0]}
}


(ts_gateway) {
    @allowLocal {
        remote_ip 192.168.0.0/16
    }
    @allowTailscale {
        remote_ip 100.64.0.0/10
    }
    handle @allowLocal {
        reverse_proxy {args.0}
    }
    handle @allowTailscale {
        reverse_proxy {args.0}
    } 
    respond "🔒 Access requires Tailscale VPN or LAN connection" 403
}

*.mydomain.com {
  tls {
    dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    ca https://acme-v02.api.letsencrypt.org/directory
    resolvers 1.1.1.1
  }
  encode gzip zstd
  @caddy host caddy.mydomain.com
  handle @caddy {
    root * /usr/share/caddy
    php_fastcgi localhost:80
    file_server
  }
  @ug host ug.mydomain.com
  handle @ug {
    import at_ugnas "7080"
    #reverse_proxy 192.168.3.198:7080
  }
  @ak host ak.mydomain.com
  handle @ak {
    reverse_proxy ak-server:9000
  }

  @portainer-ug host portainer-ug.mydomain.com
  handle @portainer-ug {
    import ts_gateway "http://192.168.3.198:9000"
  }
  @adguard host adguard.mydomain.com
  handle @adguard {
    import authentikv2
    import at_ugnas "9080"
    #reverse_proxy 192.168.3.198:9080
  }
}
    



5. Links to relevant resources: