Skip to main content

Migrating from a Legacy Deployment

If you already run FactoryThread via the older setup.sh + Azure Container Registry stack, this moves your data onto an ftctl-managed install.

The two stacks can't run at once

The ftctl stack uses the same container names and ports as the legacy one, so they conflict:

  • Running ftctl install while the old stack is up fails with a name/port conflict — it changes nothing, so this is a safe guard, not a risk.
  • The new stack uses a separate database volume, so it will not touch, reuse, or delete your existing database. Your old data is safe throughout.

You migrate the data explicitly, via backup → restore.

What must be carried over

SecretCarry over?Why
ENCRYPTION_KEYYes, requireddecrypts the connection credentials inside the restored data
POSTGRES_PASSWORD❌ Nothe new stack generates and uses its own consistently
POSTGRES_APP_PASSWORD❌ Nothe migrate step provisions the runtime role to match
RABBITMQ_PASS / REDIS_PASSWORD❌ Nofresh volumes, new values, self-consistent

A restore brings over data, not passwords — so a different DB password is fine. The one secret that must match is ENCRYPTION_KEY.

Step 0 — Confirm the schema versions match

The new bundle ships a fixed set of database migrations. Check how many the old database has applied:

docker exec factorythread-postgres \
psql -U factorythread -d factorythread -tAc \
"SELECT count(*) FROM drizzle.__drizzle_migrations;"

Compare it to the migration count in the release you're installing.

ResultMeaningWhat to do
Equalsame schemathe simple order below works
Old is lowerold DB is behindrestore into an empty DB first, then let migrate roll it forward (see note)
Old is higherold DB is ahead of this bundlestop — you'd downgrade; install a newer bundle
Verify, don't assume

A silent schema mismatch is the one thing that corrupts a migration. Run the count check before trusting that the versions line up.

Migrate

# 0. Read the OLD encryption key from the legacy env
grep '^ENCRYPTION_KEY=' /path/to/old/.env.deploy # copy the value

# 1. Back up the OLD database (the running factorythread-postgres is the old one)
sudo ./ftctl backup now --format custom # → /opt/factorythread/backups/<dump>

# 2. Stop the old stack (its volume is kept as a fallback — no -v)
cd /path/to/old/deploy
sudo docker compose -f docker-compose.deploy.yml down

# 3. Configure + load the new stack, but STOP before deploy
sudo ./ftctl install --version 2026.09.04.7 --plan
sudo sed -i 's|^ENCRYPTION_KEY=.*|ENCRYPTION_KEY=<OLD_KEY_FROM_STEP_0>|' \
/opt/factorythread/current/.env.deploy

# 4. Deploy (migrate is a no-op past a matching migration set)
sudo ./ftctl install --version 2026.09.04.7

# 5. Restore the old data
sudo ./ftctl backup restore /opt/factorythread/backups/<dump>
If the old DB is behind (Step 0 returned a lower count)

Swap the order so the data lands before the first migrate, then let migrate roll it forward. After Step 3, bring up only postgres:

cd /opt/factorythread/current
sudo docker compose -f docker-compose.deploy.yml --env-file .env.deploy up -d postgres
sudo ./ftctl backup restore /opt/factorythread/backups/<dump> # into the empty DB
sudo ./ftctl install --version 2026.09.04.7 # start the rest; migrate rolls forward

Rolling old data through the accumulated migrations is exactly what ftctl upgrade does — like any upgrade, test it on a spare box first.

Why the version bump is not a problem

The new stack runs PostgreSQL 18. This migration uses a logical dump (pg_dump), which restores cross-version forward into 18 — so an older source major (15/16) is fine. Reusing the old volume directly would not survive a major-version jump; dump + restore is the right mechanism, not a fallback.

Rollback safety net

Your old factorythread_postgres-data volume and the dump file both remain intact throughout. If anything goes wrong, bring the legacy stack back up and investigate before retrying — nothing in this process deletes the old data.