mirror of
https://github.com/jbowdre/runtimeterror.git
synced 2024-11-29 09:52:19 +00:00
publish draft
This commit is contained in:
parent
cbf00a5d48
commit
b66664e98b
1 changed files with 33 additions and 32 deletions
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
title: "Tailscale Serve in a Docker Compose Sidecar"
|
title: "Tailscale Serve in a Docker Compose Sidecar"
|
||||||
date: 2023-12-28
|
date: 2023-12-30
|
||||||
# lastmod: 2023-12-28
|
# lastmod: 2023-12-28
|
||||||
draft: true
|
description: "Using Docker Compose to deploy containerized applications and make them available via Tailscale Serve and Tailscale Funnel"
|
||||||
description: "This is a new post about..."
|
|
||||||
featured: false
|
featured: false
|
||||||
toc: true
|
toc: true
|
||||||
comment: true
|
comment: true
|
||||||
|
@ -58,14 +57,16 @@ https://tsdemo.tailnet-name.ts.net:8443/
|
||||||
|-- proxy http://127.0.0.1:8080
|
|-- proxy http://127.0.0.1:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
It would be really great if I could directly attach each container to my tailnet and then access the apps with addresses like `https://miniflux.tailnet-name.ts.net` or `https://cyberchef.tailnet-name.ts.net`. Tailscale does provide an [official Tailscale image](https://hub.docker.com/r/tailscale/tailscale) which seems like it should make this a really easy problem to address. It runs in userspace by default (neat!), and [even seems to accept a `TS_SERVE_CONFIG` parameter](https://github.com/tailscale/tailscale/blob/5812093d31c8a7f9c5e3a455f0fd20dcc011d8cd/cmd/containerboot/main.go#L43) to configure Tailscale Serve... unfortunately, I haven't been able to find any documentation about how to create the required `ipn.ServeConfig` file to be able to use of that.
|
It would be really great if I could directly attach each container to my tailnet and then access the apps with addresses like `https://miniflux.tailnet-name.ts.net` or `https://cyber.tailnet-name.ts.net`. Tailscale does have an [official Docker image](https://hub.docker.com/r/tailscale/tailscale), and at first glance it seems like that would solve my needs pretty directly. Unfortunately, it looks like trying to leverage that container image directly would still require me to configure Tailscale Serve interactively.[^ts_serve_config].
|
||||||
|
|
||||||
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.
|
[^ts_serve_config]: While not documented for the image itself, the `containerboot` binary seems like it should accept a [`TS_SERVE_CONFIG` argument](https://github.com/tailscale/tailscale/blob/5812093d31c8a7f9c5e3a455f0fd20dcc011d8cd/cmd/containerboot/main.go#L43) to designate the file path of the `ipn.ServeConfig`... but I couldn't find any information on how to actually configure that.
|
||||||
|
|
||||||
|
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) which loads 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 modify 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 Description
|
### Docker Image
|
||||||
My image starts out the same as Louis-Philippe's:
|
My image starts out basically the same as Louis-Philippe's, with just pulling in the official image and then adding the customized script:
|
||||||
|
|
||||||
```Dockerfile
|
```Dockerfile
|
||||||
# torchlight! {"lineNumbers": true}
|
# torchlight! {"lineNumbers": true}
|
||||||
|
@ -75,7 +76,7 @@ RUN chmod +x /usr/bin/start.sh
|
||||||
CMD ["/usr/bin/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 (or Funnel) configuration:
|
My `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
|
||||||
# torchlight! {"lineNumbers": true}
|
# torchlight! {"lineNumbers": true}
|
||||||
#!/bin/ash
|
#!/bin/ash
|
||||||
|
@ -103,42 +104,43 @@ wait ${PID}
|
||||||
|
|
||||||
This script starts the `tailscaled` daemon in userspace mode, and it tells the daemon to store its state in a user-defined location. It then uses a supplied [pre-auth key](https://tailscale.com/kb/1085/auth-keys) to bring up the new Tailscale node and set the hostname.
|
This script starts the `tailscaled` daemon in userspace mode, and it tells the daemon to store its state in a user-defined location. It then uses a supplied [pre-auth key](https://tailscale.com/kb/1085/auth-keys) to bring up the new Tailscale node and set the hostname.
|
||||||
|
|
||||||
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.
|
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.[^normal]
|
||||||
|
|
||||||
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.
|
[^normal]: If *neither* variable is set, the script just brings up Tailscale like normal... in which case you might as well just use the official image.
|
||||||
|
|
||||||
|
I'm using [this git repo](https://github.com/jbowdre/tailscale-docker/) to track my work on this, and it automatically builds my [tailscale-docker](https://github.com/jbowdre/tailscale-docker/pkgs/container/tailscale-docker) image. So now I can can simply reference `ghcr.io/jbowdre/tailscale-docker` in my Docker configurations.
|
||||||
|
|
||||||
On that note...
|
On that note...
|
||||||
|
|
||||||
### Compose Configuration Description
|
### Compose Configuration
|
||||||
There's also a [sample `docker-compose.yml`](https://github.com/jbowdre/tailscale-docker/blob/54da987ff5b132b75ea051a0787ec686c7efeb64/docker-compose-example/docker-compose.yml) in the repo to show how to use the image:
|
There's also a [sample `docker-compose.yml`](https://github.com/jbowdre/tailscale-docker/blob/a54e45ca717023a45d6b1d0aac7143902b02cb0b/docker-compose-example/docker-compose.yml) in the repo to show how to use the image:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# torchlight! {"lineNumbers": true}
|
# torchlight! {"lineNumbers": true}
|
||||||
services:
|
services:
|
||||||
tailscale:
|
tailscale:
|
||||||
build:
|
image: ghcr.io/jbowdre/tailscale-docker:latest
|
||||||
context: ./image/
|
|
||||||
container_name: tailscale
|
container_name: tailscale
|
||||||
environment:
|
environment:
|
||||||
TS_AUTHKEY: ${TS_AUTHKEY:?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} # optional hostname to use for this node
|
||||||
TS_STATE_DIR: "/var/lib/tailscale/" # store ts state in a local volume
|
TS_STATE_DIR: "/var/lib/tailscale/" # store ts state in a local volume
|
||||||
TS_TAILSCALED_EXTRA_ARGS: ${TS_TAILSCALED_EXTRA_ARGS:-} # optional extra args to pass to tailscaled
|
TS_TAILSCALED_EXTRA_ARGS: ${TS_TAILSCALED_EXTRA_ARGS:-} # optional extra args to pass to tailscaled
|
||||||
TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:-} # optional extra flags to pass to tailscale up
|
TS_EXTRA_ARGS: ${TS_EXTRA_ARGS:-} # optional extra flags to pass to tailscale up
|
||||||
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
|
TS_FUNNEL: ${TS_FUNNEL:-} # if set, serve publicly with tailscale funnel
|
||||||
volumes:
|
volumes:
|
||||||
- ./ts_data:/var/lib/tailscale/
|
- ./ts_data:/var/lib/tailscale/ # the mount point should match TS_STATE_DIR
|
||||||
myservice:
|
myservice:
|
||||||
image: nginxdemos/hello
|
image: nginxdemos/hello
|
||||||
network_mode: "service:tailscale"
|
network_mode: "service:tailscale" # use the tailscale network service's network
|
||||||
```
|
```
|
||||||
|
|
||||||
You'll note that most of those environment variables aren't actually defined in this YAML. Instead, they'll be inherited from the environment used for spawning the containers. This provides a few benefits. First, it lets the `tailscale` service definition block function as a template to allow copying it into other Compose files without having to modify. Second, it avoids holding sensitive data in the YAML itself. And third, it allows us to set default values for undefined variables (if `TS_HOSTNAME` is empty it will be automatically replaced with `ts-docker`) or throw an error if a required value isn't set (an empty `TS_AUTHKEY` will throw an error and abort).
|
You'll note that most of those environment variables aren't actually defined in this YAML. Instead, they'll be inherited from the environment used for spawning the containers. This provides a few benefits. First, it lets the `tailscale` service definition block function as a template to allow copying it into other Compose files without having to modify. Second, it avoids holding sensitive data in the YAML itself. And third, it allows us to set default values for undefined variables (if `TS_HOSTNAME` is empty it will be automatically replaced with `ts-docker`) or throw an error if a required value isn't set (an empty `TS_AUTHKEY` will throw an error and abort).
|
||||||
|
|
||||||
You can create the required variables by exporting them at the command line (`export TS_HOSTNAME=ts-docker`) - but that runs the risk of having sensitive values like an authkey stored in your shell history. It's not a great habit.
|
You can create the required variables by exporting them at the command line (`export TS_HOSTNAME=ts-docker`) - but that runs the risk of having sensitive values like an authkey stored in your shell history. It's not a great habit.
|
||||||
|
|
||||||
Perhaps a better approach is to set the variables in a `.env` file stored alongside the `docker-compose.yaml` but with stricter permissions. This file can be owned and only readable by root (or the defined Docker user), while the Compose file can be owned by your own user or the `docker`` group.
|
Perhaps a better approach is to set the variables in a `.env` file stored alongside the `docker-compose.yaml` but with stricter permissions. This file can be owned and only readable by root (or the defined Docker user), while the Compose file can be owned by your own user or the `docker` group.
|
||||||
|
|
||||||
Here's how the `.env` for this setup might look:
|
Here's how the `.env` for this setup might look:
|
||||||
|
|
||||||
|
@ -146,7 +148,7 @@ Here's how the `.env` for this setup might look:
|
||||||
# torchlight! {"lineNumbers": true}
|
# torchlight! {"lineNumbers": true}
|
||||||
TS_AUTHKEY=tskey-auth-somestring-somelongerstring
|
TS_AUTHKEY=tskey-auth-somestring-somelongerstring
|
||||||
TS_HOSTNAME=tsdemo
|
TS_HOSTNAME=tsdemo
|
||||||
TS_TAILSCALED_EXTRA_ARGS=
|
TS_TAILSCALED_EXTRA_ARGS=--verbose=1
|
||||||
TS_EXTRA_ARGS=--ssh
|
TS_EXTRA_ARGS=--ssh
|
||||||
TS_SERVE_PORT=8080
|
TS_SERVE_PORT=8080
|
||||||
TS_FUNNEL=1
|
TS_FUNNEL=1
|
||||||
|
@ -171,15 +173,15 @@ A few implementation notes:
|
||||||
- It's very important that the path designated by `TS_STATE_DIR` is a volume mounted into the container. Otherwise, the container will lose its Tailscale configuration when it stops. That could be inconvenient.
|
- It's very important that the path designated by `TS_STATE_DIR` is a volume mounted into the container. Otherwise, the container will lose its Tailscale configuration when it stops. That could be inconvenient.
|
||||||
- Linking `network_mode` on the application container back to the `service:tailscale` definition is [the magic](https://docs.docker.com/compose/compose-file/05-services/#network_mode) 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 enables `tailscale serve --bg 8080` to work.
|
- Linking `network_mode` on the application container back to the `service:tailscale` definition is [the magic](https://docs.docker.com/compose/compose-file/05-services/#network_mode) 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 enables `tailscale serve --bg 8080` to work.
|
||||||
|
|
||||||
### Usage
|
### Usage Examples
|
||||||
|
|
||||||
To tie this all together, I'm going to quickly run through the steps to create and publish two container-based services without having to do any interactive configuration.
|
To tie this all together, I'm going to quickly run through the steps I took to create and publish two container-based services without having to do any interactive configuration.
|
||||||
|
|
||||||
#### CyberChef
|
#### CyberChef
|
||||||
|
|
||||||
I'll start with my [CyberChef](https://github.com/gchq/CyberChef) instance.
|
I'll start with my [CyberChef](https://github.com/gchq/CyberChef) instance.
|
||||||
|
|
||||||
> CyberChef is a simple, intuitive web app for carrying out all manner of "cyber" operations within a web browser. These operations include simple encoding like XOR and Base64, more complex encryption like AES, DES and Blowfish, creating binary and hexdumps, compression and decompression of data, calculating hashes and checksums, IPv6 and X.509 parsing, changing character encodings, and much more.
|
> *CyberChef is a simple, intuitive web app for carrying out all manner of "cyber" operations within a web browser. These operations include simple encoding like XOR and Base64, more complex encryption like AES, DES and Blowfish, creating binary and hexdumps, compression and decompression of data, calculating hashes and checksums, IPv6 and X.509 parsing, changing character encodings, and much more.*
|
||||||
|
|
||||||
This will be served publicly with Funnel so that my friends can use this instance if they need it.
|
This will be served publicly with Funnel so that my friends can use this instance if they need it.
|
||||||
|
|
||||||
|
@ -202,15 +204,13 @@ TS_SERVE_PORT=8000
|
||||||
TS_FUNNEL=true
|
TS_FUNNEL=true
|
||||||
```
|
```
|
||||||
|
|
||||||
And I can add the corresponding `docker-compose.yml` to go with it. Note that I'm also pulling the `tailscale-docker` image from GHCR instead of building it locally as in the earlier example:
|
And I can add the corresponding `docker-compose.yml` to go with it:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# torchlight! {"lineNumbers": true}
|
# torchlight! {"lineNumbers": true}
|
||||||
services:
|
services:
|
||||||
tailscale: # [tl! focus:start]
|
tailscale: # [tl! focus:start]
|
||||||
build: # [tl! --:1 .nocopy:1]
|
image: ghcr.io/jbowdre/tailscale-docker:latest
|
||||||
context: ./image/
|
|
||||||
image: ghcr.io/jbowdre/tailscale-docker:latest # [tl! ++ reindex(-2)]
|
|
||||||
container_name: cyberchef-tailscale
|
container_name: cyberchef-tailscale
|
||||||
environment:
|
environment:
|
||||||
TS_AUTHKEY: ${TS_AUTHKEY:?err}
|
TS_AUTHKEY: ${TS_AUTHKEY:?err}
|
||||||
|
@ -261,9 +261,9 @@ And after ~10 minutes or so (it sometimes takes a bit longer for the DNS and SSL
|
||||||
|
|
||||||
|
|
||||||
#### Miniflux
|
#### Miniflux
|
||||||
I've lately been playing quite a bit with [my omg.lol address](https://jbowdre.omg.lol/) and [associated services](https://home.omg.lol/referred-by/jbowdre), and that's inspired me to [revisit the world](https://rknight.me/blog/the-web-is-fantastic/) of curating RSS feeds instead of relying on algorithms to keep me informed. Through that, I learned about [Miniflux](https://github.com/miniflux/v2), which is a "Minimalist and opinionated feed reader". It's written in Go, is fast and lightweight, and works really well as a PWA installed on mobile devices, too.
|
I've lately been playing quite a bit with [my omg.lol address](https://jbowdre.omg.lol/) and [associated services](https://home.omg.lol/referred-by/jbowdre), and that's inspired me to [revisit the world](https://rknight.me/blog/the-web-is-fantastic/) of curating RSS feeds instead of relying on algorithms to keep me informed. Through that experience, I recently found [Miniflux](https://github.com/miniflux/v2), a "Minimalist and opinionated feed reader". It's written in Go, is fast and lightweight, and works really well as a PWA installed on mobile devices, too.
|
||||||
|
|
||||||
It will be great for keeping track of my feeds, but I don't see a need to expose it publicly. So I'll serve it up with Tailscale Serve.
|
It will be great for keeping track of my feeds, but I need to expose this service publicly. So I'll serve it up inside my tailnet with Tailscale Serve.
|
||||||
|
|
||||||
Here's the `.env` that I'll use:
|
Here's the `.env` that I'll use:
|
||||||
```shell
|
```shell
|
||||||
|
@ -278,9 +278,9 @@ TS_EXTRA_ARGS=--ssh
|
||||||
TS_SERVE_PORT=8080
|
TS_SERVE_PORT=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
You may note that this one doesn't define `TS_FUNNEL` so Funnel will not be configured, just Serve.
|
Funnel will not be configured for this since `TS_FUNNEL` was not defined.
|
||||||
|
|
||||||
And the `docker-compose.yml` to go with it:
|
I adapted the [example `docker-compose.yml`](https://miniflux.app/docs/dacker.html#docker-compose) from Miniflux to add in my Tailscale bits:
|
||||||
```yaml
|
```yaml
|
||||||
# torchlight! {"lineNumbers": true}
|
# torchlight! {"lineNumbers": true}
|
||||||
services:
|
services:
|
||||||
|
@ -323,7 +323,6 @@ services:
|
||||||
interval: 10s
|
interval: 10s
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
```
|
```
|
||||||
This is based on the [example](https://miniflux.app/docs/dacker.html#docker-compose) from Miniflux. I've just templated some of the variables and added in my Tailscale bits.
|
|
||||||
|
|
||||||
I can bring it up with:
|
I can bring it up with:
|
||||||
```shell
|
```shell
|
||||||
|
@ -340,3 +339,5 @@ And I can hit it at `https://miniflux.tailnet-name.ts.net` from within my tailne
|
||||||
![miniflux](miniflux.png)
|
![miniflux](miniflux.png)
|
||||||
|
|
||||||
Nice, right? Now to just convert all of my other containerized apps that don't really need to be public. Fortunately that shouldn't take too long since I've got this nice, portable, repeatable Docker Compose setup I can use.
|
Nice, right? Now to just convert all of my other containerized apps that don't really need to be public. Fortunately that shouldn't take too long since I've got this nice, portable, repeatable Docker Compose setup I can use.
|
||||||
|
|
||||||
|
Maybe I'll write about something *other* than Tailscale soon. Stay tuned!
|
Loading…
Reference in a new issue