# ZaneOps: the whole installation is a remote script piped into a root shell, and setup turns your Docker daemon into a swarm

> ZaneOps is a self-hosted deployment platform for web apps, databases, static sites and long running services, positioned against commercial platform-as-a-service providers. The readme is short and mostly screenshots. The substance is in the build files, where you can see that setup rewrites the host's container runtime into a cluster, installs a package manager into the system Python, and points development webhooks at a public receiver.

**zane-ops/zane-ops** — A beautiful and fast self-hosted PaaS for deploying and managing web apps, databases, static websites and more.

- Repository: https://github.com/zane-ops/zane-ops
- Website: https://zaneops.dev
- Stars: 1,388 · Forks: 71
- Language: Python
- License: NOASSERTION
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/zane-ops-zane-ops

## The entire installation is a remote script piped into a root shell

The installation section is one command and one note:

```shell
curl -fsSL https://cdn.zaneops.dev/install.sh | sudo bash
```

That is the whole documented install. A script is fetched from the project's content delivery network, written straight into an interpreter running as root, with no checksum, no signature, no local copy and no chance to read it first. The note underneath handles failure rather than the trust question, pointing at a documentation page with step by step setup for anyone who hits a problem. The one thing a reader can do is fetch the script, read it, and then run it, and the note's existence suggests the authors expect people to arrive with questions. What is worth naming plainly is the default: for a platform whose whole purpose is to run code for you, the first thing it asks for is the ability to run anything as root on the host.

## Setup rewrites the container runtime into a swarm cluster

The project's own setup target makes a global change before it makes any project change, and it is explicit about the requirement. It asks Docker whether swarm mode is active. If it is not, it initialises it. If it is active, it then checks whether the current node is a manager, and if not it prints an error saying the platform needs to be installed on a manager node, tells you how to promote the node, tells you how to list node names, and exits:

```
docker swarm init; \
```

That is a coherent choice for a platform built on Swarm, and a consequential one for a host that runs anything else in Docker, because swarm mode is a property of the daemon, not of one application. Turning it on changes the behaviour of every container on the machine, and nothing in the visible documentation says how to turn it back off. The same target then creates a network if one does not already exist:

```
docker network create --attachable --driver overlay --label zane.stack=true zane; \
```

an attachable overlay network carrying a project label, which the platform then owns.

## Setup installs a package manager into the system Python before the project environment exists

Two more lines in the same target are worth reading before you run it. The first installs a Python package manager with an unpinned pip call, at system level:

```
python3 -m pip install uv
```

The second changes into the backend directory and syncs that project's environment with a locked lockfile, which is the part that matters: the project itself is reproducible, but the tool used to build it is whatever the resolver picks at that moment, and it goes into your system Python rather than an isolated one. After that the target makes the backend's shell scripts world readable and executable, and installs the JavaScript workspace with a frozen lockfile. The ordering is deliberate, since the environment manager has to exist before the environment can be created, but it means the one unpinned, unisolated action in the setup is the one that happens first.

## Every copy of the frontend bundler is overridden to a different package

The JavaScript workspace manifest ends with an overrides block, and it has one entry:

```
"vite": "npm:rolldown-vite@6.3.21"
```

That is not a version pin, it is a substitution. Every dependency in the tree that asks for the bundler named Vite receives a different package published under another name, at an exact version, for the lifetime of the workspace. The reason is performance, and the substitution is a recognised strategy in this ecosystem, but the practical consequence is that the bundler your frontend actually builds with is not the one its dependencies asked for and not the one its documentation describes. Anyone debugging a build, reading a plugin's compatibility note, or upgrading will be reasoning about a package that is not in their dependency graph by name. The manifest also pins the package manager to a specific older major, which is the other half of a build that is deliberately not the default one.

## Development sends webhook payloads to a public receiver

There are two root level shell scripts and one of them changes where your traffic goes. The development scripts do this:

```
WH_TOKEN="Go to https://webhook.site to get one"
```

The pre-development step makes that script executable and the development step runs it, and the variable is a token for a public webhook receiver: a service you sign up for that gives you a URL and shows you everything posted to it. So in the default development configuration, webhook deliveries from your local stack are posted to a third party's public page while you work. That is a normal thing to do when you are integrating payments or third party callbacks and need a reachable URL, and it is a poor default for a platform that manages production databases and services, because webhook payloads routinely carry identifiers and payload details. The token lives in the environment file that is excluded from version control, which is the right place for it, and there is no comment saying what the catcher forwards or where it stores what it receives.

## A script named reset-db.sh sits at the top level

