Manage nomploy services as code with
Pulumi — a dynamic provider that drives the panel's
existing REST API — no separate API layer and no Go toolchain — so you can declare
projects, applications, databases, domains and compose stacks in a Pulumi program
and pulumi up them onto your cluster.
Available in TypeScript (repo root) and Python (python/). Both
wrap the same REST API; because Pulumi dynamic providers are language-specific, each
is its own implementation of the same resources.
Status: prototype. Resources and types are complete and type-check; the TS version is validated end-to-end against a live cluster. Run it against your own instance with
pulumi up(see Quick start). APIs may still change.
nomploy exposes its whole tRPC router set over HTTP (via @dokploy/trpc-openapi)
at <endpoint>/api/<router>.<procedure> — GET for queries, POST for mutations,
x-api-key auth. This provider maps each resource's lifecycle onto those
procedures, so everything the panel can create, you can declare.
| Resource | Create | Read | Update | Delete |
|---|---|---|---|---|
Project |
project.create |
project.one |
project.update |
project.remove |
Application |
application.create |
application.one |
application.update |
application.delete |
Domain |
domain.create |
domain.one |
domain.update |
domain.delete |
Postgres / Mysql / Mariadb / Mongo / Redis (Database) |
<engine>.create |
<engine>.one |
<engine>.update |
<engine>.remove |
Compose |
compose.create |
compose.one |
compose.update |
compose.delete |
Projectauto-creates a production environment; itsenvironmentIdoutput feedsApplication/Database/Compose.Applicationsets its source from a Docker image (dockerImage+ optional registry creds →application.saveDockerProvider) or a generic git repo (gitUrl/gitBranch/buildType→application.saveGitProvidersaveBuildType), and withdeploy: truerunsapplication.deployon create and whenever the source changes.
Database(or the namedPostgres/… subclasses) covers all five engines with the same shape, anddeploy: true.Composetakes a rawcomposeFile(translated to a Nomad job) or anomadPack+nomadPackRegistry, withdeploy.
- Pulumi CLI and Node.js 18+.
- An API key: panel → Settings → API / Swagger (or
POST /api/user.createApiKeyfrom an authenticated session). The first key must be minted from the UI/session; after that the provider only needs the key.
npm install
# Local state backend (or `pulumi login` to Pulumi Cloud)
pulumi login --local
pulumi stack init dev
pulumi config set endpoint https://your-nomploy-instance.com
pulumi config set apiKey <your-x-api-key> --secret
pulumi config set dbPassword <postgres-pass> --secret
pulumi config set host app.example.com # optional — routes a domain
pulumi config set dbServerId <worker-server-id> # optional — see Databases note
pulumi preview # plan
pulumi up # project → postgres + application(+deploy) → domain
pulumi destroy # tear it all downapiKey (and other secrets) are stored encrypted in the Pulumi state — the
resources mark it an additionalSecretOutput.
import * as pulumi from "@pulumi/pulumi";
import * as nomploy from "pulumi-nomploy/src/nomploy";
const conn = {
endpoint: "https://your-nomploy-instance.com",
apiKey: new pulumi.Config().requireSecret("apiKey"),
};
const project = new nomploy.Project("web", { ...conn, name: "web" });
new nomploy.Application("api", {
...conn,
name: "api",
environmentId: project.environmentId,
dockerImage: "ghcr.io/you/api:latest", // or gitUrl/gitBranch/buildType
deploy: true,
});
new nomploy.Postgres("db", {
...conn,
name: "db",
environmentId: project.environmentId,
databaseName: "app",
databaseUser: "app",
databasePassword: new pulumi.Config().requireSecret("dbPassword"),
deploy: true,
});src/nomploy/api-types.ts is generated from the panel's OpenAPI spec with
openapi-typescript (npm run gen:types, reading the vendored openapi.json).
client.ts exports typed helpers for any spec path:
import { post, type ApiBody } from "pulumi-nomploy/src/nomploy";
await post(conn, "/project.create", { name: "x" }); // body is type-checkedThe dynamic providers themselves use the uniform untyped call (the spec's
response schemas are thin, and the Database provider builds <engine>.*
paths at runtime); the typed helpers are for request bodies/queries and new code.
Regenerate against your instance: drop a fresh spec in openapi.json (from the
nomploy repo's apps/dokploy/scripts/generate-openapi.ts, or /api at runtime)
and run npm run gen:types.
The same resources are available in Python under python/ (a Pulumi
Python dynamic provider — snake_case API).
cd python
python3 -m venv venv && . venv/bin/activate
pip install -r requirements.txt
pulumi login --local
pulumi stack init dev
pulumi config set endpoint https://your-nomploy-instance.com
pulumi config set apiKey <your-x-api-key> --secret
pulumi config set dbPassword <postgres-pass> --secret
pulumi upimport pulumi
import nomploy_pulumi as nomploy
conn = dict(endpoint="https://your-nomploy-instance.com",
api_key=pulumi.Config().require_secret("apiKey"))
project = nomploy.Project("web", name="web", **conn)
nomploy.Application("api", name="api", environment_id=project.environment_id,
docker_image="ghcr.io/you/api:latest", deploy=True, **conn)
nomploy.Postgres("db", name="db", environment_id=project.environment_id,
database_name="app", database_user="app",
database_password=pulumi.Config().require_secret("dbPassword"),
deploy=True, **conn)Resources: Project, Application, Domain, Postgres / Mysql / Mariadb /
Mongo / Redis (or Database(engine=...)), Compose — same shape as TypeScript.
-
tRPC-flavored REST: everything is GET/POST to
/<router>.<procedure>; ids go in the body/query, not the URL path.src/nomploy/client.tshandles that. -
Validate the JSON codec against your instance — the tRPC layer uses superjson internally; the OpenAPI handler is expected to emit plain JSON, which this client assumes.
-
It's a dynamic provider, so connection details (
endpoint,apiKey) travel as resource inputs and are stored in state. If the API key is later rotated or expires, operations that use the stored key (notablypulumi destroy) fail with401. Override the connection with env vars for that run — they take precedence over state:export NOMPLOY_ENDPOINT=https://your-nomploy-instance.com export NOMPLOY_API_KEY=<a-fresh-key> pulumi destroy
(As a last resort, delete the services in the panel and
pulumi stack rm <stack> --forceto abandon the state.) -
Composedestroy keeps volumes unlessdeleteVolumesOnDestroy: true. -
Database node placement. A database binds its engine port (5432, 3306, …) as a static host port and defaults to the control plane, which already runs stateful services (often a Postgres on 5432). On a multi-node cluster a second DB of the same engine on that node fails to place (
reserved port collision) and sits "deploying". Give databases aserverId(a worker node) so the port is free — only one DB per engine-port per node. ChangingserverIdreplaces the DB. -
Database deploys are synchronous (
<engine>.deploypulls the image, then registers the Nomad job, then returns) — there's no async/streaming REST variant.deploywaits up to ~2 min; the first-ever pull of a new image on a node can take longer (and a proxy may cut a very long request). If a DB doesn't come up on the firstpulumi upwith a brand-new image, deploy it once from the panel or re-runpulumi up(the image is cached by then). Apps/compose deploy asynchronously and return fast.
- Run against more real-world stacks; stabilise the resource APIs.
- Optionally package as a proper Pulumi component/provider (published to npm).
- A Terraform provider over the same REST API, if there's demand.
Apache-2.0 (same permissive terms as the Apache-2.0 portions of nomploy).