Skip to content

Deployment Pipeline

Osotechie edited this page Jun 22, 2026 · 1 revision

Deployment Pipeline

The GitHub Actions workflow that deploys all Docker stacks to the NAS.

Trigger

on:
  push:
    branches: [main]
    paths-ignore: ['**/*.md']

Runs on every push to main (excluding markdown-only changes).

Workflow Steps

# Step Purpose
1 Checkout Get the code
2 Azure Login Authenticate using SPN
3 Get KeyVault Secrets Pull all secrets, export as masked env vars
4 Replace Tokens Substitute #{TOKEN}# in .env files with secret values
5 Connect to WireGuard Establish VPN tunnel
6 Add Routes Route NAS IP over WireGuard
7 Add SSH Key Install key + pinned host fingerprint
8 Configure SSH Multiplexing + keepalives
9 Create Docker Context Point Docker CLI at NAS over SSH
10 Deploy Stacks Loop through directories, docker compose up -d
11 Sync to NAS Rsync repo content to /config/stacks

🔗 For details on steps 2-8 (secrets, VPN, SSH), see the Secrets & Credentials and Network & Connectivity pages in the nas-as-code wiki.

Docker-over-SSH

Rather than exposing the Docker API over TCP (a security risk), the workflow creates a Docker context that tunnels commands over SSH:

- name: Connect to NAS (Docker Daemon)
  run: |
    docker context create NAS --docker "host=ssh://${{ vars.HOST_USER }}@${{ vars.HOST_IP }}"
    docker context use NAS
    docker info

This means every docker compose command is executed remotely on the NAS, but initiated from the GitHub Actions runner.

🔗 Docker SSH access docs

COMPOSE_PARALLEL_LIMIT

The workflow sets COMPOSE_PARALLEL_LIMIT=3:

env:
  COMPOSE_PARALLEL_LIMIT: 3

Why? Docker Compose normally runs container operations in parallel. Over SSH, each operation opens multiple SSH channels. The NAS's sshd has a default MaxSessions=10. Without this limit, larger stacks (like media with 13 services) blew past that cap with:

Session open refused by peer

Setting the limit to 3 keeps channels comfortably below 10.

Stack Deployment Loop

The workflow iterates through every directory containing a docker-compose.yml:

for dir in */; do
  [ -f "${dir}docker-compose.yml" ] || continue
  stack="${dir%/}"
  cd "$dir" && docker compose up -d
done

If any stack fails, it's recorded but the loop continues — other stacks still deploy. The workflow fails at the end if any stack failed.

Post-Deploy Sync

After deployment, the repo content is rsync'd to /config/stacks on the NAS. This means the local backup tooling (and Dockhand) can reference the same compose files without needing Git access.

Token Replacement

.env files contain #{TOKEN}# patterns that are replaced with Azure KeyVault secret values before deployment. The cschleiden/replace-tokens action handles this:

- name: Replace tokens in .env files
  uses: cschleiden/replace-tokens@v1
  with:
    files: '["**/.env"]'

💡 The .env files in the repo contain tokens, not secrets. Secrets only exist in the runner's environment variables during the workflow run.


Validation Pipeline

A separate workflow runs on all branches except main (i.e. on PRs and feature branches) to catch issues before they reach production:

Check Tool Purpose
Docker Compose validation Pester tests Validates all docker-compose.yml files are syntactically correct
Security scan Checkov Infrastructure-as-code security best practices
Security scan Trivy Container vulnerability detection
SARIF upload CodeQL Surfaces security findings in the GitHub Security tab

This means broken compose files or known vulnerable configurations are caught before merge — the deploy workflow only runs on main after validation passes.

🔗 Checkov docs | Trivy docs | Pester docs

Clone this wiki locally