Skip to content

Self-hosting

DistroVersionsContainer runtime
Ubuntu20.04+Docker (auto-installed) or Podman
Debian11+Docker (auto-installed) or Podman
Fedora38+Docker or Podman (auto-installed)
RHEL / Rocky / AlmaLinux8+Docker or Podman (auto-installed)
CentOS Stream9+Docker or Podman (auto-installed)
openSUSE Leap / TumbleweedlatestDocker or Podman (auto-installed)
Arch LinuxrollingDocker or Podman (auto-installed)

Requirements: systemd, x86_64 or arm64, kernel ≥ 5.4. Alpine and non-systemd distros are not supported.

  • A supported Linux distro (see above)
  • At least 5 GB free disk space (images + k3s + data)
  • A domain you control, with DNS records pointing at this server. Which records depends on the DNS mode; see DNS setup
  • Ports 80 and 443 open in your firewall and not in use by other services on the host, plus 53 (TCP+UDP) in the default NS-delegation mode. Port 53 conflicts with systemd-resolved on Ubuntu 22.04+; the installer will warn you. UDP 41641 is optional: it lets nodes reach the gateway directly instead of through a relay
  • A host or provider firewall keeping everything else closed. The API (4000), the built-in registry (5000), the k3s API (6443), node_exporter (9100) and the kubelet (10250) listen on every interface so the mesh can reach them, and the registry has no authentication of its own. The dashboard warns you when the installer found no host firewall
  • Root / sudo access

Two ways to do the same install. Run it yourself, or hand it to an agent - the agent runs the same script and stops at the same questions.

Terminal window
sudo bash -c "$(curl -fsSL https://meshploy.com/install.sh)"

The script installs Docker (if needed), downloads Meshploy to /opt/meshploy, walks you through an interactive setup (domain, IP, secrets), and starts the full stack. Select Master for the gateway node or Worker to join an existing mesh.

Before the first question, the installer offers to continue in a browser instead. Say yes and it prints a setup token and an address on port 9000, where the same setup runs as a web page that checks your DNS as you go and streams the install. Port 9000 has to be reachable from wherever your browser is.

If you work through Claude Code, Codex or a similar agent with a terminal, give it the prompt below instead of running the command yourself. It carries no secret, so it is safe to copy anywhere.

The agent stops where the decisions are yours: it will not choose your domain, and it does not need your DNS provider’s password.

