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 installwhile 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
| Secret | Carry over? | Why |
|---|---|---|
ENCRYPTION_KEY | ✅ Yes, required | decrypts the connection credentials inside the restored data |
POSTGRES_PASSWORD | ❌ No | the new stack generates and uses its own consistently |
POSTGRES_APP_PASSWORD | ❌ No | the migrate step provisions the runtime role to match |
RABBITMQ_PASS / REDIS_PASSWORD | ❌ No | fresh 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.
| Result | Meaning | What to do |
|---|---|---|
| Equal | same schema | the simple order below works |
| Old is lower | old DB is behind | restore into an empty DB first, then let migrate roll it forward (see note) |
| Old is higher | old DB is ahead of this bundle | stop — you'd downgrade; install a newer bundle |
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>
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.