Install XO from sources.
-
Very nice job @acebmxer
Well done -
which is the difference with this?
https://github.com/ronivay/XenOrchestraInstallerUpdaterUsing the above around 5-6 years with no issues at all
e.g. XenOrchestraInstallerUpdater does not support Debian 13 yet.
-
Made some updates to this project. Any existing users please see note. I have also updated the initial post.
️ Upgrading from an earlier version of this script? Read this first.
This version bumps the config schema to v2 (adds PUBLIC_URL and ENCRYPT_REDIS_CREDENTIALS) and corrects two config.toml generation bugs. Your xo-config.cfg is migrated automatically and non-destructively, but the corrected /etc/xo-server/config.toml is only written by --reconfigure.Run --reconfigure once before resuming normal updates:
./install-xen-orchestra.sh --reconfigure
This regenerates config.toml with the fixes (your old file is backed up first; data in /var/lib/xo-server is untouched). It is strongly recommended if you set both REDIRECT_TO_HTTPS=true and REVERSE_PROXY_TRUST — that combination previously produced a duplicate [http] section and silently dropped one of the settings.Afterwards, run --update as normal for routine XO updates — --update does not need to be preceded by --reconfigure again.
Also added option to adjust Xen Orchestras memory allocation. Useful if you are getting out of memory errors or added more memory to XO.
Update - Previous info was incorrect. I have removed and updated.
On fresh install Debian 13 with 8gb ram node -v 24.15.0.============================================== Xen Orchestra Memory Allocation ============================================== Setting Value ----------------------------- ----------------------------------- Total system RAM 7943 MB Current xo-server heap limit ~2240 MB (node default, no --max-old-space-size set) Recommended heap limit 7431 MB -
This is a nice bit of community tooling, and thanks for keeping it updated and being upfront about the "use at your own risk" part.

