Skip to content

v0.18.7.

Complete release notes · 2026-10-29. Read upgrade instructions and runtime limits before deploying.

Added

  • GitHub Actions runners can run on a node (G4.5). Pick the node under the App's Run on. The node's owner must opt in with NINEDEPLOY_AGENT_GHA_RUNNERS=on (agent 0.18.7, gha.runner). The agent applies the panel host's firewall isolation to its own host before every start and receives the just-in-time configuration only sealed.

  • ninedeploy tunnel service and ninedeploy tunnel db reach docker services and databases on nodes (G5.8). The panel asks the node agent, in a sealed request, for a single-use channel encrypted per tunnel. The agent connects from inside the container's network namespace through a helper with no capabilities. Needs agent 0.18.7 (tunnel.tcp); NINEDEPLOY_AGENT_TUNNELS=off turns it off.

  • The image scan deploy gate covers docker services on nodes (G5.1). The node agent scans the new container before it gets traffic; a node that cannot scan records why and does not block.

  • Compose stacks move to another host with their data (G3.1). Every named volume compose made for the stack is copied to the target, with compose's labels, before the stack is deployed there. Stacks with a fixed volume name are refused. Nodes need agent 0.18.7 (volume.compose; NINEDEPLOY_AGENT_VOLUME_COMPOSE=off turns it off).

  • Image scanning covers Swarm services (G5.1). The scan runs on the panel host, the manager, against the image the service spec runs, on the schedule and on request. If that image is not present on the panel host, the result says so.

  • Compose stacks on nodes also skip the build when the commit and the resolved compose model are unchanged (G2.4). The resolved model is hashed on the panel and never logged. Older agents build as before.

  • Image scanning covers docker services on nodes (G5.1).

    • How: the node agent runs the same docker save | trivy with its own cache volume and answers the parsed summary, which the panel validates before storing.
    • When: on the schedule and on request; an unchanged image is not scanned again until the result is stale.
    • Agent: needs the new capability image.scan (0.18.7); the node owner turns it off with NINEDEPLOY_AGENT_IMAGE_SCAN=off. No deploy gate on nodes yet.
  • 3 Dokploy blueprints in the bundled Hub, each smoke-run in isolation and reviewed first (G4.7): AList, Healthchecks and Dagu. Entries that started but are not safe to expose by default stay out: mutable latest tags, known default logins, open registration, auth off, or a first visitor becoming admin. The registry holds 308 templates. The converter refuses host access: bind mounts, builds, env files, isolation-loosening keys (TEMPLATE_MIRRORS.md).

  • Backups and restores of ClickHouse, Meilisearch and RabbitMQ databases on nodes (G1.8). The agent runs the panel host's routines against its local Docker and streams the result through the sealed channel.

    • Agent: needs the new capability db.dump.extra (0.18.7); an older agent gets 422 node_agent_outdated.
    • Meilisearch: the restore reads the image and data volume from the container, never from the request.
    • Proof: a dump, index deletion and restore against a real Meilisearch v1.53.
  • Image scanning covers Compose stacks on the panel host (G5.1). Every distinct image of a running stack is scanned on the schedule and on request (POST /v1/services/:id/image-scan; Scan now on the Deploys tab once the stack has a result). The result is merged into one, and each finding names the compose services whose image has it. A stack has no deploy gate: compose up starts it at once.

  • ninedeploy tunnel service <id> [--target <port>] [--replica <n>] (G5.8): a container port of a panel-host docker service on a local port, over the same WebSocket bridge as the database tunnel.

    • Access: the service admin role (it reaches the container past its domain protections), re-checked every minute.
    • Limits: 32 tunnels per user together with database tunnels; audited with byte counts.
    • Route: GET /v1/services/:id/tunnel.
  • The service Network tab and the Domains page are translated too, so the whole panel is available in Turkish (G4.6).

  • Settings → Integrations → Create a DNS record (G1.5): the domain-presets route by hand. It creates the A, AAAA or CNAME record for a hostname through the configured DNS provider (admins).

  • Sentry-compatible error ingest (G5.4). Under Service → Activity → Errors, a service gets a DSN (https://<key>@<panel>/<serviceId>) for any Sentry SDK.

    • Grouping: events are grouped into issues by fingerprint: the exception type and its in-app frames, or the SDK's own fingerprint.
    • Deploy link: each issue names the deploy it first appeared under. With release set to the commit SHA, that is the deploy of that commit; otherwise it is the live deploy.
    • Alerts: new issues are audited as alert.error_new. A resolved issue that happens again reopens and is audited as alert.error_regressed.
    • Ingest: /api/<serviceId>/envelope/ and /store/ accept plain, gzip, deflate and br bodies up to 1 MiB. Limits: 600 requests a minute per client and 300 events a minute per service; past them SDKs get 429 with X-Sentry-Rate-Limits.
    • Storage: the key is stored hashed. Events are kept 30 days, and issues not seen for 90 days are removed.
    • Who sees what: issue titles and event payloads (exception messages, request headers, user data) are for members and up.
    • Bounds (a browser DSN is public): each issue keeps its first 5 and latest 15 events (32 KiB each), and retries are stored once. Past 500 unresolved issues (or 1,500 of any status), new ones fold into an overflow issue, which always alerts. Other alerts are limited to 10 per service per hour, then one alert.error_burst. Alerts never repeat the sender's text.
    • Migration: 0105_error_ingest adds the error_issues and error_events tables and changes nothing else.

    See OBSERVABILITY.md → Error tracking.

  • Instance GitOps (G5.3). Under Settings → GitOps, an operator points the panel at a YAML file in a repository that describes the instance: projects, their services, domains and project grants.

    • Drift: the panel reads the file on an interval or on request and lists what is only in the file, only in the panel, or different.
    • Alerts: a new drift is audited as alert.gitops_drift, and an unreadable file as alert.gitops_failed.
    • Apply: creates and updates through the ordinary API with the operator's rights. It never deletes.
    • Export: the panel can be exported as the file, to commit edits made in the UI.
    • Rules: the file is size-limited, secret-scanned and strictly validated.
    • API: routes under /v1/gitops; SDK gitops. No migration. See docs/INSTANCE_GITOPS.md.
  • Move a service with its data (G3.1). Under Service → Settings → Move with data, an operator moves a stopped docker service between the panel host and nodes together with its nd-svc-<slug>-data volume.

    • Steps: the host change is the plain PATCH /v1/services/:id { serverId } with all its checks; then the volume is archived on the source, created and restored on the target (node transfers use the sealed, checksummed stream channel), and the service is deployed there.
    • Limits: the service must be stopped, not deploying, and have no extra volume attachments; Swarm, compose and PM2 services are refused. The source volume is kept until it is removed.
    • API: POST /v1/services/:id/move answers 202; GET /v1/services/:id/move shows the step or the error. SDK: services.move. No migration.
  • SAML sign-in that works with real identity providers (G3.6). It is SP-initiated, verified with @node-saml/node-saml 5.1.0 (exclusive c14n, enveloped signatures).

    • Binding: responses are bound to the panel's own AuthnRequest and accepted once.
    • Accounts: like OIDC, an identity signs in only the account it was linked to from Settings → Account, within the provider's email domains.
    • Where: set up under Settings → SSO → SAML identity providers; the button appears on the sign-in page. The routes are /v1/auth/saml/*.
    • Change: SAML_UNAVAILABLE is lifted for new providers.

    See SECURITY_SSO.md → SAML sign-in.

  • A domain per compose service (G2.1). Under Service → Settings → Domains per compose service, a domain of a compose stack routes to another service of the stack, on its port, instead of the main one.

    • Variables: the next deploy sets SERVICE_FQDN_<SERVICE> and SERVICE_URL_<SERVICE> for it.
    • API: GET/PUT /v1/domains/:id/compose-route. There is no migration.

    See TRAEFIK_INGRESS.md → A domain per compose service.

  • Inline Dockerfile (G1.1). Under Service → Settings → Inline Dockerfile, a docker service builds from a Dockerfile pasted in the panel instead of the repository's.

    • How: it is written into the base directory as .ninedeploy.Dockerfile and built with the dockerfile pack.
    • Dockerfile only: a service with no repository and no image builds it from an empty context.
    • API: GET/PUT /v1/services/:id/inline-dockerfile. There is no migration.

    See DEPLOYMENTS.md → Inline Dockerfile.

  • More log-drain targets (G3.4): Elasticsearch/OpenSearch (_bulk), Splunk HEC, Better Stack, New Relic, Axiom and AWS CloudWatch Logs (SigV4 PutLogEvents, creating the group and stream when missing).

    • Delivery: they use the existing shipper and queue.
    • Validation: a URL that does not name the index (Elasticsearch) or the group and stream (CloudWatch) is refused when the drain is saved. There is no migration.

    See OBSERVABILITY.md → Log drain targets.

  • Panel sign-in in front of any app (forward auth, G2.3). Under Service → Settings → Panel sign-in, a domain can require visitors to sign in on the panel and hold a chosen role on the service. Traefik's forwardAuth asks the panel, so no extra container runs.

    • Flow: a one-time code on the app's domain, bound by a state cookie, sets a 12-hour session cookie.
    • Revocation: Sign out everywhere and role changes end access.
    • API: GET/PUT /v1/domains/:id/forward-auth and POST /v1/forward-auth/grant. There is no migration.

    See TRAEFIK_INGRESS.md → Panel sign-in in front of an app.

  • Maintenance mode and custom error pages (G2.2). Under Service → Settings → Maintenance mode, for services on the panel host.

    • Maintenance: a service's domains answer a 503 maintenance page served by the panel. Addresses on a bypass list still reach the app.
    • Error pages: custom 502/503/504 HTML is shown through Traefik's errors middleware.
    • API: GET/PUT /v1/services/:id/maintenance and /v1/services/:id/error-pages. There is no migration.

    See TRAEFIK_INGRESS.md → Maintenance mode and custom error pages.

  • Backup replica destinations (G3.3). Under Database → Backups → Backup replicas, an operator can name up to three more destinations for a database's scheduled dumps.

    • How: each replica receives the same encrypted dump after the main upload and keeps its own number of copies. A failing replica is shown on the card and never fails the backup.
    • API: GET/PUT /v1/databases/:id/backup-replicas. There is no migration.

    See DATABASES_BACKUPS.md → Replica destinations.

  • Workspace-enforced 2FA (G1.4). A workspace admin can require every member to sign in with TOTP, a recovery code or a passkey.

    • Effect: any other session is held to an enrolment screen, and every other route answers 403 two_factor_required. Members set up a factor if they have none, then sign in again.
    • SSO: sign-ins through listed OIDC/SAML providers can be exempted. GitHub never is.
    • API tokens: existing ones keep working.
    • API: GET/PUT /v1/workspaces/:id/two-factor and GET /v1/auth/2fa/requirement. There is no migration.

    See SECURITY_SSO.md.

  • Registry push webhooks (G1.7). Under Settings → Registry webhook, an image service gets a secret URL for Docker Hub, GitHub Packages, Harbor, a registry:2 or a CI step.

    • What it does: a push of the service's own image and tag queues a deploy as the member who made the hook. Other pushes are ignored.
    • Storage: the URL is shown once and stored as a hash. The routes are GET/POST/DELETE /v1/services/:id/registry-hook and POST /v1/registry-hooks/:serviceId/:token. There is no migration.

    See DEPLOYMENTS.md → Registry webhook.

  • Restart-loop limit (G1.3). Under Settings → Restart-loop limit, a docker service on the panel host is stopped when its containers restart N times within M minutes.

    • How it stops: the same way as the Stop button. It is audited as service.restart_limit, which notifies. A deploy in progress is left alone.
    • Off by default: set it with PUT /v1/services/:id/restart-limit. There is no migration.

    See DEPLOYMENTS.md → Restart-loop limit.

  • Compose deploys skip the build when nothing that goes into it changed (G2.4).

    • What counts as unchanged: the same commit and the same resolved compose model (build contexts, Dockerfiles, args and environment) as the last build. The images are then reused; a missing image is still built.
    • Building every time: NINEDEPLOY_COMPOSE_BUILD_SKIP=off builds on every deploy, for stacks that need fresh base images.

    See DEPLOYMENTS.md → Docker Compose Stacks.

  • Panel certificates shared with node proxies (G5.7). With PUT /v1/traefik/certificates/shared { enabled: true }, a node proxy gets the panel's own ACME certificates (including a DNS-01 wildcard) for the hosts it routes.

    • Why: a domain served by several ingress nodes then has one certificate everywhere, and a wildcard is issued once rather than once per node.
    • How the node behaves: checked on Traefik 3, the node serves the shared certificate and skips ACME for the hosts it covers. An uploaded certificate still wins.
    • Renewals: they reach the nodes within the hour.
    • Off by default: it copies private keys to those nodes. There is no migration.

    See TRAEFIK_INGRESS.md → Panel certificates shared with node proxies.

  • Pending changes tray (G5.2). Under Settings → Pending changes, a service can stage its edits instead of applying them one by one.

    • Review: the tray shows each change with its value now and after.
    • Apply: Apply and deploy (or Apply only) applies them together through the normal service PATCH with your own credentials, so every existing check applies. A deploy into an approval-gated lane waits for review. A refused apply keeps the tray.
    • API, audit and storage: /v1/services/:id/pending and services.pending.*. Each step is audited. There is no migration.

    See DEPLOYMENTS.md → Pending changes.

  • Scheduled minimums and right-sizing hints (G5.6).

    • Scheduled minimums: an autoscaled service can raise its minimum in weekly time windows (schedules with days, from, to and a minimum, in an IANA timezone), set under Settings → Autoscaling. Windows may run past midnight. The highest floor in force wins, capped at the maximum, and the scaling event names the window.
    • Right-sizing hints: Settings → Right-sizing suggests setting, raising or lowering the memory and CPU limits from up to a week of metric history (95th percentile and peak). It is advisory, and needs at least 12 hours of history. GET /v1/services/:id/sizing returns the same.
    • No migration: schedules are part of the stored autoscale configuration.

    See DEPLOYMENTS.md → Scheduled minimums and Right-sizing hints.

  • ninedeploy tunnel db <id> (G5.8). It offers a panel-host database on a local port through the panel, so the database port stays closed to the internet.

    • How it works: each local connection travels over its own WebSocket (GET /v1/databases/:id/tunnel) to the database container, byte for byte. Checked against a real PostgreSQL 15: SCRAM login, a 2 MiB row in and out, parallel connections.
    • Access: it needs the database admin role, the same as its credentials. Scoped API tokens must be allowed to write databases. Access is re-checked every minute.
    • Limits and audit: idle tunnels close after 30 minutes, and one user can hold at most 32. Tunnels are audited as database.tunnel.open and database.tunnel.close, with byte counts.
    • UI: the database page shows the command.
    • Not yet: databases on nodes.

    See DATABASES_BACKUPS.md → Tunnels through the panel.

  • Image vulnerability scanning with Trivy (G5.1). It is off by default; an operator turns it on under Settings → Image scanning.

    • During a deploy: each docker deploy on the panel host has its new image scanned before it gets traffic.
    • The gate: it reports only, or blocks critical (or critical and high) findings. A blocked deploy fails before the switch, so the previous release keeps serving. By default the gate counts only findings that have a fix.
    • Rescans: running images are rescanned every 24 hours, configurable. New fixable critical findings are audited as image_scan.critical.
    • Where results show: each service shows its latest scan on its Deploys tab.
    • How Trivy runs: in a throwaway container that reads the image from docker save, without the Docker socket. It is pinned to aquasec/trivy:0.69.3 by digest, which predates the malicious 0.69.4 to 0.69.6 releases (CVE-2026-33634). NINEDEPLOY_TRIVY_IMAGE overrides it.
    • Failures: a scan that cannot run never blocks a deploy.
    • Not scanned yet: services on nodes, Swarm and Compose.
    • No migration.

    See DEPLOYMENTS.md → Image vulnerability scanning.

  • Deploy freezes (G5.5). An operator can hold deploys under Settings → Deploy freeze, or with /v1/deploy-freezes.

    • Scope: the whole instance, a workspace (with its environments), a project or an environment. Each freeze has a reason and an optional end.
    • How it holds: the queue checks freezes when it claims a deploy, so every trigger waits as queued: the UI, the API, git pushes, deploy hooks, schedules, image polls and rollbacks. The deploy log says why.
    • When it ends: when the freeze ends or is lifted, the held deploys start. Nothing is dropped.
    • Audit and storage: changes are audited as deploy.freeze and deploy.unfreeze. There is no migration; the freezes are stored in a settings row.

    See DEPLOYMENTS.md → Deploy freezes.

  • Create nodes on Hetzner Cloud (G4.3). On the Servers page, an operator saves a Hetzner Cloud API token, then picks a location, server type, image and SSH keys.

    • What the panel does: it creates the VM and registers it as a node. The VM's cloud-init installs Docker for its distribution, using the same script as the SSH bootstrap, and starts the NineDeploy agent. The node turns online once the agent has been rekeyed (below).
    • Rekey after first contact: the first agent key's hash is in the VM's user data, which any container on the node can read from the metadata service. Right after the agent first answers, the panel replaces that key through the new agent.rekey op (agent 0.18.6). The new key is stored before the agent hears of it, and a sweep every 2 minutes finishes a rekey that a panel restart cut short. Until then the node shows as rekeying, Test and Approve are refused, and the agent itself (started with NINEDEPLOY_AGENT_REKEY_REQUIRED=1) runs nothing but agent.ping and agent.rekey. One rekey runs per node at a time, and a pending key is sent again rather than replaced. The agent ignores a rekeyed key that was made for a different NINEDEPLOY_AGENT_TOKEN.
    • Key rotation: the Hetzner token and the pending rekey keys are now re-encrypted with the other secrets when the master key rotates.
    • Deleting: Delete destroys the VM and removes the node. It is refused while the node still hosts services, databases or Swarm work.
    • The token: it is checked before it is stored, stored encrypted, and never shown back.
    • No migration: the token and each node's link to its VM are settings rows.
    • Not tested against Hetzner: the Hetzner API was only exercised through a fake in tests; no real server was created.

    See MULTI_NODE.md → Creating nodes on Hetzner Cloud.

  • More Linux distributions (G4.1). install.sh now sets up a panel host on these distributions, not only Debian and Ubuntu:

    • RHEL, Rocky, Alma, CentOS Stream, Oracle Linux and Fedora:
      • Docker and Node come from Docker's and NodeSource's signed RPM repositories.
      • The packages that conflict with Docker are removed first: podman-docker, buildah and runc.
    • Alpine:
      • Docker comes from Alpine's own package and is enabled with OpenRC.
      • A host without systemd gets the Docker mode by default; asking for --bare-metal there is refused, because the bare-metal panel is a systemd service.
      • The installer also works with BusyBox's flock, which has no -w, and in a root shell without sudo. Before this, both made it stop with a misleading "lock held" error.

    The SSH bootstrap of a node installs Docker the same way, per distribution:

    • Rocky and Alma, which get.docker.com refuses, are now covered.
    • The script is sent base64-encoded through sh, so the node's login shell never parses it.

    Testing:

    • Detection was checked against Debian, Ubuntu, Rocky, Alma, CentOS Stream, Oracle Linux, Fedora, Alpine, Amazon Linux and openSUSE images. Amazon Linux and SUSE are reported as unknown.
    • The installer's Docker and Node installs were run in Rocky 9, Fedora 41 and Alpine 3.20 containers.
    • A full Docker-mode install was run on Alpine; it ended with a healthy panel.

    See README → Requirements.

  • OS security updates from the panel (G4.2). For the panel host (Settings → System) and each node (Servers), operators can:

    • see the pending package updates, security ones first;
    • see whether the host is waiting for a reboot;
    • turn automatic security updates on or off.

    How each package manager is handled:

    • apt (Debian, Ubuntu): uses unattended-upgrades, with its automatic reboot turned off explicitly.
    • dnf (Fedora, RHEL, Rocky, Alma): uses dnf-automatic, with upgrade_type = security.
    • Other package managers: reported as unsupported.

    NineDeploy never reboots a host.

    How it runs:

    • Every action is a fixed script, run on the host through the same nsenter helper as host shells.
    • On a node, it goes through the new sealed agent op host.packages (capability host.packages, agent 0.18.6). The node's owner can turn it off with NINEDEPLOY_AGENT_HOST_PACKAGES=off.

    The scripts were run against real Debian 12, Ubuntu 24.04, Rocky 9 and Fedora 41 images, and through the nsenter helper on a Docker host. No migration. See NODE_OPERATIONS.md.

  • Self-hosted GitHub Actions runners (G4.5). A GitHub App can now run Actions jobs on the panel host.

    • How it works: on a queued workflow_job whose runs-on names one of the App's runner labels, the panel registers a just-in-time runner and starts one actions-runner container for it. That container takes one job and is removed.
    • Setup: Sources → GitHub App → Self-hosted Actions runners. The section shows what the App still lacks. New Apps can request the event and permission from the setup form:
      • an organization App needs organization_self_hosted_runners: write;
      • a personal App needs administration: write.
    • Isolation:
      • no Docker socket;
      • its own network, firewalled off from the host itself and from private, cloud-metadata and CGNAT ranges, with internet access open. Without these rules no runner starts, unless NINEDEPLOY_GHA_RUNNERS_UNISOLATED=1 is set;
      • CPU, memory and pid limits.
    • Which repositories: public repositories are refused unless allowed, and an optional repository list narrows it further.
      • GitHub enforces this as well: an organization runner is registered in an App-owned runner group, open only to the allowed repositories. Without that group, a job from another repository could take the runner.
      • Custom runner groups need GitHub Team or Enterprise; on another plan, organization runners do not start.
      • A personal installation registers repository runners, which serve only their own repository.
    • Limits and queue: jobs above the per-App limit wait and start as slots free up. Runners that never pick up a job are removed after 10 minutes.
    • Image: pinned by digest (actions-runner 2.338.2).

    No migration: the settings live in the settings table. See GITHUB_APP.md §7.1.

  • Turkish interface (G4.6). The web panel now has a translation layer and a Turkish locale.

    • Choosing a language: pick it under Settings → Appearance → Language, or with the switch under the sign-in form. The choice is kept in the browser. Without one, a Turkish browser opens in Turkish.
    • Translated: every page except the service Network tab and the Domains page, about 4,580 strings. Server error messages, API values, product names and help topics stay English.
    • Catalogue check: a test fails on any untranslated string, unused entry or placeholder mismatch.

    No new dependency and no migration. See TRANSLATIONS.md.

Changed

  • The dashboard banner for a single unhealthy service now reads "1 service needs attention".

Source CHANGELOG.md at 6046cfe8