1. Install k3s
On the first server node, as root:install.sh pins the k3s version (K3S_VERSION, default v1.36.5+k3s1), sets kernel limits for builds and
copies k3s/config.yaml, which disables k3s’ bundled Traefik, ServiceLB and metrics-server (the helmfile
installs its own) and encrypts Secrets at rest. k3s’ network policy controller stays on; the platform relies on it.
Join two more servers for a highly available control plane (etcd needs 1 or 3 servers), then any agents:
/etc/rancher/k3s/k3s.yaml from node1 to ~/.kube/config on your machine, replace 127.0.0.1 with the
node (or TLS_SAN) address, and check:
2. Optional: gVisor for builds
Builds run untrusted Gradle code. For a stronger sandbox installrunsc and containerd-shim-runsc-v1 on
every node that runs builds (gVisor install) and register the
runtime with k3s’ containerd. k3s ships containerd 2, whose config is version 3: copy the generated
/var/lib/rancher/k3s/agent/etc/containerd/config.toml to config-v3.toml.tmpl in the same directory and
append:
config.toml.tmpl with the section
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc].) Restart k3s
(systemctl restart k3s or k3s-agent), check with crictl info | grep runsc, and set atoll.gvisor: true
in your site values (step 4). The chart then creates the gvisor RuntimeClass and sets
ATOLL_BUILD_RUNTIME_CLASS.
Keep builds on their own nodes
Recommended whenever people you don’t fully trust can deploy: give builds a node pool of their own, so a build that escapes its container (or just eats its node’s disk and memory) never lands next to atoll-server, Postgres or the object storage. Label and taint the build nodes when you join them (or later withkubectl):
ATOLL_BUILD_NODE_SELECTOR and ATOLL_BUILD_TOLERATIONS (JSON), which
puts them on every build pod. Builds wait (“Waiting for cluster capacity”) while no build node is free.
3. Images
The release workflow publishesghcr.io/new-horizon-code-repo/atoll-server and atoll-builder; the site
values use them by default. To run your own build instead:
atoll.image, atoll.tag and atoll.builderImage (plus atoll.imagePullSecret if the registry is
private). After step 6 you can also push them to registry.example.com/platform/ with the zot admin user.
4. Site values
my-site.yaml (every key is explained in configuration.md):
domains: API, apps, registry and Grafana hosts.tls.email,tls.dns01.providerand its settings. Start withtls.acmeServerset to Let’s Encrypt staging until certificates are issued, then switch to production.metallb.addresses: free IPs on the node network;ingress.loadBalancerIPto pin one of them.postgres.backup: the off-site bucket.atoll.image,atoll.tag,atoll.builderImage.
my-site.yaml in your own (private) repository; it holds no secrets.
5. Secrets
atoll-builds, atoll-apps, atoll-domains and atoll-storage enforce the
restricted Pod Security Standard) and these Secrets, generating random passwords where needed. It never overwrites an
existing Secret, so it is safe to re-run.
To create the env key by hand instead (any id of letters, digits,
_.-; the first listed key encrypts new
values, the others only decrypt, which is how keys are rotated: k2:<new>,k1:<old>):
atoll.values.features.<area>.existingSecret (see configuration.md).
6. Install everything
atoll-db
Postgres cluster, Loki and Alloy (if logs) and finally the platform. Every release waits until it is ready.
A first install takes 10-15 minutes.
Upgrading an install made before the S3 users were split? Delete seaweedfs/seaweedfs-s3-config, re-run
create-secrets.sh (it keeps the existing keys) and restart the filer: kubectl -n seaweedfs rollout restart statefulset/seaweedfs-filer.
For later changes use helmfile ... apply (needs the helm-diff
plugin: helm plugin install https://github.com/databus23/helm-diff), which shows and applies only what changed.
7. Check
https://atoll.example.com, create a personal access token and point the CLI at
it (ATOLL_SERVER, atoll login). With a database the server has accounts, so there is no static API token.
Custom domains
Every custom domain gets an Ingress and TLS Secret inatoll-domains (see docs/domains.md).
Web app domains route to the ExternalName Service atoll-server there (the chart creates it), Ktor app domains
to an ExternalName Service the server creates per domain, leading to the app’s Service. Traefik’s
allowExternalNameServices is on for that. Tell users where to point their DNS with ATOLL_DOMAINS_CNAME_TARGET (default
cname.<apps domain>, which needs a DNS record at the Traefik IP) and ATOLL_DOMAINS_A_RECORDS (the public IP,
for apex domains), set through atoll.values.features.domains.config.
Rehearse locally
deploy/selfhost/local/k3d.sh runs the same helmfile on a local k3d cluster with
environments/local.yaml (self-signed CA, single replicas, no MetalLB or backups):
--state-values-set datastore.enabled=false leaves it out); its volumes use
local-path, so store resizes are refused locally (storage.md). e2e/storage.sh
tests stores end to end on its own k3d cluster.
Traefik listens on localhost:8480 (HTTP) and localhost:8443 (HTTPS). Nodes cannot resolve
registry.atoll.localhost, so app images do not deploy in this setup; it checks the platform install.
Without the helmfile
The chart works on any cluster that has an ingress controller, Postgres, S3 storage and a registry:postgres.bundled.enabled, storage.bundled.enabled, registry.bundled.enabled); see
deploy/helm/atoll/ci/bundled-values.yaml. Those are single pods without backups. Create atoll-env
(above), atoll-postgres (password) and atoll-storage (access-key, secret-key and s3.json, the
SeaweedFS user list with the same keys) first.
The bundled zot always requires a login. The chart generates its users once (atoll-registry-auth) and the
push and pull Secrets for the builds and apps namespaces. Build pods push to
atoll-registry.<namespace>.svc.cluster.local:5000, a name node container runtimes cannot resolve, so nodes
need a containerd mirror for it pointing at a NodePort (registry.bundled.nodeAccess):
-
k3d:
deploy/selfhost/local/k3d.sh upcreates the cluster with thisregistries.yaml(BUNDLED_NAMESPACEpicks the namespace): -
kind: create the cluster with
containerdConfigPatchessettingconfig_path = "/etc/containerd/certs.d"(see docs/kubernetes.md), then on every node:
localhost:30500 reach the registry from the node’s pod-network gateway (10.42.0.1 on
a single-node k3s/k3d; on kind the .1 of the node’s pod CIDR, see kubectl get node -o jsonpath='\{.items[*].spec.podCIDR\}'); list it in registry.bundled.nodeAccess.allowedCIDRs, or the
registry’s NetworkPolicy drops them. New pods may need a few seconds before k3s’ network policy controller
lets them connect.