Skip to content

Instantly share code, notes, and snippets.

@sayrer
Created April 29, 2026 17:53
Show Gist options
  • Select an option

  • Save sayrer/c8db94534e00a8519727fc9ad528a359 to your computer and use it in GitHub Desktop.

Select an option

Save sayrer/c8db94534e00a8519727fc9ad528a359 to your computer and use it in GitHub Desktop.
tell Claude to set up Jenkins on Mac and Linux
❯ let's make a plan to install Jenkins and set up one Mac and one Linux VM as agents
Listed 1 directory (ctrl+o to expand)
⏺ Here's a proposed plan. A few choices need your input before we commit to specifics — I've marked them with ?.
Decisions to lock first
1. Controller host? Cleanest is the Linux VM (long-lived, headless). Running it on the Mac is fine for solo use but ties uptime to your laptop.
2. Linux distro? Ubuntu 24.04 LTS is the path of least resistance.
3. VM hypervisor? UTM, Parallels, VMware Fusion, OrbStack, or a cloud VM (EC2/Hetzner)?
4. Mac agent — Apple Silicon or Intel? Determines JDK and toolchain choices.
5. What kind of jobs? (iOS/macOS signing → Mac agent matters; pure backend → Linux is enough.)
Plan (assuming controller-on-Linux-VM, Ubuntu 24.04, agents over SSH)
Phase 1 — Controller (Linux VM)
- Provision VM: 2 vCPU / 4 GB RAM / 40 GB disk minimum.
- Install Temurin JDK 21 (Jenkins LTS requirement).
- Install Jenkins LTS from pkg.jenkins.io apt repo; enable systemd unit.
- Reverse proxy with nginx + Let's Encrypt if you want HTTPS, or just bind to localhost + SSH-tunnel for now.
- Unlock, install suggested plugins, create admin user, set the URL.
- Plugins to add up front: SSH Build Agents, Credentials Binding, Pipeline, Git, Matrix Authorization.
- Configure security: matrix auth, disable signup, enable CSRF.
Phase 2 — Linux agent
- If single VM: skip — controller can build on itself for non-isolated jobs. Otherwise a second VM, same baseline.
- Create jenkins user, install JDK 21, Git, Docker (if needed), build deps.
- Generate an SSH keypair on the controller; install the public key on the agent.
- Add node in Jenkins: launch via SSH, set remote root /var/lib/jenkins-agent, label linux.
Phase 3 — Mac agent
- Create dedicated jenkins user on the Mac (or use your account if this is just personal).
- brew install temurin@21 git; install Xcode + CLT if doing Apple builds.
- Enable Remote Login (SSH). Disable App Nap for the agent process.
- Install the public key from the controller in the Mac user's ~/.ssh/authorized_keys.
- Add node in Jenkins: SSH launcher, label mac, set the remote root in the user's home.
- Create a LaunchAgent so the agent process auto-starts and survives reboot.
- For Apple-signed builds: store keychain unlocked at login, expose signing identity via Keychain Access; do not bake creds into Jenkins credentials directly.
Phase 4 — Smoke test
- One pipeline pinned to linux label, one to mac label, both running uname -a + git clone + a trivial build.
- Confirm both agents reconnect after controller restart.
Tradeoffs worth flagging
- SSH vs inbound (JNLP) agents: SSH is simpler when the controller can reach agents. Use inbound if the Mac is behind NAT and the VM is on a public host.
- Docker on the Linux agent adds isolation but ~1 GB RAM overhead; skip if you don't need clean per-job environments.
- Mac agent reliability is the usual pain point — sleep, FileVault, and keychain locking will bite. A LaunchDaemon (root) is more reliable than a LaunchAgent, but harder to use for Apple codesigning.
Answer the 5 decisions above and I'll turn this into concrete commands/configs.
✻ Baked for 40s
❯ Let's say I want to run it on a Mac mini
⏺ Got it. A couple of follow-ups, then a concrete plan.
Still need to know
1. Is the Mac mini also the Mac agent, or a separate Mac? Running controller + agent on the same box is fine for a home setup; keep them as logically separate Jenkins nodes anyway so labels work cleanly.
2. Where does the Linux VM live? Easiest: on the Mac mini itself via UTM (free, Apple Silicon native) or Parallels (smoother but paid). Alternative: a cheap cloud VM.
3. Apple Silicon or Intel mini? Affects JDK arch and Linux VM (ARM64 vs x86_64 guest).
4. Exposed to the internet, or LAN/Tailscale only? Big impact on TLS + auth.
Plan (assumes Apple Silicon mini, Linux VM via UTM on the same mini, LAN-only via Tailscale)
Phase 1 — Mac mini as Jenkins controller
- brew install temurin@21 jenkins-lts git
- Start as a LaunchDaemon (not Agent) so it runs without login: sudo brew services start jenkins-lts. This survives reboots and FileVault unlock.
- Default port 8080, bound to 127.0.0.1 + Tailscale interface only.
- Disable App Nap and "put hard disks to sleep" in Energy settings; set "start automatically after power failure".
- Initial unlock, suggested plugins, admin user, set Jenkins URL to the Tailscale hostname.
- Plugins: SSH Build Agents, Credentials Binding, Pipeline, Git, Matrix Authorization, Keychains and Provisioning Profiles (if doing Apple signing).
Phase 2 — Mac agent (the mini itself, as a separate node)
- Create a dedicated jenkins user (System Settings → Users) — keeps agent workspace out of your home dir and isolates keychains.
- Log in once as jenkins to materialize the home dir and unlock the login keychain.
- Enable Remote Login for that user only (System Settings → General → Sharing).
- In Jenkins: add node mac-mini, label mac, launch via SSH to jenkins@localhost, remote root /Users/jenkins/agent.
- For Apple builds later: import signing identity into the jenkins user's login keychain; use security unlock-keychain in the pipeline.
Phase 3 — Linux VM via UTM
- Install UTM from brew: brew install --cask utm.
- Download Ubuntu 24.04 ARM64 server ISO; create VM with 2 vCPU / 4 GB / 40 GB.
- Enable UTM's "Start automatically" so the VM comes up after Mac mini reboots.
- In the guest: install openjdk-21-jre-headless, git, build deps, create jenkins user.
- Networking: UTM "Shared Network" gives the VM a routable IP from the host; note it.
- Generate SSH keypair on the controller (as the user Jenkins runs under), copy pubkey to jenkins@<vm-ip>.
- Add node linux-vm, label linux, SSH launcher, remote root /home/jenkins/agent.
Phase 4 — Access & TLS
- brew install tailscale && sudo tailscaled install-system-daemon on the mini; connect both Mac and VM to your tailnet.
- Use https://<mini>.<tailnet>.ts.net with Tailscale's automatic certs (tailscale cert) — no Let's Encrypt or port-forwarding.
Phase 5 — Smoke test
- Pipeline A pinned to mac label: sw_vers && xcodebuild -version.
- Pipeline B pinned to linux label: uname -a && lsb_release -a.
- Reboot the mini; confirm Jenkins, the VM, and both agents come back without manual steps.
Things to watch on a Mac mini setup
- FileVault + headless reboot: if FileVault is on, the mini won't auto-login after a power loss until someone unlocks the disk. Either disable FileVault (LAN-trusted) or enable "auto-login" via fdesetup
authrestart workflow.
- Power & sleep: sudo pmset -a sleep 0 disksleep 0 womp 1 autorestart 1.
- Single-box failure mode: controller and Linux VM share hardware — fine for personal use, not for anything you'd page on.
Confirm the four questions above (or just say "go with the defaults") and I'll write out the actual commands and node configs.
✻ Sautéed for 31s
❯ 1) the mini is the agent, 2) the linux VM runs on the mac mini it should be Ubuntu LTS on UTM, LAN/Tailscale only
⏺ Locked in. I'll assume Apple Silicon mini (say so if it's Intel — changes JDK arch and the Ubuntu ISO). Here's the executable plan.
Prereqs (one-time, on the mini)
# Homebrew if not present, then:
brew install temurin@21 git tailscale
brew install --cask utm
sudo brew services start tailscale
sudo tailscale up
sudo pmset -a sleep 0 disksleep 0 womp 1 autorestart 1
Decide on FileVault now: off for true headless reboot, or on + accept manual unlock after power loss.
Phase 1 — Jenkins controller on the mini
brew install jenkins-lts
sudo brew services start jenkins-lts # LaunchDaemon, survives reboot
- Browse http://localhost:8080, paste the initial admin password from /Users/<you>/.jenkins/secrets/initialAdminPassword (or /var/root/.jenkins/... since we started as root).
- Install suggested plugins + add: SSH Build Agents, Credentials Binding, Matrix Authorization, Keychains and Provisioning Profiles.
- Manage Jenkins → System → set Jenkins URL to https://mini.<tailnet>.ts.net.
- Tailscale cert for HTTPS: sudo tailscale cert mini.<tailnet>.ts.net and put nginx (or Caddy: brew install caddy) in front of :8080. Caddy is one line:
mini.<tailnet>.ts.net {
reverse_proxy 127.0.0.1:8080
}
Phase 2 — Mac agent (the mini, as its own node)
sudo dscl . -create /Users/jenkins
sudo dscl . -create /Users/jenkins UserShell /bin/zsh
sudo dscl . -create /Users/jenkins NFSHomeDirectory /Users/jenkins
sudo dscl . -create /Users/jenkins UniqueID 600
sudo dscl . -create /Users/jenkins PrimaryGroupID 20
sudo mkdir -p /Users/jenkins && sudo chown jenkins:staff /Users/jenkins
sudo dscl . -passwd /Users/jenkins '<set-a-password>'
- System Settings → General → Sharing → Remote Login → on, allow jenkins only.
- Log in once as jenkins (GUI) so the login keychain materializes.
- As your admin user, generate an SSH key for Jenkins and authorize it for jenkins@localhost:
sudo -u _jenkins ssh-keygen -t ed25519 -f /var/root/.jenkins/jenkins_id_ed25519 -N ''
# copy the pubkey into /Users/jenkins/.ssh/authorized_keys, chmod 600, chown jenkins
- In Jenkins: New Node → Permanent Agent mac-mini, labels mac apple-silicon, remote root /Users/jenkins/agent, launcher = "Launch agents via SSH", host localhost, credentials = the ed25519 key above, host key
verification = "Manually trusted".
Phase 3 — Ubuntu 24.04 LTS in UTM
- Download Ubuntu Server 24.04 ARM64 ISO.
- UTM → New → Virtualize → Linux → 2 vCPU / 4 GB / 40 GB / Shared Network.
- During install: hostname linux-agent, create user jenkins, enable OpenSSH server.
- Mark VM "Start automatically" in UTM's settings so it boots when the mini boots.
- Inside the VM:
sudo apt update
sudo apt install -y openjdk-21-jre-headless git build-essential curl
sudo mkdir -p /home/jenkins/agent && sudo chown jenkins:jenkins /home/jenkins/agent
sudo tailscale up # join tailnet so the controller reaches it by name
- On the controller, copy the same Jenkins SSH pubkey into jenkins@linux-agent's ~/.ssh/authorized_keys.
- New Node linux-vm, labels linux ubuntu-24.04, remote root /home/jenkins/agent, SSH launcher to linux-agent.<tailnet>.ts.net, same credentials.
Phase 4 — Smoke test
Two trivial pipeline jobs:
// mac smoke
pipeline { agent { label 'mac' } stages { stage('x') { steps { sh 'sw_vers && uname -m' } } } }
// linux smoke
pipeline { agent { label 'linux' } stages { stage('x') { steps { sh 'lsb_release -a && uname -m' } } } }
Then sudo reboot the mini and confirm both come back green without intervention.
Open issues to handle later (don't block initial setup)
- Backups: snapshot ~/.jenkins (or /var/root/.jenkins) on a schedule.
- Updates: Jenkins LTS via brew upgrade, Ubuntu via unattended-upgrades.
- Per-job isolation on Linux: add Docker (apt install docker.io, add jenkins to docker group) when you have a job that needs it.
Want me to start executing Phase 1, or write it up as a script you'll run yourself?
✻ Brewed for 32s
❯ write up the script
⏺ Write(setup-controller.sh)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Create file
setup-controller.sh
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
1 #!/usr/bin/env bash
2 # setup-controller.sh
3 # Jenkins LTS controller + Mac agent on a Mac mini (Apple Silicon).
4 # Run as your normal admin user. Requires Homebrew.
5 set -euo pipefail
6
7 if [[ "$(uname)" != "Darwin" ]]; then
8 echo "This script is macOS-only." >&2; exit 1
9 fi
10 if ! command -v brew >/dev/null 2>&1; then
11 echo "Homebrew not found — install from https://brew.sh first." >&2; exit 1
12 fi
13
14 read -rp "Tailnet domain (e.g. tail1234.ts.net), empty to skip TLS/Caddy: " TAILNET
15 read -rsp "Password for the new local 'jenkins' user: " JENKINS_PASS; echo
16 [[ -n "$JENKINS_PASS" ]] || { echo "Password required."; exit 1; }
17
18 SHORT_HOST="$(hostname -s)"
19 FQDN=""
20 [[ -n "$TAILNET" ]] && FQDN="${SHORT_HOST}.${TAILNET}"
21
22 echo "==> Installing packages"
23 brew install temurin@21 git jenkins-lts
24 brew install --cask utm
25 if [[ -n "$TAILNET" ]]; then
26 brew install tailscale caddy
27 fi
28
29 echo "==> Power settings for headless operation"
30 sudo pmset -a sleep 0 disksleep 0 womp 1 autorestart 1
31
32 echo "==> Starting Jenkins as a LaunchDaemon"
33 sudo brew services start jenkins-lts
34
35 if [[ -n "$TAILNET" ]]; then
36 echo "==> Starting Tailscale daemon (run 'sudo tailscale up' after if not authenticated)"
37 sudo brew services start tailscale || true
38 fi
39
40 echo "==> Creating local 'jenkins' user (idempotent)"
41 if ! id jenkins >/dev/null 2>&1; then
42 sudo sysadminctl -addUser jenkins \
43 -fullName "Jenkins Agent" \
44 -password "$JENKINS_PASS" \
45 -home /Users/jenkins \
46 -shell /bin/zsh
47 fi
48
49 echo "==> Generating SSH keypair for the Jenkins agent connection"
50 KEYDIR="$HOME/jenkins-setup"
51 mkdir -p "$KEYDIR" && chmod 700 "$KEYDIR"
52 KEY="$KEYDIR/jenkins_agent_ed25519"
53 [[ -f "$KEY" ]] || ssh-keygen -t ed25519 -f "$KEY" -N '' -C "jenkins-agent"
54
55 echo "==> Authorizing pubkey for jenkins@localhost"
56 sudo install -d -o jenkins -g staff -m 700 /Users/jenkins/.ssh
57 sudo touch /Users/jenkins/.ssh/authorized_keys
58 sudo chmod 600 /Users/jenkins/.ssh/authorized_keys
59 sudo chown jenkins:staff /Users/jenkins/.ssh/authorized_keys
60 PUB="$(cat "$KEY.pub")"
61 if ! sudo grep -qF "$PUB" /Users/jenkins/.ssh/authorized_keys; then
62 echo "$PUB" | sudo tee -a /Users/jenkins/.ssh/authorized_keys >/dev/null
63 fi
64
65 echo "==> Creating agent workspace"
66 sudo install -d -o jenkins -g staff -m 755 /Users/jenkins/agent
67
68 if [[ -n "$TAILNET" ]]; then
69 echo "==> Writing Caddyfile and starting Caddy"
70 CADDY_ETC=/opt/homebrew/etc
71 [[ -d "$CADDY_ETC" ]] || CADDY_ETC=/usr/local/etc
72 sudo tee "$CADDY_ETC/Caddyfile" >/dev/null <<EOF
73 ${FQDN} {
74 reverse_proxy 127.0.0.1:8080
75 }
76 EOF
77 sudo brew services restart caddy || true
78 fi
79
80 cat <<NOTES
81
82 ==============================================================================
83 Automated steps complete. Manual steps remaining:
84
85 1) System Settings -> General -> Sharing -> Remote Login: ON, limit to 'jenkins'.
86
87 2) Log in once as 'jenkins' through the GUI so the login keychain is created.
88
89 3) Open Jenkins:
90 http://localhost:8080
91 ${FQDN:+(or https://$FQDN/ once Tailscale + Caddy are up)}
92 Initial admin password:
93 sudo cat /var/root/.jenkins/secrets/initialAdminPassword
94 Install suggested plugins, then add:
95 SSH Build Agents, Credentials Binding, Matrix Authorization,
96 Keychains and Provisioning Profiles.
97
98 4) Add SSH credential in Jenkins:
99 Manage Jenkins -> Credentials -> System -> Global -> Add Credentials
100 Kind: SSH Username with private key
101 ID: jenkins-agent-ssh
102 User: jenkins
103 Private key: paste contents of
104 $KEY
105
106 5) Add the Mac mini as a node:
107 New Node 'mac-mini', Permanent Agent
108 Remote root: /Users/jenkins/agent
109 Labels: mac apple-silicon
110 Launch: SSH, host=localhost, credential=jenkins-agent-ssh
111 Host Key Verification: Manually trusted key Verification
112
113 6) Provision the Linux VM in UTM:
114 Download Ubuntu Server 24.04 LTS ARM64 ISO.
115 New VM -> Virtualize -> Linux, 2 vCPU / 4 GB / 40 GB, Shared Network.
116 During install: hostname 'linux-agent', user 'jenkins', enable SSH.
117 Mark 'Start VM automatically' in UTM's VM settings.
118 Copy setup-linux-agent.sh into the VM and run:
119 sudo ./setup-linux-agent.sh "$PUB"
120
121 7) Add the Linux VM as a node:
122 New Node 'linux-vm', labels: linux ubuntu-24.04
123 Remote root: /home/jenkins/agent
124 Launch: SSH, host=linux-agent${TAILNET:+.$TAILNET}, credential=jenkins-agent-ssh
125
126 8) Smoke-test pipelines: Jenkinsfile.mac-smoke and Jenkinsfile.linux-smoke.
127
128 9) Reboot the mini and confirm Jenkins, the VM, and both agents come back
129 without manual intervention.
130 ==============================================================================
131 NOTES
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment