update draft

This commit is contained in:
John Bowdre 2023-12-29 16:38:55 -06:00
parent 90498d8fcc
commit 4b61d20870
4 changed files with 100 additions and 64 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -7,7 +7,7 @@ description: "This is a new post about..."
featured: false featured: false
toc: true toc: true
comment: true comment: true
series: Tips # Projects, Code series: Projects
tags: tags:
- containers - containers
- docker - docker
@ -34,7 +34,7 @@ ntfy.runtimeterror.dev, http://ntfy.runtimeterror.dev {
redir @httpget https://{host}{uri} redir @httpget https://{host}{uri}
} }
uptime.runtimeterror.dev, status.vpota.to { uptime.runtimeterror.dev {
reverse_proxy localhost:3001 reverse_proxy localhost:3001
} }
@ -45,7 +45,7 @@ miniflux.runtimeterror.dev {
*and so on...* You get the idea. This approach works well for services I want/need to be public, but it does require me to manage those DNS records and keep track of which app is on which port. That can be kind of tedious. *and so on...* You get the idea. This approach works well for services I want/need to be public, but it does require me to manage those DNS records and keep track of which app is on which port. That can be kind of tedious.
And I don't really need all of these services to be public. Not because they're particularly sensitive, but I just don't really have a reason to share my personal [Miniflux](https://github.com/miniflux/v2) or [CyberChef](https://github.com/gchq/CyberChef) instance with the world at large. Those would be great candidates to serve with [Tailscale Serve](/tailscale-ssh-serve-funnel#tailscale-serve) so they'd only be available on my tailnet. Of course, with that setup I'd have to differentiate the services based on external port numbers since they'd all be served with the same hostname. That's not ideal either. And I don't really need all of these services to be public. Not because they're particularly sensitive, but I just don't really have a reason to share my personal [Miniflux](https://github.com/miniflux/v2) or [CyberChef](https://github.com/gchq/CyberChef) instances with the world at large. Those would be great candidates to serve with [Tailscale Serve](/tailscale-ssh-serve-funnel#tailscale-serve) so they'd only be available on my tailnet. Of course, with that setup I'd have to differentiate the services based on external port numbers since they'd all be served with the same hostname. That's not ideal either.
```shell ```shell
sudo tailscale serve --bg --https 8443 8180 # [tl! .cmd] sudo tailscale serve --bg --https 8443 8180 # [tl! .cmd]
@ -62,51 +62,136 @@ It would be really great if I could directly attach each container to my tailnet
And then I came across [Louis-Philippe Asselin's post](https://asselin.engineer/tailscale-docker) about how he set up Tailscale in Docker Compose. When he wrote his post, there was even less documentation on how to do this stuff, so he used a [modified Tailscale docker image](https://github.com/lpasselin/tailscale-docker) with a [startup script](https://github.com/lpasselin/tailscale-docker/blob/c6f8d75b5e1235b8dbeee849df9321f515c526e5/images/tailscale/start.sh) to handle some of the configuration steps. His repo also includes a [helpful docker-compose example](https://github.com/lpasselin/tailscale-docker/blob/c6f8d75b5e1235b8dbeee849df9321f515c526e5/docker-compose/stateful-example/docker-compose.yml) of how to connect it together. And then I came across [Louis-Philippe Asselin's post](https://asselin.engineer/tailscale-docker) about how he set up Tailscale in Docker Compose. When he wrote his post, there was even less documentation on how to do this stuff, so he used a [modified Tailscale docker image](https://github.com/lpasselin/tailscale-docker) with a [startup script](https://github.com/lpasselin/tailscale-docker/blob/c6f8d75b5e1235b8dbeee849df9321f515c526e5/images/tailscale/start.sh) to handle some of the configuration steps. His repo also includes a [helpful docker-compose example](https://github.com/lpasselin/tailscale-docker/blob/c6f8d75b5e1235b8dbeee849df9321f515c526e5/docker-compose/stateful-example/docker-compose.yml) of how to connect it together.
I quickly realized I could probably modified his startup script to take care of my Tailscale Serve need. So here's how I did it. I quickly realized I could modify his startup script to take care of my Tailscale Serve need. So here's how I did it.
### Docker Image ### Docker Image Description
My image will start out the same as Louis-Philippe's, with just adding a startup script to the official Tailscale image: My image will start out the same as Louis-Philippe's:
```Dockerfile ```Dockerfile
# torchlight! {"lineNumbers": true} # torchlight! {"lineNumbers": true}
FROM tailscale/tailscale:v1.56.1 FROM tailscale/tailscale:v1.56.1
COPY start.sh /usr/bin/start.sh COPY start.sh /usr/bin/start.sh
RUN chmod +x /usr/bin/start.sh RUN chmod +x /usr/bin/start.sh
CMD "start.sh" CMD ["/usr/bin/start.sh"]
``` ```
The `start.sh` script has a few tweaks for brevity/clarity, and also adds a block for conditionally enabling a basic Tailscale Serve configuration: The `start.sh` script has a few tweaks for brevity/clarity, and also adds a block for conditionally enabling a basic Tailscale Serve (or Funnel) configuration:
```shell ```shell
#!/bin/ash
# torchlight! {"lineNumbers": true} # torchlight! {"lineNumbers": true}
#!/bin/ash
trap 'kill -TERM $PID' TERM INT trap 'kill -TERM $PID' TERM INT
echo "Starting Tailscale daemon" echo "Starting Tailscale daemon"
tailscaled --tun=userspace-networking --state=${TS_STATE} ${TS_OPT} & tailscaled --tun=userspace-networking --statedir="${TS_STATEDIR}" ${TS_OPT} &
PID=$! PID=$!
until tailscale up --authkey="${TS_AUTHKEY}" --hostname="${TS_HOSTNAME}"; do until tailscale up --authkey="${TS_AUTHKEY}" --hostname="${TS_HOSTNAME}"; do
sleep 0.1 sleep 0.1
done done
tailscale status tailscale status
if [ -n "${TS_SERVE_PORT}" ]; then # [tl! ++:4] if [ -n "${TS_SERVE_PORT}" ]; then # [tl! ++:10]
if ! tailscale serve status | grep -q "${TS_SERVE_PORT}"; then if [ -n "${TS_FUNNEL}" ]; then
tailscale serve --bg "${TS_SERVE_PORT}" if ! tailscale funnel status | grep -q -A1 '(Funnel on)' | grep -q "${TS_SERVE_PORT}"; then
tailscale funnel --bg "${TS_SERVE_PORT}"
fi
else
if ! tailscale serve status | grep -q "${TS_SERVE_PORT}"; then
tailscale serve --bg "${TS_SERVE_PORT}"
fi
fi fi
fi fi
wait ${PID} wait ${PID}
``` ```
That script will start the `tailscaled` daemon in userspace mode, and it will store the Tailscale state in a user-defined location. It will then use a supplied [pre-auth key](https://tailscale.com/kb/1085/auth-keys) to bring up the new Tailscale node.
If both `TS_SERVE_PORT` and `TS_FUNNEL` are set, the script will publicly proxy the designated port with Tailscale Funnel. If only `TS_SERVE_PORT` is set, it will just proxy it internal to the tailnet with Tailscale Serve.
I'm using [this git repo](https://github.com/jbowdre/tailscale-docker/) to track my work on this, and it automatically builds the [tailscale-docker](https://github.com/jbowdre/tailscale-docker/pkgs/container/tailscale-docker) image. So now I can can reference `ghcr.io/jbowdre/tailscale-docker` in my Docker configurations.
On that note...
### Compose Configuration Description
There's also a [sample `docker-compose.yml`](https://github.com/jbowdre/tailscale-docker/blob/a996ea8d28d1357beb4eea65cff7d024ba94c8e3/docker-compose-example/docker-compose.yml) in the repo to show how to use the image:
```yaml ```yaml
# torchlight! {"lineNumbers": true}
services: services:
tailscale: tailscale:
build: build:
context: ./image/ context: ./image/
container_name: tailscale container_name: tailscale
environment: environment:
TS_AUTH_KEY: ${TS_AUTH_KEY:?err} # from https://login.tailscale.com/admin/settings/authkeys TS_AUTHKEY: ${TS_AUTHKEY:?err} # from https://login.tailscale.com/admin/settings/authkeys
TS_HOSTNAME: ${TS_HOSTNAME:-ts-docker} TS_HOSTNAME: ${TS_HOSTNAME:-ts-docker}
TS_STATE_ARG: "/var/lib/tailscale/tailscale.state" # store ts state in a local volume TS_STATEDIR: "/var/lib/tailscale/" # store ts state in a local volume
TS_SERVE_PORT: ${TS_SERVE_PORT:-} # optional port to proxy with tailscale serve (ex: '80') TS_SERVE_PORT: ${TS_SERVE_PORT:-} # optional port to proxy with tailscale serve (ex: '80')
TS_FUNNEL: ${TS_FUNNEL:-} # if set, serve publicly with tailscale funnel
volumes:
- ./ts_data:/var/lib/tailscale/
myservice:
image: nginxdemos/hello
network_mode: "service:tailscale"
```
The variables can be defined in a `.env` file stored alongside `docker-compose.yaml` to avoid having to store them in the compose file:
```shell
# torchlight! {"lineNumbers": true}
TS_AUTHKEY=tskey-auth-somestring-somelongerstring
TS_HOSTNAME=tsdemo
TS_SERVE_PORT=8080
TS_FUNNEL=1
```
| Variable Name | Example | Description |
| --- | --- | --- |
| `TS_AUTHKEY` | `tskey-auth-somestring-somelongerstring` | used for unattended auth of the new node, get one [here](https://login.tailscale.com/admin/settings/keys) |
| `TS_HOSTNAME` | `tsdemo` | optional Tailscale hostname for the new node |
| `TS_STATEDIR` | `/var/lib/tailscale/` | required directory for storing Tailscale state, this should be mounted to the container for persistence |
| `TS_SERVE_PORT` | `8080` | optional application port to expose with [Tailscale Serve](https://tailscale.com/kb/1312/serve) |
| `TS_FUNNEL` | `1` | if set (to anything), will proxy `TS_SERVE_PORT` **publicly** with [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) |
- If you want to use Funnel with this configuration, it might be a good idea to associate the [Funnel ACL policy](https://tailscale.com/kb/1223/funnel#tailnet-policy-file-requirement) with a tag (like `tag:funnel`), as I discussed a bit [here](/tailscale-ssh-serve-funnel/#tailscale-funnel). And then when you create the [pre-auth key](https://tailscale.com/kb/1085/auth-keys), you can set it to automatically apply the tag so it can enable Funnel.
- It's very important that the path designated by `TS_STATEDIR` is a volume mounted into the container. Otherwise, the container will lose its Tailscale configuration when it stops. That could be inconvenient.
- Tying `network_mode` on the application container back to the `service:tailscale` definition is the magic that lets the sidecar proxy traffic for the app. This way the two containers effectively share the same network interface, allowing them to share the same ports. So port `8080` on the app container is available on the tailscale container, and that allows `tailscale serve --bg 8080` to work.
### Usage
To tie it all together, here are the steps that I took to serve up a quick CyberChef instance on my tailnet.
I started by going to the [Tailscale Admin Portal](https://login.tailscale.com/admin/settings/keys) and generating a new auth key. I gave it a description, ticked the option to pre-approve whatever device authenticates with this key (since I have [Device Approval](https://tailscale.com/kb/1099/device-approval) enabled on my tailnet). I also used the option to auto-apply the `tag:internal` tag I used for grouping my on-prem systems as well as the `tag:funnel` tag I use for approving Funnel devices in the ACL.
![authkey creation](authkey1.png)
That gives me a new single-use authkey:
![new authkey](authkey2.png)
I'll use that new key as well as the knowledge that CyberChef is served by default on port `8000` to create an appropriate `.env` file:
```shell
# torchlight! {"lineNumbers": true}
# .env
TS_AUTHKEY=tskey-auth-somestring-somelongerstring
TS_HOSTNAME=cyberchef
TS_SERVE_PORT=8000
TS_FUNNEL=true
```
And I can add the corresponding `docker-compose.yml` to go with it:
```yaml
# torchlight! {"lineNumbers": true}
# docker-compose.yml
services:
tailscale:
image: ghcr.io/jbowdre/tailscale-docker:latest
container_name: cyberchef-tailscale
environment:
TS_AUTHKEY: ${TS_AUTHKEY:?err}
TS_HOSTNAME: ${TS_HOSTNAME:-ts-docker}
TS_STATEDIR: "/var/lib/tailscale/"
TS_SERVE_PORT: ${TS_SERVE_PORT:-}
TS_FUNNEL: ${TS_FUNNEL:-}
volumes: volumes:
- ./ts_data:/var/lib/tailscale/ - ./ts_data:/var/lib/tailscale/
cyberchef: cyberchef:
@ -115,52 +200,3 @@ services:
restart: unless-stopped restart: unless-stopped
network_mode: service:tailscale network_mode: service:tailscale
``` ```
```shell
DB_USER=my_db_user
DB_PASS=my_db_password
ADMIN_USER=my_admin_user
ADMIN_PASS=my_admin_password
TS_AUTH_KEY=tskey-auth-my_auth_key
TS_HOSTNAME=miniflux
TS_SERVE_PORT=8080
```
```yaml
services:
tailscale:
image: ghcr.io/jbowdre/tailscale-docker:latest
container_name: miniflux-tailscaled
environment:
TS_AUTH_KEY: ${TS_AUTH_KEY:?err} # from https://login.tailscale.com/admin/settings/authkeys
TS_HOSTNAME: ${TS_HOSTNAME:-ts-docker}
TS_STATE_ARG: "/var/lib/tailscale/tailscale.state" # store ts state in a local volume
TS_SERVE_PORT: ${TS_SERVE_PORT:-} # optional port to proxy with tailscale serve (ex: '80')
volumes:
- ./ts_data:/var/lib/tailscale/
miniflux:
image: miniflux/miniflux:latest
container_name: miniflux
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=${ADMIN_USER}
- ADMIN_PASSWORD=${ADMIN_PASS}
network_mode: "service:tailscale"
db:
image: postgres:15
container_name: miniflux-db
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASS}
volumes:
- ./mf_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "${DB_USER}"]
interval: 10s
start_period: 30s
```