Self-hosted service
patte/fly-tailscale-exit avatar
patte/fly-tailscale-exit

patte/fly-tailscale-exit: Tailscale Exit Nodes on Fly.io

Run a VPN with global exit nodes with fly.io, tailscale and github!

1,663 stars117 forksShellLicense varies

At a glance

What is it?
patte/fly-tailscale-exit packages Tailscale as a Fly.io machine for running exit nodes across multiple regions. It offers kernel networking, ephemeral state, health checks, signed images, and two authentication paths, but its own README warns that it is not a serious VPN replacement.
Who is it for?
Adopt it for a personal or experimental Tailscale exit node when kernel-level networking and regional Fly.io placement matter, but do not treat it as a serious commercial VPN replacement without verifying the README warning, Fly.io account and plan costs, credential handling, routing behavior, and operational boundaries first. The project-specific decision is whether its documented Linux microVM setup fits the required exit-node use case.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What fly-tailscale-exit Sets Up

patte/fly-tailscale-exit is a Shell-based example and deployment setup for running Tailscale exit nodes on Fly.io microVMs. Its stated audience is people who want to choose where traffic exits, distribute nodes across regions, or share access with friends without relying on an advertising-heavy VPN provider. The README describes a globally distributed VPN service, but also says the setup was compiled while exploring Fly.io and Tailscale and probably is not a suitable replacement for a trustworthy VPN service.

The project is specifically an exit-node deployment, not a general application integration guide. For adding Tailscale to an existing Fly.io application, the README points readers to Tailscale's separate Fly.io guide. The bundled setup instead concentrates on advertising a machine as an exit node, forwarding IPv4 and IPv6 traffic, and applying `MASQUERADE` NAT for egress. A client selects the node with `tailscale set --exit-node=fly-<region>` after approving the machine in the Tailscale admin console.

The intended operating model is a small number of independently started Fly.io machines, each associated with a region. The README documents creating one app, deploying one machine, and adding more regions with a separate scaling command. It does not document a user-management service, billing system, account isolation, or a production control plane. Those absences matter when comparing it with a conventional VPN provider.

Kernel Networking and Container Startup

The container uses a two-stage Dockerfile. The first stage downloads the Tailscale release named by `TSVERSION`, with `TSFILE=tailscale_${TSVERSION}_amd64.tgz`, extracts the archive, and copies `tailscaled` and `tailscale` into the final image. The final stage is based on `alpine:3.24` and installs `ca-certificates`, `iptables`, `ip6tables`, `busybox-extras`, and `ethtool`.

The copied `start.sh` is the container entry point, while `healthz` is copied into `/var/www/cgi-bin/healthz`. The README says the startup path configures IPv4 and IPv6 forwarding, enables `MASQUERADE` NAT, authenticates Tailscale, and advertises `--advertise-exit-node` with the hostname `fly-<region>`. Tailscale state is held in memory through `tailscaled --state=mem:`, so the node is ephemeral and its state disappears when the machine shuts down. This is a deliberate operational choice, but it means the machine must authenticate again after replacement or restart.

Health reporting is based on a local `busybox` HTTP server. The README specifies that `/cgi-bin/healthz` returns `200` when the node is connected to the tailnet and `503` otherwise. The bundled `fly.toml` connects that endpoint to a Fly `[checks]` block. The repository also states that the container exits if `tailscaled` dies, allowing Fly.io to restart the machine. That restart behavior is useful for detecting a dead daemon, but it does not explain Fly.io restart limits, replacement costs, or what happens when authentication fails repeatedly.

OAuth, Auth Keys, and Exit-Node Approval

Two credential paths are supported. A Tailscale OAuth client supplies `TAILSCALE_OAUTH_CLIENT_ID` and `TAILSCALE_OAUTH_SECRET`. The README describes this path as minting a tagged, ephemeral, pre-approved key. Alternatively, the machine can receive a plain `TAILSCALE_AUTH_KEY`. Both credential types are passed as Fly secrets, so they are not placed directly in the checked-in configuration shown in the quickstart.

The authentication choice affects administration. After deployment, the node appears in the Tailscale admin console as `fly-<region>`. The operator must approve it as an exit node there. If tailnet lock is enabled, the README says the node must also be signed. Those are explicit operational steps, not actions that the container silently completes.

The setup also assumes a Tailscale tailnet with public DNS configured. It does not explain ACL creation in the quickstart, though it mentions a longer setup walkthrough for GitHub organization settings, ACLs, the `tag:fly-exit` tag, and regions. The repository does not publish releases, and its license is listed as unknown. That licensing status should be checked before copying, modifying, redistributing, or hosting a derivative deployment.

Deploying the First Exit Node

The README assumes that the `fly` CLI is installed and that a Tailscale tailnet with public DNS is ready. The fastest documented path uses the prebuilt image from GitHub Container Registry. The application name must be unique, and the region in the command is an example deployment choice.

bash
fly apps create fly-exit-node        # pick a unique name; creates no fly.toml
fly secrets set -a fly-exit-node \
  TAILSCALE_OAUTH_CLIENT_ID=<id> TAILSCALE_OAUTH_SECRET=<secret>   # or TAILSCALE_AUTH_KEY=<key>
