Stacks
A Stack groups several services — a web app, a worker, a database, a cache — into a single multi-service application you can wire together and deploy as a group. Instead of deploying each service by hand and copying credentials between them, you connect components on a visual canvas and Kuploy injects the right values and deploys everything in the correct order.
Stacks are fully k8s-native and reuse Kuploy's normal deploy engine: every component is a regular service, so logs, domains, backups, monitoring, and billing all keep working exactly as they do for standalone services.
Concepts
| Term | Meaning |
|---|---|
| Stack | A named group of services in an environment. |
| Component | A service (application or database) that belongs to a stack. It's still a normal service. |
| Connection | A typed link from a provider component to a consumer component: a value from the provider (e.g. a database connection string) is injected into the consumer as an environment variable — as soon as you connect, and re-resolved at deploy. |
| Attached service | A service that serves one or more of your applications without belonging to the stack — Transactional Email today. It appears beside the stack, not in it. See Attached services. |
Creating a stack
- Open your project, then an environment.
- Click Stacks in the environment toolbar.
- Click Create stack, give it a name, and open it.
What can be a component
Seven service types can belong to a stack:
Application (web, worker or cron — see below) · PostgreSQL · MySQL · MariaDB · MongoDB · Redis · Bucket
Everything else in an environment stays outside the stack. In particular Transactional Email and Payment Gateway are not stack components and cannot be connected on the canvas — they bind to an application and inject their own settings into it directly, so there is nothing for a connection to carry. They show up as attached services instead, and you configure them from their own pages: Email in your application and Payment Gateway.
Note that Bucket is a component, not an attached service. A bucket's endpoint and keys only exist once it has been provisioned, and being a component is what lets Deploy stack provision it before the app that reads it — see the deploy order.
Adding components
In the stack builder, click Add component and pick an existing service from the environment. The service is tagged into the stack — it keeps all of its own settings. Create the underlying services first (see Deploy Your First App and Databases) if you haven't already.
Component roles
Each application component has a role that decides what kind of workload it deploys as — the stacks equivalent of the different process types in a docker-compose file or Procfile. Select an application node and open Component role in the side panel:
| Role | Runs as | Public? | Use for |
|---|---|---|---|
| Web (default) | Deployment + Service + Ingress | Yes — gets a domain | HTTP apps, APIs, anything users hit |
| Worker | Deployment + Service | No | Queue consumers, background processors, long-running jobs |
| Cron | CronJob on a schedule | No | Scheduled tasks — nightly cleanups, report generation, etc. |
Most components are Web, which is the default — you only change the role for background or scheduled work. Cron requires a cron schedule (e.g. 0 * * * * for hourly). The role only applies to application components; databases ignore it.
A common pattern: point two application components at the same repo but give them different roles and start commands — e.g. a web (Web) and a worker (Worker) sharing one codebase, both connected to the same database.
Connecting components
Connections are the core of Stacks. A connection says "take this field from the provider and inject it into the consumer as an environment variable."
You can create one in two ways:
- Drag from the right edge of a provider node to a consumer node on the canvas, or
- Click Connect and choose the provider, source field, and consumer.
Source fields depend on the provider:
| Provider | Available fields |
|---|---|
| PostgreSQL / MySQL / MariaDB | connectionString, host, port, user, password, database |
| MongoDB | connectionString, host, port, user, password |
| Redis | connectionString, host, port, password |
| Bucket | endpoint, bucket, accessKey, secretKey, region — see Object Storage |
| Any component | host (in-cluster name), url / wsUrl (public address — needs an assigned domain), env:<KEY> (copy one of the provider's own environment variables) |
env:<KEY> is the one to reach for when two components must share a value you set yourself — a signing secret, an API token — rather than something the platform generates. url and wsUrl follow the component's custom domain when it has one, and fail with a clear message until a domain is assigned.
For example, connecting a Postgres component's connectionString into your web app as DATABASE_URL injects something like:
postgresql://user:password@my-db-appname:5432/mydb
The host is the database's in-cluster service name, so traffic stays inside your project's private network.
Editing or removing a connection
Select a component on the canvas; its connections appear in the side panel on the right. Each one has two actions:
- ✏️ Edit — opens the connection dialog with the provider/consumer locked, so you can change the source field or env-var name. Saving re-resolves the value and rewrites the consumer's env. This is the right way to change an injected value like
DATABASE_URL— don't hand-edit the managed env block. - 🗑️ Remove — deletes the connection and strips its variable from the consumer's env. (To change the provider or consumer itself, remove the connection and draw a new one.)
The moment you create a connection, Kuploy resolves the value and writes it into a platform-managed block in the consumer's environment variables — you'll see it on the consumer's Environment tab between the # >>> kuploy-stack / # <<< kuploy-stack markers. Removing the connection removes it again.
The block is derived from your connections, so don't hand-edit it — Kuploy recomputes it on connect, disconnect, and deploy, overwriting manual changes (hence the "do not edit"). To change an injected value, change its connection (pick a different source field or env-var name on the Stack page) or the provider database's credentials. Your own environment variables — anything outside that block — are fully editable on the Environment tab as usual.
Attached services
Your environment page and the stack canvas both show an Attached services section. A service is listed there when it serves one or more applications in the stack without being part of the stack: Transactional Email, Payment Gateway when you have one, and a Bucket that has been attached to one of the stack's applications without itself being a member.
An attached bucket is by definition an already-provisioned one — attaching an unprovisioned bucket is refused with the reason. Outside a stack there is no deploy ordering to provision it before the app that needs its credentials, so the platform refuses to create the promise it cannot keep. (Inside a stack the opposite holds: you may connect an unprovisioned bucket, because Deploy stack provisions it first.)
On the canvas an attached service is drawn as a dashed node with no status dot, joined by a dashed line to each application it serves. That is not a lesser kind of wire — it is a true picture of a different relationship:
- It has no deploy status of its own and Deploy stack does not deploy it.
- It is not counted in the stack's service total.
- The environment variables it injects are owned by that service, so you change them on its page, not with a connection.
One attached service can serve several applications — you will see one node with a line to each. Each application still gets its own credentials behind the scenes, so revoking one application does not affect the others; the side panel lists every application it serves with its own Configure link.
A bucket you added to the stack is a component: it appears among the members with its own wire, and Deploy stack provisions it before the app that needs its credentials. A bucket you attached from its own page to one of the stack's apps shows up here instead, as an attached service — it serves the app without being part of the stack, and no deploy ordering applies, which is why attaching an unprovisioned bucket is refused. See Object Storage for when to use which.
Deploying
Click Deploy stack. Kuploy:
- Orders components by their connections (providers before consumers).
- Resolves each connection and injects the values.
- Deploys each component using the normal per-service deploy.
Components with no connections between them deploy independently; a cycle (A needs B and B needs A) is rejected.
A bucket's endpoint and keys come into being when the bucket is provisioned, not when it is created. You can draw its connections beforehand — they simply resolve to nothing until the bucket has been provisioned, and Deploy stack provisions it in dependency order before its consumers. If you provisioned the bucket on its own page instead, connected apps are refreshed automatically; they pick the values up on their next deploy.
While a connection resolves to nothing you can see it: the wire is drawn amber and dashed, and the connection in the side panel reads "Not set — …" with the reason, e.g. bucket "media" is not provisioned yet. The variable is left out of the consumer's environment rather than set to an empty string, so an app that requires it fails on a missing variable instead of quietly talking to a blank endpoint.
Deploy stack fails fast if any application component isn't ready to build — before anything is deployed — naming the offending component. Each Git-source component needs:
- a configured source (a selected repository), with its Git provider connected (a provider showing Action Required counts as disconnected), and
- a build registry assigned on the component's Advanced → Build Registry tab (adding a registry under Settings → Registry alone is not enough; see Container Registry).
Fix the named component's General/Advanced settings and deploy again.
What "Redeploy stack" actually does
One click runs the whole contract, in order — useful to know both for everyday deploys and for understanding what a redeploy will and will not change:
- Order — components deploy providers-first, following the connections (the database and bucket before the app that consumes them).
- Bucket re-provision, idempotent — a bucket member is re-provisioned against the shared storage on every stack deploy. Your objects are never touched; the access keys rotate, and every connected consumer's environment is rewritten with the fresh values before that consumer deploys. (This is also why hand-copied credentials break on redeploy — connected ones don't.)
- Managed env regeneration — each consumer's platform-managed block (the
# >>> kuploy-stacksection of its Environment tab) is recomputed from the connections, every time. Hand-edits inside the block are overwritten by design; your own variables outside the block are preserved. - Bindings ship on deploy — services that inject their own settings (Transactional Email) write the app's stored environment when you bind, rotate, or reconfigure them, but a running container keeps its old environment until its next deploy. If you rotated an email token or re-bound a service and sending still fails, the fix is almost always: deploy the app.
So after changing anything about a stack's wiring — connections, bucket quota, an email binding — Redeploy stack is the one action that makes stored configuration and running reality agree again.
Removing a component vs. deleting a stack
Two different actions — don't confuse them:
- Remove from stack (per component, in the side panel) ungroups a service. It leaves the stack but keeps running as a standalone service, with all its settings and data intact. Use this to detach a service without destroying it.
- Delete (the stack's red Delete button) is destructive and permanent. It deletes every component service in the stack along with its data — databases and their stored volumes included — and removes the stack's domains. This cannot be undone.
Deleting a stack is not just "ungroup" — it deletes the underlying services too. If you want to keep a service, use Remove from stack on it first, then delete the stack. Any service still in the stack when you delete it is permanently deleted with it, including database volumes.
Import & export (GitOps)
Open a stack and scroll to the Spec panel to view it as YAML in two formats:
- Native — Kuploy's own format and the source of truth. Round-trippable: you can re-import it.
- Score (lossy) — a Score
score.dev/v1b1export for portability. Score is a vendor-neutral, provisioner-resolved format and Kuploy is the provisioner. It's intentionally lossy — Kuploy-specific settings aren't represented — so it's for interop, not backup.
Use Copy to grab either format for your Git repo.
To import, go to the environment's Stacks page and click Import, then paste a native spec:
name: my-app
components:
- name: web
type: web
serviceType: application
env:
- key: DATABASE_URL
fromComponent: db
field: connectionString
- name: db
type: database
serviceType: postgres
Import wires up the grouping and connections over services that already exist in the environment (matched by name). It does not create services. Create the apps and databases first, then import the spec to group and connect them.
Runbook: from nothing to a deployed stack
The order matters more than anything else on this page. Each step links to its own section rather than repeating it.
- Create the services first. A stack groups services that already exist — it never creates them. Apps come from Deploy Your First App, databases from Databases, buckets from Object Storage.
- Give every application component a source and a build registry — Deploying explains why: Deploy stack refuses to start without them and names the component that is missing one.
- Provision any bucket you plan to connect, so its keys exist.
- Create the stack and add each service to it.
- Set component roles — web, worker or cron. Only applications have one.
- Draw the connections, choosing the source field and the env-var name your code already reads. Values are injected the moment you connect.
- Deploy stack. Providers deploy before consumers.
- Check the consumer's Environment tab — the injected values sit in the managed block. If one is missing, its provider had nothing to give yet (an unprovisioned bucket, a domain-less
url); fix that and deploy again.
To take a service out later, use Remove from stack — never Delete, which destroys the services themselves.
What stays per-component
Because a component is a normal service, everything you already know keeps working per component: deployments and logs, domains and TLS, volumes, backups, monitoring, and billing. Open any component's service page (from the side panel's Open service link) to manage it.