├── .github └── workflows │ └── CLOSE_INACTIVE_TICKETS.yml ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── image ├── Dockerfile ├── release-to-dockerhub.sh └── start.sh └── visuals └── block-diagram.png /.github/workflows/CLOSE_INACTIVE_TICKETS.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 14 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 19 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docker-compose-test* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lieven Hollevoet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the repository of the Tailscale-Caddy proxy, a docker image that enables easy sharing of docker HTTP services over the Tailscale network via HTTPS. 2 | 3 | # Rationale 4 | 5 | I have a few docker web services I want to share with other users. 6 | 7 | Before Tailscale this would mean opening firewall ports and enabling authentication for the users. And then you still need to worry about the fact that your services are exposed to the world. 8 | 9 | Tailscale enables you to share devices securely. Setup is trivial and the maintenance of who can access what is very convenient. 10 | 11 | So, I want to share web services via Tailscale. I found that there are multiple existing solutions such as: 12 | * the [Tailscale Docker Desktop extension](https://tailscale.com/blog/docker/), 13 | * the [Tailscale sidecar](https://github.com/markpash/tailscale-sidecar) by markpash 14 | * the official [Tailscale docker image](https://hub.docker.com/r/tailscale/tailscale). 15 | 16 | None of those solutions fit my usage scenario so I built the solution that is available in this repo. 17 | 18 | Compared to the other solutions this image: 19 | 20 | * does an auto-refresh of the SSL certificates without any further required actions by the user 21 | * supports a restart of the service container without having to restart the Tailscale/Caddy container 22 | 23 | # Functional description 24 | 25 | I want to be able to serve a web application to users by running a container in parallel to the container that serves the application. The side container should perform: 26 | 27 | * the connection to the Tailscale network 28 | * take care of serving the application via HTTPS with valid and regularly updated SSL certificates 29 | 30 | To implement this I start from the official Tailscale docker image and I extend it with Caddy. A small script that runs when starting the container takes care of creating the right configuration file for Caddy based on the environment parameters that are set when launching the container. 31 | 32 | Once started the container brings up the Tailscale connection. Users with access to the Tailscale device can connect over HTTP to port 80 (which is redirected to HTTPS) or to port 443 over HTTPS directly. The Caddy reverse proxy takes care of negotiating SSL certificated with the Tailscale daemon in the container to present valid HTTPS certificates. 33 | 34 | ![block-diagram](https://github.com/hollie/tailscale-caddy-proxy/raw/main/visuals/block-diagram.png) 35 | 36 | # Requirements 37 | 38 | For this image to work correctly you need to enable HTTPS support and MagicDNS in your Tailscale network configuration. 39 | 40 | # Parameters and storage 41 | 42 | The docker image takes as input parameters: 43 | 44 | * `TS_HOSTNAME` : the name of the host on the Tailscale network 45 | 46 | * `TS_TAILNET`: the name of your tailnet **without** the trailing `ts.net` section. 47 | 48 | * `CADDY_TARGET`: the name and port of the service you want to connect to. 49 | 50 | You also want to declare a permanent volume to store the Tailscale credentials so that those survive a rebuild of the container. The Tailscale configuration is located in the folder `/var/lib/tailscale` in the container. 51 | 52 | # Practical use 53 | 54 | Say you have an example service called 'whoami' that is a simple webserver listening on port 80 and you want to expose it via Tailscale. 55 | 56 | We want to keep the network traffic between this container and the Tailscale proxy separated from the default docker network, so declare a network. Attach the whoami container to that network. Declare a container of the `hollie/tailscale-caddy-proxy` image next to it, attach it also to the same network and enter the right environmental parameters for the Tailscale and Caddy configuration. 57 | 58 | The resulting docker compose file looks like: 59 | 60 | ```docker 61 | version: '3' 62 | 63 | networks: 64 | tailscale_proxy_example: 65 | external: false 66 | 67 | volumes: 68 | tailscale-whoami-state: 69 | 70 | services: 71 | 72 | whoami: 73 | image: traefik/whoami 74 | networks: 75 | - tailscale_proxy_example 76 | 77 | tailscale-whoami-proxy: 78 | image: hollie/tailscale-caddy-proxy:latest 79 | volumes: 80 | - tailscale-whoami-state:/var/lib/tailscale # Persist tailscale state 81 | environment: 82 | - TS_HOSTNAME=tailscale-example # Hostname on the tailscale network 83 | - TS_TAILNET=tailnet-XXXX # Your tailnet name without the .ts.net suffix! 84 | - CADDY_TARGET=whoami:80 # Target service and port 85 | # - TS_EXTRA_ARGS= # When starting tailscale in the container, e.g. to allow exit node or override the DNS settings. 86 | restart: on-failure 87 | init: true 88 | networks: 89 | - tailscale_proxy_example 90 | ``` 91 | 92 | Run `docker-compose up` and visit the link that is printed in the terminal to authenticate the machine to your Tailscale network. Disable key expiry via the Tailscale settings page for this host and restart the containers with `docker compose up -d`. 93 | 94 | All set! Now you can access the host via the full Tailscale domainname (including the tailnet-XXX.ts.net). 95 | 96 | # Acknowledgements 97 | 98 | Thanks to lpasselin for his [example code](https://github.com/lpasselin/tailscale-docker) that shows how to extend the default Tailscale image. 99 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | tailscale_proxy_example: 3 | external: false 4 | 5 | volumes: 6 | tailscale-whoami-state: 7 | 8 | services: 9 | 10 | whoami: 11 | image: traefik/whoami 12 | networks: 13 | - tailscale_proxy_example 14 | 15 | tailscale-whoami-proxy: 16 | image: hollie/tailscale-caddy-proxy:latest 17 | volumes: 18 | - tailscale-whoami-state:/var/lib/tailscale # Persist the tailscale state directory 19 | environment: 20 | - TS_HOSTNAME=tailscale-example # Hostname you want this instance to have on the tailscale network 21 | - TS_TAILNET=tailnet-XXXX # Your tailnet name without the .ts.net suffix! 22 | - CADDY_TARGET=whoami:80 # Target service and port 23 | # - TS_EXTRA_ARGS=--accept-dns # Optional extra arguments to pass when starting tailscale 24 | # - SKIP_CADDYFILE_GENERATION=1 # Set this if you want to be able to override /etc/caddy/Caddyfile via a volume mapping 25 | restart: on-failure 26 | init: true 27 | networks: 28 | - tailscale_proxy_example 29 | -------------------------------------------------------------------------------- /image/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG TAILSCALE_VERSION=v1.82.5 2 | 3 | FROM tailscale/tailscale:$TAILSCALE_VERSION 4 | 5 | LABEL maintainer="hollie@lika.be" 6 | 7 | ENV CADDY_TARGET= 8 | ENV TS_TAILNET= 9 | ENV TS_HOSTNAME= 10 | ENV TS_EXTRA_FLAGS= 11 | ENV TS_USERSPACE=true 12 | ENV TS_STATE_DIR=/var/lib/tailscale/ 13 | ENV TS_AUTH_ONCE=true 14 | 15 | RUN apk update && apk upgrade --no-cache && apk add --no-cache ca-certificates mailcap caddy 16 | RUN caddy upgrade 17 | 18 | # Ensure Caddy can access the tailscale socket, Caddy expects it to be under /var/run/tailscale so make a symlink 19 | RUN mkdir --parents /var/run/tailscale && ln -s /tmp/tailscaled.sock /var/run/tailscale/tailscaled.sock 20 | 21 | # Add the modified startup script 22 | COPY start.sh /usr/bin/start.sh 23 | RUN chmod +x /usr/bin/start.sh 24 | 25 | # And run it 26 | CMD [ "start.sh" ] 27 | 28 | 29 | -------------------------------------------------------------------------------- /image/release-to-dockerhub.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | # SET THE FOLLOWING VARIABLES 3 | # docker hub username 4 | USERNAME=hollie 5 | # image name 6 | IMAGE=tailscale-caddy-proxy 7 | # platforms 8 | PLATFORM=linux/arm64,linux/amd64,linux/arm/v7 9 | # bump version 10 | #docker run --rm -v "$PWD":/app treeder/bump patch 11 | version=`awk -F "=" '/TAILSCALE_VERSION=/{print $NF}' Dockerfile` 12 | echo "Building version: $version" 13 | # run build 14 | docker buildx build --platform $PLATFORM -t $USERNAME/$IMAGE:latest -t $USERNAME/$IMAGE:$version --push . 15 | # tag it 16 | git add -A 17 | git commit -m "Tailscale-Caddy-proxy version $version" 18 | git tag -a "dockerhub-$version" -m "Tailscale-Caddy-proxy version $version" 19 | git push 20 | git push --tags 21 | 22 | -------------------------------------------------------------------------------- /image/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | trap 'kill -TERM $PID' TERM INT 3 | 4 | echo "This is Tailscale-Caddy-proxy version" 5 | tailscale --version 6 | 7 | if [ ! -z "$SKIP_CADDYFILE_GENERATION" ] ; then 8 | echo "Skipping Caddyfile generation as requested via environment" 9 | else 10 | echo "Building Caddy configfile" 11 | 12 | echo $TS_HOSTNAME'.'$TS_TAILNET.'ts.net' > /etc/caddy/Caddyfile 13 | echo 'reverse_proxy' $CADDY_TARGET >> /etc/caddy/Caddyfile 14 | fi 15 | 16 | echo "Starting Caddy" 17 | caddy start --config /etc/caddy/Caddyfile 18 | 19 | echo "Starting Tailscale" 20 | 21 | export TS_EXTRA_ARGS=--hostname="${TS_HOSTNAME} ${TS_EXTRA_ARGS}" 22 | echo "Note: set TS_EXTRA_ARGS to " $TS_EXTRA_ARGS 23 | /usr/local/bin/containerboot 24 | -------------------------------------------------------------------------------- /visuals/block-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollie/tailscale-caddy-proxy/f25e4c1507381598d1dbb4073545ba32e641f8ad/visuals/block-diagram.png --------------------------------------------------------------------------------