fly machine run ghcr.io/patte/fly-tailscale-exit:latest -a fly-exit-node --region fra

These commands create the app, store the selected credentials as secrets, and start the container in the `fra` region. The comments and placeholders are part of the documented example. After the machine starts, approve the node named for its region in the Tailscale admin console. Then select it from a Tailscale client:

bash
tailscale set --exit-node=fly-<region>

The README says that Tailscale will commonly establish a direct connection through Fly.io NAT, while restrictive client networks may fall back to DERP relays. It also describes the image as `linux/amd64`. The repository does not document Apple Silicon or ARM deployment in the quickstart, so the published image path should not be treated as architecture-independent without checking.

Source Builds, Regions, and Signed Images

For a deployment based on the repository's own `fly.toml`, the documented workflow clones the project and uses `fly launch` before deployment. The README says this route provides health checks, easier multi-region scaling, and a build from source.

bash
git clone https://github.com/patte/fly-tailscale-exit.git
cd fly-tailscale-exit
fly launch
fly deploy --ha=false

The `--ha=false` option in the example deploys a single machine. The README says additional regions can be created with `fly scale count 1 --region fra`. The bundled `fly.toml` can also be changed to use the prebuilt image by uncommenting its `image =` line, although that edit is not shown as a complete config snippet here.

The project publishes images to GHCR as a cosign-signed artifact with SLSA provenance and an SBOM. The documented verification command checks the image signature and restricts the certificate identity to this repository's GitHub Actions workflow.

bash
cosign verify ghcr.io/patte/fly-tailscale-exit:latest \
  --certificate-identity-regexp '^https://github.com/patte/fly-tailscale-exit/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

The `latest` tag follows the newest Tailscale release, while version tags such as `:1.98.4` pin a release. The README says the image is rebuilt weekly for Alpine base-image patches and that the 25 most recent versions are retained. These are documented build and retention details, not a guarantee about the exact image currently available.

Limits, Alternatives, and Operating Boundaries

The most important limitation is stated in the README itself. The project says it is probably not what users should choose as a serious VPN replacement. It also warns that Fly.io's free tier is not intended for proxy use and that a plan might be upgraded to the $10 per month Advanced plan. The project therefore involves both network-routing questions and an external hosting account with its own usage and cost rules.

The README recommends considering Mullvad exit nodes through Tailscale instead. That alternative uses a VPN provider's existing exit-node service rather than operating the provider's own Fly.io machines. It changes the operational boundary: the user does not deploy or authenticate the exit infrastructure described in this repository. The README links to a separate Tailscale guide for that setup, so this article does not claim that the two configurations have identical pricing, privacy, performance, or node selection.

There is also an `official-image` branch based on Tailscale's `tailscale/tailscale` image. The README says `containerboot` handles authentication, exit-node advertisement, NAT, and a native `/healthz` endpoint. The Fly-specific configuration adds an `[experimental] cmd` override to enable IP forwarding before the entrypoint. That branch is a direct architectural alternative: it uses Tailscale's official image and startup tooling instead of this repository's custom Alpine image, downloaded Tailscale archive, and `start.sh`.

The custom image is a focused deployment recipe, not a general VPN platform. The README does not document logging policy, bandwidth accounting, access revocation procedures, multi-user account isolation, incident response, rollback for a running node, or a formal upgrade procedure beyond weekly image and Tailscale update automation. The repository has no GitHub releases, and the available maintenance signal is the last push on 2026-10-11. The repository is not archived, but that fact alone does not establish production suitability.

Editorial conclusion

Adopt it for a personal or experimental Tailscale exit node when kernel-level networking and regional Fly.io placement matter, but do not treat it as a serious commercial VPN replacement without verifying the README warning, Fly.io account and plan costs, credential handling, routing behavior, and operational boundaries first. The project-specific decision is whether its documented Linux microVM setup fits the required exit-node use case.

Frequently asked questions

How do I install and start a Tailscale exit node with fly-tailscale-exit?

The README assumes the `fly` CLI and a prepared Tailscale tailnet. It documents creating a Fly app, setting either the OAuth secrets or `TAILSCALE_AUTH_KEY`, and running the GHCR image with `fly machine run`.

Does fly-tailscale-exit use userspace proxy networking?

No. The README explicitly describes Tailscale exit nodes on Fly.io microVMs with real kernel networking, IPv4 and IPv6 forwarding, and `MASQUERADE` NAT, rather than a userspace proxy.

What should I check before using fly-tailscale-exit as a serious VPN?

The README warns that the setup probably is not a serious VPN replacement and notes Fly.io plan considerations for proxy use. It also requires approval in the Tailscale admin console and, when tailnet lock is used, signing the node.

Official sources

  1. Issues
  2. patte/fly-tailscale-exit on GitHub
  3. Project website
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/patte-fly-tailscale-exit.svg)](https://hysenlabs.com/projects/patte-fly-tailscale-exit)

Was this useful?