Skip to content

Install Mesopod on your Mac

Mesopod runs your apps on a machine you own. The console issues a one-time install command; you paste it in a Mac terminal; the machine calls home. Nothing listens for inbound connections from us, and the command needs no sudo — both Mac targets run as your own user.

This takes a couple of minutes, most of it spent creating a local cluster.

(Running a spare Linux box instead? See the Linux server appendix below — same console flow, different machine. On a Windows 11 PC, the console's Windows tab gives you one PowerShell line: see Windows: one line.)

What you need

Mesopod installs on a Mac, a Windows 11 PC or a Linux machine. Here is where each setup stands:

Machine Status Notes
Mac with Apple silicon, k3d on OrbStack's Docker Tested macOS 14 or later.
Mac with Apple silicon, k3d on Docker Desktop Expected to work, in testing macOS 14 or later.
Mac with Apple silicon, OrbStack's own Kubernetes Tested macOS 14 or later. Apps open on that Mac only: see Mode 2.
Mac with an Intel chip Expected to work, in testing macOS 14 or later.
Windows 11 Pro 24H2, x64 Tested Build 22621 (22H2) or later. The installer sets up WSL itself.
Windows 11 Home, any other version from 22H2 on, or an arm64 PC Expected to work, in testing Build 22621 (22H2) or later.
Windows 10, Windows Server Not supported Windows 10 and Server 2022 are older than build 22621, which the installer needs. On Server 2025, WSL's mirrored networking doesn't work, and the install relies on it.
Ubuntu 22.04 or 24.04, Debian 12 or 13; amd64 or arm64 Expected to work, in testing On a machine of its own or in a VM. It's the same install every Windows PC runs inside WSL2, on Ubuntu 24.04.
Raspberry Pi 4 or 5 with 4 GB or more, on a 64-bit OS Expected to work, in testing Runs the same arm64 build as Apple silicon Macs. Raspberry Pi OS needs memory cgroups turned on first, and k3s recommends an external SSD over the SD card: see its Raspberry Pi notes.
Proxmox VE, in a VM running Ubuntu or Debian Expected to work, in testing The same goes for a VM under any other hypervisor.
Proxmox VE, in an LXC container Not supported k3s runs in LXC only with the container's isolation turned down (privileged, AppArmor unconfined, the host's /dev/kmsg passed in). Use a VM.
Synology, Unraid or TrueNAS, on the NAS's own OS Not supported These are appliances that own their OS. Unraid has neither systemd nor OpenRC, and k3s's installer needs one of them; a TrueNAS update replaces the whole OS image, and anything installed on it with it. Run Ubuntu or Debian in a VM on the NAS instead. To the installer, that's a Linux machine like any other.
32-bit ARM (uname -m says armv7l or armv6l), RISC-V, any other architecture Not supported The installer refuses it. Mesopod runs on amd64 and arm64.

Tested means we've run the install on it end to end. Expected to work, in testing means the installer supports it, nothing we know of stands in the way, and we're working through these runs now. Not supported means we don't support installing there, for the reason in the notes, which also say what to use instead where there's an alternative. Installing onto a Kubernetes cluster you already run needs Kubernetes 1.30 or newer, whatever the machine: see below.

Every machine needs outbound HTTPS: it must reach console.mesopod.cloud:443 and bridge.mesopod.cloud:443. No inbound ports, no port forwarding, no public IP.

On a Mac, install these first (curl is already there) to run Mesopod on a k3d cluster, which the install creates for you:

If you already run OrbStack with its Kubernetes turned on, you can skip k3d: the install can use OrbStack's cluster instead. Apps then open on that Mac only: see Mode 2.

The Mac should be one you leave on and awake. Your apps are only up while it is.

A Windows PC needs nothing installed first; the console's line sets up WSL itself (Windows: one line). A Linux machine has a few requirements of its own, sudo among them, in the Linux appendix.

1. Add the cluster in the console

In the console, open Clusters and use Add cluster. Give it a name you will recognize — daves-mbp, homelab. That name becomes the cluster's slug, and the slug shows up in every app URL, so pick it deliberately.

The console answers with the install command and shows the row as waiting for your server to call home.

2. Run the command on your Mac

Copy the command and run it in a terminal:

