Composite Forgejo CI action to deploy static pages to a Headscale node
  • Shell 94.7%
  • Go Template 5.3%
Find a file
2026-07-01 09:48:22 +01:00
scripts use rsync -c to avoide copying everything every time 2026-06-30 15:20:39 +01:00
.gitignore initial commit 2026-06-30 12:33:19 +01:00
action.yml Add ping to tailscale action, as recommended by docs 2026-06-30 15:15:13 +01:00
README.md update readme 2026-07-01 09:48:22 +01:00

Deploy a Static Site to Tailscale Node

A composite Forgejo CI action that joins a Headscale tailnet and rsyncs a built site to a remote node over Tailscale SSH.

It is designed for static-site pipelines: build your site in an earlier step, then hand the output directory to this action to publish it to /var/www/<site> on the target node.

I have built this primarily for myself, to deploy my blog and other static sites.

How it works

  1. Brings up Tailscale in userspace-networking mode with a SOCKS5 proxy on localhost:1055, authenticating to your Headscale login server with a pre-auth key.
  2. Installs rsync, ssh, and nc if they are missing (Debian/Ubuntu runners).
  3. Writes an ~/.ssh/config that reaches the deploy host through the Tailscale SOCKS5 proxy via ProxyCommand.
  4. Ensures /var/www/<site> exists on the remote, then mirrors the source directory into <deploy-root><site>/ with rsync -azc --delete.

Note: --delete means the remote target is mirrored exactly — files not present in the source directory are removed from the target.

Inputs

Input Required Default Description
site yes Site name; target becomes /var/www/<site>
ts-authkey yes Headscale pre-auth key
source-dir no . Built site directory, relative to the workspace
deploy-root no /var/www/ Target root directory on the remote node
deploy-host no 100.64.0.4 Target Tailscale IP address (100.64.0.x)
deploy-user no deploy Target SSH username
ts-login-server no https://hs.thms.uk Headscale login server

Usage

Create an ephemeral, reusable preauth key on your headscale server, making sure you give it whatever tag your ACLs need, e.g. tag:ci:

headscale preauthkeys create --ephemeral --reusable --tags tag:ci -e 876000h

Supply the key as Action secret with name TS_AUTHKEY to your Forgejo repository.

Then create a deploy workflow in your repository:

jobs:
  deploy:
    runs-on: ubuntu-latest                                 # A Debian/Ubuntu-based runner
    steps:
      - uses: https://github.com/actions/checkout@v4

      # ... build your site into ./public ...
      
      - name: Deploy
        uses: https://code.thms.uk/michael/tailscale-deploy@<version>
        with:
          site: example.com                                # Site name; target becomes `/var/www/<site>`
          source-dir: public                               # Build step's output directory 
          deploy-host: 100.x.y.z                           # Target Tailscale IP address
          deploy-user: www-data                            # the user you use to SSH to the node
          ts-authkey: ${{ secrets.TS_AUTHKEY }}            # Headscale pre-auth key
          ts-login-server: https://headscale.example.org   # Headscale login server

Requirements

  • A Debian/Ubuntu-based runner (the action uses apt-get to install any missing tools).
  • A reachable Headscale server and a valid pre-auth key (store it as a CI secret).
  • The deploy node advertised on the tailnet, running an SSH server that accepts the deploy-user, with write access to <deploy-root>.
  • The runner's tailnet identity authorized for Tailscale SSH to the target node.

Security notes

  • StrictHostKeyChecking is set to accept-new, so the host key is trusted on first connection. Connections route entirely over the tailnet via the SOCKS5 proxy.
  • Always pass ts-authkey from a secret — never inline it in the workflow file.

Questions / Comments / Bugs?

Reach me on Mastodon, or find more details on my personal website.