A Temporal CLI extension that runs temporal server start-dev and exposes it on your Tailscale tailnet.
Your dev server becomes reachable at temporal-dev:7233 from any machine on your tailnet.
- Temporal CLI v1.6.0+ (extension support was added in v1.6.0)
- A Tailscale account
curl -sSfL https://raw.githubusercontent.com/chaptersix/temporal-start-dev-ext/main/install.sh | shThis detects your OS and architecture, downloads the latest release, and installs to /usr/local/bin.
Download from the Releases page.
Extract the binary and place it somewhere on your PATH, for example /usr/local/bin:
tar -xzf temporal-ts-net_*.tar.gz
sudo mv temporal-ts_net /usr/local/bin/go build -o ./bin/temporal-ts_net ./cmd/temporal-ts_netOr using Mage:
go run mage.go buildAdd ./bin to your PATH.
temporal help --allYou should see ts-net listed as an extension command.
The binary is named
temporal-ts_netbecause the Temporal CLI extension system uses_as the separator for subcommands and dashes. Runningtemporal ts-nettriggers aPATHlookup fortemporal-ts_net.
Start the dev server on your tailnet:
temporal ts-netThis starts temporal server start-dev locally and proxies connections from your tailnet into it.
The extension uses tsnet to join your tailnet directly. Tailscale does not need to be installed on the machine.
On first run, tsnet will log an authorization URL to stderr. Open it in a browser to authorize the app with your tailnet. To skip this, provide an auth key. Generate one in the Tailscale admin console under Settings > Keys > Auth keys:
temporal ts-net --tailscale-authkey tskey-auth-...
# or
TS_AUTHKEY=tskey-auth-... temporal ts-netSet a custom hostname:
temporal ts-net --tailscale-hostname my-temporalAll other flags pass through to temporal server start-dev:
temporal ts-net --port 7234 --ui-port 8234 --db-filename /tmp/temporal.db| Flag | Default | Description |
|---|---|---|
--config |
Path to config file (see Configuration) | |
--tailscale-hostname |
temporal-dev |
Tailnet hostname |
--tailscale-authkey |
Auth key (or TS_AUTHKEY env var) |
|
--tailscale-state-dir |
Local state directory for tsnet node | |
--max-connections |
1000 |
Maximum concurrent connections |
--connection-rate-limit |
100 |
Maximum connections per second |
--dial-timeout |
10s |
Timeout for dialing backend |
--idle-timeout |
5m |
Idle timeout for proxy connections |
Flags also accept the --tsnet- prefix (e.g. --tsnet-hostname).
The extension reads its configuration from the [ts-net] section of the Temporal CLI config file.
This is the same file used by temporal config set/get -- by default ~/.config/temporalio/temporal.toml.
[ts-net]
tailscale-hostname = "my-temporal"
tailscale-authkey = "tskey-auth-..."
tailscale-state-dir = "/path/to/state"
max-connections = 500
connection-rate-limit = 50
dial-timeout = "15s"
idle-timeout = "10m"All fields are optional. Only set what you want to override.
The config file location is resolved in order:
--configflagTEMPORAL_CONFIG_FILEenvironment variable~/.config/temporalio/temporal.toml
Precedence: CLI flags > environment variables (TS_AUTHKEY) > config file > defaults. For example, --tailscale-authkey on the command line beats TS_AUTHKEY in the environment, which beats tailscale-authkey in the config file.
Once the server is running, any machine on your tailnet can connect to it.
Point the CLI at the tailnet hostname:
temporal workflow list --address temporal-dev:7233Or set it in a config profile so you don't have to pass it every time:
temporal config set --profile tailnet address temporal-dev:7233
temporal --profile tailnet workflow listTemporal SDKs support Environment Configuration.
This lets you configure the server address per environment without changing code.
Set up a dev profile pointing at the tailnet address and a prod profile pointing at Temporal Cloud:
[profile.dev]
address = "temporal-dev:7233"
namespace = "default"
[profile.prod]
address = "your-namespace.a1b2c.tmprl.cloud:7233"
namespace = "your-namespace"
api_key = "your-api-key"Then load the profile in your application.
The SDK reads the active profile from TEMPORAL_PROFILE and connects accordingly:
TEMPORAL_PROFILE=dev go run ./worker// No address hardcoded. The SDK loads it from the active profile.
c, err := client.Dial(envconfig.MustLoadDefaultClientOptions())Your application code stays the same. The connection target is determined by which profile is active at runtime.
The extension starts temporal server start-dev as a child process listening on localhost, then starts a tsnet node that listens on your tailnet.
Incoming connections on the tailnet are proxied to the local Temporal server over TCP.
Both the gRPC port and the UI port are proxied.
Be sure to have Go 1.26+ installed.
This project uses Mage for build tasks.
Mage installation is optional -- all targets work via go run:
go run mage.go -l # List targets
go run mage.go build # Build the binary
go run mage.go test # Run tests
go run mage.go fmt # Format code
go run mage.go clean # Remove build artifacts
go run mage.go install # Install to $GOPATH/bingo test ./...Tailscale integration tests use testcontrol and run entirely in-process. No Tailscale account or auth keys needed. They run the same way in CI.
The demo/ directory has a self-contained example of the proxy pattern:
go run demo/tailscale-proxy/main.goThis project uses GoReleaser with GitHub Actions. To cut a release, bump the version in VERSION and merge to main. GitHub Actions will create the tag, build binaries for Linux, macOS, and Windows (amd64/arm64), and publish a GitHub release.