|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Configuring Traefik to work over Tailscale" |
| 4 | +date: 2024-05-27 00:00:00 -0400 |
| 5 | +category: "General" |
| 6 | +tags: ["linux", "docker"] |
| 7 | +--- |
| 8 | + |
| 9 | +This is a bit of an extention to [this tailscale blog post](https://tailscale.com/blog/docker-tailscale-guide). I would start there to get an idea of setting up tags and auth, as well as just information about exactly what this does in more detail. |
| 10 | + |
| 11 | +My setup differs slightly in that I wanted to still be able to access things locally over Traefik when I was on my local network. This takes a bit of split DNS'ing and making sure to expose Traefik ports (via the Tailscale container) to the host. First to get Traefik set up, I start by creating my data directory. |
| 12 | + |
| 13 | +For the purpose of this blog post I am configuring [Immich](https://github.com/immich-app/immich) for use over tailscale. |
| 14 | + |
| 15 | +## Traefik Config |
| 16 | + |
| 17 | +```bash |
| 18 | +mkdir data |
| 19 | +cd data |
| 20 | +touch traefik.yml |
| 21 | +touch acme.json |
| 22 | +chmod 600 acme.json |
| 23 | +``` |
| 24 | + |
| 25 | +Then I configure the traefik.yml file |
| 26 | + |
| 27 | +```bash |
| 28 | +nano traefik.yml |
| 29 | +``` |
| 30 | + |
| 31 | +```yaml |
| 32 | +api: |
| 33 | + dashboard: false |
| 34 | + debug: true |
| 35 | +entryPoints: |
| 36 | + http: |
| 37 | + address: ":80" |
| 38 | + http: |
| 39 | + redirections: |
| 40 | + entryPoint: |
| 41 | + to: https |
| 42 | + scheme: https |
| 43 | + https: |
| 44 | + address: ":443" |
| 45 | + ping: |
| 46 | + address: ":8080" |
| 47 | +ping: |
| 48 | + entryPoint: "ping" |
| 49 | +serversTransport: |
| 50 | + insecureSkipVerify: true |
| 51 | +providers: |
| 52 | + docker: |
| 53 | + endpoint: "unix:///var/run/docker.sock" |
| 54 | + exposedByDefault: false |
| 55 | + file: |
| 56 | + filename: /config.yml |
| 57 | +certificatesResolvers: |
| 58 | + cloudflare: |
| 59 | + acme: |
| 60 | + storage: acme.json |
| 61 | + dnsChallenge: |
| 62 | + provider: cloudflare |
| 63 | + # disablePropagationCheck: true # uncomment this if you have issues pulling certificates through cloudflare, By setting this flag to true disables the need to wait for the propagation of the TXT record to all authoritative name servers. |
| 64 | + resolvers: |
| 65 | + - "1.1.1.1:53" |
| 66 | + - "1.0.0.1:53" |
| 67 | +``` |
| 68 | +
|
| 69 | +I think this is a pretty standard config that will allow Traefik to configure Cloudflare to create LetsEncrypt certificates automatically. Once that's created I create a `.env` with the following values. You'll need to create a [cloudflare API token](https://dash.cloudflare.com/profile/api-tokens). You'll also need to create [some kind of Tailscale auth](https://login.tailscale.com/admin/settings/keys) |
| 70 | + |
| 71 | +```env |
| 72 | +# Enviornmental Variables file .env |
| 73 | +CF_API_EMAIL={{ Replace with cloudflare api email address }} |
| 74 | +CF_DNS_API_TOKEN={{ Replace with cloudflare api token }} |
| 75 | +WAN_HOSTNAME={{ This is to add a response header with the proxy hostname for debugging }} |
| 76 | +
|
| 77 | +TS_AUTHKEY={{ Tailscale auth token }} |
| 78 | +``` |
| 79 | + |
| 80 | +Now I can add my config.yml file. |
| 81 | + |
| 82 | +```bash |
| 83 | +nano config.yml |
| 84 | +``` |
| 85 | + |
| 86 | +```yaml |
| 87 | +http: |
| 88 | + routers: |
| 89 | + immich: |
| 90 | + entryPoints: |
| 91 | + - "https" |
| 92 | + rule: "Host(`immich.mydomain.com`)" |
| 93 | + middlewares: |
| 94 | + - default-headers |
| 95 | + - https-redirectscheme |
| 96 | + tls: {} |
| 97 | + service: immich |
| 98 | + |
| 99 | + services: |
| 100 | + immich: |
| 101 | + loadBalancer: |
| 102 | + servers: |
| 103 | + - url: "http://immich-app:3001" |
| 104 | + passHostHeader: true |
| 105 | + |
| 106 | + middlewares: |
| 107 | + https-redirectscheme: |
| 108 | + redirectScheme: |
| 109 | + scheme: https |
| 110 | + permanent: true |
| 111 | + |
| 112 | + default-headers: |
| 113 | + headers: |
| 114 | + frameDeny: true |
| 115 | + browserXssFilter: true |
| 116 | + contentTypeNosniff: true |
| 117 | + forceSTSHeader: true |
| 118 | + stsIncludeSubdomains: true |
| 119 | + stsPreload: true |
| 120 | + stsSeconds: 15552000 |
| 121 | + customFrameOptionsValue: SAMEORIGIN |
| 122 | + customResponseHeaders: |
| 123 | + X-Proxy-By: {{env "WAN_HOSTNAME"}} |
| 124 | + customRequestHeaders: |
| 125 | + X-Forwarded-Proto: https |
| 126 | +``` |
| 127 | +
|
| 128 | +> Notice that I have used the immich container hostname for my URL. |
| 129 | +
|
| 130 | +Using the containername and internal port is important because Traefik will be running in the bridged network and will see the Immich container's name and port, not the host. `localhost` will reference the traefil/tailscale container in this context and will not resolve. |
| 131 | + |
| 132 | +## Docker Compose |
| 133 | + |
| 134 | +Once that's set up you can add the docker compose file. |
| 135 | + |
| 136 | +```bash |
| 137 | +nano docker-compose.yaml |
| 138 | +``` |
| 139 | + |
| 140 | +```yaml |
| 141 | +services: |
| 142 | + tailscale: |
| 143 | + image: tailscale/tailscale:latest |
| 144 | + container_name: tailscale |
| 145 | + hostname: immich |
| 146 | + environment: |
| 147 | + - TS_AUTHKEY=${TS_AUTHKEY}?ephemeral=false |
| 148 | + - TS_EXTRA_ARGS=--advertise-tags=tag:immich |
| 149 | + - TS_STATE_DIR=/var/lib/tailscale |
| 150 | + volumes: |
| 151 | + - ./tailscale/state:/var/lib/tailscale |
| 152 | + - /dev/net/tun:/dev/net/tun |
| 153 | + cap_add: |
| 154 | + - net_admin |
| 155 | + restart: unless-stopped |
| 156 | + ports: |
| 157 | + - 443:443 |
| 158 | + - 80:80 |
| 159 | + networks: |
| 160 | + - immich_immich |
| 161 | +
|
| 162 | + traefik: |
| 163 | + image: traefik:latest |
| 164 | + container_name: traefik |
| 165 | + restart: unless-stopped |
| 166 | + environment: |
| 167 | + - TRAEFIK_CERTIFICATESRESOLVERS_CLOUDFLARE_ACME_EMAIL=${CF_API_EMAIL} |
| 168 | + - CF_API_EMAIL |
| 169 | + - CF_DNS_API_TOKEN |
| 170 | + - TRAEFIK_AUTH |
| 171 | + - WAN_HOSTNAME |
| 172 | + volumes: |
| 173 | + - /etc/localtime:/etc/localtime:ro |
| 174 | + - /var/run/docker.sock:/var/run/docker.sock:ro |
| 175 | + - ./data/traefik.yml:/traefik.yml:ro |
| 176 | + - ./config.yml:/config.yml:ro |
| 177 | + - ./data/acme.json:/acme.json |
| 178 | + labels: |
| 179 | + - "traefik.enable=true" |
| 180 | + - "traefik.http.services.traefik.loadbalancer.server.port=1337" |
| 181 | + - "traefik.http.routers.traefik-secure.tls=true" |
| 182 | + - "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare" |
| 183 | + - "traefik.http.routers.traefik-secure.tls.domains[0].main=immich.mydomain.com" |
| 184 | + - "traefik.http.routers.traefik-secure.service=api@internal" |
| 185 | + network_mode: service:tailscale |
| 186 | +
|
| 187 | + watchtower: |
| 188 | + image: containrrr/watchtower:latest |
| 189 | + container_name: watchtower |
| 190 | + volumes: |
| 191 | + - /var/run/docker.sock:/var/run/docker.sock |
| 192 | +
|
| 193 | +networks: |
| 194 | + immich_immich: |
| 195 | + external: true |
| 196 | +``` |
| 197 | + |
| 198 | +## Networking Bits |
| 199 | + |
| 200 | +You'll notice that I've included the network from my other docker compose file. This network is created specifically so that I can bridge these two separate docker networks. In the other docker compose I configure this using: |
| 201 | + |
| 202 | +```yaml |
| 203 | +networks: |
| 204 | + immich: |
| 205 | + driver: bridge |
| 206 | +``` |
| 207 | + |
| 208 | +Then each container declared in that compose file specifies the network in the service definition. |
| 209 | + |
| 210 | +```yaml |
| 211 | +networks: |
| 212 | + - immich |
| 213 | +``` |
| 214 | + |
| 215 | +In my tailscale container I do something similar and specify `immich_immich`. In this case the namespace is immich and then the network is immich, because the network originates from the other compose file. You'll also see my traefik container definition does not speficy any ports, but does specify it's network is the tailscale container. Then the tailscale container specifies all the ports I want to expose for both (traefik and tailscale) containers. |
| 216 | + |
| 217 | +Once you run this with `docker compose up -d` you'll need to check your tailscale admin panel and ensure you see immich. Then in cloudflare you can set your dns record to either an A record with the tailscale IP, or a CNAME with the _something.something.ts.net_ hostname. |
0 commit comments