Migrating from v6 to v7
TL;DR
- When upgrading to v7 - you will have to run
exoframe updatetwice - first to update server to latest version, then to let server upgrade Traefik to v3 (Exoframe v6 only upgraded to latest v2). - Exoframe CLI now required
deploysubcommand to be explicitly passed. So, instead ofexoframe [-u] [folder]you'd have to doexoframe deploy [-u] [folder] - Config for both Exoframe CLI and Exoframe Server is now located under
$XDG_CONFIG_HOME/exoframeinstead of$HOME/.exoframe - Traefik update to v3
- All deps updated to latest versions (Exoframe v7 also works on Node 25)
Breaking changes at a glance
- Node 18+ required: v7 switched the CLI/server codebase to native ESM. Make sure your local Node version (and the server host) runs Node 18 or newer before updating.
- Yarn removed: all packages now use npm. Delete
yarn.lock-only workflows or replace them with npm/pnpm equivalents. - Plugins removed: the plugin system is gone. If you depended on a plugin, consider replicating the behavior via templates or custom recipes.
- Swarm removed: Docker Swarm deployments and related CLI flags are no longer available. Exoframe now targets single-host Docker setups only.
- Docker Compose removed: compose files are no longer accepted as deployment input - see the section below for the recommended alternative.
- Exoframe FaaS removed: the function-as-a-service subsystem (
exoframe-faas) has been discontinued. - Config folder moved: CLI and server configs now follow
XDG_CONFIG_HOME(falls back to~/.config/exoframe). Update any scripts or mounts that pointed to the old location.
Why docker-compose support was removed
Exoframe v6 treated an entire docker-compose.yml file as one deployment. Every push forced the server to tear down and recreate all services in the compose file - even the ones that did not change, like databases, queues, or caches. That means longer deploy times, unnecessary downtime, and extra resource usage just to redeploy a single app container.
With v7 we lean into the "Exoframe way": describe each long-lived service in its own Exoframe config once, then redeploy only the service that changed. You keep the fast iterative workflow of Exoframe without repeatedly restarting stateful dependencies.
Example migration
Common local docker-compose setup:
version: '3.6'
services:
postgres:
image: postgres:15
ports:
- 5432:5432
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgrespassword
redis:
image: redis:7
ports:
- '6379:6379'
app:
build: .
ports:
- 8080:8080
depends_on:
- postgres
- redis
environment:
DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
REDIS_HOST: redis
volumes:
db_data:
Deploying that file with Exoframe v6 restarts postgres and redis on every deployment even if only the app changes. Instead, define each service once:
{
"name": "my-postgres",
"domain": false,
"image": "postgres:15",
"volumes": ["db_data:/var/lib/postgresql/data"],
"hostname": "mypostgres",
"env": {
"POSTGRES_PASSWORD": "postgrespassword"
}
}
{
"name": "my-redis",
"domain": false,
"image": "redis:7",
"hostname": "myredis"
}
{
"name": "my-app",
"domain": "my-app.com",
"env": {
"DATABASE_URL": "postgres://postgres:postgrespassword@postgres:5432/postgres",
"REDIS_HOST": "myredis"
}
}
Now redeploy the app without touching the database or cache, and only redeploy those infrastructure services when they change. The setup takes a few extra minutes once, and every deploy after that is simpler, faster, and safer.
We are exploring helper tooling that can auto-convert compose files into Exoframe configs, but the recommended path today is to define each service explicitly as shown above.
Deployment UUIDs persist across updates
Exoframe v7 keeps the same deployment UUID when you redeploy a project using exoframe deploy --update. The container name, EXOFRAME_DEPLOYMENT env var, and Traefik router IDs no longer get regenerated on every update, so CLI commands like exoframe logs <id> and exoframe rm <id> keep working across upgrades. This behavior also means that any automatically generated domain that comes from baseDomain stays identical after an update (<project-name>.<baseDomain>), eliminating the need to refresh DNS records or cached webhooks whenever you ship a new version.