Extend
Template mirrors
The complete upstream guide to template mirrors, synchronized with the product repository.
Product v0.18.26 · Complete source guide
Template mirrors: Coolify and Dokploy
NineDeploy can convert two community template catalogs into its own compose-stack format:
- the Coolify compose templates (
coollabsio/coolify,templates/compose); - the Dokploy blueprints (
Dokploy/templates,blueprints/, MIT, added in G4.7).
Each converter writes a bundle that the Hub can load as a remote source (Settings → templates_source), or that a maintainer can promote into the bundled registry after a smoke run.
The tools never download anything themselves. You clone the catalog first.
1. Building a bundle
# Coolify
git clone --depth 1 --filter=blob:none --sparse https://github.com/coollabsio/coolify /tmp/coolify
git -C /tmp/coolify sparse-checkout set templates/compose
pnpm --filter @ninedeploy/server exec tsx scripts/buildTemplateMirror.ts \
--src /tmp/coolify/templates/compose --out ./templates-coolify.json
# Dokploy
git clone --depth 1 https://github.com/Dokploy/templates /tmp/dokploy-templates
pnpm --filter @ninedeploy/server exec tsx scripts/buildDokployMirror.ts \
--src /tmp/dokploy-templates/blueprints --out ./templates-dokploy.json- Output: each script prints how many entries it converted and why it skipped the rest.
- Validation: the bundle is checked by the production registry parser before it is written.
- Not verified yet: every entry carries
requires: "… · not runtime-verified"until it has been smoke-run.
2. How a Dokploy blueprint is converted
A blueprint keeps its settings outside the compose file:
[variables]hold generators;[config.env]is written to the stack's.env;[[config.domains]]routes a service;[[config.mounts]]writes files.
The converter folds the env into the compose file. It substitutes inside the parsed string values, as Docker Compose does, so a value containing : or # cannot change the file's structure. Each generator becomes the SERVICE_* token that makes the same kind of value at deploy time. The value is kept across redeploys.
| Dokploy | NineDeploy |
|---|---|
${domain} | ${SERVICE_FQDN_<routed service>} |
${password} / ${password:N} | ${SERVICE_PASSWORD_<N>_<VAR>}: N alphanumerics (16 by default) |
${base64} / ${base64:32|64|128} | ${SERVICE_REALBASE64_<N>_<VAR>}: base64 of N random bytes |
${hash:32|64|128} | ${SERVICE_HEX_<N>_<VAR>}: N hex characters |
${username} | ${SERVICE_LOWERCASEUSER_<VAR>} |
${email} | ${SERVICE_LOWERCASEUSER_<VAR>}@example.com |
Rules that match Dokploy's own behaviour:
- Variables: one variable is one token, so a database and its app get the same password.
- Precedence: a generator name wins over a variable of the same name, as in Dokploy.
- Env references: an env value may refer to another env key, with or without a
:-default, as Compose interpolates a.envfile.
A blueprint is skipped with its reason whenever there is no faithful equivalent:
- it uses file mounts;
- it routes more than one service, port or sub-path;
- it routes no service;
- it uses
${uuid},${jwt}, timestamps or${randomPort}; - it asks for a base64 or hash length other than 32, 64 or 128;
- it uses a variable only Dokploy provides (
DOKPLOY_*); - its compose file has a shape the Coolify mirror also refuses: host ports, host networking, build contexts or
env_file; - it reaches the host. That means it bind-mounts a host path (
/root:…, the Docker socket), a path that leaves the stack anywhere in it (../files/…,./a/../../x,~/…) or an interpolated source; declares a volume withdriver_opts, another driver orexternal; declares top-levelsecretsorconfigs, a volume or network with a fixedname, or an external network or one with another driver; or gives a service a key outside the safe list (image, command, environment, volumes, depends_on, healthcheck, …). That list leaves outbuild,env_file(andpull_policy: build),privileged,pid,ipc,devices,cap_add,security_opt,volumes_from,userns_mode,sysctls,runtimeand every other key that can loosen isolation. Named volumes,./paths and the read-only/etc/localtimeand/etc/timezonestay. The check runs on the blueprint and again on the converted file.
On the catalog of 2026-10-27, 383 of 586 blueprints converted. With the host-access rules of 2026-10-29, 322 convert. The largest skip groups were file mounts (91) and more than one routed service (38).
3. Promoting entries
A converted entry only reaches the bundled registry after it has started in an isolated smoke run:
node scripts/smoke-template-runtime.mjs --registry=./templates-dokploy.json --ids=dokploy-n8n,dokploy-umamiThe script runs compose up with generated SERVICE_* values, probes the routed port and then runs compose down -v. scripts/promote-templates.mjs then moves the passing entries in.
On 2026-10-29, 30 Dokploy entries were smoke-run and 26 passed. A security review then withdrew six of them, leaving 20 in the Hub. Trilium Next bound the host's /root. iHateMoney, Kener and Statping-ng bound ../files outside the stack, which the converter now refuses. FileGator starts with a publicly known admin login, and ByteStash with open sign-up and debug mode. A started stack is not enough: a promoted entry must also be safe to expose by default. The same review was then applied to the first batch, and 18 more were withdrawn. Only AList and Healthchecks stayed; each reason is in the evidence file. A second batch of 30 left one, Dagu, after the same review; runtime-verify-2026-10-29-dokploy-2.json records why each other one stayed out. Reasons include mutable latest tags, open registration, auth switched off, a first visitor becoming admin of an app that runs code, fixed passwords, and the converter's host-access rules. Each withdrawal is recorded as a failure, with its reason, in the evidence file. The evidence is apps/server/test/fixtures/runtime-verify-2026-10-29-dokploy.json. Etherpad, Wiki.js and Kutt exited before they were ready, and Cloud Commander listens on 8000 where its blueprint routes 80.
A third round, on 2026-10-31, filtered before running anything. Of the 324 blueprints that convert, those already tried, those with an image not pinned to a version, a literal credential, or open registration or auth switched off in their environment were set aside, leaving 138. A reading of each one's first-run behaviour left 11: each sets its admin login or API key from a generated value. Rejections at this stage included known default logins (Umami, Grafana, BookStack, Wallabag, Zabbix), a first visitor becoming admin (n8n, Strapi, Immich, Forgejo, Docmost), open sign-up (Linkwarden, GlitchTip, GitLab), phpMyAdmin's auto-login, Keycloak in start-dev and Directus's postgis:13-master image. All 11 started in the smoke run. Each was then started again and asked, without credentials, for its admin pages, APIs, install and sign-up pages; every answer had to be a login, a 401 or 403, a locked installer or a missing page. Ten passed and are in the Hub: 9Router, Dolibarr, Graylog, Kimai, Meilisearch, Miniflux, OpenObserve, Ponos, PrivatePaste and Typesense. YOURLS stayed out: its install page answers without a login and the stack does nothing until someone runs it, and its MySQL 5.7 is end-of-life. The evidence is runtime-verify-2026-10-31-dokploy-3.json.
A fourth round, on 2026-11-01, took the 125 blueprints the third round had set aside only because an image was not pinned. Apps the Hub already has, as a native or a Coolify entry, were dropped, which left 56. Reading each one's first-run behaviour left eight. The rest were rejected for reasons like those in round three, plus self-hosted modes without any auth (Domain Locker, go-whatsapp) and a fixed admin:admin superuser (Bugsink). FreeScout dropped out because its image, tiredofit/freescout, is gone from Docker Hub. The other seven were pinned by hand to the newest release at least two weeks old: Flatnotes v5.5.5, Omni-Tools 0.6.0, HortusFox v6.3, MAZANOKE v1.1.7, Kokoro Web 0.1.3, Cut 0.1.0 and Rustrak v0.18.1. Nothing else in their files was changed, apart from two wrong upstream descriptions in the Hub (HortusFox, MAZANOKE). All seven started in the smoke run. Without credentials:
- Flatnotes, Kokoro Web and Rustrak answered their APIs with 401.
- Rustrak refused registration without an invitation (403).
- HortusFox sent every page to its login, and its API answered 403.
- Cut's admin page and link creation required the owner's password.
- MAZANOKE and Omni-Tools are static pages that work in the browser and keep nothing on the server.
The evidence is runtime-verify-2026-11-01-dokploy-4.json.
A stack that fails is left out; often its upstream file is to blame. The Dokploy Ghost blueprint is an example: it has no depends_on or healthcheck, so Ghost restarts until MySQL is ready, and the smoke run reports "exited before readiness".
4. Apps that are open before they are set up
The bar above came after the native catalog and the 175 Coolify entries of 0.15.7, which were promoted because they started. On 2026-11-01 the whole bundled Hub was reviewed for what an app does before anyone has set it up. Each template where that is open carries openFirstRun, and the deploy wizard and the Hub detail show a warning:
openFirstRun | Meaning | Examples |
|---|---|---|
first-visitor-admin | The first person to open it creates the admin account or runs the installer. | n8n, Uptime Kuma, Immich, WordPress, Portainer |
default-login | It starts with a publicly known login. | Mealie, Nginx Proxy Manager, BookStack, Joplin |
open-signup | Anyone can create an account. | Vaultwarden, Rallly, Karakeep |
no-auth | It has no login of its own. | Dozzle, Node-RED, Ollama, Qdrant, Prometheus |
The review marked 174 templates. When such an app also has the Docker socket, the warning says that whoever gets in first controls the server. A test makes sure every template that reaches the host Docker is either marked or named with the generated value that closes it (WUD, Autobase).
Three Coolify entries were withdrawn, because they combine host Docker access with an open first run: Portainer (the socket, first visitor becomes admin), Authentik (the socket, an initial-setup flow that sets the akadmin password) and Glances (privileged, host PID namespace, the socket and no login). The native Portainer CE, Dockge and Dozzle stay, with the warning. runtime-verify-2026-10-10.json records each withdrawal. A stack installed from a withdrawn template keeps redeploying; only the Hub no longer offers it.
The marks come from reading each app's documented first-run behaviour, not from running all of them. An unmarked template is not a promise that it is closed.
The bar for promotion: the stack starts in the smoke run, every image is pinned to a version (no latest, no pull_policy: always), and nothing is open by default. That rules out a known default login, open registration, auth switched off, a first visitor becoming admin, and a form that reaches arbitrary hosts. A blueprint that needs a manual first step to be safe stays in the remote bundle (Settings → templates_source), not the bundled Hub.
Source guide (installer links and live inventory counts are normalized for this site): docs/TEMPLATE_MIRRORS.md.