Maestro

Update Maestro

How to move a running Maestro install to a new release without losing the workspace, agents, pipelines, secrets, or run history.

The short version: docker compose pull then docker compose up -d. The Postgres data is in a named Docker volume that survives container recreation, and the API container runs migrations on boot, so the standard flow is safe by default. The longer version below covers what’s actually persisted, how to back up before each bump, how to roll back, and the destructive operations that would lose data so you know to avoid them.


Where state lives

Three things together make up “your Maestro install”:

WhatWhereTouched by an update?
Workspace, agents, pipelines, contacts, activities, run history, secrets (encrypted)Postgres, in the named Docker volume maestro_pgdata_prodNo — docker compose up reuses the existing volume.
Config (MAESTRO_SECRET_KEY, ANTHROPIC_API_KEY, MAESTRO_PUBLIC_URL, …).env at the repo rootNo — read at container startup.
Cloudflare Tunnel configC:\ProgramData\cloudflared\ (or wherever you put it during deploy)No — separate process, separate machine state.

When you run docker compose down, the containers are removed, but the volume is preserved. When you docker compose up again, fresh containers attach to the existing volume and your data is exactly as you left it.

The destructive flag is --volumes (or -v). docker compose down -v deletes the data volume too. Don’t do that on a production install unless you genuinely want to wipe everything.


The standard update

Run from the repo root:

git pull
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env pull
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env up -d

What each step does:

  1. git pull fetches the latest compose file, migrations, skill manifests, and bundled scripts. Releases sometimes change these alongside the container images — for example a new env var on the api service, or a fresh skill. The Docker images alone don’t carry any of this.
  2. docker compose pull fetches the latest image layers from GHCR (ghcr.io/mcinnisdev/maestro-api, maestro-runtime). Only changed layers download.
  3. docker compose up -d recreates the api and runtime containers from the new images + the just-pulled compose file. Postgres is left running unchanged.
  4. The new API container’s startup script runs db:migrate (applies any new migrations) and db:bootstrap (idempotent — adds anything missing without touching existing rows). Both finish in well under a second on a small workspace.
  5. The runtime container reconnects to Postgres and resumes claiming runs.

Total downtime: roughly the time for the API/runtime containers to restart — usually 10–20 seconds. Cloudflare Tunnel buffers requests during the gap, so users typically see no error, just a brief delay on the next click.

Skipping git pull is the most common update gotcha. You’ll bump the image tags but miss compose-file changes — env vars introduced by the release won’t be set, services that should now exist won’t be wired, and you’ll get fetch errors or missing-config crashes that look unrelated. If anything’s behaving weirdly after an update, check git status against origin/main first.

What if the API container fails to start? Check docker compose logs api. The most common cause is a failed migration, which usually means the schema diverged from what the new release expects (e.g. you ran a manual ALTER TABLE outside the migration system). Fix the underlying issue, then re-run docker compose up -d.


Pinning vs. tracking latest

MAESTRO_VERSION in your .env controls which image tag to pull:

ValueBehaviorWhen to use
Unset / commented outResolves to latest — every pull fetches the newest tagged release.Convenient for early adopters who want each release as it lands.
MAESTRO_VERSION=0.1.5Pins exactly to that release. pull is a no-op until you bump the value.The right choice once you’re using Maestro for real. Forces you to opt in to upgrades.

latest only updates when there’s a newer image on GHCR — Docker doesn’t auto-restart your stack. So even on latest you control when an upgrade happens by choosing when to run pull && up -d.

To bump a pinned install:

git pull
# Edit .env: MAESTRO_VERSION=0.1.5  →  MAESTRO_VERSION=0.1.6
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env pull
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env up -d

Releases are tagged v0.1.5 in git and published as 0.1.5 on GHCR (no v prefix). Browse github.com/mcinnisdev/Maestro/releases for the changelog.


Back up before bumping

Once you have a real workspace, take a Postgres dump before any update. Restoring is the recovery path if the new release ships a bug or a destructive migration.

