Skip to content

v0.18.26.

Complete release notes · 2026-11-09. Read upgrade instructions and runtime limits before deploying.

Added

  • The panel on its own domain, with HTTPS, from the installer. install.sh --domain panel.example.com --email you@example.com serves the panel on that hostname, with a Let's Encrypt certificate from the first boot, and makes it the public URL.
    • Without the flags, a first install on a terminal asks for both. Re-runs and self-updates never ask, and an explicit flag replaces the value in .env.
    • Both values are checked strictly before anything runs, because bare metal sources .env.
    • The installer warns when the domain does not point at the server yet. It prints the setup link for the domain and for http://<server-ip>:3000, which works before the certificate exists.
    • The Docker compose file now passes NINEDEPLOY_DOMAIN to the panel. A compose file from an older pinned release does not, and the installer says so.
    • The first-run form offers to use the admin's email for Let's Encrypt when none is configured (POST /v1/setup takes acmeEmail; GET /v1/auth/status reports acmeConfigured before setup only). It never replaces an email that is already set.
    • A domain install still seeds free sslip.io service URLs: the installer records the server address in NINEDEPLOY_PUBLIC_IP.
  • A stable update channel. Until now, the panel offered every release as soon as it was published. An operator can now choose stable under Settings → System → Update channel, or with GET/PUT /v1/system/update-channel (SDK system.updateChannel). Stable offers only the release the stable tag points at. Every release stays the default, so nothing changes until you switch.
    • A daily workflow (.github/workflows/stable-channel.yml) moves the stable tag to the newest release that has been out for 7 days.
    • A release whose GitHub release notes carry <!-- stable: security --> is promoted at once. A maintainer can also promote a tag by hand from the workflow. One marked <!-- stable: hold --> is never promoted.
    • The tag never moves backwards.
    • The installer takes the same channel: install.sh --channel stable. It refuses, with a clear message, while no stable release exists yet.
    • The installer also writes NINEDEPLOY_UPDATE_CHANNEL=stable into .env, so the panel follows the same channel until an operator picks one in Settings.
  • What a service can use, in one place. GET /v1/services/:id/capabilities (SDK services.capabilities) answers, for nine features, whether each is available, limited, still being built, or refused where the service runs, with the reason. The features are maintenance mode, error pages, forward auth, restart limit, inline Dockerfile, file mounts, autoscaling, scale to zero and canary releases.
    • The routes now refuse through the same check, with the same messages as before.
    • The maintenance, restart-limit and forward-auth cards follow it. For example, the maintenance card no longer appears on a panel-host service that also runs on nodes, where saving was refused.
    • Service → Settings lists the limits with their reasons.
    • docs/SUPPORT_MATRIX.md is generated from it, so the docs cannot drift from the code. It also corrects one claim: an inline Dockerfile on a node service works only with Build on: panel.
  • A native macOS development bootstrap (node scripts/dev-macos.mjs --install) and cross-machine handoff guide in docs/MACOS_DEVELOPMENT.md, including the separate desktop lockfile and the remaining DMG/Keychain acceptance checks.
  • An IP allow-list for the panel itself. Turn it on under Settings → Security → Panel access, or with PUT /v1/settings/panel-access (SDK settings.panelAccess). It is off by default and operator only. When on, only the listed IPv4 or IPv6 addresses and CIDR ranges can sign in or use the panel with a session, over HTTP and WebSocket.
    • Integrations keep working from anywhere: git, registry and deploy webhooks, badges, status pages, CI runners, SCIM, error ingest, and the app traffic the panel serves for wake, forward auth and maintenance pages.
    • API and remote-MCP tokens are only restricted if you also turn on Also restrict API and MCP tokens.
    • A list that would exclude your own address is refused, and the card offers Add my address.
    • If you lock yourself out, set NINEDEPLOY_PANEL_ACCESS=off and restart the panel. Docker installs need the line NINEDEPLOY_PANEL_ACCESS: ${NINEDEPLOY_PANEL_ACCESS:-on} in their compose file. The installer writes it from this release; an older compose file does not pass the variable through. See docs/TROUBLESHOOTING.md §14.

Changed

  • The Plugins catalog now points its PostgreSQL PITR, Sentry and Tailscale entries at the built-in features. Entries with no shipped equivalent say "Not available" instead of "Planned".

