One-VM pilot (preview)
One Linux VM, one command. The command installs a single-node k3s, then the same Helm chart a customer cluster uses, with Postgres, Redis and TLS inside the release.
This is a pilot preview, by arrangement. Ask us first, and we will set it up with you.
It runs on one node, with no high availability and no scale-out. When you move to your own cluster, you keep the same images and the same values file.
What you need
| Need | Detail |
|---|---|
| A Linux VM | Ubuntu, Debian or the RHEL family, x86_64 or arm64, with systemd and root |
| Size | At least 4 CPUs, 7.5 GB RAM, 40 GB free disk. These are the preflight floors. |
| Ports | 80, 443 and 6443 free |
| Two DNS names | app.<domain> and api.<domain>, pointing at the VM. Only --tls acme needs them to resolve. |
| Outbound HTTPS | github.com, raw.githubusercontent.com, get.helm.sh, docker.io, your image registry, your LLM provider, and acme-v02.api.letsencrypt.org for --tls acme |
| Images and chart | A registry root and release tag from us, and a way to pull: a registry login (align-pilot registry login), pull access granted to your AWS account ID, or the offline bundle. |
| An LLM key | OpenAI or Anthropic. You paste it into sudo align-pilot secret llm after the install, so it never sits in a file. |
| Sign-in | An OIDC app in your identity provider. sudo align-pilot sso does the rest. |
docker.io is needed for the bundled Postgres and Redis, and for k3s's own images. The preflight checks the three hosts a pull needs: registry-1.docker.io, auth.docker.io and production.cloudflare.docker.com. If this machine pulls only through a registry mirror, the check fails and you need --skip-preflight.
Get the installer
Each release publishes the installer as a signed tarball of the whole deploy/pilot/ directory (the entry point and its helpers), a version-pinned install.sh, a SHA256SUMS file and a signature bundle for each. Both go to a downloads host that is not live yet (ALI-1982: it needs the downloads bucket set up first), and always as a signed artifact in the registry you already pull the chart from. Until the host exists, take them from the registry copy, or from your Align contact, who will give you the base URL. The commands below call that base URL $ALIGN_DOWNLOADS.
curl -fsSLO $ALIGN_DOWNLOADS/pilot/<release>/install.sh
sudo bash install.sh preflight --domain pilot.example.com ... # the flags align-pilot takes
install.sh is pinned to that one release. It downloads the tarball, refuses one that does not match SHA256SUMS, verifies the keyless signature as Align's release workflow for that exact tag, and refuses to go on if cosign is not installed, then unpacks to /opt/align-pilot/<release> and runs align-pilot.sh with the flags you gave. The checksum comes from the same host as the tarball, and so does install.sh itself, so neither proves anything about a tampered host. The signature is what proves the files are ours, which is why cosign is required. Install cosign (v2 or later). ALIGN_PILOT_SKIP_SIGNATURE=1 goes on with only the checksum, for a network where you trust the host and the path to it. Downloads are https only.
To check install.sh before you run it, verify its own bundle first:
curl -fsSLO $ALIGN_DOWNLOADS/pilot/<release>/install.sh.bundle
cosign verify-blob --bundle install.sh.bundle install.sh \
--certificate-identity-regexp '^https://github.com/aligndottech/align-stack/\.github/workflows/release\.yaml@refs/tags/<release>$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
You can also fetch and verify the tarball by hand:
sha256sum -c SHA256SUMS
cosign verify-blob --bundle align-pilot-<release>.tar.gz.bundle \
--certificate-identity-regexp '^https://github.com/aligndottech/align-stack/\.github/workflows/release\.yaml@refs/tags/<release>$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com align-pilot-<release>.tar.gz
Prefer the registry? oras pull <registry>/align/charts/align:<version>-pilot (a customer with chart access can read it), then verify the signature on the digest as in Verifying releases.
Install
The installer is the deploy/pilot/ directory, not one file: it is align-pilot.sh plus helper scripts
named align-pilot-<command>.sh beside it. Copy the whole directory to the VM and run the commands below from it.
install then puts every script in /usr/local/lib/align-pilot and links /usr/local/sbin/align-pilot to it,
which is also what the nightly backup timer runs. A command whose helper is missing stops and names the file.
sudo ./align-pilot.sh preflight --domain pilot.example.com --email ops@example.com \
--registry <registry root> --image-tag <release tag> --chart <chart path or oci:// ref>
sudo ./align-pilot.sh install --domain pilot.example.com --email ops@example.com \
--registry <registry root> --image-tag <release tag> --chart <chart path or oci:// ref> \
--tenant-name "Your Company" --admin-email you@example.com --values ./my-values.yaml
preflight checks the machine and the network. It changes nothing and exits 1 if a check fails.
install runs the preflight, then:
- Installs k3s
v1.34.12+k3s1, after checking the installer against a pinned sha256. - Installs Helm
v3.18.4, checked the same way. - With
--tls acme, sets up Traefik to get and keep a Let's Encrypt certificate. - Writes
/etc/align-pilot/values.yaml(root only) and installs the chart with--wait. The chart creates Postgres, Redis and the internal secrets, and runs the RLS cutover.
Sign-in
One command, after the first install:
sudo align-pilot sso --provider google
It prints the redirect URI and the origin to register in your identity provider, checks the issuer's discovery document, then asks for the client ID (or take --client-id) and the client secret. The secret goes in at a hidden prompt or on stdin, never on the command line. It writes the align-oidc Secret, upgrades from align.yaml, and waits for the gateway to report that OIDC started. Then you sign in as the --admin-email you installed with. Every later install or upgrade keeps sign-in on.
--provider | Issuer | Where you register the redirect URI and origin |
|---|---|---|
google | fixed (https://accounts.google.com) | Cloud console, APIs and Services, Credentials, OAuth client ID, type Web application |
entra | --issuer https://login.microsoftonline.com/<tenant-id>/v2.0 | App registrations, New registration, platform Web |
okta | --issuer https://<your-okta-domain> | Applications, Create App Integration, OIDC, Web Application |
oidc | --issuer <your issuer URL> | the provider's OIDC web-app settings |
Every account your identity provider lets through becomes a member of this tenant. Restrict who can sign in at the provider: for Google, set the consent screen user type to Internal, or any Google account can sign in. The issuer is checked before anything is written: the issuer in its discovery document must equal the one you gave, so a typo or the wrong tenant stops the command with nothing changed. On a pilot installed before align.yaml held a tenant id, sso finds your tenant by name, so no second tenant is created. The tenant id it uses is saved as tenant.id and shared by gateway.oidc.defaultTenantId and the bootstrap Job.
TLS modes
--tls | Result |
|---|---|
acme (default) | Let's Encrypt through Traefik. Needs --email, working DNS and port 443 reachable from the internet. |
selfsigned | Traefik's default certificate. Browsers warn. For a lab. |
none | Plain HTTP. For a lab behind your own TLS terminator. |
Pull images without an AWS account
If you have a registry user name and password or token (from us, or for a mirror you run), log in once before install:
sudo align-pilot registry login --server registry.example.com --username robot-acme
Paste the password at the hidden prompt, or pipe it from a file. It is never an argument. Before the first install there is no cluster, so it is saved in /etc/align-pilot/registry-auth.json (root only, mode 0600). install then logs helm in for the chart pull and creates the align-pull image pull Secret that the rendered values name, so the pods pull with it too. On a VM that is already installed it applies at once, and a login the registry refuses is not kept. It works with any registry that takes a user name and password or a token: Harbor, GHCR, ACR, a mirror, or an ECR token. The server must be a lower-case host name, with no https:// and no path. A token that expires (ECR tokens last about 12 hours) needs a fresh login before the next upgrade. No credential issued by us exists yet (ALI-1796 tracks the per-customer token), so today this is for a mirror you run, or a token we send by hand.
Add your LLM key
sudo align-pilot secret llm --provider anthropic # or openai
Paste the key at the hidden prompt, or pipe it from a file you then delete: sudo align-pilot secret llm < anthropic.key. Do not type it on the command line (echo key | ... is saved in your shell history), and the command refuses a key given as an argument. The key goes into a Kubernetes Secret named align-llm-pilot. The first run, or a change of provider, upgrades Align from align.yaml so the brain reads that Secret. A later run only restarts the brain. --provider is saved as model.provider in align.yaml. sudo align-pilot secret sso does the same for the identity provider's client secret, once the align-oidc Secret exists.
Add your licence
Align sends you a license.jwt file and a public key. After the install:
sudo align-pilot license --public-key license.pub --file license.jwt
It checks the licence against the key first (signature, algorithm, audience and expiry) and refuses a damaged or foreign file with nothing changed. A good one goes into the align-license Secret, the key is saved beside align.yaml, and Align upgrades so the gateway validates it. You can also pipe the licence in on stdin. The licence is never taken as an argument.
On a licensed pilot, do not set gateway.extraEnv in a --values file: Helm replaces lists, so it would drop the licence public key. A licence never stops your install: a missing, expired or invalid one is logged, not enforced, and there is a 14-day grace period after expiry. The gateway checks in with Align once a day, sending the licence id, install id, version and the number of people covered. See Licensing.
Upgrade
Your settings live in /etc/align-pilot/align.yaml, written by the first install (root only). Each later run starts from that file, and a flag you pass on that run replaces the saved value. To move to a new release:
sudo align-pilot upgrade --release <release tag>
The file is saved only after the install or upgrade succeeds, so a mistyped --release that never comes up is not remembered. A new --domain moves both hosts unless you also pass --app-host or --api-host. preflight, backup and support-bundle read the file too.
upgrade needs the file, so it refuses to run before the first install. You can also edit align.yaml and run sudo align-pilot upgrade with no flags. The file accepts plain or double-quoted values, two levels and # comments, and nothing else (no single quotes, no {} lists, no anchors). A line it cannot read stops the run before anything changes and names the line. Comments you add are not kept when align-pilot rewrites the file.
An upgrade keeps the generated secrets and reuses the --values file from the first run. Pass --values again to replace it. --release is the same flag as --image-tag. --registry is the registry root, without /align: the chart adds align/<service> itself.
Backups and the master key
Nightly backup. install sets up a systemd timer that runs at 02:30 UTC (with up to 10 minutes of random delay, and a catch-up run after downtime). It dumps the bundled Postgres with pg_dump into --backup-dir (default /var/backups/align, mode 0700) and keeps the newest --backup-keep files (default 7). A failed dump leaves the older backups alone. Run one by hand with sudo align-pilot backup.
The backup lives on the same VM. If the disk dies, the backups die with it. Copy that directory off the machine yourself (rsync, your backup agent, a mounted volume). install does not do that for you.
support-bundle shows the newest backup and its time. A re-run of install or upgrade reuses the backup settings in align.yaml unless you pass the flags again.
Master key. The first install prints master-encryption-key once, on your terminal only. It is not written to stdout or stderr, so tee and CI logs do not capture it, and it is not in shell history. Terminal scrollback, tmux logging and session recorders can still capture it. It is never printed again. If the install runs with no terminal (a pipe or a CI job), nothing is printed and a later interactive run prints it. Store it in your vault. A restored database is useless for connector tokens without it, because the key lives in the align-internal Secret and not in the dump.
If you lose it while the cluster still runs, the Secret is still readable by root. After the cluster is gone, it cannot be recovered.
Restore
Run on the VM, as root, with a dump from the backup directory. The restore replaces the contents of the database.
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
k3s kubectl -n align get deploy -o jsonpath='{range .items[*]}{.metadata.name}={.spec.replicas}{"\n"}{end}' > /root/replicas.txt
k3s kubectl -n align scale deploy --all --replicas=0
k3s kubectl -n align exec -i align-postgresql-0 -- \
sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner' \
< /var/backups/align/align-db-<time>.dump
# put each Deployment back to the count saved in /root/replicas.txt:
while IFS== read -r d n; do k3s kubectl -n align scale deploy "$d" --replicas="$n"; done < /root/replicas.txt
On a rebuilt VM, put the saved master key in your --values file as secrets.internal.masterEncryptionKey before the first install, so the new install uses your key and does not generate another. Then restore.
What has been rehearsed: the dump command, the file mode, retention and the failure paths run for real against a Postgres 16 container, and the dump restored into a fresh database with the rows intact (2 of 2). What has not: the timer on a real systemd VM, kubectl exec into the chart's align-postgresql-0, and the restore above on the chart's database with the RLS roles. Those are part of the full VM run (ALI-1925).
Check the install: doctor
sudo align-pilot doctor
It changes nothing. It prints one PASS, WARN, FAIL or SKIP line per check and exits 1 if any line is FAIL. Run it after install, after sso, and whenever something looks wrong.
| Check | FAIL means | First thing to do |
|---|---|---|
pods | a pod is not ready, or there are no pods | sudo k3s kubectl -n align describe pod <name>, then support-bundle |
images | an image cannot be pulled | check --registry and --image-tag, and the registry login |
cors | the web app cannot call the API, or the API allows every origin | upgrade to a release that includes align-stack PR #3959 |
certificate | expired, or Let's Encrypt never issued one (a WARN under --tls selfsigned) | check DNS and that port 443 is reachable from the internet |
oidc | the issuer does not match, or the gateway cannot reach it | run align-pilot sso again; check outbound access from the VM |
backup | the timer is off or has no next run (a WARN if the newest backup is over 48 hours old) | sudo systemctl enable --now align-pilot-backup.timer |
settings | the running release differs from align.yaml | the line names the key: look for it in /etc/align-pilot/extra-values.yaml, which silently beats align.yaml |
SKIP means the check does not apply (sign-in not set up, or --tls none).
Support bundle
sudo ./align-pilot.sh support-bundle
This writes align-support-bundle-<time>.tgz with versions, pods, events, the Helm release, the rendered values and the last 500 log lines of every Align pod. Every value of every Kubernetes Secret is replaced with [REDACTED]. It cannot know about a secret that lives outside the cluster. If it cannot read the Secrets, it writes nothing. Read the bundle before you send it.
Logs can carry content, and the redaction does not cover it. The redaction replaces Secret values. It does not read log lines, so the log files in the bundle may contain whatever your tools passed through Align. If protected health information appears in the tools you connect, send us the overview file only: remove the .log files from the bundle before you send it. We do not ask a healthcare customer for logs, or for any other content-bearing material, unless you have agreed in writing to send them for that request.