Skip to content
Portfolio

CI/CD Pipelines with GitHub Actions

Manual server updates are prone to human error. To professionalize the deployment lifecycle, GitHub Actions is utilized to automatically build and deploy new code every time a commit is pushed to the main branch.

However, because the server’s SSH port is completely blocked from the public internet by the Hetzner firewall, a standard deployment pipeline cannot reach the server.


To solve this, the pipeline temporarily joins the private Tailscale overlay network. The GitHub runner authenticates via an ephemeral OAuth token, pushes the code over the encrypted VPN tunnel, and then destroys its connection.

  1. Trigger: Activates strictly on push events to the main branch.
  2. Build Environment: Installs Node.js, pnpm, and compiles the static site.
  3. Zero Trust Authentication: The runner authenticates into Tailscale with a specific tag:ci role.
  4. Encrypted Deployment: Code is securely pushed to the server’s internal IP (100.x.x.x).
# Snippet: Zero Trust Deployment Workflow
steps:
# ... [Build steps omitted] ...
- name: Connect to Tailscale Network
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
- name: Deploy to Hetzner via SSH
uses: easingthemes/ssh-deploy@main
with:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
# This connects to the internal Tailscale IP, not the public internet!
REMOTE_HOST: ${{ secrets.SERVER_IP }}
REMOTE_USER: ${{ secrets.SERVER_USERNAME }}
SOURCE: "dist/"
TARGET: "/opt/docker/portfolio/public_html"

No sensitive data is hardcoded into the repository. The following variables are injected securely at runtime, adhering to the Principle of Least Privilege:

TS_OAUTH_CLIENT_ID & SECRET: Temporary network access credentials.

SERVER_IP: The internal Tailscale IP address (100.x.x.x).

SSH_PRIVATE_KEY: A dedicated, passwordless ED25519 key generated strictly for GitHub Actions.