Skip to main content
An installation the 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

  1. 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.
  2. Keep the CLI’s directory (/etc/2501 by default): it holds the manifests you go back with, and on Swarm the keys.
  3. Pause anything that applies the CLI’s manifests again, such as a GitOps job.
  4. Get the files: download and verify the chart 2501-<build>.tgz and values.example.yaml, or compose.yaml and env.example on 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.
  5. Carry over the CLI’s settings, as below: into your values.yaml on Kubernetes and OpenShift, into the .env and a site.yaml on 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.
When the CLI served the engine over HTTPS (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.
With the CLI’s Elasticsearch, or your own, copy the engine’s credentials for it too:
Then create the other secrets, as in Create the secrets, skipping the ones you already created:
  • 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-secret and 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 with pg_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 your values.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:
The diff shows each CLI object the chart changes, and the chart’s own objects as new. With 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. Apply manifests.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:
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 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. Keep engine-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:
The CLI’s PostgreSQL warns of the collation version the other way: rebuild the indexes as in step 4. The CLI’s Elasticsearch never stopped: it goes back as it is. With the backup restored, what changed on the chart’s installation since the window is lost; without it, those changes stay. Applying the CLI’s manifests leaves the objects only the chart has: its Ingress, its ConfigMaps (2501-settings, engine-extra, license-public-key, bootstrap-<hash>) and its secrets. Delete them, or the Ingress keeps the host name.
To move again later, start over from step 1: going back removes those secrets, and the CLI’s manifests take tls.crt and tls.key out of engine-tls.

The CLI’s Elasticsearch

With elasticsearch.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):
Check after step 4: the document counts in its indices grow (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 its pg-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:
The restore must end without an error. If one stops it, recreate your database empty before you try again. The chart leaves the CLI’s PostgreSQL and Elasticsearch running: once you are satisfied, delete their Deployments and Services (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. Run val <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.
With your own database, copy its URLs (if 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.
Then run the commands at the top of 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 with pg_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:
Keep the data directory and the 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. Run gone on every node. It fails after five minutes; do not go on until it succeeds:
When the CLI ran PostgreSQL, remove the chart’s database service and wait until its container is gone, so that the directory has one server again, then start the CLI’s PostgreSQL container:
If the chart’s build is newer than the version you ran, its engine has migrated the database: restore your backup now, before the CLI’s engine runs on it. With your own database, restore your own backup of step 2 into it; into the CLI’s PostgreSQL:
Remove the chart’s image services, which the CLI’s deploy would count as not ready, then deploy the CLI’s installation again from its directory, at the version you ran: it updates the engine and Command Center back.
With the backup restored, what changed on the chart’s installation since the window is lost; without it, those changes stay. The chart’s secrets, its network ${STACK}-net, the volume 2501-postgres and the node label stay too: they are harmless, and a later move reuses them.