Skip to content

v0.3.0.

Complete release notes · 2026-08-26. Read upgrade instructions and runtime limits before deploying.

Added

  • Tags Across Three Dimensions: Services carry many projects, workspaces and labels at once (service_projects, service_workspaces, service_labels). The top bar filters by all three — AND across groups, OR within one — and the selection persists per browser under ninedeploy.tagScope. A new Projects page manages the flat project list, and a per-service Tags card edits one service's membership in a single round-trip through PUT /v1/services/:id/tags.
  • Per-Service Volume Attachments: A service can mount any number of managed Docker volumes at explicit container paths, read-only or read-write, with a uniqueness guard on both the path and the volume. Detaching records the change only — the underlying volume is never deleted.
  • Volume Backups: Snapshot, restore and download any managed volume through /v1/volumes/:name/backups. Snapshots reuse the database-backup destination for off-site copies, prune to a retention cap, and refuse to restore while the owning service is still running.
  • .ninedeploy Manifest: ninedeploy manifest {init,validate,show} scaffolds, schema-checks and prints the repo-side manifest. validate runs the same secret scan the server uses and rejects a file carrying credential-shaped values before they reach git history. The deploy pipeline applies the manifest itself on every deploy — the build sections at build time, the operational sections (routes, alerts, database ref) at deploy time; manifest apply is wired into the CLI surface but reports that its server endpoint has not shipped yet.
  • Private Repository Deployment From The CLI: ninedeploy sources and ninedeploy webhooks manage encrypted source credentials, server-generated SSH deploy keys and auto-deploy webhooks from the terminal.
  • Workspace Invitations: Invite an address that has no account yet; the invitation is accepted automatically on the invitee's next login or registration.
  • Self-Healing Runtime State: At startup and every 60 seconds the panel compares each local service's desired state with the live runtime and revives anything that should be running — stopped containers are started (Compose sidecars included), a dead PM2 daemon is resurrected from the process dump, and stopped PM2 processes are restarted. A runtime that was deleted is marked error for redeploy instead of being reported as running, so a reboot, daemon crash or external docker stop can no longer leave the panel lying or services dead.
  • Boot Resilience: The installer unconditionally enables the Docker daemon at boot (previously only when it installed Docker itself), so pre-existing Docker installations no longer leave Traefik, deployed containers and the panel dead after a reboot. A new ninedeploy-pm2 systemd unit resurrects bare-metal PM2 deployments at boot from a process dump the server refreshes after every lifecycle change, and Compose deployments apply the unless-stopped restart policy to their containers so they survive daemon restarts and reboots.
  • Streaming Encrypted Backups: Database dumps are encrypted and decrypted as AES-256-GCM streams, so large backups no longer need to be loaded into server memory for creation, download or restore. Existing encrypted envelopes and legacy plaintext backups remain readable.
  • Read-Only MCP Mode: Setting NINEDEPLOY_MCP_READONLY=1 exposes a fail-closed allowlist of inspection tools and excludes mutations, secret-bearing configuration, container inspection, Compose and file operations.
  • Dashboard Crash Recovery: Unexpected React render failures now show a recoverable error screen instead of leaving the dashboard blank.
  • Override-Aware Installer Defaults: Managed-database engine defaults now follow the latest Docker Hub GA (MySQL 9.7, Mongo 8.0, MariaDB 12.3, Redis 8.8, Valkey 9.1, ClickHouse 25.8, Postgres 18, RabbitMQ 4, Meilisearch v1.53). Every engine still accepts a per-row version override, and the bare-metal / Docker installers accept NINEDEPLOY_NIXPACKS_VERSION=<release> so operators can pin to the previous LTS or a known-good buildpack without a code change.
  • Latest Runtime & Toolchain Across The Stack: Node.js 24 → 26 (latest GA, Aug 2026), pnpm 11.22 → 11.23, plus patch bumps for jose (6.2.10), @tanstack/react-query (5.102.2), @biomejs/biome (2.5.10), @testing-library/user-event (14.6.6) and @types/react-dom (19.2.5). Dockerfile, docker-compose and CI all pin Node 26; .nvmrc added so nvm use / fnm use always lands on the right major.
  • Reachable Tag Management: The panel gained an Organize navigation group holding Workspaces, Projects and Labels. Projects previously had a route with no navigation entry — it was reachable only by typing the URL — and labels had no management screen at all: they could be created as a side effect of the top-bar filter but never renamed, recoloured or deleted. The new Labels page is full CRUD over the eight-token palette, and clicking a project or label row scopes the services list to it.
  • Volume Snapshots From The Volumes Page: Snapshot, restore and download were reachable only from a service's Volumes tab, so a retained (owner-less) volume had no backup UI anywhere. Every card on Data → Volumes now expands the same panel.
  • Installer Installs The Release, Not A Clone: On the release channel install.sh downloads the source tarball GitHub publishes for the tag instead of cloning, so a host needs no git and cannot land on a half-fetched object. The tag itself is resolved from git ls-remote, then the GitHub releases API, then the tags API — a single unavailable source can no longer pin an upgrade to a stale version. --force (or NINEDEPLOY_FORCE=1) discards local modifications and rebuilds from scratch.

