How each vibe-coded app is isolated, deployed, and governed
This page is written for the person who has to verify the claims: the platform architect, the security reviewer, and the engineer. It covers how a deployed app is confined to a Kubernetes sandbox, the GitOps pipeline every deployment takes, the pod security and network posture, the trust boundaries, and how Portainer-Run installs on a dedicated appliance or your own cluster.
What you need
Portainer-Run runs on Portainer Business, and Portainer Business is the operator control plane that reconciles every deployment into your Kubernetes platform. A single installer stands the whole stack up for you, so the list of things you provide is short.
- A target to install onto: either a large virtual machine, which the installer turns into a self-contained appliance for hosting vibe-coded apps, or an existing Kubernetes cluster you already run (BYO).
- A Git repository and a personal access token for it. Every deployment is committed to Git, so a target repo is required (GitHub, GitLab, Gitea, or a compatible server). The token needs Contents read and write.
- Optionally, an AI key (Anthropic or OpenAI) to enable the in-product Assistant and log triage. Everything else works without it.
The storage class, the authenticating ingress controller, the centralized user directory, the network security policies, and the namespaces and quotas are all stood up by the installer as part of the deployment, so they are not things you configure by hand before you begin.
One installer, two modes
An IT administrator runs a single terminal installer (TUI) on the target host. It fetches and runs a bootstrap script, then walks the rest interactively, standing up the full stack and the Kubernetes components a governed deployment target needs, so there is no manual assembly of ingress, identity, storage, and policy.
curl -sfL https://get.portainer.run/get.sh | sudo sh -
The installer then asks which of the two modes below to use.
Appliance mode, onto a large VM
The installer turns a single virtual machine into a self-contained appliance for hosting vibe-coded apps. It installs KubeSolo, Portainer Business, and Portainer-Run, and deploys the required Kubernetes components: an authenticating ingress controller, a centralized user directory, a storage class, network security policies, and the namespaces and quotas. The result is an isolated environment dedicated to running these apps and nothing else.
BYO mode, onto an existing cluster
The installer targets a Kubernetes cluster you already operate. It installs Portainer Business and Portainer-Run, and deploys the same set of components: an authenticating ingress controller, a centralized user directory, a storage class, network security policies, and the namespaces and quotas. The isolation model is identical to the appliance; the appliance adds a dedicated host underneath it, not a different security posture.
Where the security comes from
The sandbox that confines a deployed app is a property of the Kubernetes components above (namespaces, network security policies, pod security, and the authenticating ingress), not of the appliance. A BYO cluster running these components isolates an app exactly as the appliance does. The appliance is additional assurance through a dedicated host, not the source of the isolation.
How a deployment works
Everything is committed to Git and reconciled by Portainer, so each deployment is governed and fully repeatable.
Runtime detection
Portainer-Run inspects the file structure and matches a runtime in priority order, first match wins. A package.json means Node.js, requirements.txt or .py means Python, .php means PHP, a Gemfile or .rb means Ruby. If everything is static assets it defaults to nginx.
Manifest and init containers
Portainer-Run generates a Kubernetes Deployment manifest for the detected runtime, with three init containers that run in sequence before the app starts:
- vibe-sync clones the committed source from Git into a PersistentVolume.
- vibe-install runs the runtime's dependency installer against the volume (
npm install --production,pip install -r requirements.txt,bundle install, orcomposer install) inside the runtime image so native modules build correctly. - vibe-env writes a
.envfile from the values entered at deploy time. These values are never committed to Git.
Once the init containers complete, the main container starts against the pre-populated volume using a stock public runtime image. Git credentials are held in a Kubernetes Secret and injected by reference, so the token never appears in the pod spec.
GitOps commit and reconciliation
Source files are committed to {environment}/{namespace}/{app}/src/ and the manifest to {environment}/{namespace}/{app}.yaml, keeping environments and namespaces cleanly separated in one repo. Portainer-Run then calls the Portainer API to create a GitOps stack pointed at that manifest. Portainer polls the repository on a set interval (five minutes by default) and applies any change.
Updates
An update takes the same path. Drop the revised files, Portainer-Run commits the change, and Portainer reconciles it on the next poll. The PersistentVolume keeps its state across restarts, so uploaded files and anything the app wrote to disk survive the update.
The sandbox: how each app is isolated
The "sandbox" a security team asks for is the isolated environment in which a deployed app can do no harm, and it is assembled from four Kubernetes mechanisms working together rather than from any single control. A deployed app runs with near-zero reach on the network beyond what you explicitly grant, and every part of that is written into the committed manifest or the cluster components, so nothing is applied invisibly. You can read all of it in the manifest committed to your own Git repo and diff it against what lands in your cluster.
- Namespaces scope each app to a designated space with its own quotas, so a deployment cannot land or consume outside the boundary you set.
- Network security policies govern what a running app can reach, so it has no lateral network access it was not explicitly granted.
- Pod security hardens the container itself: unprivileged, all Linux capabilities dropped, privilege escalation disabled, RuntimeDefault seccomp, and no service account token mounted, so the app carries no ambient credentials to the cluster API.
- An authenticating ingress pre-authenticates every request to the app, so it is never openly reachable on the network and is served only to authenticated internal users.
Default security context
Each app deploys to the Kubernetes baseline pod security profile. The application container and every init container drop all Linux capabilities, disable privilege escalation, and pin the RuntimeDefault seccomp profile. The pod does not mount a Kubernetes service account token, so a deployed app carries no ambient credentials to the cluster API. The generated Deployment sets these fields directly:
spec:
template:
spec:
automountServiceAccountToken: false
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: "100m"
memory: "1Gi"
limits:
cpu: "1"
memory: "4Gi"
Resource requests and limits are applied to every app so nothing runs unbounded, and because they live in the committed manifest a platform administrator can raise them in Git for a workload that genuinely needs more.
What is deliberately not forced, and why
We do not force runAsNonRoot or a read-only root filesystem. The sync, install, and env-writing init steps legitimately write to the mounted volume, and many AI-generated images start as root and expect a writable filesystem. Forcing either would break the common case rather than harden it. The php-apache runtime is the one place capabilities are not fully dropped: it drops all, then adds back only CHOWN, SETUID, SETGID, and NET_BIND_SERVICE, which the official Apache image needs to boot. Static sites avoid this entirely by running the unprivileged nginx image as UID 101 on port 8080.
Network access with Pomerium
A hardened pod still needs a front door you control. We recommend and fully support Pomerium as the in-cluster ingress for Run apps. Configured in front of each deployed app, Pomerium pre-authenticates every request against the identity provider you already run, so an app an AI builder deploys is never openly reachable on the network by default and is served only to authenticated internal users. This turns "who can reach this app" from an afterthought into a policy set at the ingress, consistent across every app Run deploys.
Trust boundaries
Each boundary crossing is authenticated, and each zone has a distinct owner. There is no push path from the person deploying to the cluster; the only way in is through the sanctioned repository.
- Zone 1, the person's device. Holds source files from an AI coding tool and either a browser with a Portainer session or an MCP client with a Portainer access token. It never holds cluster credentials, git credentials, or a kubeconfig.
- Crossing to Portainer. TLS to the Portainer host, the only entry point, terminated, authenticated, and authorized by Portainer Business (session or token, per-add-on access policy, strict CSP).
- Zone 2, Portainer Business and the add-on. Portainer-Run runs in its own system namespace with no external address, reachable only through Portainer's gateway. It holds git target credentials encrypted with AES-256-GCM, the key held as a Kubernetes Secret, and any AI keys server-side only.
- Crossing to Git. Verified TLS carrying the target's token. Every deployment artifact crosses here before anything runs, which is where your code scanning, secret detection, and branch protection execute.
- Zone 3, the sanctioned Git repository. The source of truth: standard source, standard manifests, full history, nothing proprietary, and the authoritative record of what is deployed, by whom, and when.
- Crossing to the cluster. Portainer Business polls the repository and reconciles changes. No out-of-band path to the cluster exists.
- Zone 4, your Kubernetes platform. Apps land in designated namespaces under the sandbox described above, subject to your own admission controllers and network policies.
Where credentials live
The git token for a source repository is stored encrypted and injected into the clone step by Kubernetes Secret reference, so it never appears in a deployment spec or on a command line. Detected application secrets travel the same way, by reference, and never reach the repository. Portainer-Run has no external address of its own and creates no new login surface; authentication and RBAC are Portainer's, and add-on access is separately grantable per user and team.
The MCP endpoint
Portainer-Run exposes a Model Context Protocol endpoint so an AI coding tool can deploy with no UI in the path. It is reached through the same authenticated Portainer gateway, authorized the same way, and runs the identical governed pipeline underneath: the same git commit, the same generated manifest, the same secrets handling, and the same RBAC scope. MCP clients authenticate with a Portainer API access token; git targets cannot be created over MCP, which remains a deliberate UI action.
Claims this architecture supports
People deploying never receive cluster credentials, git credentials, or a kubeconfig at any point. Every deployment is committed to a sanctioned repository before it runs, and the running state always tracks that repository. The application inventory is complete by construction, because the governed pipeline is the only way in and every app carries the managed-by=portainer-run label with the deploying identity recorded. Removal is as clean as deployment: one commit removes the app's manifest and source, and the workload leaves the cluster on the next reconcile.
Supported runtimes
Detection is automatic and runs in priority order. If nothing matches, Portainer-Run falls back to nginx.
package.json
Node.js 22
Runs on node:22. Start command from the start script, else node server.js / index.js / npm start.
requirements.txt / .py
Python 3.13
Runs on python:3.13-slim. Targets main.py, app.py, server.py, or run.py, else python app.py.
.php
PHP 8.4
Served with Apache on php:8.4-apache.
Gemfile / .rb
Ruby 3.4
Runs on ruby:3.4-slim. Rack apps use bundle exec rackup, else app.rb or server.rb.
static assets
Static (nginx)
HTML, CSS, JS, images, and so on, on nginxinc/nginx-unprivileged:alpine running as UID 101. A single non-index.html file is renamed on commit.
Limitations
- Kubernetes clusters only. Apps deploy to Kubernetes environments managed by Portainer. Docker and Swarm are not supported as deployment targets.
- Single-container apps only. Vibe Deploy does not handle sidecars, databases, or message queues. Multi-service apps use Simple Deploy or Manifest Builder instead.
- No compiled languages. Go, Java, Rust, and .NET need a build step and are not supported by the Vibe Deploy path, which installs runtime dependencies but does not compile.
- Ingress controller required for Ingress. Portainer-Run creates Ingress routes but does not install or configure a controller.
- Default storage class required. Each app gets a PVC; without a default storage class it stays unbound and the app will not start.
Full documentation
The documentation has the complete reference: the deployment form, the application catalog and template format, the Assistant, the aggregated status architecture, and configuration.