docker exec maestro-postgres pg_dump -U maestro maestro_prod > backup-$(date +%Y%m%d-%H%M).sql

Also back up MAESTRO_SECRET_KEY separately. The dump contains the encrypted secrets (Apollo key, Gmail tokens, etc.). Without the master key you used to encrypt them, those rows are unrecoverable. Keep the key in a password manager — not in the same place as the SQL dumps. See Security.

A simple Windows scheduled task that runs the pg_dump line nightly into a versioned filename is enough for a single-operator install. For design-partner deployments, copy the resulting .sql to off-machine storage (OneDrive, S3, Backblaze) at the same time.


Rollback

If a release goes badly, two recovery paths depending on what changed.

The release didn’t migrate the schema (most patch releases): pin the previous version and re-up.

# .env: MAESTRO_VERSION=0.1.4
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env pull
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env up -d

The release ran a destructive migration (rare): the old image won’t start cleanly against the new schema. Restore the pre-update DB dump:

# Stop the stack so nothing is writing
docker compose -f infra/docker/docker-compose.prod.yml down

# Find the data volume's actual name — Docker Compose prefixes it with
# the project name (typically `docker_` when running from the
# infra/docker/ compose file).
docker volume ls | grep maestro_pgdata_prod
# e.g. local     docker_maestro_pgdata_prod

# Wipe it so the restore populates a clean schema
docker volume rm docker_maestro_pgdata_prod

# Bring Postgres back up alone
docker compose -f infra/docker/docker-compose.prod.yml up -d postgres

# Restore the dump
cat backup-2026-05-05-1430.sql | docker exec -i maestro-postgres psql -U maestro -d maestro_prod

# Pin the old version, bring up the rest
# (edit .env: MAESTRO_VERSION=0.1.4)
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env up -d

The release notes will call out destructive migrations explicitly. Read them before bumping if you’re not on a plain latest workflow.


What not to do

These operations destroy the data volume. Don’t run them on a production install unless you truly mean to wipe everything.

# Deletes all containers AND the data volume
docker compose -f infra/docker/docker-compose.prod.yml down -v

# Manually removes the volume
docker volume rm docker_maestro_pgdata_prod

# Removes the Maestro Postgres container with its data attached (the
# volume is named, so it's safe — but if you previously deleted the
# named volume, the new container creates an empty one)
docker rm -f maestro-postgres   # then up -d → fresh DB

If you’ve been “deleting the docker container and rerunning” and your data has actually persisted, that’s because the volume is named (maestro_pgdata_prod) and survives container deletion. The pattern works but is more invasive than needed — docker compose pull && up -d does the same job without removing containers manually.


Verifying which version is running

docker compose -f infra/docker/docker-compose.prod.yml images
# Look at the IMAGE column for `api` and `runtime` — the tag after the
# colon is the running version (e.g. ghcr.io/mcinnisdev/maestro-api:0.1.5).

Or hit /api/health from the box and look at the response headers if a future release surfaces it there. (As of v0.1.5, only {"ok":true} — version surfacing is on the v1.x QOL list.)


Auto-updating with Watchtower (optional, advanced)

If you want unattended updates while tracking latest, Watchtower polls GHCR and restarts containers when new digests appear. Add this service to your compose file:

  watchtower:
    image: containrrr/watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --cleanup --interval 3600 maestro-api maestro-runtime

Watchtower checks every hour, pulls if newer, restarts the listed containers. Migrations still run on API startup as before. Don’t enable Watchtower on installs you can’t quickly revert — the rollback flow above gets you back, but the easier-life setup is a pinned version with manual pull && up -d when you’re ready.


TL;DR

# Standard update — keeps all data, runs migrations, ~15s downtime
git pull
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env pull
docker compose -f infra/docker/docker-compose.prod.yml --env-file .env up -d

Don’t pass --volumes to down. Don’t skip git pull — image bumps and compose-file edits ship together. Back up Postgres before bumping once your workspace is real. Pin MAESTRO_VERSION once you’re using Maestro for actual work.