The root listing is worth scanning for what a developer has at hand. There is a script whose name is unambiguous about what it does, one that starts a background service, and one with a single word for a name, which is either a progress indicator or a placeholder and is not explained anywhere in the visible documentation. There is also a build file named for the frontend rather than for the project, sitting beside a directory of container files, which tells you the backend image is built somewhere else entirely, and a compose-style YAML file at the root with a name that appears nowhere in the readme. None of this is alarming on its own. What is worth noting is that a repository running a production deployment platform keeps a database reset script one tab away from the top level, with nothing in the readme about when it is safe to run or what it refuses to run against.

## The workflow engine is Temporal, and deploying its interface passes registry credentials

Two make targets and one permission bit tell you the backend architecture. The setup target makes every script in a directory named for the workflow engine readable and executable by everyone on the machine, which means that directory's scripts are meant to be invoked by containers rather than by you. Then there are two targets for the engine's own web interface, deployed with a stack command that passes registry authentication:

```
docker stack deploy --with-registry-auth --detach=false --compose-file docker-stack.prod-temporal-ui.yaml zane-temporal-ui
```

So deploying the platform's admin interface for the workflow engine requires the daemon to authenticate to a container registry. That is normal for a swarm stack pulling private images, and it is one more credential the host holds after installation. The engine's presence also tells you something about the shape of the backend: long running, retryable, stateful work is delegated to a durable workflow system rather than to a database table, which is a heavier operational dependency than most self-hosted panels take on.

## The platform reports no license while the file and the manifest say MIT

Three signals, two agreements and one gap. The repository contains a licence file at the root and the JavaScript workspace manifest declares MIT, while the platform's own licence detection returns nothing for the repository. In practice most people will read the file and find a permissive licence, and the disagreement is probably about detection rather than about terms. It is still worth naming, because the pattern recurs in exactly the repositories where a licence file was added late or where the metadata scanner cannot classify it, and because a compliance check that trusts the platform metadata will reach the opposite conclusion from one that reads the tree. Everything else the project says about itself is marketing rather than legal: a description that calls it beautiful, claims the scalability of one container orchestrator and the flexibility of one reverse proxy, and positions it as a free open source alternative to three commercial platforms. The readme's remaining content is six screenshots, a link to a milestones page standing in for a roadmap, and credits that name the projects that inspired the contribution guidelines and the two panels that inspired the product.

## Conclusion

ZaneOps fits a self-hosting operator with a dedicated machine and a willingness to let one application own the container runtime on it, because the swarm requirement and the overlay network are the design rather than an implementation detail. It does not fit a shared Docker host, since setup changes global daemon state. Four things to check before you run the installer. What the script does before you can read it, because the documented path is a fetch piped into a privileged shell from the project's own content delivery network, and the readme's fallback is a documentation page rather than a local script you can inspect. What it does to your container runtime, because the setup initialises Swarm mode and creates a labelled overlay network, and both persist after you uninstall. What your development traffic is sent to, because the example environment file holds a token from a public webhook receiver and the dev script starts a catcher in the background. And what the licensing actually says, because the platform reports none while the licence file and the JavaScript manifest both say MIT.

## FAQ

### How do I install ZaneOps?

The readme documents one command that fetches an install script from the project's content delivery network and pipes it into a shell running as root. For anyone who hits a problem, the note underneath points at a documentation page with step by step setup instructions instead.

### Does ZaneOps require Docker Swarm mode?

Yes. The project's setup target initialises Swarm mode if it is not active, and if Swarm is already on it checks whether the current node is a manager, refusing to continue with an error that tells you to promote the node first.

### What does ZaneOps use for networking and traffic?

The project positions itself on the flexibility of a reverse proxy for traffic and the scalability of Docker Swarm for orchestration. Its own setup also creates a named attachable overlay network carrying a project label, unless one already exists.

### Is ZaneOps open source and under what licence?

The readme calls it free and open source, the repository contains a licence file and the JavaScript manifest declares MIT, while the platform reports no licence for the repository. The first two agree, and the third is a gap in metadata rather than in the terms.

### What does the ZaneOps development environment send to a third party?

The example environment file contains a token obtained from a public webhook receiver service, and the development scripts run a webhook catcher in the background. In the default configuration, webhook deliveries from your local stack go to that service's public URL while you develop.

### How is the ZaneOps frontend API client generated?

A root level script runs schema generation in the backend and then client generation in the frontend, and the generated output has its own directory at the top level of the repository. The JavaScript side is a package manager workspace with a frozen lockfile install in the setup target.

## Sources

- [Issues](https://github.com/zane-ops/zane-ops/issues)
- [Project website](https://zaneops.dev)
- [README](https://github.com/zane-ops/zane-ops/blob/main/README.md)
- [Releases](https://github.com/zane-ops/zane-ops/releases)
- [zane-ops/zane-ops on GitHub](https://github.com/zane-ops/zane-ops)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/zane-ops-zane-ops