curl -fsSL https://console.mesopod.cloud/api/v1/install.sh | MESOPOD_CLAIM=mpc_… sh

The claim (mpc_…) is one-time and expires in 24 hours. Copy it from the console rather than retyping it — a truncated paste fails with unknown claim.

The claim rides an environment variable, not the command line, because command lines are readable by every user on the machine while the install runs.

This page writes the production names throughout. If you are testing against a non-production Mesopod, take the origin from your own install command (console.staging.mesopod.cloud, say) and substitute it everywhere below — including the apps domain, which on staging is staging.mesopod-internal.app rather than mesopod.app, so the rebind exception and the dig checks use that name instead.

The prompt

If you didn't pass a mode explicitly, the script asks on the terminal:

Where should Mesopod run?
  1) a local k3d cluster (this script creates it)
  2) OrbStack's built-in Kubernetes
Choice [k3d]:

The bracketed default names the mode it will take, and is autodetected: if a kubectl context named orbstack already exists (you've turned on OrbStack Kubernetes), the prompt reads Choice [orbstack]:; otherwise Choice [k3d]:. Press Enter to accept it, or answer 1 for k3d / 2 for OrbStack (the words k3d and orbstack work too).

You are asked this once. The mode you land on is saved next to the token (see State on your Mac), and a later re-run resumes in it rather than asking again — so turning OrbStack's Kubernetes on afterwards cannot quietly move your install. Passing --k3d/--orbstack on a re-run still overrides, and the script says so when it does.

To skip the question — for a script, or because you already know which you want — pass a flag or set an env var ahead of time:

curl -fsSL https://console.mesopod.cloud/api/v1/install.sh | MESOPOD_CLAIM=mpc_… sh -s -- --k3d
curl -fsSL https://console.mesopod.cloud/api/v1/install.sh | MESOPOD_CLAIM=mpc_… sh -s -- --orbstack

or MESOPOD_INSTALL_MODE=k3d / MESOPOD_INSTALL_MODE=orbstack in the environment alongside MESOPOD_CLAIM.

Mode 1: k3d (a local cluster the script creates)

Needs docker (OrbStack or Docker Desktop) and k3d (brew install k3d) on PATH. The script looks for both before it exchanges your claim, so if either is missing it says exactly that and stops with the claim unspent: install the missing piece and paste the same command again.

The script creates a cluster named mesopod-<your-slug>, publishing ports 80 and 443 from the cluster onto the Mac (so LAN devices can reach your apps at the Mac's address — whether DNS actually points there is a separate concern, out of scope here). Only one cluster can hold those ports, so if something else already has them — usually a k3d cluster from an earlier install — the script says so before it spends your claim and asks whether to quit or install without publishing. --no-port-publish answers that ahead of time, which is how you run more than one k3d cluster on one Mac; apps then answer only at the cluster node's own address rather than the Mac's. See troubleshooting.md.

The cluster is created with your Mac's /Users and /Volumes mounted into the node at the same paths. A k3d node is a container, and a Mac path exists inside it only if it was mounted when the cluster was created, so this is what lets you register host directories (a Plex library under /Users/you/…, a drive under /Volumes/…) later without touching the cluster again. The mount is wide on purpose and the roots are narrow: the node can see your whole home directory, but apps only reach the directories you register as roots, and the outpost cannot mount anything else (see the host directory design's §4). It adds no reach the node did not have: the node runs privileged, and the Docker VM already exposes the whole Mac filesystem to it, so anything running at node level could read your disk with or without this mount. On Linux nothing is mounted, since the node is the host. A cluster created by an earlier version of this script has no such mounts; a re-run of the install command adds them to the running node in place, with nothing recreated or restarted (verified on OrbStack). See troubleshooting.md if every directory you register fails with doesn't resolve on the server.

Re-running is idempotent: an existing mesopod-<your-slug> cluster is reused, not recreated.

The resulting kubectl context is k3d-mesopod-<your-slug>.

Mode 2: OrbStack's built-in Kubernetes

Needs OrbStack's Kubernetes turned on: OrbStack app → Settings → Kubernetes. The script looks for a kubectl context literally named orbstack; if it isn't there, the script says so and stops before spending your claim.

This mode installs into OrbStack's own cluster — there's nothing to create. The resulting kubectl context is orbstack.

LAN reachability works differently here: OrbStack routes its Kubernetes LoadBalancer addresses to the Mac itself only, so other devices on your network reaching an app through this mode is out of scope for now.

When the script finishes

It prints:

+ outpost installed and dialling home
> return to the console — the cluster row walks: waiting for your server -> setting up -> ready

Go back to the console. The row moves to Setting up on its own — Mesopod installs the routing pieces the first time an outpost connects — and then reads Provisioned. That usually takes under a minute. Once it does, you can deploy to it: first-deploy.md.

What the script actually does

In order, so you know what you pasted:

  1. Reads the machine's architecture — amd64 or arm64 (Apple Silicon Macs report arm64); anything else stops here with a message.
  2. Resolves the mode — flag, env var, the mode a previous run saved, or the prompt above.
  3. Exchanges the claim and checks the Kubernetes version floor, in an order that depends on the mode, since each burns the claim only once the thing it needs to check first actually exists:

    • k3d checks that docker and k3d are on PATH (looking for a command spends nothing), then exchanges the claim — its response names the slug the cluster gets created with — then creates (or reuses) the cluster, then checks the version floor now that a cluster exists to check.
    • OrbStack confirms the orbstack context answers, checks the version floor on it, then exchanges the claim (recording orbstack as this cluster's kind).

    Either way, the claim is exchanged before anything that could fail for reasons outside the script's control, and the token is written to $HOME/.mesopod/outpost-token, mode 0600, alongside $HOME/.mesopod/cluster-slug and $HOME/.mesopod/install-mode, before any further step. That ordering is the point: if a later step fails, the claim is already banked as a durable token, and re-running the script picks up from there (see Re-running below). The version floor itself — Kubernetes 1.30, because the admission policies that fence the Mesopod outpost in need it to exist — should never actually bite on a Mac; both k3d and OrbStack ship well above it. 4. Downloads this cluster's manifests from the platform, authenticated with that token. 5. Downloads the outpost image as a tarball and loads it straight into the cluster's container runtime — docker exec … ctr images import for k3d, docker load for OrbStack (it shares the Mac's docker image store). Mesopod pulls from no registry: your Mac never needs a registry credential, and the image never has to be public. 6. Applies the manifests and waits up to three minutes for the outpost to roll out. On failure it prints the pod state before exiting — see troubleshooting.md.

The outpost — Mesopod's piece on your machine — runs as an ordinary in-cluster Deployment (namespace mesopod-system), with a deliberately small, fenced Kubernetes identity. It is not a host daemon and it never holds your cluster's admin credentials. Design detail: docs/features/byo-install-path/design.md, docs/features/macos-install-modes/design.md.

State on your Mac

Both modes keep their state under your home directory, unprivileged:

File What it holds
$HOME/.mesopod/outpost-token (0600) This cluster's outpost token. Its presence is what lets a re-run resume without a fresh claim.
$HOME/.mesopod/cluster-slug The slug the platform assigned. k3d mode reads this on a resume to know which cluster to reuse (mesopod-<slug>).
$HOME/.mesopod/install-mode Which mode this Mac installed in — k3d or orbstack. A re-run resumes in it instead of asking again.

--token-file PATH overrides the location (useful for dev/test setups running more than one instance side by side); the slug and mode files then live next to it.

Installing onto a cluster you already run

If you already have a Kubernetes context (1.30+) you'd rather use directly — your own cluster, a cluster in a Linux VM, anything kubectl can already reach — skip the mode question and point the script at it. Save the script first rather than piping it, so you can pass flags:

curl -fsSL https://console.mesopod.cloud/api/v1/install.sh -o install.sh
chmod +x install.sh
MESOPOD_CLAIM=mpc_… ./install.sh \
  --skip-k3s \
  --kube-context my-cluster \
  --token-file ~/.mesopod/outpost-token

This path needs no root — only a kubeconfig that can create the objects, which includes cluster-scoped roles and admission policies. Passing --kube-context on a Mac also skips the interactive prompt: you've already said where Mesopod goes. Don't combine it with --k3d/--orbstack (or a leftover MESOPOD_INSTALL_MODE in your environment) — those name a cluster to find or create, --kube-context names one that exists, and the script refuses the two together rather than guessing which you meant.

All flags, optional:

Flag What it does
--claim mpc_… Same as MESOPOD_CLAIM. Convenient for scripts; visible in ps.
--base https://… Override the console origin the script talks to.
--k3d Run Mesopod on a local k3d cluster this script creates.
--orbstack Run Mesopod on OrbStack's built-in Kubernetes.
--skip-k3s Don't install or expect k3s (irrelevant on macOS; k3s doesn't run there).
--skip-image Don't download or load the image (it is already present).
--kube-context CTX Use kubectl --context CTX instead of the mode's own cluster/context.
--import-cmd CMD Feed the image tarball to your own import command on stdin.
--token-file PATH Where the token is stored (default $HOME/.mesopod/outpost-token; /etc/mesopod/outpost-token on the Linux k3s path).
--help, -h Print this flag list on the machine and exit.

