Skip to content
NebulaCtrldocs

Back up and restore the control plane

Dump the control plane database, keep the master key and .env that seal its secrets, and restore both onto the same host or a new one.

A control plane backup has two parts that are useless apart: a dump of the PostgreSQL database, and the .env file that holds the master key. NebulaCtrl takes neither for you. Neither a console update nor the installer makes a backup.

Before you start

  • Know what is where. The database holds every project, service, release, member, audit event and sealed secret. .env holds the master key (NEBULA_MASTER_KEY), the database password, the OIDC client secret and the updater token.
  • Use the same install directory. The commands below assume /opt/nebula. Replace it if you installed elsewhere. If your user cannot reach the Docker socket, prefix docker with sudo.
  • Decide where the backup lives. A dump plus the master key opens every secret. Store them in different places, with different access.
  • Plan the rollback of a restore. A restore replaces the database. Dump the current database first, even if it looks broken, so you can go back.

What is not in the dump

  • Cluster state. Workloads run in the clusters. A restore does not touch them. Agents reconnect to the restored control plane and receive its desired state.
  • Object storage. Volume backups, database service dumps and point-in-time archives live in the object stores you connected under Settings > Integrations. The database holds the credentials (sealed) and the list of backups, not the data. Back up that bucket with your storage provider's tools.
  • Anything written after the dump. That includes clusters enrolled after it, which the restored database does not know.

1. Dump the database

docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres pg_dump -U nebula --format=custom nebula > nebula.dump

Name the file with the date and the running version, for example nebula-20261002T0900Z-0.39.0.dump. The custom format is compressed and restores with pg_restore.

2. Copy the master key

install -m 600 /opt/nebula/.env nebula.env

nebula.env is a full copy of .env. The installer also leaves .env.bak-TIMESTAMP files beside it when it changes .env; they hold the same secrets, so treat them the same way.

Nobody can regenerate the master key

If you lose the master key, the database is still readable, but every sealed value in it is gone for good: service variables, registry passwords, Git connection secrets, object store keys and Cloudflare tokens. You would re-enter each one by hand.

3. Move both off the host

Copy nebula.dump and nebula.env to storage that does not depend on this host. Keep the master key in a secret store, and the dump in ordinary backup storage.

Verify the backup

docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres pg_restore --list < nebula.dump

The command prints the dump's table of contents without touching a database. A truncated file fails with an error. With an external PostgreSQL, run pg_restore --list nebula.dump where the PostgreSQL client tools are installed. Restore the dump into a scratch database at least once, so you know the procedure works before you need it.

Restore the control plane

Restore with the same release as the dump or a newer one, never an older one. Migrations only move forward, and a newer release applies the pending ones when it starts.

  1. Put the saved .env in place. On the same host, it is already there. On a new host, create the install directory and copy nebula.env to /opt/nebula/.env with mode 600, then run the installer once so that it writes the Compose file and starts an empty installation:

    curl -fsSL https://get.nebulactrl.dev | sh -s -- --dir /opt/nebula --yes

    The installer keeps the master key and every other setting it finds in .env.

  2. Stop the control plane. Leave the database running.

    docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db stop nebula
  3. Recreate the database empty, with TimescaleDB. This deletes the current contents.

    docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres psql -U nebula -d postgres -c 'DROP DATABASE nebula' -c 'CREATE DATABASE nebula'
    docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres psql -U nebula -d nebula -c 'CREATE EXTENSION IF NOT EXISTS timescaledb' -c 'SELECT timescaledb_pre_restore()'
  4. Load the dump.

    docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres pg_restore -U nebula -d nebula --no-owner < nebula.dump
  5. Return TimescaleDB to normal operation.

    docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres psql -U nebula -d nebula -c 'SELECT timescaledb_post_restore()' -c 'ANALYZE'
  6. Start the control plane. Migrations run at start.

    docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db up -d nebula

With an external PostgreSQL, omit --profile local-db from the commands. Run steps 3 to 5 against your server with psql and pg_restore, as a user that can create databases and extensions. Restore into a new empty database, then point the control plane at it by running the installer with --database-url NEW_URL, which updates .env.

Verify the restore

curl -s http://127.0.0.1:8080/healthz
{"status":"ok","components":{}}
  • Sign in. Your organizations, projects and members are back.
  • Open a service's Variables tab and reveal a secret value. It reads correctly only when .env holds the master key that sealed it.
  • Open Clusters. Each cluster shows connected within a minute or two. A cluster enrolled after the dump is missing; connect it again.

If it goes wrong

pg_restore prints errors about the timescaledb extension. The target server lacks TimescaleDB 2.13 or newer in shared_preload_libraries, or you skipped timescaledb_pre_restore(). Recreate the empty database and repeat from step 3. See Requirements.

The control plane starts but secrets are unreadable. The .env in place holds a different master key from the one that sealed them. Put the saved .env back and restart the nebula container. If a rotation happened between the dump and now, add the keys to NEBULA_PREVIOUS_MASTER_KEYS; see Rotate the master key.

The restore made things worse. Load the dump you took before you started, the same way.

On this page