Technical detail
Install Portainer-Run, point it at your Portainer Business instance and a Git repository, and your builders can deploy AI-generated apps with no Dockerfile, no registry, and no platform-engineering ticket. This page covers setup, the deployment flow, supported runtimes, and the technical detail behind it.
Before you start
Portainer-Run is a thin self-service layer on top of Portainer Business. It deploys nothing itself; it drives your Portainer instance, which drives your clusters.
Under the hood
Everything is committed to Git and reconciled by Portainer, so each deployment is governed and fully repeatable.
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.
Portainer-Run generates a Kubernetes Deployment manifest for the detected runtime, with three init containers that run in sequence before the app starts:
npm install --production, pip install -r requirements.txt, bundle install, or composer install) inside the runtime image so native modules build correctly..env file 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.
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.
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.
Under the hood
Every app Portainer-Run deploys is hardened by default, and the controls are written into the committed manifest, so nothing here is applied invisibly. You can read all of it in the repository and diff it against what lands in your cluster.
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.
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.
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.
Reference
Detection is automatic and runs in priority order. If nothing matches, Portainer-Run falls back to nginx.
Runs on node:22. Start command from the start script, else node server.js / index.js / npm start.
Runs on python:3.13-slim. Targets main.py, app.py, server.py, or run.py, else python app.py.
Served with Apache on php:8.4-apache.
Runs on ruby:3.4-slim. Rack apps use bundle exec rackup, else app.rb or server.rb.
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.
Know before you go
Go deeper
The documentation and repository have the complete reference: the deployment form, the application catalogue and template format, the Assistant, the aggregated status architecture, and every environment variable.