Installing xrmverse
This guide installs xrmd on a Linux server with Docker, from a released CLI and the published server image, with
every organization reachable at https://<org>.<domain> on port 443. For a quick try on a laptop, or to run a server
built from a clone, see Getting started.
Preview. Releases are drafts and the server image on GHCR is private until the maintainers publish them (a publication decision, see Legal). Until then you need read access to the repository's packages:
docker login ghcr.iowith a token that hasread:packages.
1. Requirements
- Linux x86-64 (
linux/amd64). The server image is built for that platform only; arm64 is not supported yet. - Docker Engine with Compose v2 (
docker compose version). Install it from Docker's documentation. - Memory and disk. SQL Server needs at least 2 GB of RAM on its own (see
SQL Server on Linux requirements).
As a starting point, plan about 1 GB of RAM per organization (one
xrmdprocess each) and measure with your load. Disk: the images (a few GB), the SQL Server data volume (the bundled Express edition caps each database at 10 GB, see SQL Server editions), thefilesvolume (file and image columns) and the backups. - Network. Outbound access to
ghcr.io(thexrmdimage),docker.io(thecaddyimage),mcr.microsoft.com(SQL Server) andapi.nuget.org(the Microsoft SDK). Without it, follow Offline install. Inbound: ports 80 and 443 for the generated proxy. - Names. A DNS domain you control (for example
example.com), with one record per organization or a wildcard.
2. Install the CLI
Download xrmd-linux-x64.tar.gz from the GitHub release and check its SHA-256 digest against the one GitHub shows
for the asset on the release page. That digest comes from the same place as the file, so it detects a corrupted
download, not a tampered release (the CLI archive is not signed yet); authenticity is checked on the server image,
which is signed: see cosign verify in Start and verify.
sha256sum xrmd-linux-x64.tar.gz
sudo tar -C /usr/local/bin -xzf xrmd-linux-x64.tar.gz xrmd
xrmd --version
The archive holds a self-contained executable (no .NET runtime needed) and its notices. Archives for macOS
(osx-arm64, osx-x64) and Windows (win-x64) exist for the client side; the server itself runs on Linux. A
dotnet tool on NuGet.org, Homebrew and winget are not yet available (the release carries the xrmd nupkg, see
CLI xrmd).
The CLI version decides the server image: server init writes ghcr.io/power-builders-italia/xrmd:<CLI version> to
.env. No latest tag exists.
3. Microsoft SDK
xrmverse ships without the Microsoft SDK assemblies: you install them yourself, after accepting Microsoft's license
terms. server init --accept-microsoft-license does it during init; otherwise run xrmd sdk install --dir <dir>
before server up, which refuses to start without it. See Installing the Microsoft SDK.
4. Entra app registrations
Development tokens (--auth dev, the default) are for evaluation only. In production use Microsoft Entra ID
(--auth entra --tenant-id <tenant GUID> --admin-object-id <oid>), with one API app registration per organization:
- Application ID URI
https://<org>.<domain>(Entra refuses a trailing slash; the server accepts the audience with or without it). That is the organization's default audience once it has a public host, and the URL the CLI derives its scope from, so the CLI works with noORG_<ORG>_AUDIENCE. Verify with a real token: this assumes the token'saudclaim is the Application ID URI. Entra v2.0 access tokens may carry the API's application (client) id, a GUID, instead; decode one token (audclaim) before going to production, and if it is the GUID setORG_<ORG>_AUDIENCEto it. This has not been checked against a real tenant yet. Entra accepts anhttps://Application ID URI only on a domain verified in your tenant: see Restrictions on identifier URIs. If you cannot verify the domain, use anapi://URI and setORG_<ORG>_AUDIENCE(with the CLI limitation described in Multiple organizations). accessTokenAcceptedVersion2 in the manifest and the tenant GUID: see Development token for why.- For the web admin,
UI_ENTRA_CLIENT_IDandUI_ENTRA_SCOPE: see Web admin.
How to register an application is in Microsoft Learn:
Register an application and
Expose a web API.
server init prints, for each organization, the xrmd auth login --profile <org> --url https://<host>/ … command to
run once the server is up.
Without Entra: --auth local
With --auth local the server is its own identity provider, so no app registrations are needed and nothing is sent to
Microsoft. server init generates each organization's administrator and a one-time initial password, shown once (and
written to <dir>/secrets/<org>/admin-password, a folder only that organization's container mounts; the server consumes the file at its first start). Every organization needs an
https host (--domain or --public-host, with a proxy and certificates as in section 6), because ServiceClient and MSAL
refuse other authorities. pac does not work in this mode. Users, TOTP, service clients, connection strings, backups and the
move to Entra are in Local identity provider.
5. Initialize
xrmd server init --dir /srv/xrmd --org acme --org zenith \
--domain example.com --tls acme --acme-email admin@example.com \
--accept-microsoft-license --accept-sql-server-eula
- SQL Server. Without
--sql-connectionasqlserverservice runs the Express edition (--sql-editionfor another one, see SQL Server editions).--sql-connection '<connection string>'uses an existing SQL Server instead; its permissions and backups are in Multiple organizations. The bundled SQL Server needs your acceptance of Microsoft's license terms for its edition:--accept-sql-server-eula, or answer the question init asks at a terminal (see License terms). - Hosts.
--domain <domain>gives every organization the host<org>.<domain>;--public-host <org>=<host>(repeatable) gives one organization another host. A host is a lower-case DNS name with at least two labels, unique across organizations.server org add <name>later gives the new organization<name>.<domain>, or--public-host <host>. - TLS (
--tls, required with a host and the generated proxy):acme,filesorinternal, see Certificates. - Proxy. By default a Caddy proxy (
caddy:2.11.9) is generated: the only service published outside loopback, on ports 80 and 443 of every address (PROXY_BINDin.envchooses another address). The organizations publish no host port (--expose-portskeeps their loopback ports for diagnostics) and reach the proxy on the compose networkedge(--edge-subnet, default172.31.250.0/24,/27or larger); each one honors forwarded headers only from the proxy's address,.10of that subnet, and only for its own host. The other containers get addresses from the upper half of the subnet only (172.31.250.128/25by default), so no organization can take the proxy's address.--no-proxyis for your own proxy, see Your own reverse proxy. - SQL endpoint (optional,
--tds-port, with--auth local). Gives each organization a read-only SQL endpoint, see SQL endpoint. - DNS.
initprints the records to create: oneA/AAAArecord per host, or one wildcard*.<domain>record with--domain, pointing at this server (or at your proxy with--no-proxy). For the CLI the names must also resolve on the machine that runs it:server doctorreports a host that does not.
.env is the installation: docker-compose.yml and Caddyfile are rendered from it and start with
# Generated by xrmd; never edit them. The keys are listed in Configuration.
6. Certificates
ACME (--tls acme --acme-email <e-mail>). Caddy obtains and renews a certificate per host from Let's Encrypt (or
another ACME CA). The hosts must resolve publicly to this server, and port 80 (HTTP-01) or 443 (TLS-ALPN-01) must be
reachable from the internet.
Your own certificate (--tls files --tls-cert <pem> --tls-key <pem>). One pair for every host: a wildcard
*.<domain> or a certificate whose SAN list names every host. init copies it to <dir>/certs/server.crt and
server.key (mode 600), mounted read-only into the proxy. The key must be unencrypted PEM; the certificate file may
hold the chain. server up and server upgrade refuse to start (exit 5) when a file is missing. Renewal is yours:
replace both files, then run docker compose exec proxy caddy reload --config /etc/caddy/Caddyfile --force in <dir>
(or docker compose restart proxy). --force is needed: the Caddyfile has not changed, and without it Caddy skips the
reload and keeps serving the old certificate; server up does not re-create a running proxy either.
server doctor warns 14 days before the served certificate expires and reports an error once it has expired or when
it does not cover the host.
Internal CA (--tls internal). Caddy's own local CA, for tests, evaluation and air-gapped sites. Caddy issues
short-lived certificates (about 12 hours) and renews them itself; the root it signs them with is valid for years.
After each start server up exports that root to <dir>/certs/caddy-root.crt and records it in the dev profiles of
the organizations with a host (CaFile in config.json): the CLI trusts it only for that profile's host, with no
change to the machine's trust store. Profiles you create with xrmd auth login (Entra) get no CaFile yet: trust the
root on the machine instead, as for any other client. server doctor judges the root here: it warns 14 days before
caddy-root.crt expires; an expired served certificate or root is an error.
Every other client (browsers, ServiceClient, pac, XrmToolBox) must trust caddy-root.crt:
- Windows:
certutil -addstore -f Root caddy-root.crtin an elevated prompt (machine store), or the Certificates MMC snap-in, Trusted Root Certification Authorities, Import. See certutil. - macOS:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain caddy-root.crt, or open it in Keychain Access, System keychain, and set it to Always Trust. - Linux (Debian, Ubuntu):
sudo cp caddy-root.crt /usr/local/share/ca-certificates/xrmd-caddy-root.crt && sudo update-ca-certificates. Other distributions have their own tool (update-ca-truston Fedora and RHEL).
Firefox and some runtimes keep their own trust store. Remove the root from the clients when you retire the installation.
SQL endpoint
--tds-port <port> (with --auth local and a public host) gives the organizations a read-only SQL endpoint for SSMS,
Power BI and SqlClient: the first organization listens on <port>, the next ones on the next ports, and
server org add <name> --tds-port <port> adds one later. The port is published on every address (XRMD_TDS_BIND in
.env chooses another); open it in your firewall. It must differ from 80 and 443 with the generated proxy, and from the
ports of the other organizations. The endpoint requires TLS and serves the organization's host certificate:
- With the generated proxy, the
tds-certsservice copies each certificate into<dir>/tds/<org>/every 12 hours (ACME), every hour (--tls internal) or every 5 minutes (--tls files, the paircerts/server.crtandserver.key). The server picks up a changed pair within about 10 seconds, and opens the endpoint once the files exist (with ACME this takes up to a few minutes, until the certificate is issued and copied). - With
--no-proxy,--tds-cert <pem>and--tds-key <pem>are required: one certificate covering every host, copied tocerts/tds.crtandcerts/tds.key(mode 600). Replace both files to renew; the copy follows within 5 minutes. The endpoint does not go through your proxy: clients connect to the published port.
server status shows each organization's TDS port and certificate expiry, and server doctor checks the certificate.
Connecting, supported SQL, limits and troubleshooting are in SQL endpoint.
7. Your own reverse proxy
With --no-proxy --trusted-proxy <ip|cidr> (repeatable) no proxy service is generated: your nginx, Traefik or IIS
ARR terminates TLS and forwards each host to its organization's port, which the compose file publishes on
XRMD_BIND (default 127.0.0.1; set XRMD_BIND in .env to the address your proxy connects to when it runs on
another machine, and firewall it). --tls options are refused with --no-proxy.
# Your proxy runs on 10.0.0.5 and connects to the published ports over the network (XRMD_BIND in .env).
xrmd server init --dir /srv/xrmd --org acme --org zenith --domain example.com --no-proxy --trusted-proxy 10.0.0.5
Which address to trust. The organizations honor X-Forwarded-Proto, -Host and -For only from the addresses in
TRUSTED_PROXIES, one hop deep, and only for their own host. That is the address the xrmd container sees, not the
one your proxy listens on. Trust a single address, never more than you must:
- a proxy on another machine arrives from its own address: give that one, as above;
- a proxy on the same host that connects to
127.0.0.1:<port>arrives from the gateway of the compose project's default network, which Docker assigns at the firstserver up, so it is not known atinit. You can start with Docker's default address range (--trusted-proxy 172.16.0.0/12), but while that range is set, any container whose address is in it and that can reach an organization (a container on the same Docker network, for example) can forge forwarded headers for that organization's host: it can set the scheme, host and client address the server uses for its URLs and logs. Narrow it right after the first start: read the gateway withdocker network inspect <project>_default --format '{{(index .IPAM.Config 0).Gateway}}'(the project is the installation directory's name, in lower case), setTRUSTED_PROXIESin.envto that one address and runserver upagain.
Check the result: curl https://acme.example.com/api/data/v9.2/ (with a token) answers an @odata.context that starts
with https://acme.example.com/. If it starts with http://localhost, the address is not trusted. An untrusted proxy also
breaks form scripts and web resources: the web session cookie is set without Secure, and every write a script makes with
it is refused with 403 (0x80048306, origin mismatch), because the server compares the browser's Origin with the scheme
and host it sees itself.
Every proxy must: send the original host and scheme (X-Forwarded-Host, X-Forwarded-Proto) and the client
address (X-Forwarded-For, replaced rather than appended); accept request bodies up to 128 MiB (134,217,728
bytes, the server's limit for a solution import or a file upload); and allow 10 minutes for a response, because a
synchronous ImportSolution holds its request until the import ends. WebSockets are not needed. Redirect port 80 to
443 yourself.
nginx (one server block per organization):
server {
listen 443 ssl;
server_name acme.example.com;
ssl_certificate /etc/nginx/certs/example.com.crt;
ssl_certificate_key /etc/nginx/certs/example.com.key;
client_max_body_size 128m;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass http://127.0.0.1:5001; # ORG_ACME_PORT
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
Traefik (file provider; Traefik sets the X-Forwarded-* headers itself and has no body size limit by default):
http:
routers:
acme:
rule: Host(`acme.example.com`)
entryPoints: [websecure]
service: acme
tls: {}
services:
acme:
loadBalancer:
servers:
- url: http://127.0.0.1:5001 # ORG_ACME_PORT
In the static configuration set entryPoints.websecure.transport.respondingTimeouts.readTimeout (and writeTimeout)
to 600s, and list only your own trusted addresses in forwardedHeaders.trustedIPs, so that headers sent by
clients are replaced.
IIS with Application Request Routing (ARR) on a Windows machine in front of the Linux host: install the URL Rewrite
and ARR modules, enable the proxy, then, per organization's site (binding https, host name acme.example.com):
%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/proxy /enabled:"True" /preserveHostHeader:"True" /reverseRewriteHostInResponseHeaders:"False" /timeout:"00:10:00" /commit:apphost
<!-- web.config of the acme site; allow HTTP_X_FORWARDED_HOST and HTTP_X_FORWARDED_PROTO under
URL Rewrite, View Server Variables, first -->
<configuration>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="134217728" />
</requestFiltering>
</security>
<rewrite>
<rules>
<rule name="xrmd-acme" stopProcessing="true">
<match url="(.*)" />
<serverVariables>
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
</serverVariables>
<action type="Rewrite" url="http://linux-host.example.com:5001/{R:1}" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
ARR adds X-Forwarded-For itself. Trust the IIS machine's address (--trusted-proxy). Microsoft Learn describes the
modules in Reverse proxy with URL Rewrite and ARR.
8. Start and verify
xrmd server up --dir /srv/xrmd
xrmd server status --dir /srv/xrmd # one row per organization, with its public URL
xrmd server doctor --dir /srv/xrmd # the server's checks, plus dns and certificate per public host
curl https://acme.example.com/healthz # -> ok
status exits 1 unless every organization is healthy; doctor exits 1 on any error (a host that does not resolve on
this machine, a certificate that has expired or does not cover the host). In dev mode set XRMD_DEV_SIGNING_KEY to
AUTH_DEV_SIGNING_KEY of .env first; with Entra sign in with the commands init printed.
Verify the image signature with cosign. The release signs the image without a key, from its GitHub Actions workflow:
cosign verify ghcr.io/power-builders-italia/xrmd:<version> \
--certificate-identity-regexp '^https://github\.com/Power-Builders-Italia/XRMverse/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify-attestation --type cyclonedx ghcr.io/power-builders-italia/xrmd:<version> \
--certificate-identity-regexp '^https://github\.com/Power-Builders-Italia/XRMverse/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com > /dev/null # the server SBOM
9. Offline install
The release carries the server image as xrmd-server-<version>-linux-amd64.tar.gz and its .sha256. It holds the
xrmd image only: SQL Server, Caddy and the Microsoft SDK are never redistributed, so you fetch them yourself on a
connected machine.
On a connected machine:
docker pull mcr.microsoft.com/mssql/server:2022-latest # not with --sql-connection
docker pull caddy:2.11.9 # not with --no-proxy
docker save mcr.microsoft.com/mssql/server:2022-latest caddy:2.11.9 | gzip > third-party-images.tar.gz
Download the Microsoft SDK package Microsoft.PowerPlatform.Dataverse.Client 1.2.27 from NuGet.org (the only version
xrmd accepts, checked against its pinned hash), and the CLI archive. Copy everything to the server, then:
sha256sum -c xrmd-server-<version>-linux-amd64.tar.gz.sha256
docker load -i xrmd-server-<version>-linux-amd64.tar.gz
docker load -i third-party-images.tar.gz
xrmd server init --dir /srv/xrmd --org acme --domain example.com --tls internal # or --tls files
xrmd sdk install --dir /srv/xrmd --from microsoft.powerplatform.dataverse.client.1.2.27.nupkg
xrmd server up --dir /srv/xrmd
server up starts the loaded images without pulling. ACME needs the internet: use files or internal.
What these checks cover: the .sha256 comes from the same release as the archive, so sha256sum -c detects a
corrupted copy, not a substituted one; cosign verify (section 8) verifies the image in the registry, not the archive
you docker load. To tie the two together, on the connected machine run cosign verify on
ghcr.io/power-builders-italia/xrmd:<version>, then compare the config digest of that registry image with the one the
archive names; they must be equal:
docker buildx imagetools inspect --raw ghcr.io/power-builders-italia/xrmd:<version> | jq -r .config.digest
tar -xzOf xrmd-server-<version>-linux-amd64.tar.gz manifest.json | jq -r '.[0].Config' # names the same sha256 digest
Without that comparison, the archive is only as trustworthy as the channel you downloaded it from.
10. Backup, upgrade and uninstall
Backup and restore. xrmd org backup <org> --profile <org> writes a COPY_ONLY .bak and a .files.tar to the
backups volume; xrmd org restore <org> --file <path> restores them. See CLI xrmd and
Multiple organizations (external SQL Server). Also keep a copy of <dir>/.env (it holds
the secrets) and of <dir>/certs.
Upgrade. xrmd server upgrade --dir <dir> --to <version> rewrites the tag of XRMD_IMAGE in .env, pulls,
restarts every organization and waits until each one is healthy. Install the CLI of the same version first, so that
the generated files match the server. Offline, upgrade cannot pull: docker load the new archive, set the tag of
XRMD_IMAGE in .env and run server up. A new Caddy version comes with a new CLI release.
Uninstall.
xrmd server down --dir /srv/xrmd --volumes --yes # also deletes the data volumes: back up first
docker network rm <project>_edge # if it is left over (see below)
docker image rm ghcr.io/power-builders-italia/xrmd:<version> caddy:2.11.9 mcr.microsoft.com/mssql/server:2022-latest
xrmd auth logout --profile acme # each profile of the installation
rm -rf /srv/xrmd
When the last organization with a host is removed (server org remove), the proxy and the Caddyfile go away but
the Docker network <project>_edge stays: the compose file no longer declares it, so server down does not remove it
either. docker network rm <project>_edge does (the project is the installation directory's name, in lower case).
Remove caddy-root.crt from the clients that trusted it.
11. Not supported yet
A Windows service, hosting in IIS, systemd units, Kubernetes or Helm, and arm64 servers. A CLI on NuGet.org,
Homebrew or winget, and public images, wait for the first public release.