Re-running the install

Re-running is safe and is the normal fix for anything that failed midway:

curl -fsSL https://console.mesopod.cloud/api/v1/install.sh | sh

With no claim, the script resumes from the saved token (and, for k3d, the saved slug) and redoes the rest — manifests, image, apply. You only need a new claim if the token was never written (the failure happened before the claim exchange in step 3) or you revoked it. Generate one in the console with Generate new install command; doing so retires the old one immediately.

If you installed onto a cluster you already run, or with an explicit mode flag, re-run the saved script with the same flags you used the first time — --skip-k3s, --kube-context, --k3d/--orbstack, --token-file — or it will ask the mode question again or look for state where you didn't put it.

Make your app names resolve at home

Each cluster gets one wildcard DNS record, *.<cluster-slug>.mesopod.app, pointing at the server's own IP address — usually a private one like 192.168.1.40. Certificates are real and public; only the address is local. (This applies to the k3d mode's published ports; OrbStack mode's LoadBalancer address is reachable from the Mac itself only, per the OrbStack section above.)

Many home routers refuse to pass through a public name that answers with a private address. The feature is called DNS rebind protection, and the app will simply not resolve until you make an exception for mesopod.app. Where to look:

  • OpenWrt (dnsmasq): Network → DHCP and DNS → Rebind protection → add mesopod.app to Domain whitelist.
  • FRITZ!Box: Home Network → Network → Network Settings → DNS Rebind Protection → add mesopod.app under Hostname exceptions.
  • pfSense / OPNsense: Services → DNS Resolver → Custom options → private-domain: "mesopod.app", or turn off DNS Rebind Check under System → Advanced → Networking.
  • Pi-hole (also dnsmasq): add rebind-domain-ok=/mesopod.app/ to a file under /etc/dnsmasq.d/, then restart the DNS service.