# Install Meshploy on my server
Set up a Meshploy gateway on a Linux server I own, and leave me at the setup
page with the install running.
## Work out how to reach the machine
119 collapsed lines
Look before you ask. Read my SSH config for hosts I have already set up:
```
grep -iE '^host ' ~/.ssh/config 2>/dev/null
```
Then ask me one question, not four:
- If that listed candidates, show them and ask which one this is - or whether
it is a machine not in there.
- If it listed nothing, or I say it is a new machine, ask for the address. The
user is **root** unless I say otherwise; only ask about the port and key if
the defaults do not work.
Confirm it works **without anything being typed**, and stop if it does not:
```
ssh -o BatchMode=yes -o ConnectTimeout=8 <host> 'hostname; id -u'
```
`BatchMode=yes` is not optional. Without it, a server that wants a password
leaves this command waiting for input you cannot give, and it looks to me like
you are still working.
If it fails, do **not** ask me for a password, and do **not** use `sshpass`:
- **SSH wants a password.** I run `ssh-copy-id <host>` once, and you carry on.
- **It cannot connect at all.** Give me the error; the address or the port is
probably wrong.
If it answered but `id -u` was not `0`, you are not root. Check whether that
account can become root without typing anything:
```
ssh -o BatchMode=yes <host> 'sudo -n true' && echo "sudo ok"
```
If that fails, stop and tell me - I will either give you a root login or run
the install myself and tell you when the setup page is up.
Use whatever I gave you as `<host>` from here on - an alias from my config if
that is what it was, and `user@address` if not.
## Check the machine is one we can use
```
ssh <host> 'cat /etc/os-release | head -2; systemctl --version | head -1; df -h / | tail -1; ls /opt/meshploy 2>/dev/null'
```
- Ubuntu 20.04+, Debian 11+, Fedora 38+, RHEL/Rocky/Alma 8+, CentOS Stream 9+,
openSUSE, or Arch. **Alpine and anything without systemd will not work** -
stop and tell me.
- At least 5 GB free on `/`.
- If `/opt/meshploy` already exists, this server already has Meshploy. Stop and
ask me whether I want to reinstall or upgrade instead.
Also check nothing already holds the ports Meshploy needs:
```
ssh <host> 'ss -Hltnp "sport = :80" ; ss -Hltnp "sport = :443" ; ss -Hltnp "sport = :53"'
```
If something does, tell me what it is and stop. Do not stop or remove it.
## Get my approval
Show me this and wait for a yes:
```
Server: <host>
Installs: Docker (if missing), k3s, Headscale, CoreDNS, Caddy, Meshploy
Opens: 80, 443, and 53 unless I choose self-managed DNS
Needs: root (sudo), a domain I control, and about 10 minutes
```
Ask me which domain I will use, and tell me I will need to add DNS records for
it partway through. Do not pick a domain for me.
## Run it
```
ssh -t <host> 'bash -c "$(curl -fsSL https://meshploy.com/install.sh)"'
```
As root that is all it needs. On a non-root login that could become root above,
put `sudo` in front of `bash`.
When it offers to continue in a browser, say **yes**. It prints a setup token
and an address on port 9000.
## Open the setup page for me
Port 9000 is usually closed to the internet, so tunnel it rather than asking me
to open a firewall:
```
ssh -N -L 9000:127.0.0.1:9000 <host>
```
Leave that running, open `http://localhost:9000` in my browser, and give me the
setup token. Then stop and let me take over: the domain, the DNS records and
the owner account are mine to enter.
## If it fails
- Port 53 is taken on Ubuntu: that is `systemd-resolved`. Tell me, and mention
that `--dns-mode=ondemand` avoids needing port 53 at all. Do not disable it
yourself.
- Unsupported distro, too little disk, or Meshploy already installed: stop and
tell me. Do not work around it.
- Anything else: show me the output and stop. Do not re-run the installer.
## Rules
- Do not choose or register a domain for me.
- Do not change firewall rules, DNS records, or anything else on the server.
- Do not read or print my environment files.
Docs: https://docs.meshploy.com/self-hosting/

Adding a worker later needs no prompt of this size: the console’s Cluster → Add a node gives you a one-line command carrying a single-use token, and a matching prompt beside it.

Pick a domain for Meshploy, ideally a dedicated subdomain such as meshploy.example.com. The console, the API and every app you deploy get hostnames under it. There are two ways to point it at the server; the installer asks which one when it cannot see a delegation already in place.

NS delegation (default). The gateway runs its own authoritative DNS (CoreDNS) for the domain, which lets it obtain one wildcard certificate. Add two records where the parent zone is hosted (for meshploy.example.com, that is example.com), the A record first:

ns1.meshploy.example.com A <gateway-public-ip>
meshploy.example.com NS ns1.meshploy.example.com

An NS record names a nameserver rather than an address, which is why the ns1 A record has to exist for the delegation to resolve. Verify once it has propagated:

Terminal window
dig @<gateway-public-ip> console.meshploy.example.com A

Self-managed DNS (--dns-mode=ondemand). For providers that cannot delegate a subdomain (Hostinger, for example). Keep DNS with your provider and add two A records; Caddy then issues a certificate per hostname the first time each one is requested:

*.meshploy.example.com A <gateway-public-ip>
meshploy.example.com A <gateway-public-ip>

Some providers call the second one the root or @ record. Verify:

Terminal window
dig +short console.meshploy.example.com A

Re-running the installer keeps whichever mode the server was installed with.

When the install finishes, open https://console.<your-domain> and register. The first account owns the instance, so the form asks for the one-time setup token the installer printed. If you no longer have it, print it again on the gateway:

Terminal window
sudo meshploy setup-token show

The token is only accepted until that first account exists. If someone else may have seen it before then, issue a new one with sudo meshploy setup-token rotate and restart the API (cd /opt/meshploy && docker compose up -d api).

Installing, reinstalling and removing all go through the install script:

CommandWhat it does
sudo bash -c "$(curl -fsSL URL)"Fresh install
sudo bash -c "$(curl -fsSL URL)" _ --reinstallUpdate images and config, preserve database and TLS certs
sudo bash -c "$(curl -fsSL URL)" _ --reinstall --wipe-dataFull reinstall from scratch, wipes database and TLS cert cache
sudo bash -c "$(curl -fsSL URL)" _ --uninstallRemove Meshploy (interactive)
sudo bash -c "$(curl -fsSL URL)" _ --cli-onlyInstall or update the meshploy CLI binary only — safe on existing nodes
sudo bash -c "$(curl -fsSL URL)" _ --dns-mode=ondemandInstall without NS delegation — you add a wildcard A record and Caddy issues a certificate per hostname
sudo bash -c "$(curl -fsSL URL)" _ --edgeInstall edge builds from main instead of the latest stable release

