GitHub Actions can deploy to a private home server when a self-hosted runner has a network path to it. A runner inside the home LAN can do this without a VPN; a runner elsewhere can use an always-connected VPN path instead of joining a VPN for every job. HomeGrid VPN supports the latter design with a Spoke runner, a Cloud Hub, and a Grid connected to the destination LAN.
Why a hosted runner cannot reach your home LAN directly
You push an application to GitHub and want the home server to update automatically. But pointing a standard GitHub-hosted runner at a private address such as 192.168.x.x does not create a route into your LAN. A private cloud server has the same problem unless you provide connectivity to its network. A VPS with a public address and publicly accessible SSH is a different configuration and can accept connections over that public route.
With a reachable public IPv4 address, you could forward an SSH port through the home router. Dynamic addresses also require a way to track changes, such as DDNS. However, CGNAT and DS-Lite can prevent users from configuring the upstream port mapping. MAP-E services may restrict which IPv4 ports are available. Double NAT or a building’s upstream router can add another barrier.
IPv6 direct access is another option, but it requires a global IPv6 address at home, IPv6 connectivity at the runner, and appropriate inbound rules on the router and server. Removing NAT port forwarding does not remove inbound firewall design.
For deployment, the essential requirement is more specific: the runner must be able to reach the deployment target. The server does not need to accept SSH connections from the whole Internet.
Start with the simplest option: a runner inside the LAN
A self-hosted runner executes GitHub Actions jobs on a machine you manage. It initiates outbound connections to GitHub to receive jobs. You do not need to forward a home router port so GitHub can initiate an inbound connection. The runner still needs outbound access to the required GitHub services. GitHub’s runner communication requirements
Developer pushes code to GitHub
↑
Runner initiates connection
│
Runner inside the home LAN
│ SSH
↓
Private application server
If the runner and server share a LAN, a VPN may be unnecessary. You could also install the runner on the deployment target and update the application locally.
A separate runner becomes useful when you want to keep build jobs away from production, place the runner on a VPS, or deploy to several locations. You then need a network path from that runner to each target.
The runner and deployment target are separate roles
| Component | Responsibility |
|---|---|
| Runner | Execute jobs, tests, builds, transfers, and deployment commands |
| Deployment target | Run the application or service |
| Network path | Carry traffic between the runner and target |
A VPS-hosted runner can deploy to a home server, and a home-hosted runner can deploy to another site. A VPN supplies connectivity; SSH authentication and the application update procedure remain separate responsibilities.
Join a VPN per job, or keep the runner connected
One approach is to connect a GitHub-hosted runner to a VPN at the start of each job and disconnect afterward. Depending on the VPN and identity system, short-lived credentials or OIDC may be available. That design adds connection setup, authentication, readiness checks, and routing checks to the job.
With a self-hosted runner, you can instead maintain the VPN connection as a host service:
Host setup: register with VPN → connect → maintain connection
Each job: test → build → deploy over the existing private route
The workflow uses connectivity that already exists. You still need to monitor the host, VPN connection, and routes. An always-connected design does not guarantee uninterrupted connectivity.
Connect the runner as a HomeGrid VPN Spoke
Register the runner’s Linux host as a HomeGrid VPN Spoke, and provide a Grid connected to the home LAN. Both the runner and the home Grid initiate outbound VPN connections to the Cloud Hub. Once connected and authorized, the runner can reach the private server through that path.
A Spoke runner receives a GitHub Actions job and deploys through the Cloud Hub and home Grid to a private server. Both the runner and home Grid initiate outbound VPN connections to the Cloud Hub. The hub is a relay; no Internet exit is shown.
GitHub ActionsPush to main
Job
Outbound HTTPS
Self-hosted RunnerSpoke · Always connected
Cloud HubVPN relay
HOME LAN
Home Grid
Private ServerSSH Deploy
VPN route
Jobs execute on the runner and deployment follows the green route. The runner initiates the connection to GitHub. VPN connections start as Runner → Cloud Hub ← Home Grid. The Cloud Hub is a relay, and no Internet exit is shown.
The green deployment arrows show traffic after VPN connectivity is established. They do not mean that the Cloud Hub initiates a new inbound connection to your home’s public address.
This design does not depend on home-side IPv4 port forwarding, a static public IP, DDNS, or direct inbound IPv6 access. It can work on CGNAT, MAP-E, or DS-Lite connections when the required outbound traffic to the Cloud Hub is permitted. The connection type alone does not guarantee success; ISP and local network restrictions still matter.
See the Linux Spoke setup guide for registration. Use --split as the normal choice for a deployment runner. Traffic for the configured LANs and tenant ranges uses the VPN, while GitHub HTTPS connections and package or container-image downloads use the runner’s own Internet connection. Check route refresh requirements when destination LANs change.
The Linux Spoke defaults to a full tunnel. In that mode, Internet traffic also travels through the Cloud Hub and home connection. A runner hosted elsewhere can lose its connection to GitHub when the home connection or Grid fails, and downloads delivered back to that runner consume home upload bandwidth. Choose a full tunnel when you intentionally want the runner’s Internet traffic to exit through home. With --split, deployment to a home server still depends on the home side being available.
Remove VPN enrollment from the workflow, not credential management
Once the host maintains its VPN connection, a normal deployment job does not need a HomeGrid VPN enrollment token or a command to establish the tunnel each time.
| Credential or configuration | Where it remains relevant |
|---|---|
| VPN enrollment token | Initial registration, rather than every deployment job |
| VPN keys and configuration | Protected on the connected runner host |
| GitHub runner credentials | Runner registration and ongoing operation |
| SSH private key and trusted host keys | Authentication to the deployment target |
| Registry or cloud credentials | Fetching or publishing artifacts, if required |
VPN enrollment moves from job execution into host administration. If a runner is compromised or retired, revoke its GitHub registration, VPN device registration, and deployment credentials as appropriate.
Set up routing and SSH before registering the runner
1. Prepare a Linux host
Use a home machine or a VPS with the resources your jobs require. Permit outbound access to GitHub and the Cloud Hub. Create a dedicated, unprivileged user for job execution. Keeping the runner separate from the application server makes the maintenance and permission boundaries easier to manage.
2. Register the host as a Spoke
Register the Linux host with HomeGrid VPN using --split. Confirm that the home Grid is online and that the destination LAN is registered. The application server behind the Grid does not itself need to be registered as a Spoke.
3. Grant access through LAN ACL
Allow the runner device to access the destination LAN. Confirm connectivity after the change takes effect, and leave unrelated LANs unapproved. Use server firewalls for narrower host and port restrictions.
4. Configure SSH public-key authentication
Create a deployment account such as deploy on the target. Configure its authorized public key and prepare the private key, SSH configuration, and known_hosts for the user that will run the runner service. A successful SSH session from an administrator’s account does not prove that the service user can connect.
Verify the target’s host-key fingerprint through a trusted channel, such as its console. Accepting unverified ssh-keyscan output alone does not authenticate the first connection.
ssh -o BatchMode=yes -o StrictHostKeyChecking=yes deploy@192.168.1.100 'true'
The private IP above is illustrative; replace it with your target. A static public IP is unnecessary, but keep the LAN address stable with a DHCP reservation or equivalent arrangement.
Prepare /usr/local/bin/deploy-app on the target. Implement artifact selection and retrieval, application updates, health checks, and recovery as appropriate, then test the procedure manually. Grant only the required permissions rather than unrestricted sudo access.
5. Register and run the GitHub runner as a service
In the private repository, open Settings → Actions → Runners → New self-hosted runner. Choose Linux and the host’s architecture. Follow GitHub’s displayed download, verification, and registration commands under the dedicated user, and add the custom label homegrid-deploy. GitHub’s runner registration instructions
After registration, Linux hosts using systemd can install the service from the runner directory. Replace runner with your dedicated account:
sudo ./svc.sh install runner
sudo ./svc.sh start
sudo ./svc.sh status
Stop any manually started runner process first. Confirm that GitHub shows the runner as available and that both the VPN and runner recover after a host reboot. A job must fail safely if it starts before connectivity returns. GitHub’s service configuration instructions
Run the deployment on a push to main
Place this minimal workflow in .github/workflows/deploy.yml. It assumes a trusted private repository, a protected main branch, and a Linux runner with the homegrid-deploy label.
name: Deploy private server
on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: private-server-deploy
cancel-in-progress: false
jobs:
deploy:
runs-on: [self-hosted, linux, homegrid-deploy]
timeout-minutes: 15
steps:
- name: Deploy over private network
run: >-
ssh -o BatchMode=yes -o StrictHostKeyChecking=yes
-o ConnectTimeout=15
deploy@192.168.1.100 '/usr/local/bin/deploy-app'
The runner label selects where the job runs; it is not an authorization boundary. If you use environment approvals, configure the environment’s protection rules and specify the matching environment in the job.
This workflow invokes a target-side script, so it needs no source checkout on the runner. contents: read limits the GitHub token’s permissions; it does not supply SSH credentials. If the runner also tests or builds the application, add checkout, tests, build, and artifact transfer before deployment. Pin any actions you introduce to reviewed full commit SHAs.
The minimal example does not pass the pushed commit to the target. To guarantee that a push deploys its corresponding artifact, identify the artifact by commit SHA or image digest, pass that reference to the deployment procedure, and validate it before applying the update.
Design deployment to stop if the route or authentication is unavailable. Implement health checks and rollback for your application. Serializing workflow runs does not, by itself, guarantee release order; reject stale artifacts when that matters.
Extend the design to VPS and private cloud networks
The same connectivity model can serve a VPS whose management SSH is not publicly accessible, an AWS VPC, an Azure VNet, or another site’s development network.
HomeGrid VPN can connect additional LANs using a Linux gateway inside the destination network. Give the runner a route and permission to that LAN, then deploy to the server behind the gateway.
Registering a network does not guarantee access to every subnet. Check gateway-to-server reachability, registered address ranges, security groups, network ACLs, OS firewalls, and any necessary return routing. Overlapping address ranges require deliberate network design. Connecting home, office, and cloud LANs
The common layer is private connectivity. Operating systems, deployment procedures, application settings, and cloud IAM still need their own configuration.
Subnet ACLs, host firewalls, and SSH serve different purposes
HomeGrid VPN LAN ACL controls which destination LANs or subnets a registered runner device may access. It does not provide per-server or per-port rules.
| Layer | What it controls |
|---|---|
| LAN ACL | Destination LANs or subnets available to the runner |
| Host firewall and related controls | Traffic and ports accepted by a server |
| SSH authentication | User and key allowed to log in |
| OS permissions | Files and commands the deployment account may change or execute |
Use a dedicated deployment account, restrict its privileges, and keep SSH host-key verification enabled. Base source-address firewall rules on what the destination actually observes: a gateway or NAT may change the source address, so do not assume the runner’s VPN address always appears unchanged.
If a target shares a subnet with personal computers or a NAS, account for the breadth of that LAN permission. A separate deployment subnet or VLAN may help when stronger separation is required. LAN ACL configuration
Treat the connected runner as part of your trust boundary
A job can use the runner’s network access and permissions. Untrusted code on a connected runner can become a foothold for lateral movement into accessible networks. Prefer private repositories, but do not treat repository privacy as sufficient protection.
- Do not execute untrusted pull requests or unreviewed code on the deployment runner.
- Restrict eligible repositories and use runner-group controls where available.
- Protect main and workflow changes; use environment approvals when appropriate.
- Update the OS, runner, and dependencies, and manage files and processes left by jobs.
- Avoid running jobs as root, and restrict powerful interfaces such as the Docker socket.
- Protect VPN keys and deployment credentials; keep them out of logs and artifacts.
A compromised persistent runner can retain changes that affect later jobs. GitHub documents these risks in its self-hosted runner security guidance.
You can separate build and deployment environments so the connected runner only distributes approved artifacts. Where stronger execution isolation is needed, consider runners whose execution environments are destroyed after each job.
The Cloud Hub is not the Internet exit
The Cloud Hub is a component on the VPN route between the runner and home. Traffic passing through it does not imply that Internet traffic exits through its public IP.
For deployment, the private application server is the destination. There is no need to use the home connection as an Internet exit.
If you separately select a full tunnel with a home exit, Internet-bound traffic follows the Cloud Hub, home Grid, and residential ISP before reaching the Internet. In that configuration, the exit is the home connection. With --split, the configured private routes and ordinary Internet traffic follow separate paths.
Cloud Hub ≠ Internet Exit. Check LAN routing, access permissions, and Internet egress separately. The Cloud Hub remains part of the system’s operational and trust boundaries; target authentication and authorization are still required.
Make deployment depend on a private route, not a public SSH port
A runner inside the destination LAN can deploy without a VPN. A runner elsewhere needs a suitable route, which an always-connected VPN can provide.
With HomeGrid VPN, a Spoke runner uses its existing connection to reach approved home or site LANs. That removes home-side port forwarding, a static public IP, DDNS, and per-job VPN enrollment from this deployment design, subject to outbound connectivity requirements.
Start by choosing where the runner lives, which subnets it may reach, and which changes its SSH account may make. Validate manual deployment and recovery before enabling automatic execution. Then a push to GitHub can trigger an update to a private home server without making its management port public.
Originally published at https://www.homegrid-vpn.com/blog/en/github-actions-private-deploy/
Top comments (0)