A reusable template repository for configuring persistent and secure auto-mounting of remote NFS storage to GitHub Actions runners over a Cloudflare TCP Tunnel.
graph LR
subgraph GitHub Runner Host
Runner[GitHub Runner] -->|Local Mount| LocalPath["/mnt/sharedroot"]
LocalPath -->|Mount Loopback| Loopback["127.0.0.1:2049"]
Loopback -->|Cloudflare TCP Tunnel| CFTunnel["cloudflared access tcp"]
end
subgraph Remote NFS Host
CFTunnel -.->|Secure WAN Tunnel| CFEdge["Cloudflare Edge"]
CFEdge -.->|Tunnel Ingress| CFDaemon["cloudflared tunnel daemon"]
CFDaemon -->|Internal Port| NFSServer["NFS Daemon (2049)"]
NFSServer -->|Shared Storage| SharedDir["/sharedroot"]
end
style Runner fill:#2A85FF,stroke:#000,stroke-width:2px,color:#fff
style NFSServer fill:#2BD980,stroke:#000,stroke-width:2px,color:#fff
style CFTunnel fill:#FF6B6B,stroke:#000,stroke-width:2px,color:#fff
style CFDaemon fill:#FF6B6B,stroke:#000,stroke-width:2px,color:#fff
This architecture allows runner hosts to access high-speed centralized build caches without exposing raw NFS ports (2049) to the public internet, completely bypassing firewall complications and keeping traffic encrypted in transit.
Before running the setup on your runner host, ensure you have:
- A Cloudflare Tunnel created and pointing to your NFS server (
tcp://localhost:2049). - A public hostname assigned (e.g.,
nfs-sharedroot.iamrp.dev). - An active Access/Service Token set up in Cloudflare Zero Trust dashboard with Policy allowing the runner host to bypass authentication.
- Provide the Cloudflare Access Service Token credentials as environment variables when running the script:
export CF_ACCESS_CLIENT_ID="<your-access-client-id>" export CF_ACCESS_CLIENT_SECRET="<your-access-client-secret>" sudo -E ./setup_remote_nfs.sh
Simply run the auto-configuration script on your runner host:
chmod +x ./setup_remote_nfs.sh
sudo ./setup_remote_nfs.sh- Installs
cloudflared: Downloads and installs the latest stable deb package if not present. - Configures Systemd: Creates
cloudflared-nfs.servicerunning asrootto persistently manage the loopback TCP client on port2049. - Applies Auto-Mounting: Appends a persistent
fstabentry that mounts the virtual loopback mount to/mnt/sharedroot, with safety flags (soft,timeo=100,retrans=2,_netdev) and a systemd dependency wrapper (x-systemd.requires=cloudflared-nfs.service). - Executes Mount: Triggers systemd mount daemon reloading and performs the initial mount validation.
Use the provided example workflow to integrate NFS cache mounting into your pipelines:
name: Compile Job with NFS Cache
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux]
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup NFS Mount
env:
CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}
run: |
chmod +x ./setup_remote_nfs.sh
sudo -E ./setup_remote_nfs.sh
- name: Build with Cached Assets
run: |
# Use /mnt/sharedroot/github_runners/shared/ as your cache target
make build CACHE_DIR=/mnt/sharedroot/github_runners/shared/build-cachesudo systemctl status cloudflared-nfs.servicemount | grep "/mnt/sharedroot"If the mount fails, verify that the TCP listener is running locally:
ss -tulpn | grep 2049Or check if nfs-common is installed on your Linux host:
sudo apt install nfs-common