<!doctype html><htmllang=enclass=no-js><head><metacharset=utf-8><metaname=viewportcontent="width=device-width,initial-scale=1"><metaname=descriptioncontent="An open source, self-hosted implementation of the Tailscale control server."><metaname=authorcontent="Headscale authors"><linkhref=https://juanfont.github.io/headscale/development/ref/integration/reverse-proxy/rel=canonical><linkhref=../../remote-cli/rel=prev><linkhref=../web-ui/rel=next><linkrel=iconhref=../../../assets/favicon.png><metaname=generatorcontent="mkdocs-1.6.1, mkdocs-material-9.5.49"><title>Reverse proxy - Headscale</title><linkrel=stylesheethref=../../../assets/stylesheets/main.6f8fc17f.min.css><linkrel=stylesheethref=../../../assets/stylesheets/palette.06af60db.min.css><linkrel=preconnecthref=https://fonts.gstatic.comcrossorigin><linkrel=stylesheethref="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback"><style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style><script>__md_scope=newURL("../../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script><metaproperty=og:typecontent=website><metaproperty=og:titlecontent="Reverse proxy - Headscale"><metaproperty=og:descriptioncontent="An open source, self-hosted implementation of the Tailscale control server."><metaproperty=og:imagecontent=https://juanfont.github.io/headscale/development/assets/images/social/ref/integration/reverse-proxy.png><metaproperty=og:image:typecontent=image/png><metaproperty=og:image:widthcontent=1200><metaproperty=og:image:heightcontent=630><metacontent=https://juanfont.github.io/headscale/development/ref/integration/reverse-proxy/property=og:url><metaname=twitter:cardcontent=summary_large_image><metaname=twitter:titlecontent="Reverse proxy - Headscale"><metaname=twitter:descriptioncontent="An open source, self-hosted implementation of the Tailscale control server."><metaname=twitter:imagecontent=https://juanfont.github.io/headscale/development/assets/images/social/ref/integration/reverse-proxy.png></head><bodydir=ltrdata-md-color-scheme=defaultdata-md-color-primary=whitedata-md-color-accent=indigo><inputclass=md-toggledata-md-toggle=drawertype=checkboxid=__drawerautocomplete=off><inputclass=md-toggledata-md-toggle=searchtype=checkboxid=__searchautocomplete=off><labelclass=md-overlayfor=__drawer></label><divdata-md-component=skip><ahref=#running-headscale-behind-a-reverse-proxyclass=md-skip> Skip to content </a></div><divdata-md-component=announce></div><divdata-md-color-scheme=defaultdata-md-component=outdatedhidden></div><headerclass=md-headerdata-md-component=header><navclass="md-header__inner md-grid"aria-label=Header><ahref=../../..title=Headscaleclass="md-header__button md-logo"aria-label=Headscaledata-md-component=logo><imgsrc=../../../logo/headscale3-dots.svgalt=logo></a><labelclass="md-header__button md-icon"for=__drawer><svgxmlns=http://www.w3.org/2000/svgviewbox="0 0 24 24"><pathd="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg></label><divclass=md-header__titledata-md-component=header-title><divclass=md-header__ellipsis><divclass=md-header__topic><spanclass=md-ellipsis> Headscale </span></div><divclass=md-header__topicdata-md-component=header-topic><spanclass=md-ellipsis> Reverse proxy </span></div></div></div><formclass=md-header__optiondata-md-component=palette><inputclass=md-optiondata-md-color-mediadata-md-color-scheme=defaultdata-md-color-primary=whitedata-md-color-accent=indigoaria-label="Switch to dark mode"type=radioname=__paletteid=__palette_0><labelclass="md-header__button md-icon"title="Switch to dark mode"for=__palette_1hidden><svgxmlns=http://www.w3.org/2000/svgviewbox="0 0 24 24"><pathd="M128a44000-44
</span></code></pre></div><h2id=nginx>nginx<aclass=headerlinkhref=#nginxtitle="Permanent link">¶</a></h2><p>The following example configuration can be used in your nginx setup, substituting values as necessary. <code><IP:PORT></code> should be the IP address and port where headscale is running. In most cases, this will be <code>http://localhost:8080</code>.</p><divclass="language-Nginx highlight"><pre><span></span><code><spanid=__span-1-1><aid=__codelineno-1-1name=__codelineno-1-1href=#__codelineno-1-1></a><spanclass=k>map</span><spanclass=w></span><spanclass=nv>$http_upgrade</span><spanclass=w></span><spanclass=nv>$connection_upgrade</span><spanclass=w></span><spanclass=p>{</span>
</span></code></pre></div><h2id=istioenvoy>istio/envoy<aclass=headerlinkhref=#istioenvoytitle="Permanent link">¶</a></h2><p>If you using <ahref=https://istio.io/>Istio</a> ingressgateway or <ahref=https://www.envoyproxy.io/>Envoy</a> as reverse proxy, there are some tips for you. If not set, you may see some debug log in proxy as below:</p><divclass="language-text highlight"><pre><span></span><code><spanid=__span-2-1><aid=__codelineno-2-1name=__codelineno-2-1href=#__codelineno-2-1></a>Sending local reply with details upgrade_failed
</span></code></pre></div><h3id=envoy>Envoy<aclass=headerlinkhref=#envoytitle="Permanent link">¶</a></h3><p>You need to add a new upgrade_type named <code>tailscale-control-protocol</code>. <ahref=https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig>see details</a></p><h3id=istio>Istio<aclass=headerlinkhref=#istiotitle="Permanent link">¶</a></h3><p>Same as envoy, we can use <code>EnvoyFilter</code> to add upgrade_type.</p><divclass="language-yaml highlight"><pre><span></span><code><spanid=__span-3-1><aid=__codelineno-3-1name=__codelineno-3-1href=#__codelineno-3-1></a><spanclass=nt>apiVersion</span><spanclass=p>:</span><spanclass=w></span><spanclass="l l-Scalar l-Scalar-Plain">networking.istio.io/v1alpha3</span>
</span></code></pre></div><h2id=caddy>Caddy<aclass=headerlinkhref=#caddytitle="Permanent link">¶</a></h2><p>The following Caddyfile is all that is necessary to use Caddy as a reverse proxy for headscale, in combination with the <code>config.yaml</code> specifications above to disable headscale's built in TLS. Replace values as necessary - <code><YOUR_SERVER_NAME></code> should be the FQDN at which headscale will be served, and <code><IP:PORT></code> should be the IP address and port where headscale is running. In most cases, this will be <code>localhost:8080</code>.</p><divclass="language-text highlight"><pre><span></span><code><spanid=__span-4-1><aid=__codelineno-4-1name=__codelineno-4-1href=#__codelineno-4-1></a><YOUR_SERVER_NAME> {
</span></code></pre></div><p>Caddy v2 will <ahref=https://caddyserver.com/docs/automatic-https>automatically</a> provision a certificate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary.</p><p>For a slightly more complex configuration which utilizes Docker containers to manage Caddy, headscale, and Headscale-UI, <ahref=https://blog.gurucomputing.com.au/smart-vpns-with-headscale/>Guru Computing's guide</a> is an excellent reference.</p><h2id=apache>Apache<aclass=headerlinkhref=#apachetitle="Permanent link">¶</a></h2><p>The following minimal Apache config will proxy traffic to the headscale instance on <code><IP:PORT></code>. Note that <code>upgrade=any</code> is required as a parameter for <code>ProxyPass</code> so that WebSockets traffic whose <code>Upgrade</code> header value is not equal to <code>WebSocket</code> (i. e. Tailscale Control Protocol) is forwarded correctly. See the <ahref=https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html>Apache docs</a> for more information on this.</p><divclass="language-text highlight"><pre><span></span><code><spanid=__span-5-1><aid=__codelineno-5-1name=__codelineno-5-1href=#__codelineno-5-1></a><VirtualHost *:443>