If your router offers no exception, point the affected devices at a resolver that doesn't rebind-protect, or add hosts entries as a stopgap.

Away from home, see remote-access-tailscale.md.

Removing Mesopod

Uninstall your apps in the console first — that removes their workloads cleanly and keeps their data.

k3d mode — deleting the cluster removes everything:

k3d cluster delete mesopod-<your-slug>
rm -f "$HOME/.mesopod/outpost-token" "$HOME/.mesopod/cluster-slug" \
      "$HOME/.mesopod/install-mode"

OrbStack mode, or any cluster you brought yourself (--kube-context) — removing the cluster from the console runs the outpost's decommission_cluster command, which deletes everything in mesopod-system, the cluster-scoped GatewayClass, and the k3s HelmChartConfig. What remains is cluster-scoped and the outpost has no permission to delete it (ADR-027) — so the list, deploy/in-cluster-outpost/manifests.yaml, and render.ClusterDecommission change together. Use --context orbstack for OrbStack mode, or the context you passed to --kube-context otherwise:

# The outpost itself lives in mesopod-system — already gone if you removed
# the cluster from the console, since decommission_cluster deletes it. Do it
# first here, so the outpost is not still running while its permissions go.
kubectl --context orbstack delete namespace mesopod-system
kubectl --context orbstack delete clusterrolebinding mesopod-outpost
kubectl --context orbstack delete clusterrole mesopod-outpost-cluster mesopod-outpost-runtime mesopod-outpost-helm
kubectl --context orbstack -n kube-system delete rolebinding mesopod-outpost-helm
kubectl --context orbstack delete validatingadmissionpolicybinding \
  mesopod-outpost-rolebinding-cage-binding mesopod-outpost-namespace-cage-binding \
  mesopod-outpost-workload-cage-binding mesopod-outpost-volume-cage-binding \
  mesopod-outpost-pv-create-cage-binding mesopod-outpost-pv-roots-cage-binding
