Skip to main content

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.

Planned for a post-1.0 release

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 planeon Docker 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:

ComponentArtifact
apiplaneon-api_linux_amd64.tar.gz
workerplaneon-worker_linux_amd64.tar.gz
gatewayplaneon-gateway_linux_amd64.tar.gz
migrate CLIplaneon-migrate_linux_amd64.tar.gz
webplaneon-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_URL and REDIS_ADDR must point at wherever Postgres and Redis actually run — there's no compose network here to resolve service names like postgres or redis.
  • GUACD_ADDRESS should 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:

  1. 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).

  2. Re-run the migrate binary, as above.

  3. 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.