Changed

  • Command Palette Ranks Before It Truncates: Every navigation entry carries the type Navigate, so a single-letter query matched all of them at once and filled the 24-result cap before any service, database or template could appear. Label matches now outrank description matches, which outrank type matches. The palette also lists Manifest Creator, Workspaces, Projects, Labels, Networks, Traefik and Docker, which it had never indexed.
  • Menu Permissions Fail Closed: getItemsForSlot takes an operator boolean rather than a role string, and an item gated on permission: 'admin' is hidden when the flag is absent instead of shown to everyone.
  • Project Env Resolution Follows The Link Table: Project-scope shared environment variables are the union of every project a service is linked to, replacing the single services.projectId lookup.
  • Visible Installer Progress: The installer's long silent phases no longer look like a hang. Node.js, Docker and base-package APT installs stream apt's own progress lines (Get:/Unpacking/Setting up) live with a still-working heartbeat, expected durations are printed before the big downloads, and failed APT commands surface their error tail instead of failing silently.
  • Reusable Health Probes: Docker readiness checks reuse one supervised ninedeploy-prober container instead of creating an ephemeral container for every retry.
  • Serialized Singleton Lifecycles: PM2 sessions and Traefik recreation are serialized, preventing concurrent callers from disconnecting active PM2 work or racing to replace the shared proxy container.
  • Header-Based WebSocket Authentication: Current dashboard clients carry bearer credentials in the WebSocket subprotocol header instead of query strings, reducing exposure through URLs and proxy history while preserving compatibility for older clients.

