Created
April 29, 2026 17:53
-
-
Save sayrer/c8db94534e00a8519727fc9ad528a359 to your computer and use it in GitHub Desktop.
tell Claude to set up Jenkins on Mac and Linux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ❯ 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