On the naming point a few people raised, I think the distinction folks are drawing is fair: XOA is the Vates-built appliance, and anything you compile yourself is XO from sources.
The official docs cover that path at https://docs.xen-orchestra.com/xo5/installation#from-the-sources, including the note that there is no pro support for that method.
Hope the project keeps going well! -
A acebmxer referenced this topic on
-
While looking into the following issue - https://xcp-ng.org/forum/topic/11976/trying-to-enable-v2v-and-difficulty-adding-nbdinfo-on-xo-6/10
I found a few issues with my install script... Looking for recommendations on the fix approach.
I am noticing that are some confusion or inconsistencies with the documentation. Areas it suggest to use root and/or a non root account. Part of these issues would not be if the account used was root or with root access.
The two failures
Reason 1 — privileges (SERVICE_USER=xo-service, non-root):
XO's button runs apt-get install ..., make install, and ldconfig with no sudo (confirmed in esxi.mjs source). As xo-service, those commands are permission-denied: it can't apt-install, can't write to /usr/local/..., can't run ldconfig. XO's own dependency checker even says V2V requires root (UID 0) — which is why your sample-xo-config.cfg:56 already warns "V2V import requires root."So yes — your instinct is correct. With SERVICE_USER=xo-service, the button cannot succeed no matter what.
But Reason 2 — build tools absent:
Even if you set SERVICE_USER=root and click the button, it will then try apt-get install -y git dh-autoreconf pkg-config make libxml2-dev ocaml libc-bin and compile from GitLab. That will work if the box has internet and apt is healthy — but it's a multi-minute source compile pulling ~hundreds of MB (ocaml!) every time, done live inside the web request. Pre-staging those packages via the installer makes the button fast and reliable instead of a long live compile.So, is the fix "just set SERVICE_USER=root"?
To make the button function: yes, that is the necessary fix. Pre-installing build deps is an optional reliability/speed improvement on top.Given your answers, here's my refined proposal — minimal, docs-aligned:
Proposed changes
-
Add INSTALL_V2V_DEPS=false opt-in to sample-xo-config.cfg. When true, the installer pre-stages XO's exact build-dep list (git dh-autoreconf pkg-config make libxml2-dev ocaml libc-bin) on apt systems only, so the button compiles quickly instead of installing them live. Default false → no ocaml bloat on normal installs.
-
Root guard (your "Warn only" preference): When INSTALL_V2V_DEPS=true and SERVICE_USER is non-root, print a prominent warning:
"VMware V2V import requires xo-server to run as root (per XO docs). The in-XO 'install nbdinfo' button runs apt-get/make install without sudo and will fail as user '$SERVICE_USER'. Set SERVICE_USER=root to use V2V import."
…and continue (don't abort).-
RHEL/dnf: if INSTALL_V2V_DEPS=true on a non-apt system, log "VMware V2V import is only officially supported on Debian 12/13 per XO docs — skipping V2V dependency setup." and do nothing.
-
Keep the existing /usr/local/lib/vddk dir creation (it's correct — XO untars VDDK there), and don't touch APT contrib/multiverse or pre-compile nbdkit (XO compiles from source itself; distro packages are never used by the button).
-
Docs: README row for INSTALL_V2V_DEPS + a sharpened note in sample-config that V2V needs SERVICE_USER=root, linking the official guide.
This stays strictly within what XO's own code does, scopes the heavy deps behind opt-in, and surfaces the real root-user requirement you correctly identified.
Shall I implement this? If yes, I'll make the edits and run shellcheck/bats afterward to verify nothing breaks. (I have not edited anything yet.)
Prior to looking into this issue. I was trying to work on switching from standard user to root but that broke alot of things and will need rework. Dont belive it would be an issue if root was used to initially deploy xo.
-
-
After building new xo with root and more testing, I have come to this conclusion...
Both things are true, and they're in tension
The official docs prefer non-root for the long-running service — that's a least-privilege hardening recommendation for the daemon. Normal XO (UI, backups, hosts, VMs, NFS/CIFS remotes) works fine non-root.
But several XO features assume root anyway. The ESXi/VMware import "install from source" buttons are hard-coded to refuse unless id -u == 0. You already hit this same pattern once before — the credential-encryption/XenStore work (commit 5e8b7fd) existed precisely because non-root broke that too.
So "everything fails non-root" isn't quite it — what fails is the specific subset of features XO wrote assuming it runs as root. Each one needs a separate workaround. The import button is one that cannot be worked around for a non-root process: it's a uid check on the running daemon, full stop.The honest trade-off
You can pick at most two of these three:Service runs non-root (docs' preference)
In-app "install nbd from source" button works
Script doesn't pre-install packages
The button (#2) requires the daemon to be uid 0. So:Want the button to work → run that box as SERVICE_USER=root. Simplest, everything XO ships just works, zero manual steps. You give up the non-root hardening.
Want to stay non-root → the button is permanently dead; the only way to get import working is the binaries being placed by root once (script or by hand). The binaries run fine as non-root — only their installation needs root.
My recommendation
Use SERVICE_USER=root on this box. XO's own codebase keeps assuming root (import, and you already saw it with encryption/XenStore), so non-root is a recurring fight against upstream for marginal hardening. Root is fully supported, it's what the official XO appliance ships, and it makes the buttons you want work with no manual package steps. Keep non-root only if hardening that box is a hard requirement and you're fine never using the in-app import installer. -
Version 0.2.0 has been release - https://github.com/acebmxer/install_xen_orchestra/blob/main/CHANGELOG.md
Last changes has been about 1 month ago. I have been using it daily, mostly on Ubuntu server but have done some basic test on the other distros that it installs and you can log in with the default credentials. Just need some more feedback from other users, who would be willing to try the project out.
Also some other useful projects I have been working on
-
Hi Everyone,
If I may advertise what I did you have a dedicated thread about my project, this is an easy to setup XCP-ng and XO from the source solution.
Sorry sharing the link to the topic as is couldn't find better way to do that :XCP-ng HL — an ISO for home labs, deploy any XOA image from XO Lite directly
Essentially you deploy XCP-ng ISO and then in XO Lite deploy XOA section you can deploy your prebuilt or XOA VM directly.
Hope that helps Home labers to get in to XCP-ng easily.
Same here this is still work in progress, also I'm personally already using this version in my lab.
Any feedback is welcome !
With regards.
-
A acebmxer referenced this topic
-
Update V0.3.0
Deploying to a New VM (--deploy)
If you don't already have a Linux VM to install into, --deploy builds one for you. Run it from your own workstation — it is the only operation in this script that does not run on the machine Xen Orchestra ends up on:git clone https://github.com/acebmxer/install_xen_orchestra.git cd install_xen_orchestra ./install-xen-orchestra.sh --deployIt will ask for your pool master's address and root password, let you pick a storage repository and network from what the pool actually has, then ask for the VM's size, admin account (optionally with a password), where to clone this repository inside the guest, and its static address. From there it:
Creates the VM and streams a stock Debian 13 cloud image from cloud.debian.org straight into its disk. The download runs on the pool master, so the 3 GB never crosses your workstation's link and never lands on dom0's root filesystem.
Attaches a cloud-init config drive that creates your admin user, installs a freshly generated SSH key, applies the static address, and clones this repository into the guest — either /opt/install_xen_orchestra or /home/<admin>/install_xen_orchestra, whichever you pick.
SSHes in and runs --install --non-interactive, streaming the output to your terminal so you see the build as it happens.
Verifies XO answers on /signin, then detaches and destroys the cloud-init config drive — it has served its purpose, and it holds the admin password hash. If the guest refuses the hot-unplug, you get the xe commands to remove it by hand rather than a failed deploy.
Changing settings the prompts don't cover--deploy only asks about the HTTP/HTTPS ports and the git branch. Everything else the VM is installed with — INSTALL_DIR, SERVICE_USER, NODE_VERSION, SSL and backup paths — comes from a base config, and you get to see it before anything is created on the pool:
The base is sample-xo-config.cfg from the repo. If you also keep an xo-config.cfg beside the script, you are asked which of the two the VM should start from. (Check its paths first — they were written for whatever machine it came from, not a fresh Debian VM.)
Right after the prompts, --deploy offers to open the generated config in your editor ($EDITOR/$VISUAL, else the base config's PREFERRED_EDITOR, else nano/vim/vi). Save and quit and the VM is built with exactly what you left there.
The edit happens on a throwaway copy in a temp directory, so neither the tracked sample nor your own xo-config.cfg is modified. Changing the ports in the editor is picked up too — the review screen, the post-install check and the summary all follow what the file ends up saying.Requirements
The pool master must have outbound internet access.
A free static IP — this is required, not optional. A stock Debian cloud image has no xe-guest-utilities, so the host cannot report a DHCP lease back and the script would have no address to install over.
On your workstation: ssh, scp, ssh-keygen, and an ISO writer (genisoimage or xorriso). Only the ISO writer might need installing, and that is the sole reason --deploy would ask for sudo — nothing else about this operation touches your machine. sshpass is optional: with it you are asked for the pool master password once, without it ssh asks a second time.
Afterwards the VM contains an ordinary checkout of this repository, so updates work there exactly as anywhere else:ssh -i xo-deploy-<hostname>.key <admin>@<ip>
cd <clone dir> && ./install-xen-orchestra.sh --update
The generated SSH private key is saved next to the script as xo-deploy-<hostname>.key (git-ignored). Keep it, or add your own key to the VM and delete it.The admin account's password is optional and asked for during the prompts. The account always gets the generated SSH key, so a password only matters for the VM's console in XO Lite or XCP-ng Center, where no key can be offered, and for su. Leave the prompt empty for a key-only account. If you do set one, a second prompt asks whether SSH should accept it too; the default is no, keeping SSH key-only. Setting the password needs openssl (or mkpasswd) on your workstation — without either, the prompt is skipped and the account stays key-only.
The VM is created with SERVICE_USER=root (the current default) and XO's usual admin@admin.net / admin starting credentials — change that password before putting the VM to use.
To deploy a different Debian release, set XO_DEPLOY_IMAGE_VERSION and XO_DEPLOY_IMAGE_RELEASE, or point XO_DEPLOY_IMAGE_URL at any raw cloud image with cloud-init installed.
Checking a host before deploying
--deploy depends on XAPI behaviour that the test suite cannot exercise without a hypervisor. If a deploy fails, or you want to check a host first, run the probe:./tests/probe-xapi-deploy.sh --host 192.168.1.10
It verifies each assumption in turn — SR and network enumeration, the pool master's internet access, vdi-import from a pipe (round-tripped and checksummed, not just exit-code checked), the /import_raw_vdi HTTP fallback, and VM creation with the boot and memory parameters deploy sets. Everything it creates is named xo-probe-<run id> and destroyed on exit, including on failure; it never touches objects it did not create, and never starts a VM. -
v0.4.0 Release - https://github.com/acebmxer/install_xen_orchestra/releases
0.4.0 - 2026-08-23
Changed
The cloud image is staged on the pool master by default instead of being streamed. Staging is the only path that can resume a broken download, retry a transient failure, and check the downloaded size before anything reaches the VDI; streaming can do none of those, because a pipe already feeding a fixed-Content-Length PUT cannot be rewound. On a link that drops the occasional TLS record — which any multi-gigabyte transfer eventually meets — streaming failed every attempt while staging rode it out. Streaming is now what it should always have been: the fallback for a host without the few gigabytes of scratch space staging needs.
Fixed
A stalled streaming import no longer has to be interrupted by hand. When the download end died, the upload sat waiting for a response XAPI would never send. --speed-time could not help — by that point curl is waiting, not transferring, so the speed meter has stopped ticking and only --max-time 3600 would eventually fire. The two transfers now run as separate processes with the download's exit status watched, and the upload is killed the moment it fails.A pool master short on scratch space no longer fails the whole deploy. The staged path signalled "no room" with a plain non-zero return, which under set -e took the script down before the fallback could be reached.
A failed cloud-image download no longer looks like a successful import. The streaming import piped one curl into another, and the remote shell reported only the upload side's exit status. A download that died partway — a transient SSL_read ... bad record mac on a 3 GB transfer is the usual cause — therefore produced a truncated disk that the deploy reported as imported, and a VM that booted into a corrupt filesystem. The pipeline now runs under bash -o pipefail.
A broken image download no longer hangs the deploy for an hour. When the download end died, the upload curl had already promised XAPI an exact Content-Length and sat waiting to send bytes that were never coming, with
XAPIwaiting alongside it until--max-time 3600expired — the visible symptom being aXAPItask frozen at partial progress and a script that had to be interrupted. Both ends now abort after 60s below 1 KiB/s.The staged image download resumes instead of starting over. It now uses
-C - with --retry 5 --retry-delay 3 --retry-all-errors, so the transient TLS failures that a multi-gigabyte single-connection download eventually hits are ridden out rather than failing the deploy. The streaming path deliberately does not retry: curl re-issues from byte 0, and piped into a fixed-Content-Length PUT those bytes would be appended to the ones already sent, corrupting the image while appearing to succeed.A short staged download is refused rather than imported. The file's size is now checked against the length the server advertised before anything is written into the VDI.
The staged download reports progress. It was silent for several minutes on the longest step of the deploy, which reads as a hang worth killing.
A failed deploy names the VM it left behind, with the xe vm-destroy command, instead of leaving a half-built VM to be rediscovered later in the pool's VM list. Nothing is destroyed automatically.
--update/--reconfigure/--rebuild no longer abort on a root install. Reading User= from a systemd unit that has no such line (which is what a root install looks like) failed the pipeline under set -o pipefail and took the script down before the fallback could run.
Deploy prompts validate values, not just their shape.
999.999.999.999was accepted as an address and70000as a port; both were only rejected after the VM existed, by an unreachable guest or by the installer inside it.Prompted settings are no longer lost when the base config omits the key. The generated xo-config.cfg was patched with sed, which silently does nothing for a key that is not there — so the VM installed on the default while the summary showed the value you typed. Missing keys are now appended.
An
$EDITORwith arguments works. code --wait passed the availability check and then failed with"No such file", since the whole string was treated as one executable path.Values edited into the config are validated. An unusable port or branch was silently ignored, leaving the summary showing one thing and the VM installing another.
Troubleshooting commands point at a key that still exists. The
ssh -ilines printed on a failed install and on a non-200 health check named the temporary key, which the exit trap had already deleted.A second VM with the same hostname no longer overwrites the first one's SSH key, which was the only way into that machine.
The disk-space check before staging an image on the pool master is derived from the image's actual size instead of a hard-coded 4 GiB, which rejected small images and let large ones fill
/var/tmp mid-download.tests/probe-xapi-deploy.shacquires itsXAPIsession from the pool master, the way--deploydoes, so a firewall that blocks port 443 from your workstation no longer skips the HTTP transport probes that matter. It also validates--host, --user, --sr, --image and --payload-mbbefore they reach a shell, drops the eval in the workstation-side transport, and exits non-zero when any probe failed rather than whenever one transport worked.The menu example in the README and the layout comment above
MENU_NAMESdescribed the old fixed 5/4/centered grid; with ten items the menu draws five entries in each column.Security
The cloud image is verified against its published checksum. The size check catches a download that was cut short; it cannot catch one that arrived complete from the wrong place, because a substituted image has a perfectly consistent Content-Length. The staged image is now checked against theSHA512SUMSits origin publishes beside it — which is what Debian ships — and a mismatch aborts before anything reaches the disk. An origin that publishes no sums warns and continues, so a customXO_DEPLOY_IMAGE_URLkeeps working. SetXO_DEPLOY_IMAGE_SHA512to require a specific digest instead: that makes the check mandatory, aborting rather than continuing unverified, and refuses the streaming import outright because a pipe fed straight into the VDI leaves no file to hash. Fetching sums over the same connection as the image is not a detached signature — it defends against a bad mirror or a stale cache, not an attacker holding the TLS session for both requests.
A pinned pool-master fingerprint is now enforced instead of advised.deploy_verify_host_keyfingerprinted the host key and then returned success on every path that could not complete the check — so withXO_DEPLOY_POOL_FINGERPRINTset, a ssh-keyscan that timed out meant the host password was sent to whatever answered on that address, which is exactly what pinning exists to prevent and the easiest outcome for an on-path attacker to arrange. A pin that cannot be checked is now a hard failure.
The verified host key is bound to the connection that carries the password. The scanned key was fingerprinted, shown, and then discarded, whiledom0_execconnected withStrictHostKeyChecking=accept-newagainst the default known_hosts — verifying one transaction and trusting another. Nothing stopped a different key, or the host's RSA key when theED25519one had been displayed, being accepted at connect time. The whole scan is now pinned into a run-scopedknown_hoststhatdom0_execenforces withStrictHostKeyChecking=yes, the same way deploy_wait_for_guest already treated the guest.
A hostile pool master can no longer run commands on the workstation. The free-space probe indeploy_import_vdi_stagedfed the host's reply straight into(( )), which expands an array subscript before evaluating it — so an answer ofPATH[$(...)]executed locally rather than being rejected. It is now checked against^[0-9]+$first, matching the guards already applied to size and got in the same function. Note thatset -euo pipefaildoes not cover this:set -ublocks only the unbound-variable form of the payload. This mattered more after staging became the default import path, because the probe went from rarely reached to running on every deploy.
The pool master's root password is no longer visible in ps. Three calls predating dom0_exec still usedsshpass -p "$HOST_PASSWORD", putting the password in the process list where any other user on the workstation could read it. They now usesshpass -e with $SSHPASS, asdom0_execdoes.
The admin password hash is kept out ofXO_DEBUG=1output. deploy_harden_guest_sudo and deploy_build_config_drive were missing the local - / set +x guard the rest of the script uses, so the hash was printed by xtrace. Previously masked on automated runs only because --non-interactive left the hash empty; requiring a password made it reachable on every deploy.
Revoking the deployment key can no longer empty authorized_keys. A grep failure — no space for the temporary file, an unreadable source — was swallowed by || true and the empty result written back, taking the operator's own key with it. grep's "nothing matched" (a legitimate empty result) is now distinguished from a real error, which aborts and leaves the file untouched.
The streaming import's FIFO is created inside a private directory. mktemp -u returns a name without creating anything, leaving a window in dom0's world-writable /tmp. The FIFO now lives in amktemp -d directory.
DSA public keys are rejected. ssh-dss was accepted bydeploy_load_pubkey, but OpenSSH has refused DSA since 7.0 and removed it in 9.8, so it only installed a key that silently never worked.
The cloud-init cache scrub coverscloud-config.txt. The rendered config holds hashed_passwd just as the raw user-data does; only the latter was being redacted.
The deployment SSH key is destroyed at the end of a deploy. It used to be...
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login