Fixed

  • CI runner revocation fences both lease creation and assignment; pending attempts are lost/requeued before publication, and stale claims cannot overwrite a newer runner's attempt (F1063).
  • Concurrent CI claims reserve runner capacity inside the attempt insert; a losing request leaves its job queued without consuming an attempt or reviving cancelled work (F1062).
  • CI cancellation closes the attempt current at cancellation time, and claims that lose assignment to cancellation retire their attempt instead of returning cancelled work or retaining runner capacity (F1061).
  • Three web panel faults the first browser journeys found.
    • Every page tried to load Google Fonts, which the panel's own content security policy blocks. Each page load logged a console error, and loading the fonts would have sent every visitor to Google. The import is gone. The panel uses Inter and JetBrains Mono when they are installed, and the system fonts otherwise, which is what it already showed.
    • The sign-in page asked for the project list before anyone was signed in, and got a 401.
    • The Settings and service pages each had a second <main> element. A page should have only one; the Layout owns it.
  • CI expiry sweeps recheck the persisted lease atomically before declaring an attempt lost, preserving heartbeat/restart renewals and counting only attempts actually lost (F1060).
  • Scheduled deploys count CI-waiting rows toward the shared queue limit and reserve the last slot atomically, auditing concurrent losers as skipped while preserving the schedule trigger (F1059).
  • Scheduled jobs keep their active crons while a refresh query waits or fails, preserving upcoming job deadlines until a successful reload replaces the schedule set (F1058).
  • Image scan schedulers suppress critical-result callbacks after their application closes, preventing late audit alerts from a retained scan callback (F1057).
  • Advice that cut a panel off from its own domain. Doctor, the README, the compose file and the panel IP allow-list docs said to bind the panel to 127.0.0.1 once it had an HTTPS domain. Traefik reaches the panel through the Docker gateway, so after that change the domain stopped answering. This was proven in a Linux Docker-in-Docker run: a loopback-bound panel is unreachable, 0.0.0.0 is reachable. The advice now is to keep port 3000 reachable from Docker and to block it from the internet with a firewall in front of the host. The README's first-boot steps also pointed at Settings → System for the ACME email. It lives under Settings → Security.
  • GitOps result publication respects configuration removal/replacement, preserves cleared state and protects newer results; stale checks report a conflict and completed apply results remain available to their caller (F1056).
  • Instance GitOps scheduler ticks no longer start new checks or publish late drift/failure alerts after their application closes (F1055).
  • Active PITR retention now excludes competing pruning, recovery, base-backup and disable operations on the same archive; backup requests report a conflict instead of accepting work that cannot start (F1054).
  • PITR recovery admission reserves capacity before asynchronous name/volume checks, keeping concurrent requests within the one-recovery limit and releasing capacity on admission failure (F1053).
  • PITR maintenance reloads the database's retention policy before pruning, honoring retention increases committed while the archiver query or a base backup was running (F1052).
  • Panel backup scheduler ticks waiting for settings now stop when their application closes, preventing a late backup from starting against closed resources (F1051).
  • Panel backup retention now rechecks the selected destination and keep count before each deletion, preserving old recovery artifacts when settings change during an upload or prune (F1050).
  • Loki log searches keep a reverse proxy's path prefix when deriving the query endpoint from a configured /loki/api/v1/push URL.
  • Log delivery and Loki search apply header precedence without regard to name casing, avoiding duplicate authentication and Content-Type values from differently cased custom headers.
  • Loki log searches preserve the drain's custom headers, including tenant IDs, and avoid duplicating an existing Bearer prefix, matching log delivery authentication.
  • ZIP source uploads publish through a complete temporary file, so a partial write cannot corrupt an already accepted archive or expose a failed upload as deployable source.
  • Failed .tar.gz source upload writes and final moves remove their owned temporary files while preserving an already published archive and its retention history.
  • HTTP readiness probes tolerate unexpected informational replies such as 103 Early Hints and wait for the final status instead of rejecting the workload early.
  • Concurrent identical source archive uploads use separate temporary files, so one request cannot consume or remove another request's pending upload.
  • Failed system imports remove newly installed database, key, routing and environment files when those paths did not exist before the import, preserving the original state during rollback.
  • Inline Compose workspaces accept a repository directory configured at a filesystem root while continuing to reject invalid service IDs.
  • S3 build-cache misses cancel unused HTTP response bodies, releasing transport resources while preserving the cache-miss result if cancellation fails.
  • S3 multipart backup uploads fill short filesystem reads and abort on premature EOF instead of completing an object containing unread buffer bytes.
  • Truncated encrypted backup envelopes are rejected before opening a plaintext output, preventing late empty staging files and leaked write descriptors when the authentication tag is incomplete.
  • Backup replica retention rechecks the current destination list and copy limit before every remote delete. Removing a replica or raising its retained-copy count during a slow upload no longer lets the old policy delete recovery points afterward.
  • Image cleanup no longer reports images removed or space reclaimed when both deletion and the follow-up inspection are unconfirmed; a daemon, permission or timeout error is kept separate from a missing image.
  • Webhook replay detection keeps delivery IDs separate per provider and service, so one repository event can reach each configured service. Docker runtime patrols skip unreadable inspections instead of reporting a missing container and a false outage.
  • Cloning, exporting and importing a service no longer drops settings kept outside the service record. These settings were silently lost: restart limit, custom error pages, CI pipeline, commit statuses, sticky sessions, maintenance mode, and per-domain forward auth and compose routes. Before, only the inline Dockerfile survived a clone, and an exported inline-Dockerfile service lost its Dockerfile.
    • Each value is checked with its own route's validation. Anything the new service cannot use is skipped and listed in the import response.
    • A clone starts outside maintenance mode.
    • Secrets (the registry push-webhook token and the error-ingest key) never travel; the import names them so you can create new ones.
    • Older bundles import as before. See docs/DEPLOYMENTS.md.
  • Deleting a service, database, domain, GitHub App or SSO provider now also deletes its stored settings. Several features keep per-item state in the panel's settings table rather than in their own tables: restart limits, maintenance pages, forward auth, compose routes, registry-hook tokens, error-ingest keys, backup replica logs, runner configurations and SAML identity links. Until now this state was left behind after its owner was deleted. That included the hashed registry-hook token and the error-ingest key.
    • Leftover rows from earlier deletes, and rows of deleted workspaces, are removed by the hourly housekeeping run.
    • Cloud-VM links are kept, so a VM created through the panel can still be deleted from it.
    • IDs are never reused, so no new item ever picked up an old row. The restart-limit check stops walking deleted services every pass.

Changed

  • Settings lookups by key prefix use the table's index. These lookups run on every Traefik config render: maintenance pages, error pages, compose routes and forward auth. They now read a key range instead of LIKE 'prefix%'. LIKE could not use the index, and it treated the _ in a prefix such as restart_limit: as a wildcard.

Source CHANGELOG.md at 6046cfe8