One-command Docker and Kubernetes
Requirements
Section titled “Requirements”- Node.js 22.12 or newer and npm, with access to GitHub Container Registry.
- Docker with Compose v2, or
kubectlconnected to an existing Kubernetes cluster. - Allow approximately 6 GB available memory, 2 CPUs and 45 GB persistent storage for the starter stack. Leave additional capacity for the operating system and workloads.
- Kubernetes needs a default StorageClass (or
--storage-class NAME). Production defaults to a LoadBalancer Service; the cluster must provide one.
The CLI installs published images; cloning the repository, PHP and a local database are unnecessary. Docker hosts on a VPS, AWS, Google Cloud or Azure work the same way. Managed platforms need their own container deployment configuration.
Install the CLI
Section titled “Install the CLI”Download the versioned package from the CLI release:
npm install -g https://github.com/ulams-dev/ulams/releases/download/cli-v0.2.0/ulams-0.2.0.tgzAlternatively, replace ulams in a command below with:
npx --yes --package=https://github.com/ulams-dev/ulams/releases/download/cli-v0.2.0/ulams-0.2.0.tgz ulamsLocal development: Phase 2 trial
Section titled “Local development: Phase 2 trial”ulams stack upThis launches API, reference learner frontend, admin, H5P, PDF, MJML, PostgreSQL, Valkey, S3-compatible storage and Caddy. It provisions the academy tenant and waits for health checks. The local gateway listens only on loopback.
- Academy:
http://academy.ulams.localhost:8480 - Studio:
http://academy.ulams.localhost:8480/studio/ - Admin:
http://academy-admin.ulams.localhost:8480 - API:
http://academy-api.ulams.localhost:8480
Development uses the deterministic fake AI provider for the Phase 2 course creation workflow. It makes no paid model calls. Upload a source, answer the brief, review the outline, apply it and approve publication. It does not build the application from source or enable hot reload; use the repository development commands for that.
Credentials are in ./ulams-stack/credentials.json, permission 600. Sign in to the academy with admin@academy.ulams.localhost and TENANT_INITIAL_PASSWORD. The platform administrator uses admin@ulams.localhost and the separate PLATFORM_ADMIN_PASSWORD. No password is printed in terminal output.
Browsers resolve *.localhost locally; the CLI does too. For clients that do not, map the hostnames to loopback. Use --http-port if port 8480 is occupied.
Docker production on a server
Section titled “Docker production on a server”Create DNS records pointing to the server for learn.example.org, api.learn.example.org, admin.learn.example.org, storage.learn.example.org, academy.learn.example.org, academy-api.learn.example.org and academy-admin.learn.example.org. A wildcard *.learn.example.org can cover the subdomains. Keep ports 80 and 443 reachable for ACME; use DNS-only records initially if Cloudflare manages DNS.
ulams stack up --mode production --domain learn.example.orgProduction enables HTTPS, uses unique persisted secrets and disables the fake AI provider and demo auto-login. The initial tenant slug can be changed with --tenant training. Use your own domain, not the example above.
AI is disabled until an operator configures a real provider. Email is logged until SMTP is configured. This starter makes those defaults explicit; it does not send real email or select a paid model automatically. Follow AI provider configuration and email configuration before onboarding users.
Kubernetes
Section titled “Kubernetes”For a local cluster, the CLI creates the namespace, private Secret, four PVCs, StatefulSets, Services and an ingress NetworkPolicy. It starts a loopback port forward automatically:
ulams stack up --target kubernetes --context my-local-clusterFor a production cluster with a load balancer and DNS:
ulams stack up --target kubernetes --mode production \ --domain learn.example.org --context productionThe CLI waits for an assigned LoadBalancer address. Point the DNS records listed above to that address. Your kubeconfig’s selected context is never changed; the install records its context for future commands.
--gateway ClusterIP: local access through automatic loopback port forwarding. Re-runstack upif forwarding stops.--gateway NodePort --node-port 30480: direct node access. Development uses port 30480; production uses HTTPS 30480 and HTTP 30479. Resolve the configured domain and subdomains to the node’s reachable address. ACME requires external 80/443 forwarded to these ports, or use internal TLS on a private network.--storage-class NAME: choose an existing class explicitly.
The application containers share one pod and persistent volume; the database, cache and object store have their own StatefulSets and PVCs. This is a complete starter deployment, not a highly available cluster. Do not increase replicas without changing storage and application topology. See high availability for that design. A Helm chart is Coming.
Inspect YAML, configure and operate
Section titled “Inspect YAML, configure and operate”ulams stack render --target kubernetes --directory ./review-stackulams stack status --directory ./ulams-stack --jsonulams stack down --directory ./ulams-stackulams stack up --directory ./ulams-stackrender writes inspectable compose.yaml, Caddyfile, kubernetes.yaml, a separate private kubernetes-secret.yaml, database bootstrap and private credentials. It does not start containers or contact a cluster. Files are generated from the installed CLI version. stack up regenerates them; do not treat hand edits to generated files as durable overrides. For advanced SMTP, external storage or AI settings, maintain your own deployment configuration derived from render, then operate it with Compose or kubectl directly.
Stopping retains volumes, PVCs, credentials, encryption keys and Passport keys. Starting the same install resumes it. Back up the entire install directory and database/object volumes together. Never remove credentials.json from a populated installation: the CLI refuses to replace lost keys.
To deliberately delete this installation’s data:
ulams stack down --directory ./ulams-stack --purge --yesThe CLI checks installation ownership before operating on an existing Compose project or namespace. Choose a different --name and directory for independent installations. A cancelled or failed launch leaves data in place; repeat stack up to resume and inspect logs if a service failed.
Private TLS and image upgrades
Section titled “Private TLS and image upgrades”For an isolated staging network:
ulams stack up --mode production --domain training.internal --tls internalTrust the exported ulams-stack/ca.crt in your browser or client. For Node-based CLI requests set NODE_EXTRA_CA_CERTS to that file; never disable TLS verification globally.
The first launch resolves a complete matching SHA release across all six application images and pins it in stack.json. Subsequent starts retain that release. To deliberately upgrade, back up first and supply --image-tag sha-COMMIT; the CLI checks the public release before starting. Custom registries require an explicit --image-registry and --image-tag. Infrastructure versions are pinned in the CLI.