Replace URL with https://meshploy.com/install.sh

Release channel: by default the installer tracks the latest stable release: :latest images and the deploy/ config from the newest release tag. Pass --edge to track main instead, which pulls :main images and branch config. Edge carries work that has not been released yet, so prefer stable unless you need something that has just landed. The choice is not remembered: a later re-run or sudo meshploy server-upgrade without --edge writes MESHPLOY_CHANNEL=latest and moves the install back to stable, so pass --edge every time if you mean to stay on it. Upgrades from the console keep the channel the server is on. To change it there, the server’s owner uses Settings → Server, which shows both channels and where the server sits on them. The console only switches forward: edge to stable becomes available once a release includes the build the server runs, so it never installs older code.

TLS cert cache: Caddy stores Let’s Encrypt certificates in a Docker volume. --reinstall always preserves this volume to avoid hitting rate limits (5 certs per domain per week). Use --wipe-data only when you genuinely need a clean slate.

Upgrading: sudo meshploy server-upgrade on the gateway downloads the configuration and images for the latest release, then installs them and restarts the services, keeping your data. If the services do not come back, it puts the previous version back. The server’s owner can also upgrade from the console: Update available in the sidebar opens an Upgrade now button. New installs are set up for that; on an older server, run sudo meshploy updater start once after upgrading. Add --edge to upgrade to main. Run sudo meshploy update first, so the upgrade runs with the latest CLI. See the CLI reference.


Community is complete and stays free: everything above is Apache-2.0, with one organization per server. Enterprise adds organizational features under a licence (the console’s Settings → Licence → Compare editions lists them) and runs as a separate pair of private images, ghcr.io/meshploy/api-ee and ghcr.io/meshploy/web-ee, built from the same source and version as each Community release.

Moving a server to Enterprise keeps its data and settings:

  1. Get a licence at meshploy.com/enterprise. It is bound to your domain and names the image it grants.
  2. Activate it on the Community server: an admin pastes it into Settings → Licence, or runs meshploy license activate <token>. It is verified and stored, and grants nothing until the switch, since Community has no Enterprise features built in.
  3. Give the gateway registry access once, as root, with a GitHub token that can read packages, for the account the licence was granted to: echo <token> | sudo docker login ghcr.io -u <github-user> --password-stdin (or podman). Credentials never pass through the console.
  4. Switch. The server’s owner presses Switch to Enterprise in the licence section, which runs through the same updater as an upgrade, or runs sudo meshploy server-upgrade --ee on the gateway. Either points the API and the console at the Enterprise images and restarts them; if the images cannot be pulled, nothing changes.

Upgrades then work as before, on either channel. The Enterprise images for a release appear a few minutes after it, and an upgrade started before they do stops without changing anything. meshploy license status shows the edition and the licence.

To go back to Community, remove MESHPLOY_API_IMAGE and MESHPLOY_WEB_IMAGE from /opt/meshploy/.env and run sudo meshploy server-upgrade. The licence and any Enterprise data stay in the database, unused.


  • Go 1.25+
  • Node.js 20+
  • PostgreSQL 15+
  • Docker
Terminal window
git clone https://github.com/meshploy/meshploy
cd meshploy
cp .env.example .env

Edit .env:

Terminal window
DATABASE_URL=postgres://user:password@localhost:5432/meshploy?sslmode=disable
JWT_SECRET=your-long-random-secret
ENCRYPTION_KEY=0123456789abcdef0123456789abcdef # exactly 32 characters: openssl rand -hex 16
Terminal window
docker compose -f deploy/docker-compose.dev.yml up -d

This starts only PostgreSQL on port 5432. Headscale, CoreDNS, Caddy, and the registry are not needed for local development — the API and frontend work without them.

Terminal window
cd apps/api && go run main.go

API at http://localhost:4000. The OpenAPI spec is at /openapi.json; it needs a bearer token, like every non-public route.

Terminal window
cd apps/proxy && go run main.go
Terminal window
cd apps/web && npm install && npm run dev

Dashboard at http://localhost:5173