2501 infra CLI created moves to the chart once, in place. The chart’s objects carry the names, the selectors and the database names the CLI gives its own, so applying the chart updates the CLI’s Deployments, Services, volume claims, Role and ConfigMaps, as an upgrade would: no second installation starts beside them, and the data stays where it is. Only the keys and passwords the data is bound to are copied, from the CLI’s secrets into the chart’s.
On Kubernetes and OpenShift, the move renders the chart with a values.yaml you write and applies the result with kubectl or oc, as you applied the CLI’s manifests: nothing of Helm runs in the cluster. Everything the CLI set (its host name and stack name, a proxy, a storage class and size, your own database, its Elasticsearch, HTTPS to the engine) becomes a value in that values.yaml. On Swarm, the move deploys the published compose.yaml with docker stack deploy, the CLI’s host name goes in the .env, and what it set beyond the standard installation in a second compose file. The engine and PostgreSQL restart once, as for an upgrade: plan a short maintenance window, during which Command Center is unavailable and no task runs.
What carries over
The tenant, organizations, users, credentials, LLM providers and everything else configured in Command Center live in the database, so they carry over with it. The chart’s first run finds them and creates nothing.
Before you start
- Pick the build. If the chart’s release is newer than the one you run, the move is also an upgrade: the release notes of every release in between list, under Upgrade actions, what changed in what you apply. Read them as for any upgrade.
- Keep the CLI’s directory (
/etc/2501by default): it holds the manifests you go back with, and on Swarm the keys. - Pause anything that applies the CLI’s manifests again, such as a GitOps job.
- Get the files: download and verify the chart
2501-<build>.tgzandvalues.example.yaml, orcompose.yamlandenv.exampleon Swarm, from the build’s Release on github.com/2501-ai/install, and make the images reachable, see Pull the images from 2501’s registry. - Carry over the CLI’s settings, as below: into your
values.yamlon Kubernetes and OpenShift, into the.envand asite.yamlon Swarm.
The chart’s first run finds your tenant as the database’s only tenant and creates nothing once the installation has users and an LLM provider. Leave
identity and the LLM settings out of values.yaml, identity.tenantId above all: one that is not your tenant’s exact id makes the first run refuse the database (“pins tenant …, the license would not match”).
Kubernetes and OpenShift
The commands use the CLI’s namespace in$NS. Apply into the CLI’s namespace: its objects are the CLI’s.
The chart adds its own objects beside them: the ConfigMaps
2501-settings, engine-extra and license-public-key, a bootstrap Job and its ConfigMap, and the Ingress command-center (a Route on OpenShift).
1. Create the secrets
Copy the four keys and the database password from the CLI’s secrets into the chart’s, then check each copy. The database password is the one PostgreSQL already holds: a new one would not open it. The chart puts the database password into its database URL as is, so it may hold letters, digits and- . _ ~ only; the CLI generates one that fits.
engine.tls.enabled) and you keep HTTPS, set engine.tls.existingSecret: engine-tls in step 3, and copy the CLI’s certificate under the names the chart reads, beside the CLI’s own, which going back needs. It names the engine’s Service, <stackName>-engine, which keeps its name. To drop HTTPS, skip this: the engine then serves plain HTTP.
- the first administrator’s password: any value, it is only read by an installation without users;
- the LLM API key: your real key, read only by an installation without an LLM provider;
- your registry’s read-only login, which replaces
ecr-pull-secretand the AWS keys; cc-tls: the certificate for the host name, which the chart’s Ingress reads.
2. Back up the database
Back up the database withpg_dump while the CLI’s installation runs: the backup is not part of the move, it is your way back if the chart’s build migrates the database, or if anything goes wrong. With your own database, back it up as you usually do. Deleting a claim deletes the data on most storage classes; the move deletes none, but if your role may change PersistentVolumes, make the two volumes outlive their claims as well:
3. Prepare and review
Write yourvalues.yaml from values.example.yaml with the values the table above lists, the CLI’s host name in commandCenter.host and its stack name in stackName, and leave identity and the LLM settings out: the database already holds them. Then render and review:
executors.namespace set, the Role, RoleBinding and NetworkPolicy name that namespace and every other object names $NS: run the commands that read manifests.yaml without -n, since kubectl refuses it for a file that spans two namespaces. No selector changes. If you added a Route or an Ingress of your own for Command Center, save it and delete it before you apply: the chart’s Ingress claims the same host name.
4. Apply
The window starts here. Applymanifests.yaml, -n "$NS", --prune and its allowlist included, as in Apply. The CLI’s objects carry none of the chart’s labels yet, so pruning removes nothing of theirs.
Then check:
+ probe ... lines). While the database starts, migrate.sh: refusing to migrate with P1001 Can't reach database server can appear a few times: the bootstrap container exits, and Kubernetes restarts it after a delay that doubles each time, from 10 seconds up to 5 minutes, so its pod shows a few restarts and BackOff meanwhile. If it created a tenant, the chart is not on your data: go back (below). Then sign in as before, open a task from before the move to see its logs, and run a task on a host that uses a stored credential: it proves the credential key.
The CLI’s PostgreSQL ran on Debian 12 and the chart’s runs on Debian 13, whose C library sorts text in a newer version. PostgreSQL warns of a “collation version mismatch” on every connection until the indexes that sort text are rebuilt. Rebuild them once the chart’s PostgreSQL runs, with the engine and Command Center stopped so nothing writes through the old indexes meanwhile; each database reports changing version from 2.36 to 2.41:
5. Remove what the CLI left
Once you are satisfied, remove the CLI’s secrets and ConfigMap the chart does not read. Keepengine-extra-env, which the chart reads, engine-tls if you kept HTTPS to the engine, the claims, which hold its data, and, with the CLI’s Elasticsearch, elasticsearch-secrets and elasticsearch-tls, which it reads.
Going back
Until step 5, stop the chart’s engine, Command Center and bootstrap (which retries its migrations), restore your backup if the chart’s build is newer than the version you ran (its engine has migrated the database), then apply the CLI’s manifests again: the objects return to the CLI’s settings, with the data where it was. With ArgoCD, delete the application first without deleting its resources (non-cascading), or it applies the chart again:2501-settings, engine-extra, license-public-key, bootstrap-<hash>) and its secrets. Delete them, or the Ingress keeps the host name.
tls.crt and tls.key out of engine-tls.
The CLI’s Elasticsearch
Withelasticsearch.mode: external, the chart runs no Elasticsearch of its own, so the one the CLI ran (elasticsearch.enabled) stays the CLI’s: its Deployment, Service, claim elasticsearch-pvc and secrets carry only the CLI’s labels (app, stack), which the prune selector never matches, so applying the chart leaves it running as it is, with its logs. Point the engine and Command Center at it in your values.yaml, with the CLI’s values from engine-config (kubectl -n "$NS" get configmap engine-config -o jsonpath='{.data}'): its URL, https://elasticsearch:9200, and its index prefix, ELASTICSEARCH_ENVIRONMENT. Its certificate is the CLI’s self-signed one, so accept it as the CLI did, or trust the CA in elasticsearch-tls with caBundle (Corporate CA):
kubectl -n "$NS" exec deploy/elasticsearch -c elasticsearch -- curl -sk -u "elastic:<ELASTIC_PASSWORD>" 'https://localhost:9200/_cat/indices?h=index,docs.count'). It keeps running on the CLI’s image: update it later as your own object, or move its logs to an Elasticsearch you run.
OpenShift with the CLI’s PostgreSQL
On OpenShift the chart runs no PostgreSQL and no Elasticsearch, so the CLI’s cannot be taken over. Create your own database, empty, with a user that owns it, and itspg-connection-v1 secret, as in Create the secrets, and set database.managed: false. Before step 4, stop the CLI’s engine and Command Center and copy its database into yours from a machine that reaches it, with pg_restore 16 or newer; the chart then starts them again on yours:
postgres, elasticsearch), then their claims, which deletes their data.
Docker Swarm
The commands use the CLI’s directory in$D (/etc/2501, or ~/.2501/default for an install under your home directory) and its stack name in $STACK (deploy.stack_name in 2501-infra.yml, 2501 by default). Run them on the manager where you ran 2501 infra deploy, next to compose.yaml, your .env (from env.example) and a site.yaml of yours (see Adapt to your site).
The CLI ran the services engine and command-center in its stack, read their settings from env.engine and env.command-center, and, with postgres.enabled, a PostgreSQL container $STACK_postgres outside the stack, with its data in a directory of this node (postgres.data_path, /var/lib/2501/postgres by default). compose.yaml has the same engine and command-center services: deployed under the same stack name, it updates them in place, and adds the database, which takes over the container’s directory, and the bootstrap. The CLI ran no Elasticsearch on Swarm.
When the CLI ran PostgreSQL, its container holds the user, the database and the password the data was created with:
compose.yaml runs its database as user and database 2501. Give site.yaml the CLI’s instead, and its data directory as the volume, here for the defaults postgres and 2501 (the CLI’s container and ours keep the data in the same place, PGDATA=/var/lib/postgresql/data/pgdata):
$$(POSTGRES_PASSWORD) stays as it is: the services put the password from the postgres-password-v1 secret there when they start. Check the merge with docker stack config -c compose.yaml -c site.yaml: the three services show the CLI’s user in their DATABASE_URL, and postgres-data is 2501-postgres.
1. Create the secrets and the volume
Copy the four keys from the CLI’s files, and the database password from its container. Runval <name> <file> and pg POSTGRES_PASSWORD alone first: each must print a value, and a Docker secret cannot be corrected in place. The database password goes into the database URL as is, so it may hold letters, digits and - . _ ~ only (the CLI’s default, change-me, fits). If yours holds anything else, change it first, while the CLI still runs: ALTER ROLE <user> PASSWORD '<new password>'; in its database. The container keeps the password it was created with, so create the secret from the new one, printf %s '<new password>' | secret postgres-password-v1, instead of the pg POSTGRES_PASSWORD line below.
env.engine has no DIRECT_URL, use DATABASE_URL for both), and with your own Elasticsearch, its credentials. The engine connects with the URL exactly as the secret holds it. The CLI’s URLs often say sslmode=disable, so the commands below change it to verify-full, which the production profile requires. A URL with no sslmode keeps none: add sslmode=verify-full to its parameters yourself.
compose.yaml, once you have replaced every <...> value in them: a Docker secret never changes in place, and the commands skip a secret that exists, so a placeholder would stay. They create the first administrator’s password (any value, only read by an installation without users), your LLM API key and the registry login. Skip the docker stop line for now: step 2 runs it.
When the CLI ran PostgreSQL, give the stack its data directory as the Docker volume 2501-postgres your site.yaml names, and label this node, where the directory lives, for the database. Creating the volume does not touch the data:
2. Back up and stop the CLI’s PostgreSQL
The window starts here. Back up the database withpg_dump (with your own database, back it up as you usually do): it is your way back if the chart’s build migrates the database, or if anything goes wrong. Then stop the CLI’s PostgreSQL container, without removing it: two PostgreSQL servers on one directory damage it. The CLI’s engine and Command Center keep running, without a database, until the deploy replaces them.
3. Deploy the chart’s stack
Deploy under the CLI’s stack name, with your.env loaded and site.yaml merged over compose.yaml. The deploy stops with the name of any setting the .env still lacks.
docker stack deploy updates $STACK_engine and $STACK_command-center in place and starts the chart’s PostgreSQL on the CLI’s directory, under its user and database. The bootstrap log must report the tenant, the organizations, the first administrator and the LLM provider as already set. Its log starts with the migration step’s checks (+ probe ... lines). While the database starts, migrate.sh: refusing to migrate with P1001 Can't reach database server can appear a few times: the bootstrap container exits, and Swarm starts a new one 10 seconds later, up to 10 times. If it created a tenant, the stack is not on your data: go back (below). Then point your load balancer at COMMAND_CENTER_PORT if it changed, sign in as before, and run a task on a host that uses a stored credential.
The CLI created its database with the C locale, which sorts text the same on every system. If docker service logs "${STACK}_postgres" still warns of a “collation version mismatch”, rebuild the indexes as on Kubernetes, with docker exec in the postgres container and the CLI’s user.
4. Remove what the CLI left
Once you are satisfied:2501-postgres volume: they hold the chart’s database now. The CLI’s directory holds the keys in plain text: keep it somewhere protected, or delete it, then uninstall the CLI.
Going back
Until step 4, stop the chart’s engine, Command Center and bootstrap (which retries its migrations), and wait until their containers are gone, so that nothing writes to the database while it is restored: with your own database, they still reach it. Rungone on every node. It fails after five minutes; do not go on until it succeeds:
${STACK}-net, the volume 2501-postgres and the node label stay too: they are harmless, and a later move reuses them.