kubectl --context orbstack delete validatingadmissionpolicy \
  mesopod-outpost-rolebinding-cage mesopod-outpost-namespace-cage \
  mesopod-outpost-workload-cage mesopod-outpost-volume-cage \
  mesopod-outpost-pv-create-cage mesopod-outpost-pv-roots-cage
kubectl --context orbstack delete crd outpostnodedirectoryroots.mesopod.io
# OrbStack only — Traefik's ClusterRole/ClusterRoleBinding, applied at
# install time because the caged outpost has no verb to author them:
kubectl --context orbstack delete clusterrolebinding mesopod-traefik
kubectl --context orbstack delete clusterrole mesopod-traefik
rm -f "$HOME/.mesopod/outpost-token" "$HOME/.mesopod/cluster-slug" \
      "$HOME/.mesopod/install-mode"

If you force-removed the cluster while its node was offline, mesopod-system and your app namespaces are still there; delete mesopod-system first — a new Mesopod cluster refuses to provision over another cluster's ownership marker (ADR-025). A force never ran decommission_cluster, so nothing was deleted for you: run the whole operator list in the block above as well, plus the two objects decommission_cluster would otherwise have deleted itself — the cluster-scoped GatewayClass named mesopod-traefik, and, on k3s only, the HelmChartConfig named traefik in kube-system (k3s's own Traefik chart owns that object; OrbStack's Traefik profile has no HelmChartConfig at all — it's a plain Service/Deployment/ServiceAccount inside mesopod-system, so deleting that namespace above already took it):

kubectl --context orbstack delete namespace mesopod-system
kubectl --context orbstack delete gatewayclass mesopod-traefik
# k3s only — OrbStack's Traefik profile has no HelmChartConfig to delete:
kubectl --context orbstack -n kube-system delete helmchartconfig traefik

App namespaces and the data under them are left alone. Remove them yourself if you want the cluster clean.

Next


Appendix: Installing on a Linux server

Everything above is Mac-first, because that's the alpha audience. Mesopod still installs the same way onto a spare Linux machine or VM (amd64 or arm64, with systemd) — this is the original path, and it's unchanged except that the console's command shown to you will already be for the mode you picked when adding the cluster.

What you need

The table at the top of this page gives the support status of each distribution, board and hypervisor. Beyond that, the Linux path needs:

  • A Linux machine or VM — amd64 or arm64, with systemd. 2 CPU / 4 GB RAM is a comfortable floor for a handful of apps. On a distribution the table doesn't name, k3s's requirements list what some need first.
  • The ability to run sudo on that machine (the install brings up k3s and writes /etc/mesopod/, both of which need root). If you are logged in as root, sudo is simply a no-op.
  • curl installed.
  • Outbound HTTPS. The machine must reach console.mesopod.cloud:443 and bridge.mesopod.cloud:443 — plus get.k3s.io during the install itself, since it is installing k3s. No inbound ports, no port forwarding, no public IP.
  • Kubernetes 1.30 or newer — only if you are installing onto a cluster you already run. Otherwise the script installs k3s for you and the version is already right.

The machine should be one you leave on. Your apps are only up while it is.

Run the command on the server

The Mac one-liner needs no sudo; the Linux k3s path still needs root, and the script knows it. Run the command the console gives you:

curl -fsSL https://console.mesopod.cloud/api/v1/install.sh | MESOPOD_CLAIM=mpc_… sh

If you aren't root, the script stops immediately and prints the exact sudo-prefixed line to re-run — it holds your claim already, so it can echo the full command back to you:

curl -fsSL https://console.mesopod.cloud/api/v1/install.sh | sudo MESOPOD_CLAIM=mpc_… sh

sudo prompts for your password on the terminal, not on the pipe, so the prompt appears normally.

