Technical detail

Run AI-built apps on the Kubernetes you already operate

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

What you need

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

How Deploy 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:

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.

Under the hood

Pod security and hardening

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.

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.

Reference

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.

Default port 3000
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.

Default port 8000
.php

PHP 8.4

Served with Apache on php:8.4-apache.

Default port 80
Gemfile / .rb

Ruby 3.4

Runs on ruby:3.4-slim. Rack apps use bundle exec rackup, else app.rb or server.rb.

Default port 9292
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.

Default port 8080

Know before you go

Limitations

Go deeper

Full documentation and source

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.