Install on a VM (systemd)
Run Planeon's control-plane services as systemd units on a Linux host,
instead of the containers used by the Docker Compose install.
This path uses the same release artifacts, the same planeon.env
variables, and the same ordering guarantees — just packaged as binaries and
a Node.js bundle managed by systemd rather than container images managed
by Compose.
Standalone release binaries — and the downloads.planeon.io artifact host they'd be published to — are not part of the v1.0.0 release; they're planned for a release after v1.0.0. The supported install path for v1.0.0 is Docker Compose. Everything below (artifact layout, systemd units) describes the intended design so you can plan ahead; treat it as provisional until that later release ships.
Prerequisites
- A systemd-based Linux x86_64 host.
- PostgreSQL 16 or newer and Redis 7 or newer, reachable from this host — on this host, another host, or a managed service. Neither is installed by this guide.
- Node.js 22 or newer — the web console is the one Planeon component shipped as a Node.js bundle rather than a native binary, and needs a Node.js runtime present on the host to run it.
- Docker or Podman — needed only to run guacd (see guacd below); every other Planeon service in this guide is a native binary managed directly by systemd, no container runtime involved.
- A DNS name, a TLS-terminating reverse proxy in front of the web console, API, and gateway, and an existing OIDC identity provider with an application already created for Planeon — the same three prerequisites as the Docker Compose install, for the same reasons.
- Access to the
planeonDocker Hub organization (or a private mirror of it) — guacd is still distributed as a container image even in this install path.
If you haven't already, read Requirements for control-plane sizing, browser support, and template OS support before you continue.
Download the release artifacts
Each Planeon service is published as a standalone linux_amd64 archive,
plus a Node.js bundle for the web console:
| Component | Artifact |
|---|---|
| api | planeon-api_linux_amd64.tar.gz |
| worker | planeon-worker_linux_amd64.tar.gz |
| gateway | planeon-gateway_linux_amd64.tar.gz |
| migrate CLI | planeon-migrate_linux_amd64.tar.gz |
| web | planeon-web_node.tar.gz |
curl -fsSLO https://downloads.planeon.io/v1.0.0/planeon-api_linux_amd64.tar.gz
curl -fsSLO https://downloads.planeon.io/v1.0.0/planeon-worker_linux_amd64.tar.gz
curl -fsSLO https://downloads.planeon.io/v1.0.0/planeon-gateway_linux_amd64.tar.gz
curl -fsSLO https://downloads.planeon.io/v1.0.0/planeon-migrate_linux_amd64.tar.gz
curl -fsSLO https://downloads.planeon.io/v1.0.0/planeon-web_node.tar.gz
The exact artifact set — naming, checksums, signing — is finalized by the release pipeline; the names above follow the pattern the v1.0.0 release publishes under.
Install layout and configuration
Create a dedicated system user and a target directory per component:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin planeon
sudo mkdir -p /opt/planeon/{api,worker,gateway,web,migrate} /etc/planeon
Extract each artifact into its directory and hand the tree to the
planeon user:
sudo tar -xzf planeon-api_linux_amd64.tar.gz -C /opt/planeon/api
sudo tar -xzf planeon-worker_linux_amd64.tar.gz -C /opt/planeon/worker
sudo tar -xzf planeon-gateway_linux_amd64.tar.gz -C /opt/planeon/gateway
sudo tar -xzf planeon-migrate_linux_amd64.tar.gz -C /opt/planeon/migrate
sudo tar -xzf planeon-web_node.tar.gz -C /opt/planeon/web
sudo chown -R planeon:planeon /opt/planeon
That produces this layout:
/opt/planeon/
├── api/planeon-api
├── worker/planeon-worker
├── gateway/planeon-gateway
├── migrate/planeon-migrate
└── web/ # Node.js bundle: server.js and supporting files
Configuration lives in a single file, /etc/planeon/planeon.env, shared by
all four units below — the same variables as
planeon.env
on the Docker Compose install page; this guide doesn't repeat that
reference here. Copy that file to /etc/planeon/planeon.env and adjust it
for a host install:
DATABASE_URLandREDIS_ADDRmust point at wherever Postgres and Redis actually run — there's no compose network here to resolve service names likepostgresorredis.GUACD_ADDRESSshould point at wherever you bind guacd (see guacd below).
The configuration reference describes every variable in that file — default, whether it's required, whether it's a secret, and what it does.
sudo chown planeon:planeon /etc/planeon/planeon.env
sudo chmod 640 /etc/planeon/planeon.env
systemd units
Save each of these as /etc/systemd/system/<unit-name>.
planeon-api.service
[Unit]
Description=Planeon API
After=network-online.target
Wants=network-online.target
# planeon-api needs PostgreSQL and Redis reachable to report ready (see
# "Verify the install" below). Start Postgres and Redis -- however you
# manage them on this host or network -- before enabling this unit.
[Service]
Type=simple
User=planeon
Group=planeon
EnvironmentFile=/etc/planeon/planeon.env
ExecStart=/opt/planeon/api/planeon-api
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
planeon-worker.service
[Unit]
Description=Planeon Worker
After=network-online.target
Wants=network-online.target
# Same Postgres/Redis ordering note as planeon-api.service.
[Service]
Type=simple
User=planeon
Group=planeon
EnvironmentFile=/etc/planeon/planeon.env
# api and worker share this host's network stack, unlike the separate
# containers in the Docker Compose install, so they can't both bind the
# METRICS_ADDR default of 0.0.0.0:9090. Give the worker the 9091 port the
# platform already uses for it elsewhere -- see Observability:
# /docs/administration/observability
Environment=METRICS_ADDR=0.0.0.0:9091
ExecStart=/opt/planeon/worker/planeon-worker
Restart=on-failure
RestartSec=5
# Exceed WORKER_SHUTDOWN_TIMEOUT in planeon.env (default 30s) so systemd
# doesn't SIGKILL a job that is still draining gracefully on shutdown --
# mirrors stop_grace_period in the Docker Compose install.
TimeoutStopSec=45
[Install]
WantedBy=multi-user.target
planeon-gateway.service
[Unit]
Description=Planeon Gateway
After=network-online.target planeon-guacd.service
Wants=network-online.target
Requires=planeon-guacd.service
# Also needs PostgreSQL and Redis reachable, same ordering note as
# planeon-api.service.
[Service]
Type=simple
User=planeon
Group=planeon
EnvironmentFile=/etc/planeon/planeon.env
ExecStart=/opt/planeon/gateway/planeon-gateway
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
planeon-web.service
[Unit]
Description=Planeon Web Console
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=planeon
Group=planeon
EnvironmentFile=/etc/planeon/planeon.env
ExecStart=/usr/bin/node /opt/planeon/web/server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
The exact entrypoint filename and any Node-specific runtime variables (for
example PORT/HOSTNAME) are confirmed with the v1.0.0 release notes;
server.js reflects the standard Next.js standalone-bundle layout the web
console is built on.
guacd
Unlike the services above, guacd isn't distributed as a native binary — it
is Planeon's own build of guacd with H.264 decoding enabled. The stock
community guacamole/guacd image and most distro packages lack it, which
renders modern Windows (10/11, Server 2019+) desktop sessions as a black
screen with only a live cursor. Run it as a container, still managed by
systemd like everything else (substitute podman for docker throughout
if that's your runtime):
[Unit]
Description=Planeon guacd
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
Restart=on-failure
RestartSec=5
ExecStartPre=-/usr/bin/docker rm -f planeon-guacd
ExecStart=/usr/bin/docker run --rm --name planeon-guacd -p 127.0.0.1:4822:4822 -e LOG_LEVEL=info planeon/guacd:v1.0.0
ExecStop=/usr/bin/docker stop -t 10 planeon-guacd
[Install]
WantedBy=multi-user.target
Save this as /etc/systemd/system/planeon-guacd.service. Point
GUACD_ADDRESS in planeon.env at 127.0.0.1:4822 (or wherever you bind
it if you change the port).
Apply database migrations
Before starting planeon-api or planeon-worker for the first time — and
before every upgrade — apply migrations with the migrate binary. It is the
same tool as the planeon/migrate container image used on the Docker
Compose
install,
and of the planeon.env variables it needs only the database ones —
DATABASE_URL (and DATABASE_MAX_CONNS, if you changed it) — which the
command below forwards explicitly; applying every pending migration and
exiting is its default behavior, no extra flags needed:
set -a
source /etc/planeon/planeon.env
set +a
sudo -u planeon --preserve-env=DATABASE_URL,DATABASE_MAX_CONNS /opt/planeon/migrate/planeon-migrate
Migrations are never applied automatically when planeon-api or
planeon-worker start — the same explicit, separate step as the Docker
Compose install, for the same reasons: visible, auditable schema changes,
and a failed migration stops the rollout before the new version starts
serving traffic.
Start the platform
sudo systemctl daemon-reload
sudo systemctl enable --now planeon-guacd.service
sudo systemctl enable --now planeon-api.service planeon-worker.service planeon-gateway.service planeon-web.service
Verify the install
systemctl status planeon-api planeon-worker planeon-gateway planeon-web planeon-guacd
Confirm every unit is active (running). Then check readiness — each of
planeon-api and planeon-worker serves /readyz on its ops port
(METRICS_ADDR, 9090 for the api and 9091 for the worker with the
override above):
curl -fsS http://127.0.0.1:9090/readyz # planeon-api
curl -fsS http://127.0.0.1:9091/readyz # planeon-worker
Both return 200 once Postgres, Redis, and the migration state all check
out; a 503 names the first failing dependency. The api also serves
/healthz and /readyz on its public port (API_ADDR, 8080 by
default) if you'd rather check through the same origin your reverse proxy
uses. See Observability → Readiness
semantics for the
exact response shape. If a unit won't reach active (running) or
/readyz keeps failing, see Troubleshooting.
Once every unit is up and ready, open the web console at the public origin you configured on your reverse proxy and sign in — the same first-login bootstrap flow as the Docker Compose install.
Upgrading
Same explicit ordering as install, plus a restart:
-
Download the new artifacts and replace the binaries/bundle under
/opt/planeon/...(keep/etc/planeon/planeon.env, adding any new variables the release notes call out). -
Re-run the migrate binary, as above.
-
Restart the services:
sudo systemctl restart planeon-api planeon-worker planeon-gateway planeon-web
Migrations always run before the new binaries start serving — never after, never automatically — the same ordering guarantee, checked by the same readiness gate, as the Docker Compose install's upgrade path.