Fixed

  • Stale Panel Bundle After An Upgrade: apps/web/dist is gitignored, so a checkout never replaced it — an upgrade whose build was skipped or cached kept serving the previous release's dashboard, which is exactly the "upgraded but the UI still shows the old version" report. The installer now clears dist/ and the turbo cache before building, verifies the built package.json version against the requested tag, and fails outright if apps/web/dist/index.html is missing afterwards.
  • Upgrades From A Shallow Clone: fresh installs were cloned with --depth 1, so git fetch --tags succeeded while fetching none of the objects a newer tag needs and the checkout silently stayed on the old commit. The installer deepens a shallow checkout first, repoints a renamed origin at the canonical repository, force-fetches with --prune-tags, and hard-resets to the fetched ref.
  • Lexical Tag Sorting: the installer's tag resolution relied on sort -V, which busybox and BSD coreutils either lack or ignore — ranking v0.2.9 above v0.2.36 and pinning those hosts to a stale release. Version components are zero-padded before a plain lexical sort, which is correct everywhere.
  • Dead Project Links: the Projects page linked to /services?projectId=N, but the services list reads its filter from the shared tag scope and ignores that query string, so the link navigated without filtering. Both Projects and Labels now set the chip scope.
  • Wrong In-Panel Release Notes: the About page's changelog carried the 0.2.31 Ghost release notes under the 0.3.0 heading and was missing 0.2.31 through 0.2.36 entirely.
  • Committed Coverage Artifacts: apps/web/cov-json*/coverage-final.json (1.6 MB of v8 reporter scratch output) were tracked in git. Removed and ignored.
  • Server Would Not Build Or Start: volumeBackups and serviceVolumes imported backupVolume, restoreVolume and createDockerVolume from the database engine, but none of the three had been written. The package did not compile, and loading either module at runtime would have failed the import outright. All three are implemented, snapshotting and restoring through a throwaway sidecar container so a containerised panel never needs a path into the daemon's storage directory — and a restore empties the volume first rather than merging the archive over whatever was already there.
  • Half-Migrated Service Tagging: The schema, database, web dashboard and tag endpoints had all moved to the N-N model while modules/services.ts still read and wrote the removed services.projectId. Listing now filters on tagProjectIds / tagWorkspaceIds / tagLabelIds, responses carry the three id arrays the API contract declares, a created service picks up its requested tags (or every workspace the caller belongs to), and a clone inherits the original's tags. The CLI's users.role reads move to the derived isOperator flag for the same reason.
  • Blocked Upgrade From 0.2.2 And Later: Releases from 0.2.2 added databases.owner_user_id through the server's boot-time self-healing step, before an equivalent SQL migration existed. The new migration then tried to add the same column, so Drizzle's batch migrator aborted the whole upgrade with duplicate column name: owner_user_id and the panel never started. The runtime migrator now retries such a batch statement by statement, skipping only objects that already exist and journalling the migration so the upgrade completes.
  • Disappearing Label Chip: A label created from the top-bar filter was selected and then immediately pruned, because the tag scope's own label query still held the pre-creation list. Both queries are refreshed before the new chip is applied.
  • Corrupted Source Encoding: 0.2.36 shipped 90 files whose non-ASCII characters had been double-encoded — · written as ·, em dashes as three characters, and several emoji mangled past a plain round-trip. Comments in the databases, invitations and jobs modules were affected alongside the test suites; every occurrence is restored.
  • Workspace Role Fields: A bad rename replaced the role field with isOperator on workspace members, invitations and SSO provider defaults in the test fixtures, so those suites asserted a contract the API never had. The canonical role naming is restored.
  • Fresh-Install Watchdog False Positive: The post-install systemd policy check rejected WatchdogUSec=infinity — the modern systemd spelling of a disabled watchdog on a never-started unit — and aborted the installer at the very end of an otherwise successful fresh installation. infinity is now accepted alongside 0.
  • Honest Service Lifecycle: stop/start/restart no longer swallow Docker/PM2 failures while still writing a success status to the database. Starting or restarting a runtime that no longer exists now returns 409 and marks the service error, an unreachable Docker daemon returns 503, and stopping an already-gone runtime is treated as the idempotent success it is.
  • Bounded Clone Slug Generation: the clone slug-deduplication loop is now bounded, so a pathological collision run cannot spin forever.
  • Tenant-Scoped Inventory Views: Domain, metrics, topology, network and volume responses now exclude resources outside the authenticated non-admin user's ownership scope.
  • Safer Preview Deployments: Pull-request previews reject invalid refs and external fork repositories before they can inherit service environment variables or enter the build queue.
  • Atomic Deployment Claims: Competing worker slots can no longer claim two queued deployments for the same service at the same time.
  • Hardened Secret Handling: Docker environment files escape multiline values, runtime log redaction handles quoted credentials, config secrets require an explicit admin reveal request, webhook token comparison hides secret length, and installer-created .env files are restricted to mode 0600.
  • Reliable Startup and UI Controls: Database connection PRAGMAs finish before migrations and application queries begin; CLI reachability checks require the real health endpoint; terminal clearing no longer reconnects the session.

Documentation

  • Four Missing Guides: the marketing site documented none of the 0.3.0 surface. Added Tags: Projects, Workspaces & Labels, Volumes & Storage, The .ninedeploy manifest and Private repos, sources & webhooks, wired them into the docs mega-menu, and corrected the hard-coded guide count (17 → 21). The features page gained the tag dimensions, volume attachments, volume snapshots, workspace invitations and the manifest.

Verified

  • Full Suite Green: 4,405 tests across the workspace pass and every package meets its coverage gate — 100% in db, schemas, sdk and the CLI, 99%+ statements in the web app, 95%+ in the server. New suites cover the Projects page, the top-bar filters, the service Tags card, the volume-backups panel, and the label, service-tag and volume-backup APIs.
  • Upgrade Recovery Against A Real Database: The duplicate column name recovery path is exercised against an actual 0.2.2-era SQLite file and pinned by a regression test that reproduces the unjournalled-migration state.

Source CHANGELOG.md at 6046cfe8