What the script does on Linux

  1. Reads the machine's architecture — amd64 or arm64.
  2. Installs k3s from get.k3s.io if it isn't already there, pinned to the v1.33 channel, and waits for the node to be Ready. This is the only thing the install fetches from outside Mesopod. Set MESOPOD_K3S_CHANNEL to pin a different line.
  3. Checks the Kubernetes version — below 1.30 it stops (see the Mac section above for why), and it checks before spending your one-time claim.
  4. Exchanges the claim for this cluster's token and writes it to /etc/mesopod/outpost-token, mode 0600, before touching anything else.
  5. Downloads this cluster's manifests, authenticated with that token.
  6. Downloads the outpost image as a tarball and loads it straight into the machine's container runtime via k3s ctr images import.
  7. Applies the manifests and waits up to three minutes for the outpost to roll out.

Windows: one line

On a Windows 11 PC (22H2 or newer, x64 or arm64) the console's Windows tab shows a PowerShell line instead of the curl one:

$env:MESOPOD_CLAIM='mpc_…'; irm https://console.mesopod.cloud/api/v1/install.ps1 | iex

Paste it in an ordinary PowerShell window — not an administrator one, and not inside a Linux shell. Windows asks for permission once; the rest runs in the window that opens and ends with the same "return to the console" line as the Mac path. What it does, in order, each step skipped when already done:

  1. WSL. Installed if the PC has none — the script then says to restart Windows and paste the same line again; nothing has been spent and the command is still valid. Updated if older than 2.4.4.
  2. %UserProfile%\.wslconfig gains networkingMode=mirrored (other devices can reach the distro at the PC's address) and hostAddressLoopback=true (so can the PC itself). Every other line in the file is kept.
  3. A distro of its own, named mesopod (Ubuntu 24.04), set up as a server: systemd on, root as its user, no first-run wizard. An Ubuntu you already have is not touched.
  4. The Hyper-V firewall opens 80 and 443 to the WSL VM — one VM for every distro on the PC, so to any other distro's listener too — and allows host loopback.
  5. The Linux install runs inside the distro: the same script as above, with its WSL2 checks, the port hold, k3s, the claim exchange and the outpost. It prints the address the console should show as the cluster's DNS target — under mirrored networking, the PC's own.
  6. A startup task, "Mesopod WSL", keeps the distro running across reboots without storing your password. If Task Scheduler cannot start WSL that way on your PC, the script asks for the password and stores it there instead.

Re-running the same line repairs a half-finished install; with no claim it resumes from the token inside the distro. If the console shows the cluster offline after your first restart, run the saved copy with the password option:

& "$env:LOCALAPPDATA\Mesopod\install.ps1" -StartupTaskPassword

If you would rather set the distro up by hand, every step above is one the Linux path's WSL2 checks will name as it meets it — and the script inside the distro needs sudo, a WSL distro with systemd and mirrored networking on, and something holding ports 80/443, which it provides itself.

Storage (WSL's own disk versus Windows drives), VPN clients and remote access are their own guide: Install Mesopod on Windows (WSL2).

Re-running on Linux

curl -fsSL https://console.mesopod.cloud/api/v1/install.sh | sudo sh

With no claim, the script resumes from the saved token at /etc/mesopod/outpost-token.

Registering a host directory on Linux

On a server the script set up, the console shows the host directory registration command with sudo, for the same reason the install needed it: k3s keeps its kubeconfig readable by root only. Copy it from the console, which fills in the cluster and every registered directory, and run it on the server:

curl -fsSL https://console.mesopod.cloud/api/v1/register-roots.sh | sudo bash -s -- --cluster-id … --root …

Run without sudo, the script stops and prints the line to re-run. If you installed onto a cluster you already run, with your own kubeconfig, leave sudo out.

Removing Mesopod from a Linux server

On a machine where the script installed k3s, uninstalling k3s removes everything:

sudo /usr/local/bin/k3s-uninstall.sh
sudo rm -f /etc/mesopod/outpost-token

On Windows, after removing the cluster in the console, the saved installer undoes everything it set up — the startup task, the firewall rule, the mesopod distro and its own state — and leaves .wslconfig alone, since mirrored networking is a machine-wide setting you may want to keep:

& "$env:LOCALAPPDATA\Mesopod\install.ps1" -Uninstall