Skip to main content

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​

NeedDetail
A Linux VMUbuntu, Debian or the RHEL family, x86_64 or arm64, with systemd and root
SizeAt least 4 CPUs, 7.5 GB RAM, 40 GB free disk. These are the preflight floors.
Ports80, 443 and 6443 free
Two DNS namesapp.<domain> and api.<domain>, pointing at the VM. Only --tls acme needs them to resolve.
Outbound HTTPSgithub.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 chartA 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 keyOpenAI or Anthropic. You paste it into sudo align-pilot secret llm after the install, so it never sits in a file.
Sign-inAn 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:

  1. Installs k3s v1.34.12+k3s1, after checking the installer against a pinned sha256.
  2. Installs Helm v3.18.4, checked the same way.
  3. With --tls acme, sets up Traefik to get and keep a Let's Encrypt certificate.
  4. 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.

--providerIssuerWhere you register the redirect URI and origin
googlefixed (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.0App 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​

--tlsResult
acme (default)Let's Encrypt through Traefik. Needs --email, working DNS and port 443 reachable from the internet.
selfsignedTraefik's default certificate. Browsers warn. For a lab.
nonePlain 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.

CheckFAIL meansFirst thing to do
podsa pod is not ready, or there are no podssudo k3s kubectl -n align describe pod <name>, then support-bundle
imagesan image cannot be pulledcheck --registry and --image-tag, and the registry login
corsthe web app cannot call the API, or the API allows every originupgrade to a release that includes align-stack PR #3959
certificateexpired, or Let's Encrypt never issued one (a WARN under --tls selfsigned)check DNS and that port 443 is reachable from the internet
oidcthe issuer does not match, or the gateway cannot reach itrun align-pilot sso again; check outbound access from the VM
backupthe 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
settingsthe running release differs from align.yamlthe 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.