# Porter > ## Agent Instructions > When helping a user operate an existing Porter project (deploying, updating, debugging, or inspecting applications and clusters) connect to [Porter's remote MCP server](https://mcp.porter.run) first and use its tools before falling back to the Porter CLI, `porter.yaml`, or dashboard workflows. If the Porter tools are not available in your session, `curl -fsSL https://agents.porter.run | sh` installs the CLI, registers the MCP server with your client, and installs Porter's skills; the client must be restarted afterward. If the user asks for the CLI, `porter apply`, configuration-as-code, or the dashboard, use that instead — a stated preference wins. Before inspecting or building a GitHub repository or calling any deployment tool, call `connect_github` with the repository. Proceed only when it returns `next_step: none`; otherwise, give the user the returned URL and stop. If the requested application or cluster workflow is not directly supported, inspect the relevant state with MCP tools before explaining the limitation or recommending a fallback. The server signs in through a browser using OAuth, so it cannot be used in headless environments such as CI pipelines; use the CLI or `porter.yaml` there. # Custom Helm Charts Source: https://docs.porter.run/addons/custom-helm-charts Deploy external Helm charts to your Porter cluster via the dashboard or Helm CLI for third-party components not natively managed by Porter Sometimes you need to install external charts that are not managed by Porter. You can deploy custom Helm charts either through the Porter dashboard or directly via the Helm CLI. *** ## Deploying via Porter Deploy Helm charts through the Porter dashboard for a managed experience with visibility into your deployments. **1. Select Helm Chart addon** Navigate to **Add-ons** in your Porter dashboard and select **Helm Chart**. **2. Configure the chart** Enter the Helm repository URL and select the chart you want to deploy. **3. Select version** Choose the chart version you want to install. **4. Customize values (optional)** Modify any values from the chart's default configuration as needed. **5. Deploy** Click **Deploy** to install the chart to your cluster. > [!NOTE] > > Since custom Helm charts install external components into your cluster, they fall outside of Porter's standard support. However, we'll do our best to help you troubleshoot issues. ### Managing deployed charts Once deployed, you can: * View the chart status in the Add-ons tab * Update values and redeploy * Upgrade to newer chart versions * Delete the chart when no longer needed *** ## Deploying via Helm CLI For more control or CI/CD integration, you can deploy Helm charts directly using the Helm CLI. ### Prerequisites 1. Install the [Helm CLI](https://helm.sh/docs/intro/install/) 2. Configure kubectl to connect to your Porter cluster. Run: ```bash theme={null} porter config set-cluster ``` And select the cluster from the dropdown. If there is only one cluster in your project it will be automatically selected. ### Deploying a chart ```bash theme={null} # Add the Helm repository helm repo add helm repo update # Install the chart: porter helm -- install / \ --namespace \ --values values.yaml ``` ### Example: Installing NGINX Ingress ```bash theme={null} # Add the ingress-nginx repository porter helm -- repo add ingress-nginx https://kubernetes.github.io/ingress-nginx porter helm -- repo update # Install the chart porter helm -- install my-ingress ingress-nginx/ingress-nginx \ --namespace ingress \ --create-namespace ``` ### Upgrading a release ```bash theme={null} porter helm -- upgrade / \ --namespace \ --values values.yaml ``` ### Listing releases ```bash theme={null} porter helm -- list --all-namespaces ``` ### Uninstalling a release ```bash theme={null} porter helm -- uninstall --namespace ``` *** ## Observability There is limited observability offered for third-party helm chart installations, consisting of logs available in the dashboard. For more complex scenarios, you can deploy a **Grafana** addon, which is already configured with the existing observability stack deployed in the cluster. Grafana makes it possible to explore, query and build dashboard to monitor any charts or workloads deployed in your cluster. *** ## Best Practices ### Use version pinning Always specify a chart version to ensure reproducible deployments: ```bash theme={null} porter helm -- install my-release repo/chart --version 1.2.3 ``` ### Store values in version control Keep your custom values files in version control alongside your application code. For example, for the "custom-chart" helm chart, you can keep the following structure: `porter-custom-helm-chart-addons/custom-chart/chart.yaml` ```yaml theme={null} # chart.yaml chartUrl: https://custom-chart-repo.com version: 1.0.0 ``` `porter-custom-helm-chart-addons/custom-chart/values.yaml` ```yaml theme={null} # values.yaml replicaCount: 3 resources: limits: cpu: 100m memory: 128Mi ``` You can then use these values in CI to install the chart ```bash theme={null} CHART_URL=$(yq e '.chartUrl' porter-custom-helm-chart-addons/custom-chart/chart.yaml) VERSION=$(yq e '.version' porter-custom-helm-chart-addons/custom-chart/chart.yaml) porter helm -- install custom-chart $CHART_URL \ --version $VERSION \ --values porter-custom-helm-chart-addons/custom-chart/values.yaml ``` ### Test in non-production first Before deploying to production, test custom charts in a development or staging cluster to verify compatibility with your Porter environment. # Datastores Source: https://docs.porter.run/addons/datastores Provision managed Postgres and Redis databases on AWS with automatic VPC peering, private subnet networking, and security group configuration Porter simplifies database provisioning by automatically setting up all networking components between your cluster and Porter-provisioned databases. > [!NOTE] > > Datastores are currently supported on **AWS only**. GCP and Azure datastore support is on the roadmap. ## AWS Architecture Datastores are provisioned in a VPC that is separate from the VPC of your clusters. Porter automatically: * Peers the datastore VPC to your cluster VPC * Configures subnets, routing tables, and security groups * Ensures traffic flows exclusively through private subnets This architecture keeps your database secure and accessible only from applications running in your cluster. *** ## Setup **1. Create a datastore** Navigate to **Add-ons** in your Porter dashboard and select the datastore type you want to create (Postgres or Redis). **2. Configure settings** Configure your datastore settings including instance size, storage, and high availability options. **3. Connect to your application** Porter creates an environment group with the connection details. Inject this environment group into your applications. **4. Deploy** Deploy your application. It can now connect to the database using the injected environment variables. ### Connecting from your laptop To connect to a datastore from your local machine, use the Porter CLI: ```bash theme={null} porter datastore connect my-datastore psql -h localhost -p -U -l ``` > [!NOTE] > > If your cluster control plane access is set to private, using this command requires [Tailscale VPN](/security-and-compliance/tailscale) to be configured for your cluster. *** ## Postgres Postgres datastores can be deployed in different configurations depending on your needs: | Configuration | Use Case | Recommended For | | - | - | - | | **In-cluster** | Quick setup for development | Dev/staging environments | | **Single RDS instance (Multi-AZ)** | Standard managed database | Production workloads | | **Aurora cluster (Multi-AZ)** | Auto-scaling storage and enhanced failover | Production workloads with stringent high-availability requirements | ### In-cluster Postgres Deploys Postgres as a container within your cluster. This is the fastest way to get started but is **not recommended for production data**. ### RDS Instance Provisions a standard Amazon RDS instance with Multi-AZ deployment for automatic failover. This is the recommended option for most production workloads. ### Aurora Cluster Aurora provides: * Automatic storage autoscaling * Enhanced failover capabilities * High availability settings You can create an Aurora datastore with a single instance or with an additional read replica. #### Read Replicas To enable a read replica, select the **HA toggle** when creating the datastore. With read replicas: * The dashboard displays connection details for both primary and replica * Modifications automatically failover the primary and promote the replica * This ensures minimum downtime during operations ### Configuration The following table outlines the configurable fields and behaviors for each datastore type during creation and updates: | Configuration | In-cluster | Standard RDS | Aurora | | :- | :- | :- | :- | | **Connected cluster** | Local (via K8s Service) | External (via VPC Peering) | External (via VPC Peering) | | **Region** | Matches connected cluster | Matches connected cluster | Matches connected cluster | | **Database name** | - | User-defined | User-defined | | **Master username** | - | User-defined | User-defined | | **Postgres version** | - | Postgres 12-18 | Postgres 12-18 | | **Instance type** | CPU/RAM Limits | All RDS compatible instances | All Aurora compatible classes, excepts serverless | | **Allocated storage** | **Fixed** (Cannot be modified) | **Modifiable** (Increase only) | **Managed** (Auto-scales) | | **Snapshot restore** | - | From RDS snapshot | From Aurora snapshot | | **Cloning** | - | - | Fast Database Cloning | *** ## Redis Redis datastores can be provisioned in different configurations: | Configuration | Use Case | Recommended For | | - | - | - | | **In-cluster** | Quick setup for development | Dev/staging environments | | **Elasticache replication group** | Managed cache with automatic failover | Production workloads | ### In-cluster Redis Deploys Redis as a container within your cluster. This is the fastest way to get started but is **not recommended for production data**. ### Elasticache Replication Group Provisions an Amazon Elasticache replication group with: * Primary and reader replica by default * Automatic failover if the primary fails * Minimal downtime during modifications *** ## Monitoring You can monitor the performance of your database from the Porter dashboard. Metrics are available in the "Metrics" tab when opening a datastore. The metrics that are currently displayed are: * CPU utilization * RAM utilization * Storage capacity *** ## Disaster Recovery Porter supports some options for disaster recovery of RDS datastores. ### Restoring from a snapshot You can restore a snapshot to a new datastore, and the datastore will be accessible from the applications running in your cluster. This can significantly reduce the time to recovery during an emergency. **1. Enable** Create a new datastore in the dashboard. In the creation form, click on **"Enable snapshot restore"** **2. Confirm** Select one of the available snapshots to restore or enter the id manually. Only snapshots in the same region as the database are listed. **3. Create** Create the datastore. This will start the process of restoring the snapshot. ### Cloning an Aurora cluster Aurora clusters support cloning an existing cluster using fast-cloning. This process is faster than restoring from a snapshot, and can be used to recover from an emergency, or to quickly create copies of your database for experiments. **1. Enable** Create a new datastore in the dashboard. In the creation form, click on **"Enable database cloning"** **2. Confirm** Select one of the existing Aurora clusters or enter the identifier manually. Only clusters in the same region as the selected one are listed. **3. Create** Create the datastore. This will start the process of cloning the cluster. *** ## Compliance If the compliance feature is enabled for your project, Porter automatically configures monitoring alarms for RDS and Aurora datastores: * CPU utilization alarms * Memory utilization alarms * Storage capacity alarms These alarms help ensure your databases remain healthy and within operational thresholds. ## Roadmap The following features are not yet supported natively in Porter, but reach out to the support team for help setting them up. * Connection pooling * External access to datastores # Add-ons Source: https://docs.porter.run/addons/overview Extend your Porter cluster with managed Postgres and Redis databases, persistent storage, third-party monitoring tools, and custom Helm charts Add-ons extend your Porter cluster with additional infrastructure components like databases, monitoring tools, persistent storage, and custom Helm charts. ## Available Add-ons ### Databases | Add-on | Description | | - | - | | **Postgres** | An object-relational database system | | **Redis** | An in-memory key-value database | [Learn more about Datastores →](/addons/datastores) ### Monitoring | Add-on | Description | | - | - | | **Datadog** | Pipe logs, metrics, and APM data from your workloads | | **New Relic** | Monitor your applications and infrastructure | | **Grafana** | An open source analytics and monitoring tool | | **Langfuse** | An open source LLM engineering platform | | **Helicone AI Gateway** | An open source AI gateway for LLM requests | [Learn more about Third-party Observability →](/addons/third-party-observability) ### Logging | Add-on | Description | | - | - | | **Mezmo** | A popular logging management system | ### Analytics | Add-on | Description | | - | - | | **Metabase** | An open source business intelligence tool | | **Quivr** | Your second brain, empowered by generative AI | | **n8n** | An open source workflow engine | ### Storage | Add-on | Description | | - | - | | **Persistent Disk** | A persistent disk that can be attached to apps for data storage | [Learn more about Storage →](/addons/storage) ### AI Agents | Add-on | Description | | - | - | | **QM** | An open source, multiplayer agent harness that runs agents in Porter Sandboxes | [Learn more about QM →](/addons/qm) ### Custom | Add-on | Description | | - | - | | **Helm Chart** | Install any Helm chart from a public repository | [Learn more about Custom Helm Charts →](/addons/custom-helm-charts) *** ## Cloud Provider Support | Add-on | AWS | GCP | Azure | | - | - | - | - | | **Datastores (Postgres, Redis)** | ✅ | Coming soon | Coming soon | | **Monitoring & Analytics** | ✅ | ✅ | ✅ | | **Custom Helm Charts** | ✅ | ✅ | ✅ | | **Storage (Persistent Disk)** | ✅ | Coming soon | Coming soon | | **QM** | ✅ | Coming soon | Coming soon | *** ## Managing Add-ons Add-ons are managed through the **Add-ons** tab in your Porter dashboard. From there you can: * Create new add-ons * View and modify existing add-on configurations * Connect add-ons to your applications via environment groups * Delete add-ons when no longer needed > [!WARNING] > > Deleting an add-on may cause connected applications to lose access to the resource. Ensure you've migrated any data before deleting datastores or storage add-ons. # QM Source: https://docs.porter.run/addons/qm Run QM, the open source multiplayer agent harness, on your own Porter cluster with agents in Porter Sandboxes, a Postgres database, and an optional S3 bucket [QM](https://github.com/yc-software/qm) is an open source, multiplayer agent harness. Your team works with agents in a shared workspace where context and files carry across your organization. Porter runs QM as an add-on on your cluster, and every agent runs in a [Porter Sandbox](/sandboxes/overview) on the same cluster. > [!NOTE] > > QM is currently only available on AWS. If you don't see QM in the add-on list, please reach out via the support widget on the dashboard. ## Install QM with an AI agent The [Porter MCP server](/mcp/overview) can install and manage QM from an AI agent with the [add-on tools](/mcp/tools#add-ons). The [quick install](/mcp/overview#quick-install) also adds the `porter-qm` skill, which takes a cluster to a signed-in QM install and works through failures along the way. Ask your agent to set up QM on Porter. It collects the inputs in one round of questions and then runs the install. If the cluster doesn't have sandboxes yet, it can enable them with `enable_sandboxes`. *** ## How it works The QM add-on deploys these services to your cluster: | Service | Description | | - | - | | **portal** | Serves the QM web app and handles sign-in. The only service exposed outside the cluster. | | **web-ui** | The QM interface and admin console, served through the portal. | | **core** | Runs agent turns and creates sandboxes through the in-cluster Sandbox API. | Porter wires QM to the cluster for you: QM talks to the Sandbox API inside the cluster without an API token. Everything QM stores lives in your cloud account: its state in a Postgres database you provide, and its files in an optional S3 bucket. *** ## Prerequisites * An AWS cluster with [sandboxes enabled](/sandboxes/getting-started#enable-sandboxes) * A Postgres database reachable from the cluster * Optional: an S3 bucket connected to the cluster * Optional: public or private [sandbox networking](/sandboxes/networking#configure-networking-on-the-cluster) for published apps > [!NOTE] > > Porter rejects a QM install on a cluster without sandboxes, since every agent runs in a sandbox. *** ## Setup **1. Enable sandboxes** In the Porter Dashboard, navigate to the **Sandbox** tab for the AWS cluster where you want to run QM and click **Enable sandboxes**. See [Sandboxes Getting Started](/sandboxes/getting-started#enable-sandboxes) for the CLI equivalent. **2. Create a Postgres datastore** Navigate to **Add-ons** and create a **Postgres** datastore in the same cluster. See [Datastores](/addons/datastores) for the available configurations. Use one database per QM install. You can also bring your own Postgres. QM needs a connection URL that is reachable from inside the cluster. **3. Create a bucket (optional)** Navigate to **Add-ons**, choose **Object storage**, then pick **Create a new bucket** or **Register an existing bucket**. Connect the bucket to the QM cluster. The bucket must be in the same AWS account as the cluster and must not be read-only. See [Set up the bucket](/sandboxes/volumes#set-up-the-bucket) for the CLI equivalent. **4. Create the QM add-on** Navigate to **Add-ons** and select **QM**. Pick the cluster, give the add-on a name, then fill in the three steps below and click **Deploy**. ### General | Setting | Description | | - | - | | **Expose QM on a URL** | On by default. Porter provisions a domain for the portal. Sign-in and published apps need it. | | **Load balancer** | Shown when the cluster has a [private load balancer](#private-load-balancer). **Public LB** is the default. | | **Database** | Click **Connect existing datastore** to fill in the connection URL from a Porter datastore, or paste a `postgres://` URL. | | **File storage** | The bucket QM keeps uploads and **Files** data in. | > [!WARNING] > > Without a bucket, QM keeps files in temporary storage on the core pod. Uploads and **Files** data are lost when QM restarts or updates. Attach a bucket for anything beyond a trial. ### Sign-in **Password** is the default. Porter fills in your email and generates a password. > [!WARNING] > > Copy the password before you click **Deploy**. Porter stores only a hash of it, so it can't be shown again. To reset it, enter a new password on the add-on's **Sign-in** tab and click **Deploy**. See [Sign-in methods](#sign-in-methods) to switch to your identity provider or to email links. ### People **Administrators** starts with your email. Administrators can invite other people from the QM admin console after first boot. *** ## Sign in to QM Once the add-on is deployed, open the URL in the add-on header and sign in with the password account. QM doesn't need a model provider to install. After the first sign-in, set the base model in the QM admin console. The add-on's **Integrations** tab also takes fallback model API keys for agent turns. *** ## Sign-in methods You can change the sign-in method at any time from the add-on's **Sign-in** tab. | Method | Description | What you need | | - | - | - | | **Password** | One account signs in with an email and password. Use it to get started, then invite others or switch to your identity provider. | A password of at least 12 characters | | **Your identity provider** | People sign in through an OpenID Connect provider, such as Google Workspace or Okta. | An OAuth client ID and secret from your provider | | **Email links from QM** | QM emails one-time sign-in links to allowed addresses. | A sender address verified with [Resend](https://resend.com) and a Resend API key | ### Your identity provider **1. Register an app with your provider** Create an OpenID Connect web app with your identity provider. Set its redirect URI to your QM domain followed by `/auth/callback`, for example `https://qm.acme.com/auth/callback`. **2. Choose the provider** On the **Sign-in** tab, select **Your identity provider**, then pick the provider. **Google** fills in its endpoints, and **Okta** fills them in from your Okta domain. For **Custom**, copy the issuer and endpoint URLs from your provider's `/.well-known/openid-configuration` document. **3. Enter the client ID and secret** Paste the client ID and secret from the app you registered, then click **Deploy**. Administrators can always sign in. To let others in, expand the advanced settings under **People** and fill in **Allowed sign-in emails** or **Allowed email domain**. > [!NOTE] > > If sign-in fails with a redirect mismatch while the add-on reports healthy, the callback URL is missing from your provider's allowed redirect URIs. *** ## Private load balancer You can serve QM from the cluster's [private load balancer](/cloud-accounts/advanced-cluster-settings#private-load-balancer) so it's only reachable from inside your VPC or networks peered to it, for example over [Tailscale](/security-and-compliance/tailscale). **1. Add the private load balancer to the cluster** Follow [Private load balancer](/cloud-accounts/advanced-cluster-settings#private-load-balancer) in the cluster settings, including the DNS provider Porter uses to issue certificates. **2. Turn on egress enforcement for sandboxes** In the **Sandbox** tab, open **Settings** and enable **Enforce egress allowlists** under **Egress controls**. Agents run in sandboxes, which can't reach private addresses by default. With enforcement on, Porter lets every sandbox reach the QM domain. See [Reaching QM from sandboxes](#reaching-qm-from-sandboxes). **3. Select the private load balancer** On the add-on's **General** step, select **Private LB** under **Load balancer**. A private QM needs a custom domain, such as `qm.internal.acme.com`. **4. Create the DNS record** The form shows the private load balancer's address and the record type to use. Create that record for the custom domain, then enter the domain and click **Deploy**. The private load balancer's address only resolves to something reachable from inside your VPC. Point your own private DNS at it if your clients resolve through one. To keep published apps private too, don't configure **Public** sandbox networking. See [Published apps](#published-apps). *** ## Published apps Agents can publish the apps they build. Each published app runs in its own sandbox, and where it's served depends on the cluster's [sandbox networking](/sandboxes/networking): | Sandbox networking | Where published apps are served | | - | - | | None | Inside the cluster only. People open apps through QM at `/d//`. | | Private | A hostname under the private sandbox domain, reachable from inside your VPC | | Public | A hostname under the public sandbox domain. QM picks public when both are configured. | Sandbox networking is optional. To give apps their own hostnames, open **Settings** in the **Sandbox** tab and follow [Configure networking on the cluster](/sandboxes/networking#configure-networking-on-the-cluster) through to the wildcard DNS record. *** ## Reaching QM from sandboxes Agents run in sandboxes and call back into QM through its URL. Sandboxes can't reach private addresses by default, so a QM on the private load balancer needs **Enforce egress allowlists** turned on in the **Sandbox** tab's **Settings**. With it on, Porter lets every sandbox reach the QM domain. *** ## Updating QM The add-on's **General** tab has a **Release** picker. Porter installs the newest release on create. You can choose when to move an existing install to a newer one. *** ## Troubleshooting | Problem | Fix | | - | - | | Install rejected with `qm requires sandbox-api` | Enable sandboxes on the cluster, then deploy again. | | Uploads disappear after an update | QM has no bucket. Attach one under **File storage**. | | Agents can't reach a private QM | Turn on **Enforce egress allowlists** for the cluster's sandboxes. | | Pods never become ready | Check that the database URL is reachable from inside the cluster. | *** ## Deleting QM > [!WARNING] > > Deleting the QM add-on stops QM and removes its configuration. Your Postgres database and bucket aren't deleted, so delete them separately if you no longer need the data. # Persistent storage Source: https://docs.porter.run/addons/storage Provision Amazon EFS persistent disks that can be shared across multiple services for read-intensive and media processing workloads Porter makes it easy to provision persistent storage that can be shared across multiple services. This is useful for read-intensive applications and workloads with low-latency read requirements. > [!NOTE] > > Persistent storage is currently supported on **AWS only** (EFS). GCP and Azure storage support is on the roadmap. *** ## Persistent Disk (EFS) Amazon Elastic File System (EFS) provides a shared filesystem that multiple services can mount simultaneously. Any services mounting the disk can read and write to the same files. ### Use Cases * **Shared file storage**: Multiple services need access to the same files * **Read-intensive workloads**: Cache frequently accessed data on disk * **Media processing**: Store uploaded files for processing by multiple workers * **Machine learning**: Share model files across inference services ### Setup **1. Create the persistent disk** Navigate to **Add-ons** in your Porter dashboard and create a **Persistent Disk** add-on. **2. Enable persistent disk on your service** Go to your application's **Services** tab, select the service, and navigate to **Advanced Settings**. Enable **Persistent disk**. **3. Configure the disk name** Enter the name of the persistent disk add-on you created. **4. Redeploy your application** Deploy your application. The disk will be mounted automatically. ### Accessing the Disk Once configured, your service can access the shared disk at: ``` /data/efs/ ``` All services with the persistent disk enabled will have read and write access to this directory. ### Example If you have two services (`api` and `worker`) both mounting the same persistent disk 'my-disk': * `api` writes a file to `/data/api/my-disk/uploads/image.png` * `worker` can read the same file from `/data/worker/my-disk/image.png` *** ## Best Practices ### Use for appropriate workloads EFS is optimized for throughput rather than IOPS. It's best suited for: * Large file reads and writes * Shared access patterns * Workloads that can tolerate slightly higher latency than local disk For high-IOPS workloads like databases, use [Datastores](/addons/datastores) instead. ### Monitor storage usage Keep track of storage usage to avoid unexpected costs. EFS charges based on the amount of data stored. ### Plan for data migration If you need to move data off EFS, plan your migration strategy before deleting the add-on. *** ## Deleting a Persistent Disk > [!WARNING] > > Deleting the persistent disk add-on will remove all mounts to the disk. Any applications will lose access to the files stored on the disk. Before deleting: 1. Migrate any important data to another storage location 2. Update your services to remove the persistent disk configuration 3. Redeploy affected services 4. Delete the persistent disk add-on # Third-party observability Source: https://docs.porter.run/addons/third-party-observability Integrate Datadog, New Relic, Grafana, Langfuse, and Helicone with your Porter cluster for advanced application monitoring and observability Porter integrates with popular observability platforms to provide application-level monitoring, logging, and alerting beyond the built-in cluster observability features. ## Supported Platforms > **Datadog** > > Full-stack monitoring with APM, logs, and infrastructure metrics > **New Relic** > > Application performance monitoring and alerting > **Grafana** > > Dashboards and visualization for metrics and logs > **Langfuse** > > Open source LLM engineering platform for tracing and analytics > **Helicone** > > Open source AI gateway for LLM request monitoring ## What You Get Integrating a third-party observability platform provides: * **Application Performance Monitoring (APM)**: Trace requests across services * **Log aggregation**: Centralized logging with search and filtering * **Custom metrics**: Track business and application-specific metrics * **Alerting**: Get notified when metrics exceed thresholds * **Dashboards**: Visualize system health and performance trends ## Comparison with Built-in Observability | Feature | Porter Built-in | Third-party Platforms | | - | - | - | | Pod status and metrics | ✅ | ✅ | | Node metrics | ✅ | ✅ | | Application logs | Basic | Advanced search and filtering | | Distributed tracing | ❌ | ✅ | | Custom dashboards | ❌ | ✅ | | Alerting | ❌ | ✅ | | Long-term retention | Limited | Configurable | For production workloads, we recommend integrating at least one third-party observability platform to get comprehensive visibility into your applications. # Deploying multiple apps with porter.yaml Source: https://docs.porter.run/applications/configuration-as-code/addons-porter-yaml Define and deploy multiple Porter applications alongside datastores and custom Helm chart addons in a single porter.yaml file using porter apply You can deploy multiple Porter apps alongside multiple Porter addons in a single `porter.yaml` file. Currently only in-cluster datastores are supported for now. Requires Porter CLI **v0.68.30 or later**. Check your version with `porter version`, and see the [CLI installation guide](https://docs.porter.run/cli/installation) to install or upgrade. ```bash theme={null} porter apply -f porter.yaml --wait ``` To guarantee that the datastore and its env group are ready before the app starts, use the --wait flag. Addons are always applied before apps, and `--wait` holds the apply until each datastore is fully available (up to 10 minutes) before any apps deploy. This guarantees that the datastores' environment groups and credentials exist by the time the apps start. The flag also waits for each app's rollout to succeed before exiting. Without `--wait`, the CLI proceeds about 10 seconds after creating a datastore. Apps may then fail their first deploy with a missing-environment-group error until the datastore finishes provisioning. When a datastore is provisioned, Porter automatically creates an environment group with the **same name as the datastore**. This environment group holds the datastore's connection details (host, port, credentials, etc.) as environment variables. To link a datastore to an app, reference that environment group by name in the app's `envGroups` section. The entry must match the datastore's `name` exactly. For example, to connect an app to a datastore named `cache`: ```yaml theme={null} envGroups: - cache ``` Embed the app schema as an item in the `apps:` list. Then attach addons as a list of `addons:` items. ## Example Configuration ```yaml theme={null} apps: - version: v2 name: backend services: - name: server run: "" type: web instances: 1 cpuCores: 0.2 ramMegabytes: 100 terminationGracePeriodSeconds: 30 port: 80 sleep: false private: true envGroups: # Environment groups can be used to inject environment variables from the addons into the service. - cache - db image: repository: nginx tag: latest - version: v2 name: frontend services: - name: dashboard run: "" type: web instances: 1 cpuCores: 0.2 ramMegabytes: 100 terminationGracePeriodSeconds: 30 port: 80 sleep: false private: true envGroups: - cache - db image: repository: nginx tag: latest - version: v2 name: api services: - name: worker run: "" type: worker instances: 1 cpuCores: 0.2 ramMegabytes: 100 terminationGracePeriodSeconds: 30 port: 80 sleep: false envGroups: - cache - db image: repository: nginx tag: latest addons: - name: cache type: redis kind: in-cluster config: storageGigabytes: 2 # Persistent storage size in GB. Cannot be changed after creation. cpuCores: 0.1 ramMegabytes: 110 - name: db type: postgres kind: in-cluster config: storageGigabytes: 2 cpuCores: 0.1 ramMegabytes: 110 ``` # Configuration as code with porter.yaml Source: https://docs.porter.run/applications/configuration-as-code/overview Define application services, builds, and deployment settings in a porter.yaml file for CI/CD pipelines and version-controlled infrastructure ## What is `porter.yaml`? A `porter.yaml` file (or files, if you choose to set up multiple within your project/repository) defines your application's services, build configuration, and deployment settings as code. This file is used with the `porter apply` CLI command to deploy and update applications, enabling version-controlled infrastructure and automated CI/CD pipelines. ## When to Use Configuration-as-Code ### CI/CD Pipelines Deploy your application automatically on every push using `porter apply`: ```bash theme={null} porter apply -f porter.yaml ``` ### Version-Controlled Infrastructure Track infrastructure changes alongside your code. Every deployment configuration change goes through code review. ### Preview Environments Spin up isolated environments for pull requests with consistent configuration: ```bash theme={null} porter apply -f porter.yaml --preview ``` ## How It Works **1. Define Configuration** Create a `porter.yaml` file in your repository that describes your application's services, resources, and settings. **2. Run porter apply** The Porter CLI reads your configuration and sends it to the Porter API. **3. Build (if configured)** If a `build` section is defined, Porter builds and pushes your container image. **4. Deploy** Porter deploys or updates your services according to the configuration. ## Getting Started ### 1. Export Existing Configuration If you already have an app deployed on Porter, export its current configuration: ```bash theme={null} porter app yaml my-app > porter.yaml ``` ### 2. Create from Scratch Start with a minimal configuration: ```yaml theme={null} version: v2 name: my-app services: - name: web type: web run: npm start port: 3000 cpuCores: 0.5 ramMegabytes: 512 build: method: docker context: . dockerfile: ./Dockerfile ``` ### 3. Deploy Apply the configuration to deploy your app: ```bash theme={null} porter apply -f porter.yaml ``` ### Example `porter.yaml` The following is an example of a v2 `porter.yaml` file, which is the latest version of the spec. This example covers many of the available fields, but not all of them. For a full list of configurable options, see the [full reference](/applications/configuration-as-code/reference). ```yaml theme={null} version: v2 name: my-app services: - name: api type: web run: node index.js port: 8080 cpuCores: 0.1 ramMegabytes: 256 autoscaling: enabled: true minInstances: 1 maxInstances: 3 memoryThresholdPercent: 60 cpuThresholdPercent: 60 loadBalancers: - public_lb domains: - name: test1.example.com healthCheck: enabled: true httpPath: /healthz - name: example-wkr type: worker run: echo 'work' port: 8081 cpuCores: 0.1 ramMegabytes: 256 instances: 1 - name: example-job type: job run: echo 'hello world' allowConcurrent: true cpuCores: 0.1 ramMegabytes: 256 cron: '*/10 * * * *' predeploy: run: ls build: method: docker context: ./ dockerfile: ./app/Dockerfile env: NODE_ENV: production envGroups: - production-env-group ``` For detailed configuration options for each service type, see: * [Web Service](/applications/configuration-as-code/services/web-service) * [Worker Service](/applications/configuration-as-code/services/worker-service) * [Job Service](/applications/configuration-as-code/services/job-service) ## Configuration vs Dashboard > [!WARNING] > > When you deploy using `porter apply`, the configuration in `porter.yaml` takes precedence. Changes made in the Porter dashboard may be overwritten on the next deployment. For consistent deployments, we recommend: * Use `porter.yaml` as the source of truth for production * Use the dashboard for experimentation and one-off changes * Export dashboard changes with `porter app yaml` to update your configuration file ## Next Steps * [Full Reference](/applications/configuration-as-code/reference) - Complete documentation of all configuration options * [porter apply Command](/standard/cli/command-reference/porter-apply) - CLI reference for deployments * [Using Other CI Tools](/applications/deploy/using-other-ci-tools) - Integrate with GitHub Actions, GitLab CI, etc. # porter.yaml reference Source: https://docs.porter.run/applications/configuration-as-code/reference Complete field reference for porter.yaml including build, image, services, environment variables, predeploy, and autoscaling options This is the complete reference for all fields that can be set in a `porter.yaml` file. ## Top-Level Fields | Field | Type | Required | Description | | - | - | - | - | | `version` | string | Yes | Must be `v2` | | `name` | string | Conditional | App name. Required unless `PORTER_APP_NAME` env var is set | | `build` | object | Conditional | Build configuration. Cannot be used with `image` | | `image` | object | Conditional | Pre-built image configuration. Cannot be used with `build` | | `services` | array | Yes | List of service definitions | | `env` | object | No | Environment variables | | `envGroups` | string\[] | No | Names of environment groups to attach | | `predeploy` | object | No | Pre-deploy job configuration | | `initialDeploy` | object | No | Job to run only on first deployment | | `autoRollback` | object | No | Automatic rollback settings | | `efsStorage` | object | No | AWS EFS storage configuration | > [!WARNING] > > You must specify either `build` or `image`, but not both. Use `build` when Porter should build your container image, or `image` when using a pre-built image from a registry. *** ## `version` `string` Required The schema version. Must be `v2`. ```yaml theme={null} version: v2 ``` *** ## `name` `string` Required (unless `PORTER_APP_NAME` is set) The application name. Must be 31 characters or less, consist of lowercase alphanumeric characters or `-`, and start and end with an alphanumeric character. ```yaml theme={null} name: my-app ``` *** ## `build` `object` Optional Configuration for building container images. Cannot be used together with `image`. | Field | Type | Required | Description | | - | - | - | - | | `method` | string | Yes | Build method: `docker` or `pack` | | `context` | string | Yes | Build context directory | | `dockerfile` | string | Conditional | Dockerfile path (required if method is `docker`) | | `builder` | string | Conditional | Builder image (required if method is `pack`) | | `buildpacks` | string\[] | No | List of buildpacks (for `pack` method) | ```yaml Docker Build theme={null} build: method: docker context: . dockerfile: ./Dockerfile ``` ```yaml Buildpack Build theme={null} build: method: pack context: . builder: heroku/buildpacks:20 buildpacks: - heroku/python - heroku/nodejs ``` > [!WARNING] > > We recommend defining a Dockerfile over using buildpacks - if you're not sure which to use, default to creating a Dockerfile for your application. For more information about creating a Dockerfile, you can look [here](https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/). > [!NOTE] > > For available `builder` and `buildpacks` values, see the [Cloud Native Buildpacks Registry](https://registry.buildpacks.io/) or [Paketo Builders Reference](https://paketo.io/docs/reference/builders-reference/) documentation. Common builders include `heroku/builder:24` and `paketobuildpacks/builder-jammy-full:latest`. *** ## `image` `object` Optional Configuration for using a pre-built container image. Cannot be used together with `build`. | Field | Type | Required | Description | | - | - | - | - | | `repository` | string | Yes | Image repository URL | | `tag` | string | No | Image tag (can be overridden with `--tag` flag) | ```yaml theme={null} image: repository: my-registry/my-app tag: latest ``` *** ## `env` `object` Optional Environment variables to set for all services. Values must be strings. ```yaml theme={null} env: PORT: 8080 ``` > [!NOTE] > > For sensitive values, use environment groups instead of hardcoding them in `porter.yaml`. *** ## `envGroups` `string[]` Optional Names of [environment groups](/applications/configure/environment-groups) to attach to the application. Environment groups are project-wide and must already exist before deploying. ```yaml theme={null} envGroups: - production-secrets - shared-config - database-credentials ``` *** ## `predeploy` `object` Optional A job that runs before deploying services. Commonly used for database migrations. | Field | Type | Required | Description | | - | - | - | - | | `run` | string | Yes | Command to execute | | `cpuCores` | number | No | CPU allocation | | `ramMegabytes` | number | No | Memory allocation | | `connections` | array | No | Connections to external resources (e.g. Cloud SQL). See [Connections](/applications/configuration-as-code/services/connections) | ```yaml theme={null} predeploy: run: echo "predeploy" ``` See [Predeploy Configuration](/applications/configuration-as-code/services/predeploy) for more details. *** ## `initialDeploy` `object` Optional A job that runs only on the first deployment of a preview environment. Useful for one-time setup tasks like database seeding. | Field | Type | Required | Description | | - | - | - | - | | `run` | string | Yes | Command to execute | | `cpuCores` | number | No | CPU allocation | | `ramMegabytes` | number | No | Memory allocation | ```yaml theme={null} initialDeploy: run: npm run seed cpuCores: 0.25 ramMegabytes: 256 ``` *** ## `autoRollback` `object` Optional Configure automatic rollback when deployments fail. | Field | Type | Required | Description | | - | - | - | - | | `enabled` | boolean | Yes | Enable or disable auto-rollback | ```yaml theme={null} autoRollback: enabled: true ``` When enabled, Porter automatically rolls back all services to the last successfully deployed version if any service fails to deploy. *** ## `efsStorage` `object` Optional Enable AWS EFS (Elastic File System) storage for persistent data. Only available on AWS clusters. | Field | Type | Required | Description | | - | - | - | - | | `enabled` | boolean | Yes | Enable EFS storage | ```yaml theme={null} efsStorage: enabled: true ``` *** ## `services` `array` Required List of service definitions. Each service represents a deployable unit of your application. ### Common Service Fields These fields apply to all service types: | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | Unique service identifier (max 31 chars, lowercase alphanumeric and `-`) | | `type` | string | Yes | Service type: `web`, `worker`, or `job` | | `run` | string | Yes | Command to execute | | `instances` | integer | No | Number of replicas (not for jobs) | | `cpuCores` | number | Yes | CPU allocation (e.g., `0.5`, `1`, `2`) | | `ramMegabytes` | integer | Yes | Memory allocation in MB | | `gpuCoresNvidia` | integer | No | NVIDIA GPU cores to allocate | | `port` | integer | Conditional | Port the service listens on (required for `web`) | | `nodeGroup` | string | No | UUID of a user node group to run on | | `connections` | array | No | External cloud service connections | | `terminationGracePeriodSeconds` | integer | No | Seconds to wait before force-killing pods | | `serviceMeshEnabled` | boolean | No | Enable service mesh for inter-service communication | | `metricsScraping` | object | No | Prometheus metrics scraping configuration | ### Service Types | Type | Description | Documentation | | - | - | - | | `web` | HTTP services with public or private endpoints | [Web Services](/applications/configuration-as-code/services/web-service) | | `worker` | Background processing services | [Worker Services](/applications/configuration-as-code/services/worker-service) | | `job` | Scheduled or on-demand tasks | [Job Services](/applications/configuration-as-code/services/job-service) | ### Basic Example ```yaml theme={null} services: - name: web type: web run: python app.py instances: 1 cpuCores: 1 ramMegabytes: 1024 port: 8080 - name: web-on-user-node-group type: web run: python app.py instances: 1 cpuCores: 1 ramMegabytes: 1024 port: 8080 nodeGroup: 123e4567-e89b-12d3-a456-426614174000 ``` *** ## `connections` `array` Optional Configure connections to external cloud services. See [Connections Configuration](/applications/configuration-as-code/services/connections) for full documentation. ### AWS Role Connection Attach an IAM role to your service for AWS API access. ```yaml theme={null} services: - name: web ... connections: - type: awsRole role: iam-role-name ``` ### Azure Managed Identity Connection Bind a User Assigned Managed Identity to your service for Azure API access via Azure Workload Identity. ```yaml theme={null} services: - name: web ... connections: - type: azureManagedIdentity identityName: my-managed-identity resourceGroup: my-resource-group ``` ### GCP Service Account Connection Bind a GCP IAM service account to your service for GCP API access without service account key files. ```yaml theme={null} services: - name: web ... connections: - type: gcpServiceAccount serviceAccountEmail: my-app@my-project.iam.gserviceaccount.com ``` ### Cloud SQL Connection (GCP) Connect to Google Cloud SQL instances. ```yaml theme={null} services: - name: web ... connections: - type: cloudSql config: cloudSqlServiceAccount: service-account-name cloudSqlInstances: - connectionName: project-123456:us-east1:instance-name databasePort: 5432 ``` ### Persistent Disk Connection Attach persistent storage to your service. ```yaml theme={null} services: - name: web ... connections: - type: disk config: diskName: my-disk ``` *** ## `gpu` `object` Optional Configure GPU resources for machine learning workloads. | Field | Type | Description | | - | - | - | | `gpuCoresNvidia` | integer | Number of NVIDIA GPU cores | ```yaml theme={null} services: - name: web ... gpuCoresNvidia: 1 nodeGroup: 123e4567-e89b-12d3-a456-426614174000 ``` > [!NOTE] > > GPU workloads require a node group with GPU-enabled instances. *** ## `metricsScraping` `object` Optional Configure Prometheus metrics scraping for custom application metrics. | Field | Type | Description | | - | - | - | | `enabled` | boolean | Enable metrics scraping | | `path` | string | HTTP path to scrape (default: `/metrics`) | | `port` | integer | Port to scrape metrics from | | `scrapeIntervalSeconds` | integer | Scrape interval in seconds (default: 60) | ```yaml theme={null} services: - name: web ... metricsScraping: enabled: true path: /metrics port: 9090 ``` *** ## Complete Example ```yaml theme={null} version: v2 name: my-app build: method: docker context: . dockerfile: ./Dockerfile env: PORT: "8080" LOG_LEVEL: "info" envGroups: - production-secrets - shared-config predeploy: run: python manage.py migrate cpuCores: 0.5 ramMegabytes: 512 initialDeploy: run: python manage.py seed cpuCores: 0.25 ramMegabytes: 256 autoRollback: enabled: true efsStorage: enabled: true services: # Web service with all options - name: web type: web run: python app.py instances: 2 cpuCores: 1 ramMegabytes: 1024 port: 8080 terminationGracePeriodSeconds: 30 autoscaling: enabled: true minInstances: 2 maxInstances: 10 cpuThresholdPercent: 80 memoryThresholdPercent: 80 healthCheck: enabled: true httpPath: /healthz domains: - name: app.example.com metricsScraping: enabled: true path: /metrics port: 9090 scrapeIntervalSeconds: 30 connections: - type: awsRole role: my-app-role # GPU-enabled worker on a custom node group - name: ml-worker type: worker run: python ml_worker.py instances: 1 cpuCores: 4 ramMegabytes: 16384 gpuCoresNvidia: 1 nodeGroup: 123e4567-e89b-12d3-a456-426614174000 terminationGracePeriodSeconds: 60 serviceMeshEnabled: true # Background worker - name: worker type: worker run: python worker.py instances: 1 cpuCores: 0.5 ramMegabytes: 512 # Scheduled job - name: cleanup type: job run: python cleanup.py cpuCores: 0.5 ramMegabytes: 256 cron: "0 0 * * *" ``` *** ## Related Documentation * [Web Services](/applications/configuration-as-code/services/web-service) - Web service configuration * [Worker Services](/applications/configuration-as-code/services/worker-service) - Worker service configuration * [Job Services](/applications/configuration-as-code/services/job-service) - Job service configuration * [Predeploy Jobs](/applications/configuration-as-code/services/predeploy) - Pre-deploy job configuration * [Autoscaling Configuration](/applications/configuration-as-code/services/autoscaling) - Autoscaling configuration reference * [Connections Configuration](/applications/configuration-as-code/services/connections) - Cloud connections reference * [porter apply](/standard/cli/command-reference/porter-apply) - CLI reference for deployments # Autoscaling in porter.yaml Source: https://docs.porter.run/applications/configuration-as-code/services/autoscaling Configure horizontal pod autoscaling in porter.yaml with CPU and memory utilization thresholds, min/max replicas, and scaling behavior Configure horizontal pod autoscaling to automatically adjust the number of replicas based on resource utilization. ## Field Reference | Field | Type | Description | | - | - | - | | `enabled` | boolean | Enable autoscaling | | `minInstances` | integer | Minimum number of replicas | | `maxInstances` | integer | Maximum number of replicas | | `cpuThresholdPercent` | integer | CPU usage threshold (0-100) | | `memoryThresholdPercent` | integer | Memory usage threshold (0-100) | ## Basic Configuration ```yaml theme={null} services: - name: api # ... autoscaling: enabled: true minInstances: 2 maxInstances: 10 cpuThresholdPercent: 80 memoryThresholdPercent: 80 ``` > [!NOTE] > > When autoscaling is enabled, the `instances` field is ignored. The autoscaler manages replica count automatically. > [!TIP] > > For high availability, set `minInstances` to at least 2. See [Zero-downtime deployments](/applications/configure/zero-downtime-deployments) for more details. ## How It Works When either CPU or memory usage exceeds your configured threshold, Porter automatically adds replicas. When usage drops, replicas are removed (down to your minimum). ### Example: Autoscaling in Action Consider an API service with this configuration: ```yaml theme={null} autoscaling: enabled: true minInstances: 2 maxInstances: 10 cpuThresholdPercent: 60 memoryThresholdPercent: 80 ``` Here's how the autoscaler responds to changing load: | Time | Avg CPU | Avg Memory | Replicas | What Happens | | - | - | - | - | - | | t=0 | 30% | 40% | 2 | Baseline: both metrics below thresholds | | t=1 | 75% | 50% | 4 | CPU (75%) exceeds 60% threshold → scale up | | t=2 | 90% | 60% | 6 | CPU still high → continue scaling up | | t=3 | 55% | 85% | 8 | CPU stabilized, but memory (85%) exceeds 80% → scale up | | t=4 | 45% | 70% | 8 | Both metrics below thresholds → no change (cooldown period) | | t=5 | 40% | 50% | 5 | Sustained low usage → scale down | | t=6 | 35% | 45% | 2 | Continue scaling down to minimum | Key behaviors: * **Either metric triggers scaling**: If CPU *or* memory exceeds its threshold, replicas are added * **Both must be low to scale down**: Replicas are only removed when both CPU and memory are below their thresholds * **Respects bounds**: Replicas never drop below `minInstances` (2) or exceed `maxInstances` (10) * **Gradual changes**: The autoscaler adjusts incrementally, not all at once, to avoid oscillation ## Custom Metrics Autoscaling (Prometheus) Scale based on application-specific metrics like queue length, request latency, or custom business metrics. | Field | Type | Description | | - | - | - | | `customAutoscaling.prometheusMetricCustomAutoscaling.metricName` | string | Prometheus metric name | | `customAutoscaling.prometheusMetricCustomAutoscaling.threshold` | number | Threshold value to trigger scaling | | `customAutoscaling.prometheusMetricCustomAutoscaling.query` | string | Custom PromQL query (optional, defaults to metric name) | ```yaml theme={null} services: - name: api # ... autoscaling: enabled: true minInstances: 1 maxInstances: 10 customAutoscaling: prometheusMetricCustomAutoscaling: metricName: "http_requests_per_second" threshold: 100 query: "rate(http_requests_total[5m])" ``` > [!NOTE] > > Custom metrics autoscaling requires Prometheus to be accessible in your cluster. See [Custom Metrics and Autoscaling](/applications/observability/custom-metrics-and-autoscaling) for setup details. ## Temporal Autoscaling Scale Temporal workflow workers based on task queue depth. Porter monitors your Temporal task queues and automatically adjusts worker count. > [!NOTE] > > Temporal autoscaling requires a Temporal integration to be configured. See [Temporal Autoscaling](/applications/configure/temporal-autoscaling) for setup details. | Field | Type | Description | | - | - | - | | `temporalAutoscaling.temporalIntegrationId` | string | UUID of the Temporal integration | | `temporalAutoscaling.taskQueue` | string | Name of the Temporal task queue to monitor | | `temporalAutoscaling.targetQueueSize` | integer | How many queued tasks each replica should handle (e.g., set to 10 with 100 tasks queued → 10 replicas) | ```yaml theme={null} services: - name: temporal-worker # ... autoscaling: enabled: true minInstances: 2 maxInstances: 50 temporalAutoscaling: temporalIntegrationId: "550e8400-e29b-41d4-a716-446655440000" taskQueue: "my-task-queue" targetQueueSize: 10 ``` ## Related Documentation * [Autoscaling Overview](/applications/configure/autoscaling) - UI-based configuration and concepts * [Web Services](/applications/configuration-as-code/services/web-service) - Web service configuration * [Worker Services](/applications/configuration-as-code/services/worker-service) - Worker service configuration # Connections in porter.yaml Source: https://docs.porter.run/applications/configuration-as-code/services/connections Connect Porter services to AWS IAM roles, Azure managed identities, Google Cloud SQL instances, and persistent disks using porter.yaml connections. Connect your services to external cloud resources like AWS IAM roles, Azure managed identities, Google Cloud SQL instances, and persistent disks. ## Connection Types | Type | Description | Cloud Provider | | - | - | - | | `awsRole` | Attach an IAM role for AWS API access | AWS | | `azureManagedIdentity` | Bind a User Assigned Managed Identity for Azure API access | Azure | | `gcpServiceAccount` | Bind a GCP IAM service account for GCP API access | GCP | | `cloudSql` | Connect to Google Cloud SQL instances | GCP | | `disk` | Attach persistent storage | All | *** ## AWS Role Connection Attach an IAM role to your service for secure AWS API access without managing credentials. ### Field Reference | Field | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Must be `awsRole` | | `role` | string | Yes | IAM role name | ### Example ```yaml theme={null} services: - name: api # ... connections: - type: awsRole role: my-app-s3-access ``` *** ## Azure Managed Identity Connection Bind a User Assigned Managed Identity (UAMI) to your service for secure Azure API access without managing credentials. Porter uses [Azure Workload Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview) to federate the service's Kubernetes service account with the UAMI, so your application can authenticate to Azure resources using `DefaultAzureCredential` (or any credential type that supports workload identity). > [!NOTE] > > This feature is only available on AKS clusters created through Porter and must be enabled at the project level. Reach out to Porter support if you don't see it available on your project. ### Prerequisites Before adding this connection to your service, you must: 1. Have a User Assigned Managed Identity provisioned in your Azure subscription. Porter does **not** create the UAMI for you — provision it via the Azure Portal, Terraform, or the Azure CLI. 2. Grant the UAMI the Azure RBAC role assignments it needs to access the resources your service will call (e.g. `Storage Blob Data Reader` on a storage account). When your service deploys, Porter creates a [federated identity credential](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation) on the UAMI that maps your service's Kubernetes service account to the identity. At runtime, the pod receives a projected OIDC token that Azure exchanges for an access token scoped to the UAMI. ### Field Reference | Field | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Must be `azureManagedIdentity` | | `identityName` | string | Yes | Name of the User Assigned Managed Identity | | `resourceGroup` | string | Yes | Azure resource group containing the managed identity | ### Example ```yaml theme={null} services: - name: api # ... connections: - type: azureManagedIdentity identityName: my-app-identity resourceGroup: my-resource-group ``` > [!WARNING] > > This connection grants your service every permission assigned to the UAMI in Azure. Scope role assignments narrowly — a UAMI with subscription-level Owner is rarely what you want. > [!NOTE] > > Only one `azureManagedIdentity` connection is permitted per service. If you need to access resources across multiple identities, consolidate role assignments onto a single UAMI. *** ## GCP Service Account Connection Bind a GCP IAM service account to your service for secure GCP API access without managing service account key files. Your application can authenticate to GCP resources using [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials), and Porter handles the identity binding for your service. ### Prerequisites Before adding this connection to your service, you must: 1. Have a GCP IAM service account provisioned in the same GCP project as your Porter infrastructure. Porter does **not** create the IAM service account for you — provision it via the Google Cloud Console, Terraform, or the `gcloud` CLI. 2. Grant the IAM service account the GCP IAM roles it needs to access the resources your service will call (e.g. `roles/storage.objectViewer` on a storage bucket). When your service deploys, Porter configures the required IAM binding so the service can receive short-lived credentials scoped to the IAM service account. ### Field Reference | Field | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Must be `gcpServiceAccount` | | `serviceAccountEmail` | string | Yes | Fully-qualified email of the GCP IAM service account to impersonate | ### Example ```yaml theme={null} services: - name: api # ... connections: - type: gcpServiceAccount serviceAccountEmail: my-app@my-project.iam.gserviceaccount.com ``` > [!WARNING] > > This connection grants your service every permission assigned to the IAM service account in GCP. Scope IAM role bindings narrowly — a service account with project-level `roles/owner` is rarely what you want. > [!NOTE] > > Only one `gcpServiceAccount` connection is permitted per service. If you need to access resources across multiple service accounts, consolidate IAM role bindings onto a single service account. *** ## Cloud SQL Connection (GCP) Connect to Google Cloud SQL instances using the Cloud SQL Auth Proxy for secure database access. > [!NOTE] > > Your GCP Service account must be configured in the Connections tab of your cluster settings before it can be used in `porter.yaml`. ### Field Reference | Field | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Must be `cloudSql` | | `config.cloudSqlServiceAccount` | string | Yes | GCP service account name | | `config.cloudSqlInstances` | array | Yes | List of Cloud SQL instances to connect to | | `config.cloudSqlInstances[].connectionName` | string | Yes | Cloud SQL instance connection name | | `config.cloudSqlInstances[].databasePort` | integer | Yes | Database port (e.g., 5432 for PostgreSQL) | ### Example ```yaml theme={null} services: - name: api # ... connections: - type: cloudSql config: cloudSqlServiceAccount: my-service-account cloudSqlInstances: - connectionName: my-project-123456:us-east1:my-instance databasePort: 5432 ``` > [!NOTE] > > The connection name follows the format `project-id:region:instance-name`. You can find this in the Google Cloud Console under your Cloud SQL instance details. *** ## Persistent Disk Connection Attach persistent storage to your service for data that needs to survive pod restarts. > [!NOTE] > > Your persistent disk must be created in the Add-Ons tab of Porter before it can be used in `porter.yaml`. ### Field Reference | Field | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Must be `disk` | | `config.diskName` | string | Yes | Name of the persistent disk | ### Example ```yaml theme={null} services: - name: api # ... connections: - type: disk config: diskName: my-persistent-data ``` > [!WARNING] > > Persistent disks are tied to specific availability zones. Services using persistent disks cannot be scheduled across multiple zones. *** ## Multiple Connections You can attach multiple connections to a single service (but only one of each type of connection): ```yaml theme={null} services: - name: api # ... connections: - type: awsRole role: api-s3-access - type: disk config: diskName: cache-storage ``` *** ## Related Documentation * [Web Services](/applications/configuration-as-code/services/web-service) - Web service configuration * [Worker Services](/applications/configuration-as-code/services/worker-service) - Worker service configuration * [Job Services](/applications/configuration-as-code/services/job-service) - Job service configuration * [porter.yaml Reference](/applications/configuration-as-code/reference) - Complete configuration reference # Job services in porter.yaml Source: https://docs.porter.run/applications/configuration-as-code/services/job-service Complete field reference for job services in porter.yaml including cron schedules, timeout settings, concurrency controls, and resource limits Job services are for scheduled or on-demand tasks that run to completion. They're ideal for cron jobs, data processing, and one-time tasks. This is a complete reference for all fields that can be set for a job service in `porter.yaml`. ## Field Reference | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | Service identifier (max 31 chars) | | `type` | string | Yes | Must be `job` | | `run` | string | Yes | Command to execute | | `cpuCores` | number | Yes | CPU allocation | | `ramMegabytes` | integer | Yes | Memory allocation in MB | | `cron` | string | No | Cron schedule expression | | `suspendCron` | boolean | No | Temporarily disable cron schedule | | `allowConcurrent` | boolean | No | Allow concurrent job runs | | `timeoutSeconds` | integer | No | Maximum job duration | | `connections` | array | No | External cloud connections | | `terminationGracePeriodSeconds` | integer | No | Graceful shutdown timeout | | `gpuCoresNvidia` | integer | No | NVIDIA GPU cores | | `nodeGroup` | string | No | Node group UUID | *** ## Basic Example ```yaml theme={null} services: - name: cleanup type: job run: npm run cleanup cpuCores: 0.25 ramMegabytes: 256 cron: "0 0 * * *" ``` *** ## `cron` `string` Optional A cron expression that defines when the job should run. Uses [standard 5-field cron syntax](https://en.wikipedia.org/wiki/Cron). ```yaml theme={null} cron: "0 0 * * *" ``` > [!NOTE] > > If no cron expression is provided, the job will default to run every 5 minutes. *** ## `suspendCron` `boolean` Optional Disable the cron schedule without removing it. The job won't run on schedule but can still be triggered manually. ```yaml theme={null} suspendCron: true ``` > [!TIP] > > Use this to pause scheduled jobs during maintenance windows or while debugging, or to create one-off jobs. *** ## `allowConcurrent` `boolean` Optional Allow multiple instances of the job to run simultaneously. By default, a new job run won't start if a previous run is still in progress. ```yaml theme={null} allowConcurrent: true ``` > [!WARNING] > > Be careful enabling this for jobs that modify shared resources. Concurrent runs may cause race conditions or data inconsistency. *** ## `timeoutSeconds` `integer` Optional Maximum number of seconds the job is allowed to run before being terminated. ```yaml theme={null} timeoutSeconds: 3600 ``` > [!NOTE] > > If not specified, jobs may run indefinitely. It's recommended to set a reasonable timeout for all jobs. *** ## `connections` `array` Optional Connect to external cloud services. See [Connections Configuration](/applications/configuration-as-code/services/connections) for full documentation. *** ## `terminationGracePeriodSeconds` `integer` Optional Seconds to wait for graceful shutdown before forcefully terminating the job. ```yaml theme={null} terminationGracePeriodSeconds: 30 ``` > [!TIP] > > Use this to ensure jobs can clean up resources or checkpoint progress before termination. *** ## `gpuCoresNvidia` `integer` Optional Allocate NVIDIA GPU cores for ML training, inference, or GPU-accelerated processing. ```yaml theme={null} gpuCoresNvidia: 1 nodeGroup: gpu-node-group-uuid ``` > [!NOTE] > > Requires a node group with GPU-enabled instances. *** ## Triggering Jobs Manually Jobs can be triggered manually using the Porter CLI: ```bash theme={null} # Trigger a job run porter app run my-app --job my-job # Trigger and wait for completion porter app run my-app --job my-job --wait # Override concurrent restriction porter app run my-app --job my-job --allow-concurrent ``` *** ## Complete Example ```yaml theme={null} services: - name: daily-report type: job run: python generate_report.py cpuCores: 1 ramMegabytes: 2048 # Schedule: Daily at 6 AM UTC cron: "0 6 * * *" # Allow up to 2 hours timeoutSeconds: 7200 # Don't allow concurrent runs allowConcurrent: false # Cloud connections connections: - type: awsRole role: report-s3-access # Graceful shutdown terminationGracePeriodSeconds: 60 ``` *** ## Common Use Cases ### Database Cleanup ```yaml theme={null} services: - name: db-cleanup type: job run: npm run cleanup-old-records cpuCores: 0.25 ramMegabytes: 256 cron: "0 3 * * *" # 3 AM daily timeoutSeconds: 1800 allowConcurrent: false ``` ### Data Export ```yaml theme={null} services: - name: weekly-export type: job run: python export_data.py cpuCores: 0.5 ramMegabytes: 1024 cron: "0 0 * * 0" # Sunday at midnight timeoutSeconds: 14400 # 4 hours connections: - type: awsRole role: s3-export-role ``` ### ML Training Job ```yaml theme={null} services: - name: model-training type: job run: python train.py cpuCores: 8 ramMegabytes: 32768 gpuCoresNvidia: 4 nodeGroup: gpu-node-group-uuid timeoutSeconds: 86400 # 24 hours terminationGracePeriodSeconds: 300 ``` ### Health Check / Monitoring ```yaml theme={null} services: - name: healthcheck type: job run: ./check_dependencies.sh cpuCores: 0.1 ramMegabytes: 128 cron: "*/5 * * * *" # Every 5 minutes timeoutSeconds: 60 allowConcurrent: false ``` ### Manual Migration Job ```yaml theme={null} services: - name: data-migration type: job run: python migrate.py cpuCores: 2 ramMegabytes: 4096 suspendCron: true # otherwise, the job will run every 5 minutes by default timeoutSeconds: 28800 # 8 hours allowConcurrent: false terminationGracePeriodSeconds: 120 ``` > [!NOTE] > > Jobs `suspendCront: true` must be triggered manually using `porter app run --job`. # Predeploy in porter.yaml Source: https://docs.porter.run/applications/configuration-as-code/services/predeploy Configure predeploy jobs in porter.yaml to run database migrations or setup tasks after each build completes but before deployment begins Predeploy is run following a new build, but before the build is deployed to the cluster. This is a good place to run migrations, or to generate any files that are needed for the deploy. For more information on pre-deploy jobs in Porter, see [Pre-deploy Jobs](/applications/deploy/pre-deploy-jobs). ```yaml theme={null} predeploy: run: 'npm run migrate' ``` Predeploy jobs support the same [connections](/applications/configuration-as-code/services/connections) as services, such as connecting to Cloud SQL for migrations: ```yaml theme={null} predeploy: run: 'npm run migrate' connections: - type: cloudSql config: cloudSqlServiceAccount: my-service-account cloudSqlInstances: - connectionName: my-project-123456:us-east1:my-instance databasePort: 5432 ``` # Web services in porter.yaml Source: https://docs.porter.run/applications/configuration-as-code/services/web-service Complete field reference for web services in porter.yaml including ports, custom domains, health checks, autoscaling, and path-based routing Web services are HTTP-based services that can be exposed publicly or kept private within your cluster. This is a complete reference for all fields that can be set for a web service in `porter.yaml`. ## Field Reference | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | Service identifier (max 31 chars) | | `type` | string | Yes | Must be `web` | | `run` | string | Yes | Command to execute | | `port` | integer | Yes | Port the service listens on | | `cpuCores` | number | Yes | CPU allocation | | `ramMegabytes` | integer | Yes | Memory allocation in MB | | `instances` | integer | No | Number of replicas (default: 1) | | `loadBalancers` | array | No | The load balancers that serve the service (`public_lb`, `private_lb`, or `public_alb`) | | `private` | boolean | No | Give the service no load balancer (default: false) | | `loadBalancerConfig` | object | No | Do not use this field. Use `loadBalancers`. | | `disableTLS` | boolean | No | Disable TLS termination | | `autoscaling` | object | No | Autoscaling configuration | | `domains` | array | No | Custom domain configuration | | `healthCheck` | object | No | Combined health check config | | `livenessCheck` | object | No | Liveness probe config | | `readinessCheck` | object | No | Readiness probe config | | `startupCheck` | object | No | Startup probe config | | `pathRouting` | array | No | Path-based routing rules | | `pathRoutingConfig` | object | No | Path routing options | | `ingressAnnotations` | object | No | Custom ingress annotations | | `connections` | array | No | External cloud connections | | `serviceMeshEnabled` | boolean | No | Enable service mesh | | `metricsScraping` | object | No | Prometheus metrics config | | `terminationGracePeriodSeconds` | integer | No | Graceful shutdown timeout | | `gpuCoresNvidia` | integer | No | NVIDIA GPU cores | | `nodeGroup` | string | No | Node group UUID | *** ## Basic Example ```yaml theme={null} services: - name: api type: web run: node server.js port: 8080 cpuCores: 0.5 ramMegabytes: 512 instances: 2 ``` *** ## `loadBalancers` `array` Optional The load balancers that serve the web service. Use this field if the service must receive traffic from outside the cluster. Each item is a mode. Write each mode as a simple string. | Mode | Description | | - | - | | `public_lb` | The default public load balancer of the cluster. Porter uses this mode if you do not set `loadBalancers`. | | `private_lb` | The private load balancer of the cluster. Only peered networks can send traffic to it. | | `public_alb` | The additional public application load balancer of the cluster. | Set `loadBalancers` when you write a web service in `porter.yaml`. Do not write `loadBalancerConfig`, because that field is obsolete. Do not write `private: false`, because it is the default. Write `private: true` only for a service that has no load balancer. Do not write `private: true` together with a `loadBalancers` list that is not empty. Porter reads these settings in this sequence: a `loadBalancers` list that is not empty, then `loadBalancerConfig`, then `private: true` for no load balancer, then the default public load balancer. An empty `loadBalancers` list does not mean no load balancer. Porter reads the same sequence for an empty list. The `private_lb` mode and the `public_alb` mode need the applicable load balancer on the cluster. Make sure that the cluster has that load balancer first. The `private_lb` mode needs a [private load balancer](/cloud-accounts/advanced-cluster-settings#private-load-balancer) on the cluster. If a private service has custom domains, the cluster must also have DNS credentials. Porter uses these credentials to issue the certificates. The `public_alb` mode needs an [application load balancer](/cloud-accounts/advanced-cluster-settings#application-load-balancer) on the cluster. The domains of the service must be below one of the root domains of that load balancer. ```yaml theme={null} services: - name: api type: web run: node server.js port: 8080 loadBalancers: - public_alb domains: - name: api.alb.example.com ``` *** ## `private` `boolean` Optional If `private` is `true`, the service has **no load balancer**. The service has no external address, public or private. Only other workloads in the cluster can send traffic to it, at its internal URL. ```yaml theme={null} private: true ``` > [!NOTE] > > This is the only condition that [`loadBalancers`](#loadbalancers) does not include, so `private: true` continues to be available. Set all other conditions with `loadBalancers`, including the private load balancer of the cluster. The private load balancer is the `private_lb` mode, not this field. Do not set both fields. If you set both fields, Porter reads the list and ignores the flag. *** ## `loadBalancerConfig` `object` Optional > [!WARNING] > > Do not use this field. Use [`loadBalancers`](#loadbalancers), which also supports the `public_alb` mode. This field has one field, `mode`. The `mode` field accepts `public_lb` or `private_lb`. Existing services continue to operate. Porter reads this field if `loadBalancers` is not present. The dashboard replaces this field at the next save. Use `loadBalancers` for new services. Only `loadBalancers` can select the `public_alb` mode. ```yaml theme={null} # Deprecated loadBalancerConfig: mode: private_lb # Use instead loadBalancers: - private_lb ``` *** ## `disableTLS` `boolean` Optional Disable TLS termination at the load balancer. Only use this for services that handle their own TLS or for internal testing. ```yaml theme={null} disableTLS: true ``` > [!WARNING] > > Disabling TLS exposes your service over HTTP. Only use this when you have a specific requirement. *** ## `autoscaling` `object` Optional Configure horizontal pod autoscaling based on CPU and memory utilization. See [Autoscaling Configuration](/applications/configuration-as-code/services/autoscaling) for full documentation. *** ## `domains` `array` Optional Configure custom domains for your web service. | Field | Type | Description | | - | - | - | | `name` | string | Domain name | ```yaml theme={null} domains: - name: example.com ``` *** ## `healthCheck` `object` Optional Configure a combined health check that applies to liveness, readiness, and startup probes. | Field | Type | Description | | - | - | - | | `enabled` | boolean | Enable health checks | | `httpPath` | string | HTTP endpoint to check | | `timeoutSeconds` | integer | Request timeout (min: 1) | | `initialDelaySeconds` | integer | Initial delay before checking (min: 0) | ```yaml theme={null} healthCheck: enabled: true httpPath: /healthz timeoutSeconds: 1 initialDelaySeconds: 15 ``` > [!WARNING] > > Cannot be used together with `livenessCheck`, `readinessCheck`, or `startupCheck`. Use either the combined `healthCheck` or the individual checks. > [!TIP] > > For best practices on combining health checks with graceful shutdown for zero-downtime deployments, see [Zero-Downtime Deployments](/applications/configure/zero-downtime-deployments). *** ## Advanced Health Checks For fine-grained control, configure liveness, readiness, and startup probes separately. ### `livenessCheck` `object` Optional Determines if the container should be restarted. ```yaml theme={null} livenessCheck: enabled: true httpPath: /livez timeoutSeconds: 1 initialDelaySeconds: 15 ``` ### `readinessCheck` `object` Optional Determines if the container is ready to receive traffic. ```yaml theme={null} readinessCheck: enabled: true httpPath: /readyz timeoutSeconds: 1 initialDelaySeconds: 15 ``` ### `startupCheck` `object` Optional Used for slow-starting containers. Other probes are disabled until this passes. ```yaml theme={null} startupCheck: enabled: true httpPath: /startupz timeoutSeconds: 1 initialDelaySeconds: 15 ``` *** ## `pathRouting` `array` Optional Configure path-based routing to direct requests to different ports or services. | Field | Type | Required | Description | | - | - | - | - | | `path` | string | Yes | URL path prefix | | `port` | integer | Yes | Port to route to | | `serviceName` | string | No | Service to route to (defaults to current) | | `appName` | string | No | Application to route to (requires `serviceName`) | ```yaml theme={null} pathRouting: - path: /api/v1/ port: 8080 - path: /api/v2/ port: 8081 - path: /admin/ port: 9000 serviceName: admin-service - path: /auth/ port: 8080 appName: auth-app serviceName: auth-service ``` > [!NOTE] > > A path must be specified for the default port set in `services.port`. *** ## `pathRoutingConfig` `object` Optional Configure path rewriting behavior for path-based routing. | Field | Type | Description | | - | - | - | | `rewriteMode` | string | Path rewrite mode | **Rewrite Modes:** | Mode | Description | Example: `/api/v1/users` | | - | - | - | | `rewrite-all` | Rewrite entire path to root (default) | `/` | | `rewrite-prefix` | Remove the matched prefix only | `/users` | | `rewrite-off` | No rewriting, keep original path | `/api/v1/users` | ```yaml theme={null} pathRouting: - path: /api/v1/ port: 8080 - path: /api/v2/ port: 8081 pathRoutingConfig: rewriteMode: rewrite-prefix ``` *** ## `ingressAnnotations` `object` Optional Add custom NGINX ingress annotations for advanced configuration. ```yaml theme={null} ingressAnnotations: nginx.ingress.kubernetes.io/proxy-connect-timeout: "18000" ``` > [!TIP] > > Common use cases include increasing upload limits, configuring timeouts, and enabling WebSocket support. *** ## `connections` `array` Optional Connect to external cloud services. See [Connections Configuration](/applications/configuration-as-code/services/connections) for full documentation. *** ## `serviceMeshEnabled` `boolean` Optional Enable service mesh for enhanced inter-service communication with improved performance, reliability, and monitoring. ```yaml theme={null} serviceMeshEnabled: true ``` > [!NOTE] > > Recommended for applications with multiple services that communicate with each other, especially those using gRPC or WebSockets. *** ## `metricsScraping` `object` Optional Configure Prometheus metrics scraping for custom application metrics. | Field | Type | Description | | - | - | - | | `enabled` | boolean | Enable metrics scraping | | `path` | string | HTTP path to scrape (default: `/metrics`) | | `port` | integer | Port to scrape metrics from | | `scrapeIntervalSeconds` | integer | Scrape interval in seconds (default: 60) | ```yaml theme={null} metricsScraping: enabled: true path: /metrics port: 9090 scrapeIntervalSeconds: 60 ``` *** ## `terminationGracePeriodSeconds` `integer` Optional Seconds to wait for graceful shutdown before forcefully terminating the container. ```yaml theme={null} terminationGracePeriodSeconds: 60 ``` > [!TIP] > > Increase this value for services that need time to complete in-flight requests or cleanup tasks. *** ## `gpuCoresNvidia` `integer` Optional Allocate NVIDIA GPU cores for ML inference or GPU-accelerated workloads. ```yaml theme={null} gpuCoresNvidia: 1 nodeGroup: gpu-node-group-uuid ``` > [!NOTE] > > Requires a node group with GPU-enabled instances. *** ## Complete Example ```yaml theme={null} services: - name: api type: web run: npm start port: 8080 cpuCores: 1 ramMegabytes: 1024 # Autoscaling autoscaling: enabled: true minInstances: 1 maxInstances: 10 cpuThresholdPercent: 80 memoryThresholdPercent: 80 # Custom domains domains: - name: example.com # Health checks livenessCheck: enabled: true httpPath: /livez timeoutSeconds: 1 initialDelaySeconds: 15 readinessCheck: enabled: true httpPath: /readyz timeoutSeconds: 1 initialDelaySeconds: 15 # Path routing pathRouting: - path: /api/v1/ port: 8080 - path: /api/v2/ port: 8081 pathRoutingConfig: rewriteMode: rewrite-prefix # Ingress configuration ingressAnnotations: nginx.ingress.kubernetes.io/proxy-connect-timeout: "18000" # Service mesh and metrics serviceMeshEnabled: true metricsScraping: enabled: true path: /metrics port: 9090 scrapeIntervalSeconds: 60 # Cloud connections connections: - type: awsRole role: api-s3-access # Graceful shutdown terminationGracePeriodSeconds: 30 ``` # Worker services in porter.yaml Source: https://docs.porter.run/applications/configuration-as-code/services/worker-service Complete field reference for worker services in porter.yaml including health checks, autoscaling, GPU allocation, and resource limits Worker services are background processing services that don't expose HTTP endpoints. They're ideal for queue consumers, background jobs, and long-running processes. This is a complete reference for all fields that can be set for a worker service in `porter.yaml`. ## Field Reference | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | Service identifier (max 31 chars) | | `type` | string | Yes | Must be `worker` | | `run` | string | Yes | Command to execute | | `cpuCores` | number | Yes | CPU allocation | | `ramMegabytes` | integer | Yes | Memory allocation in MB | | `instances` | integer | No | Number of replicas (default: 1) | | `autoscaling` | object | No | Autoscaling configuration | | `healthCheck` | object | No | Combined health check config | | `livenessCheck` | object | No | Liveness probe config | | `readinessCheck` | object | No | Readiness probe config | | `startupCheck` | object | No | Startup probe config | | `connections` | array | No | External cloud connections | | `serviceMeshEnabled` | boolean | No | Enable service mesh | | `terminationGracePeriodSeconds` | integer | No | Graceful shutdown timeout | | `gpuCoresNvidia` | integer | No | NVIDIA GPU cores | | `nodeGroup` | string | No | Node group UUID | *** ## Basic Example ```yaml theme={null} services: - name: queue-worker type: worker run: npm run worker cpuCores: 0.5 ramMegabytes: 512 instances: 3 ``` *** ## `autoscaling` `object` Optional Configure horizontal pod autoscaling based on CPU and memory utilization. See [Autoscaling Configuration](/applications/configuration-as-code/services/autoscaling) for full documentation. *** ## `healthCheck` `object` Optional Configure a combined health check that applies to liveness, readiness, and startup probes. Worker services use command-based health checks since they don't expose HTTP endpoints. | Field | Type | Description | | - | - | - | | `enabled` | boolean | Enable health checks | | `command` | string | Command to run for health check | | `timeoutSeconds` | integer | Command timeout (min: 1) | | `initialDelaySeconds` | integer | Initial delay before checking (min: 0) | ```yaml theme={null} healthCheck: enabled: true command: ./healthcheck.sh timeoutSeconds: 5 initialDelaySeconds: 15 ``` > [!WARNING] > > Cannot be used together with `livenessCheck`, `readinessCheck`, or `startupCheck`. Use either the combined `healthCheck` or the individual checks. > [!TIP] > > For best practices on combining command-based health checks with graceful shutdown (SIGTERM handling, shutdown markers), see [Zero-Downtime Deployments: Workers](/applications/configure/zero-downtime-deployments#workers). *** ## Advanced Health Checks For fine-grained control, configure liveness, readiness, and startup probes separately. ### `livenessCheck` `object` Optional Determines if the container should be restarted. ```yaml theme={null} livenessCheck: enabled: true command: ./livez.sh timeoutSeconds: 5 initialDelaySeconds: 15 ``` ### `readinessCheck` `object` Optional Determines if the container is ready to receive work. ```yaml theme={null} readinessCheck: enabled: true command: ./readyz.sh timeoutSeconds: 5 initialDelaySeconds: 5 ``` ### `startupCheck` `object` Optional Used for slow-starting containers. Other probes are disabled until this passes. ```yaml theme={null} startupCheck: enabled: true command: ./startupz.sh timeoutSeconds: 10 initialDelaySeconds: 0 ``` *** ## `connections` `array` Optional Connect to external cloud services. See [Connections Configuration](/applications/configuration-as-code/services/connections) for full documentation. *** ## `serviceMeshEnabled` `boolean` Optional Enable service mesh for enhanced inter-service communication with improved performance, reliability, and monitoring. ```yaml theme={null} serviceMeshEnabled: true ``` > [!NOTE] > > Useful for workers that need to communicate with other services in your cluster. *** ## `terminationGracePeriodSeconds` `integer` Optional Seconds to wait for graceful shutdown before forcefully terminating the container. ```yaml theme={null} terminationGracePeriodSeconds: 120 ``` > [!TIP] > > Set this to a value higher than your longest expected job. This gives workers time to complete in-progress work before shutdown. *** ## `gpuCoresNvidia` `integer` Optional Allocate NVIDIA GPU cores for ML workloads or GPU-accelerated processing. ```yaml theme={null} gpuCoresNvidia: 1 nodeGroup: gpu-node-group-uuid ``` > [!NOTE] > > Requires a node group with GPU-enabled instances. *** ## Complete Example ```yaml theme={null} services: - name: queue-processor type: worker run: npm run worker cpuCores: 1 ramMegabytes: 2048 # Autoscaling autoscaling: enabled: true minInstances: 2 maxInstances: 20 cpuThresholdPercent: 70 memoryThresholdPercent: 80 # Health checks livenessCheck: enabled: true command: ./healthcheck.sh timeoutSeconds: 5 readinessCheck: enabled: true command: ./ready.sh timeoutSeconds: 3 # Cloud connections connections: - type: awsRole role: worker-sqs-access # Service mesh serviceMeshEnabled: true # Graceful shutdown (allow 2 minutes for jobs to complete) terminationGracePeriodSeconds: 120 ``` *** ## Common Use Cases ### Queue Consumer ```yaml theme={null} services: - name: sqs-consumer type: worker run: node src/workers/sqs-consumer.js cpuCores: 0.5 ramMegabytes: 512 instances: 5 terminationGracePeriodSeconds: 60 connections: - type: awsRole role: sqs-consumer-role ``` ### Background Job Processor ```yaml theme={null} services: - name: job-processor type: worker run: bundle exec sidekiq cpuCores: 1 ramMegabytes: 1024 autoscaling: enabled: true minInstances: 2 maxInstances: 10 cpuThresholdPercent: 70 terminationGracePeriodSeconds: 300 ``` ### ML Inference Worker ```yaml theme={null} services: - name: ml-worker type: worker run: python worker.py cpuCores: 4 ramMegabytes: 8192 gpuCoresNvidia: 1 nodeGroup: gpu-node-group-uuid instances: 2 ``` # Advanced networking Source: https://docs.porter.run/applications/configure/advanced-networking Customize NGINX ingress annotations for your Porter web services including read/write timeouts, request body size limits, and WebSocket support ## Customizing Network Settings for an Application[](#customizing-network-settings-for-an-application "Direct link to heading") On Porter, you have the flexibility to customize the NGINX configuration for each of your Web service by adding an "Ingress Annotation" when deploying a web service. This can be found in the "Networking" tab of the web service. ![Ingress Annotations](https://mintcdn.com/porter/NkmSD5B3JYkw7n0Y/images/deploy/v2/nginx-annotations.webp?fit=max\&auto=format\&n=NkmSD5B3JYkw7n0Y\&q=85\&s=7c38f51b237213ee6cc34de688b2818e) To add an annotation, add the key-value pairs for an annotation you want to add. For a full list of NGINX annotations you can add to the service, please consult [this documentation](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/). Below are the most common custom networking options we see among our users. ### Setting Custom Read/Write Timeouts[](#setting-custom-readwrite-timeouts "Direct link to heading") Read/write timeouts are very application-specific, and are subject to change on the Porter templates. For example, if you have a websocket application that does not handle dropped connections gracefully, you may be inclined to set your read/write timeout to be much higher than what is standard (\~30 seconds). For those coming from Heroku, the Heroku router enforces a write timeout of 30 seconds, and will keep a connection alive if a single byte is sent within a 55 second window. On Porter, you can configure your own read/write timeouts by adding annotations: ``` nginx.ingress.kubernetes.io/proxy-connect-timeout: "60" nginx.ingress.kubernetes.io/proxy-read-timeout: "60" nginx.ingress.kubernetes.io/proxy-send-timeout: "60" ``` Note: All timeout values are unitless and in seconds - for instance, `nginx.ingress.kubernetes.io/proxy-read-timeout: "120"` sets a valid 120 seconds proxy read timeout. For an explanation of these values: | Annotation | Description | | - | - | | nginx.ingress.kubernetes.io/proxy-connect-timeout | The timeout for NGINX to establish a connection with your application. | | nginx.ingress.kubernetes.io/proxy-send-timeout | The timeout for NGINX to transmit a request to your application. | | nginx.ingress.kubernetes.io/proxy-read-timeout | The timeout for NGINX to read a response from your application. | It is thus recommended to set these values with the ordering `proxy-connect-timeout <= proxy-send-timeout <= proxy-read-timeout`. ### Client Max Body Size[](#client-max-body-size "Direct link to heading") If you are getting undesired `413 Request Entity Too Large` errors, you can increase the maximum size of the client request by setting the field [client\_max\_body\_size](http://nginx.org/en/docs/http/ngx%5Fhttp%5Fcore%5Fmodule.html#client%5Fmax%5Fbody%5Fsize). You can do this by adding the following annotation: ``` nginx.ingress.kubernetes.io/proxy-body-size: 8m ``` This will set the maximum client request body size to 8 megabytes. To learn more about NGINX units, see [this document](http://nginx.org/en/docs/syntax.html). ### Header Size[](#header-size "Direct link to heading") If you are occasionally getting 500-level errors on certain endpoints, your application may be sending response headers which are too large for NGINX to process. You can try increasing the maximum value of the response header size by setting something like the following (default is `1k`): ``` nginx.ingress.kubernetes.io/proxy-buffer-size: 10k ``` # Autoscaling Source: https://docs.porter.run/applications/configure/autoscaling Scale your web and worker services automatically using CPU and memory thresholds, custom Prometheus metrics, or Temporal task queue depth Porter supports several autoscaling strategies for your web and worker services. This guide covers the available options and when to use each. > [!NOTE] > > For configuration-as-code (porter.yaml) autoscaling settings, see [Autoscaling Configuration](/applications/configuration-as-code/services/autoscaling). ## Autoscaling Options | Method | Best For | Trigger | | - | - | - | | **CPU/Memory** | General workloads | Resource utilization thresholds | | **Custom Metrics** | Queue processors, API rate limiting | Prometheus metrics | | **Temporal** | Temporal workflow workers | Task queue depth | ## Standard Autoscaling (CPU/Memory) The default autoscaling method scales your service based on CPU and memory utilization. ### Configuration 1. Navigate to your application dashboard 2. Select your service 3. Go to the **Resources** tab 4. Enable **Autoscaling** 5. Configure the settings: * **Min instances**: Minimum number of replicas (e.g., 1) * **Max instances**: Maximum number of replicas (e.g., 10) * **CPU threshold**: Target CPU utilization percentage (e.g., 70%) * **Memory threshold**: Target memory utilization percentage (e.g., 70%) ![Autoscaling Configuration](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/autoscaling-config.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=52175399e24665cab9f03fedb4d1b047) ### How It Works When either CPU or memory usage exceeds your configured threshold, Porter automatically adds replicas. When usage drops, replicas are removed (down to your minimum). For example, with a 70% CPU threshold: * If average CPU across pods exceeds 70%, new replicas are added * If average CPU drops below 70%, excess replicas are removed * The system maintains \~30% headroom for traffic spikes ## Advanced Autoscaling For workloads that need to scale based on external signals rather than resource usage, Porter offers advanced autoscaling options: ### Custom Metrics (Prometheus) Scale based on application-specific metrics like queue length, request latency, or custom business metrics. **Use cases:** * Message queue consumers (RabbitMQ, Redis, SQS) * Rate-limited API services * Batch processing workers [Set up Custom Metrics Autoscaling →](/applications/observability/custom-metrics-and-autoscaling) ### Temporal Autoscaling Scale Temporal workflow workers based on task queue depth. Porter monitors your Temporal task queues and automatically adjusts worker count. **Use cases:** * Temporal workflow workers * Activity workers with variable load * Event-driven processing pipelines [Set up Temporal Autoscaling →](/applications/configure/temporal-autoscaling) # Basic configuration Source: https://docs.porter.run/applications/configure/basic-configuration Set start commands, allocate CPU and RAM resources, and enable sleep mode for web, worker, and job services from the Porter dashboard These are basic configuration options that are available on all three services - **Web**, **Worker**, and **Jobs**. ## Setting the Start Command You can configure your application service to run any start command by simply editing it from the Porter Dashboard. If your application was built using a `Dockerfile`, the start command you put in here will override the `CMD` directive that is specified in your `Dockerfile`. ## Assigning resources From the **General** tab, you can assign the amount of vCPU and RAM for each of your applications. The maximum amount of resources you can assign to a single application is capped by the size of the virtual machines you are using for the underlying cluster. ![CPU and RAM Configuration](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/cpu-ram-sliders.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=212cdce68da5199c9e7b5e9ded75ce9b) ## Sleep Mode "Sleeping" a service can be enabled from the **Advanced** tab. When a service is put to sleep, it will be stopped and no instances will run. This is useful if you want to keep a service around but don't want to pay for the resources when it's not being used. Note that Sleep Mode is enabled at the service level, rather than at the app level. # HTTPS certificates and custom domains Source: https://docs.porter.run/applications/configure/custom-domains Configure custom domains with automatic Let's Encrypt SSL certificate issuance and renewal for Porter web services on AWS, GCP, and Azure Porter secures all Web services with SSL certificates issued by [Let's Encrypt](https://letsencrypt.org/). Porter will automatically handle the issuance and renewal of your certificates. ## Porter Domains Porter generates placeholder domains for all Web services. These placeholder domains follow the format of `*.onporter.run`. While these domains can be used in production, we highly recommend attaching a custom domain to your production web services. ## Custom Domains Setting up a custom domain involves two steps: setting up a DNS record to point to that domain, and then configuring your Web service to listen on that custom domain: * [DNS Setup](#dns-setup) * [Deploy your Application](#deploying-on-the-custom-domain) ## DNS Setup[](#dns-setup "Direct link to heading") You must first find the DNS name assigned to the load balancer of your cluster. This can be found under the **Networking** tab of your Web Service: ![Ingress IP Address](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/custom-domain-dns.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=ce7e922f7d1adaef805963340162e2a3) The DNS records that need to be created for your Web service vary slightly depending on the cloud provider and the DNS provider you are using. Copy this address, as you will need it to create the DNS record. ### Google Cloud and Azure On Google Cloud and Azure, the load balancer that sits in front of your infrastructure has a static IP address. Therefore, you have to create an `A` record in your DNS provider that points to that static IP address. The name of the record should be the subdomain you want to use (e.g. `app.mydomain.com`), and the value should be the IP address of the load balancer that you copied above. ### Amazon Web Services[](#amazon-web-services "Direct link to heading") For clusters deployed on AWS, you need to create a `CNAME` record that points at the DNS name for your load balancer. Please follow instructions below, based on the type of domain you are creating: * Subdomain -> domains of the format `*.porter.run` such as `cloud.porter.run`, `myapp.porter.run` * Apex domain -> top level domains, of the format `porter.run` **Subdomain** Create a `CNAME` record on your DNS provider for your desired subdomain, which points to the load balancer URL you have copied above. Make sure you exclude the protocol `http://` and any trailing `/` from the URL string. For example, on Route 53, this looks like the following: ![CNAME record](https://imagedelivery.net/l4LYM_vOYKe7O1NCT_Nc_g/be14bc42-4f63-4d3b-d0eb-5d4d71438700/large "CNAME record") If your DNS is on Route 53, we recommend an alias `A` record instead of a `CNAME`: select **Alias to Network Load Balancer**, pick the region your cluster runs in, and choose the load balancer's DNS name. Alias records resolve in a single lookup and Route 53 doesn't charge for alias queries. A `CNAME` still works if you prefer it. **Apex Domain** Because AWS creates a load balancer that is assigned a domain name, rather than an IP address, you must use a DNS provider which allows for `ALIAS` records, since `CNAME` records are not supported for apex domains. Create an `ALIAS` record for your root domain which points to the load balancer address that you copied above. If you've purchased your domain through a service like GoDaddy that does not support `ALIAS` records, we recommend that you switch your service to Route 53. Please follow [this guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html) to manage your existing domains with Route 53, and then see the instructions below for Route 53 setup. #### Route 53 Instructions[](#route-53-instructions "Direct link to heading") When creating a new record, leave the Record name empty and select the **Alias to Network Load Balancer** option. After you choose the region your EKS cluster is provisioned in, you will be able to select the DNS name of the load balancer that was displayed from the Porter dashboard. Set Record type as **A record** and create the record. ![A Record](https://imagedelivery.net/l4LYM_vOYKe7O1NCT_Nc_g/90f234e2-30f0-463a-8334-fb68cce1b900/large "Screen Shot 2021-01-18 at 6.56.04 PM.png") Once DNS has propagated, you can now [deploy your application using the custom domain](#deploying-on-the-custom-domain)! > [!NOTE] > > After you complete the previous steps, it might take up to 30 minutes for DNS records to fully propagate. Please wait before deploying your applications until the DNS propagation is complete. You can check this using tools like [dnschecker.org](https://dnschecker.org) or running `nslookup `. ## Deploying on the Custom Domain[](#deploying-on-the-custom-domain "Direct link to heading") Once the DNS record changes have been propagated, you will be able to attach the custom domain to your application. Click on **Add Custom Domain**, input the custom domain you have just pointed to the load balancer, then hit deploy. In a few minutes, you will be able to view the application on the custom domain, secured with an SSL certificate. # Environment groups Source: https://docs.porter.run/applications/configure/environment-groups Share environment variables and secrets across multiple applications using project-wide groups synced to AWS, GCP, or Azure secret managers An environment group is a set of environment variables and secrets that can be shared across multiple applications. Environment groups are **project-wide** — they can be used across all clusters and cloud accounts within your project. For example, if all of your web services need a shared set of API keys and database credentials, you can create an environment group containing those values and sync it to each service. > [!NOTE] > > Environment variables configured directly on an application always take precedence over values from an environment group. This override applies on a per-variable basis — if an app sets `API_KEY=xyz` and a synced environment group has `API_KEY=abc`, the app-level value (`xyz`) is used. ## How Secrets Are Stored Environment group secrets are automatically synced to the secret manager of every cloud account linked to your project that has a running cluster: * **AWS** — AWS Secrets Manager * **GCP** — GCP Secret Manager * **Azure** — Azure Key Vault No secret data is stored on Porter's infrastructure. Secrets only exist in memory on Porter's servers momentarily during creation and updates. If you already manage your secrets in a third-party secret manager, you can sync them into Porter as a read-only environment group instead. See the [Doppler](/integrations/doppler) and [Infisical](/integrations/infisical) integrations. ## Creating an Environment Group You can create a new environment group from the **Env Groups** tab on the Porter dashboard. Click **New Env Group**, enter a name, and add your variables and secrets. Environment group names must be up to 63 characters and may only contain lowercase letters, numbers, and hyphens (`-`). You can also create environment groups from the CLI: ```bash Interactive theme={null} porter env create ``` ```bash Non-Interactive theme={null} porter env create --name production-secrets ``` ```bash With Variables and Secrets theme={null} porter env create --name production-secrets \ -v NODE_ENV=production -v LOG_LEVEL=info \ -s DB_PASSWORD=secret -s API_KEY=sk-123 ``` ## Variables and Secrets Environment groups support two types of values: | Type | Description | Visibility | | - | - | - | | **Variables** | Non-sensitive configuration values (max 25 KB per value) | Visible in the dashboard and CLI after creation | | **Secrets** | Sensitive values such as API keys, passwords, and tokens (max 25 KB per value) | Hidden after creation; stored in your cloud provider's secret manager | When creating or updating an environment group, use the lock icon in the dashboard (or the `-s` flag in the CLI) to mark a value as a secret. ## Files Environment groups can also contain files for sensitive data such as certificates or configuration files. Files are managed through the Porter dashboard and are injected into your application's container at the path: ``` /etc/secrets/ ``` ## Syncing Environment Groups to Applications Environment groups can be synced to applications so that when the group is updated, all synced applications are automatically redeployed with the new values. ### From the Dashboard You can sync an environment group to an application during app creation or by navigating to the application's **Env Groups** tab and adding the group. Click **Update app** to apply. ### From porter.yaml Add the `envGroups` field to your `porter.yaml`: ```yaml theme={null} version: v2 name: my-app envGroups: - production-secrets - shared-config services: - name: web type: web run: npm start port: 3000 cpuCores: 0.5 ramMegabytes: 512 ``` Environment groups listed in `envGroups` must already exist in the project before deploying. ## Updating an Environment Group When you update an environment group, all applications synced to it are automatically redeployed with the new values. ### From the Dashboard Navigate to the **Env Groups** tab, click the environment group you want to update, make your changes, and click **Update**. ### From the CLI Use `porter env set` to add or update variables, and `porter env unset` to remove them: ```bash Set Variables theme={null} porter env set --group production-secrets -v LOG_LEVEL=debug -v FEATURE_FLAG=true ``` ```bash Set Secrets theme={null} porter env set --group production-secrets -s DB_PASSWORD=new-password ``` ```bash Remove Variables theme={null} porter env unset --group production-secrets -v OLD_VAR -v UNUSED_VAR ``` ```bash Remove Secrets theme={null} porter env unset --group production-secrets -s ROTATED_KEY ``` ## Version History Every update to an environment group creates a new version. You can view the full history of an environment group — including who made each change and when — from the **Versions** tab on the environment group's page. ### Reverting to a Previous Version If a change introduces a bad value or you need to roll back to a known-good configuration, you can revert an environment group to any previous version from the **Versions** tab. 1. Open the environment group from the **Env Groups** tab. 2. Switch to the **Versions** tab. 3. Find the version you want to restore and click **Revert to v\**. 4. Review the diff between the current version and the target version, then confirm. Reverting does not rewrite history — it creates a **new version** containing the values from the version you selected. The previous versions remain in the history, so you can roll forward again if needed. > [!NOTE] > > All applications synced to the environment group are automatically redeployed with the reverted values, just like a normal update. ## Pulling Environment Variables Locally You can pull the contents of an environment group to your local machine for development: ```bash Print to stdout theme={null} porter env pull --group production-secrets ``` ```bash Write to File theme={null} porter env pull --group production-secrets --file .env.local ``` ```bash Variables Only theme={null} porter env pull --group production-secrets -v ``` ```bash Secrets Only theme={null} porter env pull --group production-secrets -s ``` > [!NOTE] > > The `--variables` (`-v`) and `--secrets` (`-s`) flags are mutually exclusive. If neither is specified, both variables and secrets are included in the output. ## Listing Environment Groups To see all environment groups in your project: ```bash theme={null} porter env list ``` This displays a table with each group's name, current version, and last updated time. ## Version History Every update to an environment group creates a new version. Porter retains the **10 most recent versions** of each environment group; older versions are pruned automatically along with their underlying secret data in your cloud provider's secret manager. Because applications resolve their environment values against a pinned version of the group, an app that has not redeployed in a long time can fall behind. If a synced application is still pinned to a version older than the 10 most recent, redeploying it will pull values from the current version instead. ## Reverting to a previous version Every update to an environment group creates a new version. If a recent change causes problems — for example, a bad value rolled out to every synced application — you can revert the group to any prior version. Reverting re-applies the target version's variables, secrets, and files as a **new** version on top of the current one. The historical version itself is preserved, so the revert is fully auditable and can itself be reverted. > [!NOTE] > > Reverting an environment group automatically redeploys every application synced to it, just like a normal update. Apps pick up the restored values on their next rollout. Reverts are performed through the Porter dashboard from the environment group's version history. The action is admin-scoped — only project admins can revert a group to a previous version. ## Deleting an Environment Group Environment groups can be deleted from the **Settings** tab on the environment group's page in the dashboard. > [!WARNING] > > You cannot delete an environment group that is synced to an application. Unsync the environment group from all applications before deleting it. ## CLI Reference For the full list of flags and options, see the [porter env](/standard/cli/command-reference/porter-env) CLI reference. # Secure cloud access Source: https://docs.porter.run/applications/configure/secure-cloud-access Grant Porter applications secure access to AWS, GCP, and Azure resources using workload identity, IAM roles, and IRSA—no static credentials required. Porter services can authenticate to your cloud accounts without you having to embed static access keys or service-principal secrets in environment variables. Instead, each pod assumes a cloud identity at runtime via your cloud provider's native workload-identity mechanism. This page covers how to configure these identities for each supported cloud provider, and how to scope their permissions to follow the principle of least privilege. *** ## AWS Porter uses [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) to associate an IAM role with a service's pod. Your application code can then use the default AWS SDK credential chain (e.g. `DefaultAWSCredentialsProviderChain` in Java, `aws.config.LoadDefaultConfig` in Go, `boto3.Session()` in Python) and credentials will be resolved automatically — no access keys, no `sts:AssumeRole` calls in application code. ### How it works When you attach an IAM role to a service, Porter creates an [EKS Pod Identity Association](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) on your cluster that maps the service's Kubernetes service account to the IAM role you specify. At runtime, the EKS Pod Identity agent injects short-lived credentials for that role into the pod. You are responsible for creating the IAM role and granting it the permissions it needs. Porter does not create the role or modify its permissions. ### Step 1 — Create the IAM role In your AWS account, create a new IAM role with two things: 1. A **trust policy** that allows EKS Pod Identity to assume the role: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "pods.eks.amazonaws.com" }, "Action": ["sts:AssumeRole", "sts:TagSession"] } ] } ``` 2. A **permissions policy** scoped to only the AWS APIs and resources your service needs. We strongly recommend following the principle of least privilege: avoid wildcard actions and `Resource: "*"`, and create a dedicated role per service rather than sharing roles across workloads. ### Step 2 — Attach the role to your service In the Porter dashboard, open the service you want to grant access to and scroll to the **IAM Role Connection** section. Toggle the connection on and enter the IAM role name or full ARN: ![IAM Role Connection UI](https://mintcdn.com/porter/Y8RLz4C78cfu_XJV/images/configure/aws-assume-role-example.webp?fit=max\&auto=format\&n=Y8RLz4C78cfu_XJV\&q=85\&s=685559197730e7f6dc833f551de15d30) On the next deploy, your pods can use the role immediately — no application restart logic required. > **Configure as code (porter.yaml)** > > If you manage your app config as code, you can declare the same connection in [`porter.yaml`](/applications/configuration-as-code/overview): > > ```yaml theme={null} > services: > - name: api > # ... > connections: > - type: awsRole > role: my-app-api-role > ``` > > See [Connections in porter.yaml](/applications/configuration-as-code/services/connections#aws-role-connection) for the full schema. *** ## Azure Porter uses [Azure Workload Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview) to federate a service's Kubernetes service account with a User Assigned Managed Identity (UAMI) in your Azure subscription. Your application code can then use `DefaultAzureCredential` (or any credential type that supports workload identity) and Azure will issue tokens for the UAMI on the pod's behalf. ### How it works When you attach a managed identity to a service, Porter creates a [federated identity credential](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation) on the UAMI you specify. The credential trusts the AKS cluster's OIDC issuer to vouch for a specific Kubernetes service account. At runtime, the pod presents a projected OIDC token and Azure exchanges it for an access token scoped to the UAMI. You are responsible for creating the UAMI and granting it role assignments. Porter does not create the UAMI or modify its permissions. ### Step 1 — Create a User Assigned Managed Identity In the same Azure subscription as your AKS cluster, create a User Assigned Managed Identity and grant it the Azure RBAC role assignments needed to access the specific resources your service will call. We strongly recommend following the principle of least privilege: scope role assignments to individual resources rather than resource groups or subscriptions, prefer narrow data-plane roles (e.g. `Storage Blob Data Reader`) over broad roles like `Contributor`, and create a dedicated UAMI per service. You don't need to add any federated credentials yourself — Porter creates the one mapping your service's Kubernetes service account to this identity on the next deploy. ### Step 2 — Attach the identity to your service In the Porter dashboard, open the service you want to grant access to and scroll to the **Azure Managed Identity Connection** section. Toggle the connection on and enter the managed identity name and resource group: ![Azure Managed Identity Connection UI](https://mintcdn.com/porter/Y8RLz4C78cfu_XJV/images/configure/azure-workload-identity-example.webp?fit=max\&auto=format\&n=Y8RLz4C78cfu_XJV\&q=85\&s=39fceb97f09dfed953db797f8da8ad7a) On the next deploy, Porter creates the federated identity credential on the UAMI. From your application code, use `DefaultAzureCredential` from the Azure SDK and Azure will resolve credentials automatically — no client secrets, no certificates. > **Configure as code (porter.yaml)** > > If you manage your app config as code, you can declare the same connection in [`porter.yaml`](/applications/configuration-as-code/overview): > > ```yaml theme={null} > services: > - name: api > # ... > connections: > - type: azureManagedIdentity > identityName: my-app-api-identity > resourceGroup: my-resource-group > ``` > > See [Connections in porter.yaml](/applications/configuration-as-code/services/connections#azure-managed-identity-connection) for the full schema. *** ## GCP Porter can bind a service to a GCP IAM service account in your project. Your application code can then use [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) (e.g. `google.auth.default()` in Python, `google.NewClient` in Go, `GoogleCredentials.getApplicationDefault()` in Java), and GCP will issue tokens for the bound IAM service account. ### How it works When you attach a GCP IAM service account to a service, Porter configures the required IAM binding so the service can receive short-lived credentials scoped to the IAM service account. You are responsible for creating the IAM service account and granting it the IAM roles it needs. Porter does not create the service account or modify its project-level permissions. ### Step 1 — Create a GCP IAM service account In the same GCP project as your Porter infrastructure, create an IAM service account and grant it only the IAM roles needed to access the specific resources your service will call. We strongly recommend following the principle of least privilege: prefer narrow predefined roles (e.g. `roles/storage.objectViewer` on a single bucket) over broad roles like `roles/editor`, scope role bindings to individual resources rather than the project, and create a dedicated service account per Porter service. You don't need to add the service identity binding yourself — Porter adds it on the next deploy. ### Step 2 — Attach the service account to your service In the Porter dashboard, open the service you want to grant access to and scroll to the **GCP Service Account Connection** section. Toggle the connection on and enter the fully-qualified service account email: ![GCP Service Account Connection UI](https://mintcdn.com/porter/Ct8PxZVCw_c9iwag/images/configure/gcp-service-account-connection.webp?fit=max\&auto=format\&n=Ct8PxZVCw_c9iwag\&q=85\&s=db36ad9bb9f688ec1f7976f72775c010) ``` my-app@my-project.iam.gserviceaccount.com ``` On the next deploy, Porter adds the service identity binding on the IAM service account. From your application code, use the standard Google Cloud client libraries and credentials will be resolved automatically — no service account key files, no `GOOGLE_APPLICATION_CREDENTIALS` env var. > **Configure as code (porter.yaml)** > > If you manage your app config as code, you can declare the same connection in [`porter.yaml`](/applications/configuration-as-code/overview): > > ```yaml theme={null} > services: > - name: api > # ... > connections: > - type: gcpServiceAccount > serviceAccountEmail: my-app@my-project.iam.gserviceaccount.com > ``` > > See [Connections in porter.yaml](/applications/configuration-as-code/services/connections#gcp-service-account-connection) for the full schema. ### Enabling Workload Identity on a node group When you turn on Workload Identity for a node group, GKE deploys a metadata server on those nodes that intercepts calls to the GCE metadata endpoint. Once enabled, pods can no longer reach the node's default Compute Engine service account through that endpoint — they obtain Google credentials through a Workload Identity–bound Kubernetes service account instead. > [!WARNING] > > Enabling Workload Identity can interrupt workloads that rely on the node's default service account. If any pod on the node group authenticates to Google APIs using the default node service account (for example, through the GCE metadata endpoint or the default credential chain), it will lose access until it's reconfigured to use a Workload Identity–bound Kubernetes service account. Before enabling it on a node group that already runs production traffic, confirm that none of its workloads depend on the default node service account. If you're not sure, create a new node group with Workload Identity enabled and [schedule your workloads](/cloud-accounts/node-groups#assigning-workloads-to-node-groups) on it to test before changing an existing node group. *** ## Related documentation * [Configuration as code](/applications/configuration-as-code/overview) — Overview of managing Porter apps via `porter.yaml` * [Connections in porter.yaml](/applications/configuration-as-code/services/connections) — Reference for all connection types, including the YAML schema * [AWS EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) — AWS documentation * [Azure Workload Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview) — Azure documentation * [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) — GCP documentation # Temporal autoscaling Source: https://docs.porter.run/applications/configure/temporal-autoscaling Automatically scale Temporal worker services based on task queue backlog depth using a Temporal Cloud API key and Porter integration # Temporal Autoscaling If you're using [Temporal](https://temporal.io) for workflow orchestration, Porter can automatically scale your worker services based on task queue depth. This ensures your workers scale up when there's a backlog of tasks and scale down when queues are empty. ## Prerequisites Before configuring Temporal autoscaling, you'll need: * A Temporal Cloud account * The task queue name your workers poll from ## Step 1: Create a Service Account and API Key Porter needs an API key to monitor your task queue depth. We recommend creating a dedicated service account with minimal permissions rather than using a personal API key. ### Required Permission Porter only needs **Read** permission on your namespace. This allows Porter to call `DescribeTaskQueue` to retrieve the queue backlog count for scaling decisions. Read permission does not allow starting, terminating, or modifying workflows. ### Create a Service Account 1. Log in to [Temporal Cloud](https://cloud.temporal.io) 2. Navigate to **Settings** → **Identities** 3. Click **Create Service Account** 4. Configure the service account: * **Name**: A descriptive name (e.g., `porter-autoscaling`) * **Description**: Optional description (e.g., "Used by Porter for autoscaling workers") * **Account Level Role**: Select **Read** (minimum required) * **Namespace Permissions**: Add your namespace with **Read** permission 5. Click **Create Service Account** ![Temporal Service Account Creation](https://mintcdn.com/porter/hPI6NuWj_11g4ex9/images/configure/temporal-service-account-creation.png?fit=max\&auto=format\&n=hPI6NuWj_11g4ex9\&q=85\&s=e94408390dc76cf68e132d810b01dba4) *Creating a service account in Temporal Cloud* ### Create an API Key for the Service Account 1. After creating the service account, navigate to **Settings** → **API Keys** 2. Click **Create API Key** 3. Configure the API key: * **Identity Type**: Select **Service Account** * **Service Account**: Select the service account you created (e.g., `porter-autoscaling`) * **Name**: A name for this key (e.g., `porter-production`) * **Description**: Optional description * **Expiration**: Set an appropriate expiration date 4. Click **Generate API Key** 5. **Copy the API key immediately** — it will only be displayed once ![Temporal API Key Creation](https://mintcdn.com/porter/hPI6NuWj_11g4ex9/images/configure/temporal-api-key-creation.png?fit=max\&auto=format\&n=hPI6NuWj_11g4ex9\&q=85\&s=5e72a23e3d672af46adca8a509be924c) *Creating an API key for the service account* **Important:** Store your API key securely. Porter encrypts the key before storing it. For more details, see the [Temporal Cloud Service Accounts](https://docs.temporal.io/cloud/service-accounts) and [API Keys](https://docs.temporal.io/cloud/api-keys) documentation. **Note:** Porter requires API key authentication and does not support certificate-only (mTLS) authentication. If your Temporal namespace is configured to only allow certificate authentication, contact [Temporal Support](https://support.temporal.io) to enable API keys on your namespace. ## Step 2: Create a Temporal Integration in Porter Before you can use Temporal autoscaling, you need to register your Temporal cluster as an integration in Porter. 1. Navigate to **Integrations** in the Porter dashboard 2. Select the **Temporal** tab 3. Click **Add Integration** 4. Fill in the integration details: * **Name**: A friendly name for this integration (e.g., `production-temporal`) * **Endpoint**: Your Temporal Cloud endpoint (e.g., `my-namespace.a1b2c.tmprl.cloud:7233`) * **Namespace**: Your Temporal namespace (e.g., `my-namespace.a1b2c`) * **API Key**: The API key you created in Step 1 5. Click **Add integration** ![Temporal Integration Modal](https://mintcdn.com/porter/hPI6NuWj_11g4ex9/images/configure/temporal-integration-modal.png?fit=max\&auto=format\&n=hPI6NuWj_11g4ex9\&q=85\&s=b2c3614e5d830de3d1db82019e9a75fa) *Adding a Temporal integration in Porter* Once created, your Temporal integration will appear in the integrations list and can be used for autoscaling any worker service. ![Temporal Integration List](https://mintcdn.com/porter/hPI6NuWj_11g4ex9/images/configure/temporal-integration-list.png?fit=max\&auto=format\&n=hPI6NuWj_11g4ex9\&q=85\&s=706b7b607a76c60153ea791a56ce0b30) *Temporal integrations in the Integrations page* ## Step 3: Configure Temporal Autoscaling for Your Service With your Temporal integration set up, you can now configure autoscaling for your worker services. 1. Navigate to your application dashboard 2. Select your worker service 3. Go to the **Resources** tab 4. Enable **Autoscaling** and select **Temporal** as the autoscaling type 5. Configure the autoscaling settings: * **Min instances**: Minimum number of worker replicas (e.g., 1) * **Max instances**: Maximum number of worker replicas (e.g., 20) * **Temporal Integration**: Select the integration you created in Step 2 * **Task queue name**: The name of the task queue your workers poll (e.g., `my-workflow-queue`) * **Target queue size**: The target number of tasks per worker instance ![Temporal Autoscaling Configuration](https://mintcdn.com/porter/hPI6NuWj_11g4ex9/images/configure/temporal-autoscaling-config.png?fit=max\&auto=format\&n=hPI6NuWj_11g4ex9\&q=85\&s=65937decf4a8f287f87dfabe3a19e36f) *Configuring Temporal autoscaling for a worker service* ## How Target Queue Size Works The **target queue size** determines how Porter scales your workers. Porter calculates the desired number of replicas as: ``` desired_replicas = ceil(current_queue_depth / target_queue_size) ``` For example: * If your queue has 100 pending tasks and target queue size is 10, Porter scales to 10 workers * If your queue has 5 pending tasks and target queue size is 10, Porter scales to 1 worker * If your queue is empty, Porter scales to your minimum instances **Choosing a target queue size:** * **Lower values** (e.g., 5-10): More aggressive scaling, lower latency for processing tasks * **Higher values** (e.g., 50-100): More conservative scaling, better resource efficiency ## Example: Document Processing Pipeline Consider a document processing system built with Temporal: ### Document Upload API A web service that receives document uploads and starts Temporal workflows for processing. ```python theme={null} from temporalio.client import Client @app.post("/upload") async def upload_document(file: UploadFile): # Start a Temporal workflow for each uploaded document await temporal_client.start_workflow( ProcessDocumentWorkflow.run, args=[file.filename], id=f"doc-{uuid4()}", task_queue="document-processing" ) return {"status": "processing"} ``` ### Document Processing Worker A worker service that runs the document processing workflows. ```python theme={null} from temporalio.worker import Worker async def main(): client = await Client.connect("your-namespace.tmprl.cloud:7233") worker = Worker( client, task_queue="document-processing", workflows=[ProcessDocumentWorkflow], activities=[extract_text, analyze_content, store_results], ) await worker.run() ``` ### Autoscaling Configuration * Min instances: `1` * Max instances: `20` * Task queue: `document-processing` * Target queue size: `10` With this configuration, Porter will: * Keep at least 1 worker running during quiet periods * Scale up to 20 workers during document upload spikes * Scale based on the actual queue depth, ensuring documents are processed promptly # Zero-downtime deployments Source: https://docs.porter.run/applications/configure/zero-downtime-deployments Prevent downtime during redeployments by configuring health checks, readiness probes, and graceful shutdown behavior for your services Every time an application is redeployed on Porter (through a GitHub action, configuration change, etc), a new set of application instances will replace the old ones. While the update process attempts to prevent downtime, there are additional configuration settings to ensure zero downtime during re-deployment: * **At least 2 instances** - During node maintenance, Porter keeps services with 2 or more instances available by replacing them a few at a time. A service with a single instance restarts briefly. Set this in the **Resources** tab, or set the autoscaling minimum to at least 2. * [Health Checks](#health-checks) - Ensure new instances are ready before receiving traffic * [Graceful Shutdown](#graceful-shutdown) - Allow old instances to finish processing before termination ## Health Checks Health checks indicate whether an application is healthy and ready to receive traffic. When enabled, traffic won't switch from the old application instance until the health check confirms the new instance is ready. Health checks can be configured in the **Advanced** tab of your service settings. > [!NOTE] > > Readiness probes should be used in conjunction with graceful shutdown behavior > to control exactly when applications stop receiving traffic. See the [graceful > shutdown](#graceful-shutdown) section for more information. ### Web Services For Web services, configure HTTP-based health checks where your application returns: * `200` status code when ready to receive traffic * `500`-level error code when not ready Health checks can be configured in the **Advanced** tab: ![Health Check Configuration](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/health-check-config.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=13827a7859e2cba1588c14900f963c37) For example, if you configure `/healthz` as your health check endpoint, no traffic will be routed to the web service until that endpoint returns a `200` status code. ### Workers For Worker services, you must set up health checks using custom commands or scripts instead of HTTP endpoints. This is useful for monitoring services like Celery workers that don't expose HTTP endpoints. To configure command-based health checks, create a health check script that: 1. Executes the necessary commands to check your worker's health 2. Returns exit code `0` if the worker is healthy 3. Returns a non-zero exit code if the worker is unhealthy Then specify the path to your health check script in the **Advanced** tab of your service settings. > [!NOTE] > > Worker services only support command-based health checks. HTTP health checks are only available for Web services. ## Graceful Shutdown When applications are being re-deployed, old instances receive a `SIGTERM` termination signal. They are then given a **Termination Grace Period**—the number of seconds before the application is forcefully killed. During this period, your application should: 1. Stop accepting new work 2. Complete or close existing connections 3. Exit gracefully The termination grace period can be configured in the **Advanced** tab. ### Web Services Web services will continue to receive traffic until they exit, unless you configure the readiness probe to fail. The recommended graceful shutdown sequence is: 1. When `SIGTERM` is received, immediately return a `500`-level response on your health check endpoint to stop receiving new traffic. 2. Close the server to prevent additional connections. 3. Drain all existing connections before the grace period ends. 4. Exit gracefully after connections are drained. ### Workers For Worker services, implement graceful shutdown using signal handling and file-based coordination: 1. **Trap the SIGTERM signal** in your worker process or an init script for the service. 2. **Write a shutdown marker** to a file when the signal is received, indicating the worker should stop. 3. **Adapt your health check script** to check for this shutdown marker: * If the marker indicates shutdown is in progress, the script should return a non-zero exit code. * This signals to Porter that the worker is no longer healthy and should not receive new work. # Common errors Source: https://docs.porter.run/applications/debug/common-errors Troubleshoot application restarts caused by memory limits, failing health probes, incorrect start commands, and build failures on Porter ## Application Restarts[](#application-restarts "Direct link to heading") ### Memory Usage[](#memory-usage "Direct link to heading") One of the most common issues for application restarts is that your applications continuously runs out of memory. You can try to allocate more memory to your application, and check the **Metrics** tab to view the memory consumption. If the memory usage continues to hit the memory limit (which is set in the **Resources** tab), increase the memory limit. ### Failing Liveness or Startup Probes[](#failing-liveness-or-startup-probes "Direct link to heading") As documented in the [zero-downtime deployments doc](/applications/configure/zero-downtime-deployments), enabling health checks via liveness probes are a good way to ensure that traffic only reaches healthy application instances. When the liveness probe or startup probes fail, the application will be restarted. There are a few common reasons why the application may fail its health check: * The liveness probe or startup probe are misconfigured. * Your application is experiencing resource pressure, and does not respond to an HTTP probe within 1 second. This is common for runtimes which share a single thread, like Rails or Node. In high-traffic scenarios, the latency of the health check can exceed 1 second, and thus the application will be restarted. * You have not give your application enough time to start up, and thus the application fails its startup probe. * The health check depends on an external service, like a database connection, which is currently unavailable. ### Start Command[](#start-command "Direct link to heading") When the start command isn't set correctly, application logs will never show in the dashboard, and you will see a message in the "System" logs stating that the OCI container runtime is unable to start the process. Make sure that you've set the start command correctly. > [!NOTE] > > You can read more about the start command for web applications in the [basic configuration](/applications/configure/basic-configuration#setting-the-start-command). One method to check which commands are set in the `$PATH` of your container is to set the start command to `sleep infinity`, and then use the porter run command to get shell access. From this shell, you could for example run: ``` $ which $ echo $PATH ``` ### Application Issues and Non-Zero Exit Codes[](#application-issues-and-non-zero-exit-codes "Direct link to heading") Your application may be restarting due to an application-level error which is causing the process to exit. To investigate if this may be the cause of application restarts, you can view the logs for failing applications by navigating to the **Events** tab. If your application is killed due to a non-zero exit code, it typically indicates that your application restarted due to an application error, or was killed by an external signal. For an overview on exit codes: * A valid exit code is between 0 and 255, 0 means that the container exited normally. * Generally speaking, if the container exited due to an internal signal then the exit code is between 1 and 128 and if it exited due to an external signal, the exit code is between 129 and 255. * The above will not hold true if the application programmer chooses to follow a different convention of using exit codes. #### Typical exit codes[](#typical-exit-codes "Direct link to heading") * `137`: indicates that the process was killed by `SIGKILL`. The most common reason for this is that your application does not handle graceful shutdown when it receives a `SIGTERM` signal. After receiving `SIGTERM`, your application should close existing connections and terminate with exit code `0`. See the [graceful shutdown doc](/applications/configure/zero-downtime-deployments#graceful-shutdown) for more information. * `1`: indicates common issues. Check container logs for further troubleshooting. For example, this could be the result of `exit(1)`. * `255`: this could either be the result of `exit(-1)` (which is translated to `255`) from the application, or could indicate that the application was forcibly killed by the underlying Kubernetes node. This is common if the application moves between nodes during a node scale-down event. While Porter typically kills processes running on the nodes gracefully, there are rare cases where the containers are abruptly stopped. To avoid downtime in these instances, it is recommended that at least 2 replicas are running for each application instance. * `2`: This could happen because of a misuse of a shell builtin when using Bash. * `126`: A command was invoked that could not be executed by the system. * `127`: Command was not found. Please check your `$PATH` or for a possible typo. * `128`: Invalid argument to `exit()`. * `130`: Process terminated with `Ctrl+C`. **Note:** Normally, an exit code of `128+n` denotes the fatal signal `n` from the standard [Linux interruption signals](https://man7.org/linux/man-pages/man7/signal.7.html). ## Image Pull Errors[](#image-pull-errors "Direct link to heading") Under the hood, every application which runs on Porter is running a [Docker image](https://docs.docker.com/get-started/overview/#images). This Docker image is pulled from a registry, which typically requires authentication credentials. If you are facing an image pull error, make sure you've checked the following items: * The image repository exists in the registry, and the image repository contains an image with the image tag set on Porter * You have connected an image registry to Porter. For more information, see the [deploy overview guide](/applications/deploy/overview) or the doc on [deploying from a Docker registry](/applications/deploy/deploy-from-docker-registry) * Your authentication credentials have not been revoked and have not expired. To check this, navigate to the **Integrations** tab on Porter, and select **Docker Registry**. If you are able to view the list of images for your registry, Porter is able to access that image registry. ## Networking Issues[](#networking-issues "Direct link to heading") ### Frequent Connection Resets/Dropped Connections[](#frequent-connection-resetsdropped-connections "Direct link to heading") This can be caused by a number of issues, which can either be at the application level, or at the networking level: * If your requests take longer than 30 seconds to resolve, or you are running a websocket-based application, the default read and write timeouts may not be long enough. See the [networking configuration doc](/applications/configure/advanced-networking#setting-custom-readwrite-timeouts) for how to increase the read/write timeouts. * If your application sends very large headers as part of the response body, consider increasing the response header size, as documented [here](/applications/configure/advanced-networking#header-size). * If you are seeing dropped connections while redeploying, follow the instructions for [zero-downtime deployments](/applications/configure/zero-downtime-deployments). * If you are seeing `recv` errors in your NGINX logs, the application is sending sending a connection reset message before a response is sent. This can usually be resolved by increasing the `keepalive` value in your application code. ### `413 Request Entity Too Large`[](#413-request-entity-too-large "Direct link to heading") This is caused by the NGINX instance rejecting requests that are too large. See the [networking configuration doc](/applications/configure/advanced-networking#client-max-body-size) for how to resolve these errors. ### `502 Bad Gateway`[](#502-bad-gateway "Direct link to heading") You will see `502 Bad Gateway` when your application is not starting correctly. See [application restarts](#application-restarts) to troubleshoot the error. This could also be a port number error -- make sure that you've set the port number correctly in the `Main` application tab. ### `503 Temporarily Unavailable`[](#503-temporarily-unavailable "Direct link to heading") The most common cause of this error is not setting the port number correctly. If not set correctly, your application will often show `503 Temporarily Unavailable` permanently when visiting the public URL. Make sure that you've set the port number correctly in the `Main` application tab. If the port number is set correctly, this may be shown when there is an application restart: [see above](#application-restarts) for more information. # Building your application Source: https://docs.porter.run/applications/deploy/builds How Porter builds applications from GitHub repositories using GitHub Actions, including build settings, Dockerfiles, and buildpacks When an application is deployed from a GitHub repository, Porter will build your application by opening a PR that contains a GitHub Actions file in your repository. > [!NOTE] > > > > For those who are not familiar with GitHub Actions, we recommend getting a basic > overview from [their documentation](https://docs.github.com/en/actions). ## Merging in the PR for GitHub Actions When you create an application, you will be asked to merge in the PR that Porter has opened in your repository. Porter will open a PR that contains the GitHub Actions file from a new branch called `porter-stack`. The GitHub actions file included in the PR will, by default, be triggered on any pushes to the specified git branch. Here's an example file that could be written to your repository: ``` "on": push: branches: - master name: Deploy to storm-king jobs: porter-deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Set Github tag id: vars run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: Setup porter uses: porter-dev/setup-porter@v0.2.0 - name: Deploy stack timeout-minutes: 30 run: porter apply -f ./porter.yaml env: PORTER_CLUSTER: "37" PORTER_HOST: https://dashboard.porter.run PORTER_PR_NUMBER: ${{ github.event.number }} PORTER_PROJECT: "18" PORTER_STACK_NAME: testasdfasdfasdf PORTER_TAG: ${{ steps.vars.outputs.sha_short }} PORTER_TOKEN: ${{ secrets.PORTER_STACK_18_37 }} ``` You can customize this GitHub action to be triggered on different actions than `push`. Please see the GitHub Actions [docs](https://docs.github.com/en/actions) for reference. As long as the steps **Setup porter** and **Deploy stack** are present in your github actions file, you can also prepend or append any steps as desired. ## Viewing your Build Logs You can view the build logs of your application from either the Porter dashboard or the GitHub Actions tab. To view your build logs on GitHub, navigate to the **Actions** tab in your repository. Click on the most recent **Deploy to Porter** workflow to view the build logs of your application. ## Retrying a Build on Failure When builds fail, you can troubleshoot by viewing the logs from the Porter Dashboard or the **Actions** tab on GitHub. To retry a build, simply toggle the option to **Re-run build and deploy on save**, then click on **Save Build Settings**. This will re-trigger the github actions file present in your github repository. You can also trigger a rerun of this GitHub Actions directly from GitHub by clicking on **Re-run failed jobs** button after selecting the job run. ## Build-time Environment Variables Many languages and frameworks require certain environment variables to be present during the build process. **Any environment variable you add to an application on Porter will automatically be piped into your build process; there is no need for you to manually add these environment variables to the GitHub Actions file**. Any supplementary environment variables you add to the GitHub Actions file will also be made available to your build process, however, as long as they are prefixed with `PORTER_`. Please note that `Secrets` will not be made available to your build process. Learn more about `Secrets` [here](/applications/configure/environment-groups). ### Using build-time environment variables for `Dockerfile` builds If you are building your application via `Dockerfile` and require environment variables to be used during the build process, you can use the `ARG` keyword to make these variables available to your `Dockerfile` and the `${env-var-name}` syntax to reference them elsewhere in the file. Example snippet from a `Dockerfile`: ``` ... ARG PORTER_ENV_VARIABLE RUN echo "Here is my env variable: ${PORTER_ENV_VARIABLE}" ... ``` Note that if you are building via buildpacks, no additional configuration is required to make your build-time environment variables available to your build process. ## Storing Container Images When your application has built successfully, Porter will automatically push the resulting container images into a container registry in your connected cloud account and update your application with the new build. ## Using Buildkit For certain Dockerfile-based builds, Buildkit may be required to build successfully, such as when using `COPY --chown`. To enable buildkit, set the `DOCKER_BUILDKIT` environment variable to `1`. This can be set at the Github Actions Workflow-level with the following snippet at the top-level of your Github Actions file: ``` # ... other parts of the workflow file go here env: DOCKER_BUILDKIT: "1" ``` # Configuring application services Source: https://docs.porter.run/applications/deploy/configuring-application-services Set up web, worker, and job services with start commands, port mappings, resource allocation, and instance scaling from the Porter dashboard A Porter application consists of one or more **services**. Services are the individual processes that make up your application, and they come in three types: **Web services** handle HTTP traffic. If your application serves a website, API, or any other HTTP endpoint, it runs as a web service. Web services can be exposed to the internet with custom domains, or kept private within your cluster for internal communication. **Worker services** run continuously in the background without accepting HTTP traffic. Use workers for queue processors, background job runners, event consumers, or any long-running process that doesn't need to respond to web requests. **Job services** run on a schedule or on-demand, execute their task, and then stop. Use jobs for database maintenance, report generation, cleanup tasks, or any work that runs periodically rather than continuously. ![Service type selector](https://mintcdn.com/porter/NkmSD5B3JYkw7n0Y/images/deploy/v2/service-type-dropdown.webp?fit=max\&auto=format\&n=NkmSD5B3JYkw7n0Y\&q=85\&s=294ce6d436c148ee48f92a15acbb690f) You can add additional services by clicking **Add Service** and selecting the appropriate type. Each service within an application shares the same codebase and build, but can have different start commands, resource allocations, and configurations. Service names must be lowercase letters, numbers, and hyphens only. They're used internally for routing and identification. *** ## Start command and port Every service needs to know how to run your application. ### Start command The start command tells Porter what process to run inside your container. For GitHub deployments, Porter often detects this automatically based on your framework. For Docker deployments, leave the start command empty to use your image's default CMD, or specify a command to override it. If your image supports multiple modes, you can run the same image with different commands for each service: * Web service: `npm start` or leave empty for default * Worker service: `npm run worker` * Job service: `npm run cleanup` ### Port Web services require a port number—the port inside the container where your application listens for HTTP traffic. This must match what your application actually binds to, not an external port. Common ports include 3000 (Node.js), 8080 (many frameworks), 80 (nginx), and 5000 (Flask). Check your application's configuration or Dockerfile if you're unsure. *** ## Resource allocation and scaling Every service needs compute resources. Porter lets you configure exactly how much CPU and memory each service receives, and how it scales under load. ### CPU and memory CPU is measured in cores, configurable from 0.1 (one-tenth of a core) up to 8 cores. Memory is measured in megabytes, from 128 MB up to 16 GB. The defaults (0.5 cores and 1 GB of memory) work well for lightweight services. Increase these values for compute-intensive workloads or applications with large memory footprints. ![Resource allocation sliders for CPU and RAM](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/cpu-ram-sliders.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=212cdce68da5199c9e7b5e9ded75ce9b) These values represent guaranteed resources. Your service will always have access to at least this much CPU and memory, regardless of what else is running in the cluster. Note that the memory value is also a hard limit. If your service exceeds it, it will be restarted. ### Node groups If your cluster has more than one node group to pick from, you can select them here. When you choose a node group with GPU support, an additional slider appears for configuring GPU allocation. For most applications, the default node group is appropriate. ### Scaling By default, Porter runs a single instance of each service. For production workloads, you'll typically want multiple instances for redundancy and to handle traffic spikes. See [Autoscaling](/applications/configure/autoscaling) for a complete guide. *** ## Networking and domains Web services can be exposed to the internet or kept private within your cluster. ### Public services By default, web services are public, i.e., accessible from the internet. Porter provisions a URL where your service is reachable immediately after deployment. ![Public/private toggle](https://mintcdn.com/porter/NkmSD5B3JYkw7n0Y/images/deploy/v2/public-private-toggle.webp?fit=max\&auto=format\&n=NkmSD5B3JYkw7n0Y\&q=85\&s=92b52c232f058dee1d57d1fac7e79503) ### Private services Toggle a service to **private** when it should only be reachable by other services in your cluster. Private services are useful for internal APIs, admin interfaces, or services that sit behind a public-facing gateway. Private services get internal DNS names that other services in your cluster can use to communicate. ### Custom domains To serve your application on your own domain, add it in the Domains section. You can configure multiple domains for a single service—useful for handling `www` and non-`www` versions, or serving the same application on different domains. After adding a domain, configure DNS by creating a CNAME record (or an A record for apex domains) pointing to your cluster's ingress IP address. Porter displays this address with a copy button. DNS propagation typically takes a few minutes, though it can occasionally take longer. ![Custom domain configuration with DNS instructions](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/custom-domain-dns.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=ce7e922f7d1adaef805963340162e2a3) Porter automatically provisions and renews SSL certificates for your custom domains using Let's Encrypt. ### Advanced routing with NGINX annotations For complex routing scenarios, you can add custom NGINX ingress annotations. These key-value pairs are applied directly to the Kubernetes ingress resource, giving you access to NGINX's full feature set. Common uses include custom rewrite rules, rate limiting, authentication requirements, CORS headers, and proxy buffer configuration. The annotation keys follow the `nginx.ingress.kubernetes.io/` prefix convention. ![Custom NGINX annotations](https://mintcdn.com/porter/NkmSD5B3JYkw7n0Y/images/deploy/v2/nginx-annotations.webp?fit=max\&auto=format\&n=NkmSD5B3JYkw7n0Y\&q=85\&s=7c38f51b237213ee6cc34de688b2818e) *** ## Environment variables and secrets Most applications need configuration values that vary between environments: database URLs, API keys, feature flags, and other settings. ### Adding variables On the app configuration page, expand the **Environment variables** accordion to define key-value pairs that become environment variables in your running containers. Type the variable name and value, and click the lock icon to mark a value as a secret. ![Environment variable editor with lock icons](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/env-vars-accordion.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=b2a7271adc010c992f0a929d2f5ca094) The distinction between variables and secrets affects visibility in the Porter dashboard. Secret values cannot be viewed after they're set. You can update them, but not retrieve them. Both are stored securely and injected into your containers at runtime. ### Environment groups If you have variables shared across multiple applications (like a database connection string or third-party API key) you can organize them into environment groups. Select existing groups from the dropdown to sync their variables into your application. ![Environment groups selector](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/env-groups-selector.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=1709c022925e10c0ef0a64c9191cb19f) When you update a variable in an environment group, Porter automatically triggers a deployment for all applications using that group. ### Uploading .env files For applications with many environment variables, you can upload an existing `.env` file rather than entering each variable manually. Click **Upload an .env file** and paste in your file contents. Porter parses the `KEY=VALUE` format, skipping comments and empty lines. ![.env file upload modal](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/env-file-upload.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=f2d061f216df8d0ccac2d17d0aa3deac) ### Copying variables as .env You can export your application's environment variables in `.env` format by clicking **Copy as .env** in the environment variables section. This opens a modal displaying all variables formatted as `KEY=VALUE` pairs, ready to copy to your clipboard. Secret variables with hidden values appear as commented lines at the bottom of the output (e.g., `# SECRET_KEY=`). A banner displays the count of hidden secrets so you know which values are excluded. To include secret values in the export, you must first reveal them in the environment variables editor. This is useful when you need to replicate an application's configuration locally, share it with a teammate, or use it as a starting point for a new application. *** ## Health checks Health checks enable zero-downtime deployments by ensuring new instances are ready before receiving traffic. When enabled, Porter waits for your health endpoint to return a successful response before routing traffic to a new instance, and automatically restarts instances that become unhealthy. ![Health check configuration](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/health-check-config.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=13827a7859e2cba1588c14900f963c37) Configure the health check with an HTTP path (like `/health` or `/api/health`) that your application exposes. The endpoint should return a 200-level status code when the service is ready to handle requests. For worker services, health checks use a command instead of an HTTP endpoint. Specify a shell command that exits with code 0 when the worker is healthy. The **initial delay** gives your application time to start up before health checks begin. Set this higher if your application has a slow initialization process. The **timeout** determines how long Porter waits for a response before considering the check failed. For best practices on combining health checks with graceful shutdown, see [Zero-Downtime Deployments](/applications/configure/zero-downtime-deployments). ### Metrics scraping If your application exposes Prometheus metrics, Porter can scrape and forward them to your monitoring infrastructure. Enable metrics scraping and specify the port and path where your application serves metrics (commonly `/metrics` on the application port or a dedicated metrics port). See [Custom Metrics Autoscaling](/applications/observability/custom-metrics-and-autoscaling) for details on using these metrics for autoscaling. ![Metrics scraping configuration](https://mintcdn.com/porter/NkmSD5B3JYkw7n0Y/images/deploy/v2/metrics-scraping.webp?fit=max\&auto=format\&n=NkmSD5B3JYkw7n0Y\&q=85\&s=ab20fc34f3a349d1333e46e6aa1cdf23) ### Sleep mode For non-production environments, sleep mode lets you pause a service to save costs. Sleeping services maintain their configuration but stop running instances. This is useful for staging environments that don't need to run overnight or on weekends. *** ## Pre-deployment jobs Some deployments need setup work before the main application starts, most commonly, database migrations. Pre-deployment jobs (also called migration jobs) run after your new code builds but before traffic routes to new instances. ![Pre-deployment job configuration](https://mintcdn.com/porter/NkmSD5B3JYkw7n0Y/images/deploy/v2/pre-deploy-config.webp?fit=max\&auto=format\&n=NkmSD5B3JYkw7n0Y\&q=85\&s=6e38aa7aa778f14f34ac6e515ae9b837) Enable the pre-deployment job and configure a start command for your migration script: for example, `npm run migrate`, `python manage.py migrate`, or `bundle exec rails db:migrate`. Pre-deployment jobs have their own resource allocation separate from your application services. Migrations are typically short-lived but may need more memory than your running application, especially for large data transformations. Configure an appropriate timeout based on how long your migrations typically take. The job must complete successfully before deployment continues. If it fails, the deployment halts, and your previous version continues running—your users never see a partially-migrated state. *** ## Scheduled jobs and workers Not every workload serves HTTP traffic. Porter supports background workers and scheduled jobs for processing that happens outside the request-response cycle. ### Workers Workers run continuously, processing tasks from queues, handling events, or performing ongoing background work. Configure a worker with a start command that runs your processing logic: for example, `python worker.py` or `node src/consumer.js`. ![Worker service configuration](https://mintcdn.com/porter/NkmSD5B3JYkw7n0Y/images/deploy/v2/worker-config.webp?fit=max\&auto=format\&n=NkmSD5B3JYkw7n0Y\&q=85\&s=12d53488b1f6b832a2ce7a93887bc7e7) Workers support the same resource allocation and autoscaling options as web services. For queue-based workers, custom autoscaling with KEDA lets you scale based on queue depth, adding workers when messages back up and removing them when the queue empties. ### Jobs Jobs run on a schedule and exit when complete. The cron schedule field accepts standard cron syntax with five fields: minute, hour, day of month, month, and day of week. Porter displays a human-readable description of your schedule as you type. ![Job configuration with cron schedule](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/job-cron-config.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=722b6e8ebc11c17e12530d0f9ba44d93) Some example schedules: * `0 0 * * *` runs daily at midnight * `0 */4 * * *` runs every 4 hours * `0 9 * * 1-5` runs at 9 AM on weekdays * `*/15 * * * *` runs every 15 minutes Configure the timeout to set a maximum execution time. Jobs exceeding this limit are terminated, preventing runaway processes from consuming resources indefinitely. The **concurrent execution** toggle controls whether multiple instances of the same job can run simultaneously. Disable this for jobs that shouldn't overlap. **Suspend cron job** temporarily pauses the schedule without removing the job configuration, useful during maintenance windows. # Connect GitHub Source: https://docs.porter.run/applications/deploy/connect-github Install the Porter GitHub App so Porter can access a repository, detect its applications, and create deployment workflows Before inspecting or building the app or calling any deployment tool, call `connect_github` with the repository. Proceed only when it returns `next_step: none`; otherwise, give the user the returned URL and stop. Do not author a `porter.yaml` to create the application; `create_app` handles that, and the CLI and configuration-as-code paths apply only when the user asks for them. Porter needs its GitHub App installed on a repository before it can detect applications, build from source, or create a GitHub Actions deployment workflow. This prerequisite applies whether you create the application with an agent or in the dashboard. ## Connect with an agent An agent connected to [Porter's MCP server](/mcp/overview) can check the repository and take you directly to the GitHub step that is still required. **1. Ask your agent to connect the repository** ```text theme={null} Connect this GitHub repository to Porter. ``` The agent calls `connect_github` with the repository in `owner/name` form. **2. Complete the returned action** If the agent returns a URL, open it to authorize GitHub, install the Porter GitHub App, or add the repository to an existing installation. If the repository is already reachable, the agent skips this step and continues. Open an OAuth authorization link while signed into the same Porter account your agent uses. An organization owner has to approve an installation when the repository's organization does not already have one. **3. Continue after Porter can reach the repository** Ask the agent to check again after completing the action. It proceeds only after `connect_github` reports that the repository is reachable. ## Connect from the dashboard **1. Open the repository connection flow** In the Porter dashboard, open your project and click **Create Application**, then click **Connect repositories**. **2. Install the Porter GitHub App** GitHub opens the authorization and installation flow. Choose the GitHub account or organization that owns the repository and authorize the Porter GitHub App. Depending on your GitHub organization settings, you may need to request the installation and wait for an organization administrator to approve it. **3. Choose repositories** Grant Porter access to all repositories or select the specific repositories you want to deploy. After installation, return to Porter. You can now [deploy from the GitHub repository](/applications/deploy/deploy-from-github-repo) with an agent or the dashboard. # Deploy from a Docker registry Source: https://docs.porter.run/applications/deploy/deploy-from-docker-registry Deploy pre-built container images from Amazon ECR, Google GAR, Azure ACR, Docker Hub, or any OCI registry to Porter without managing builds If you already have a container image built and pushed to a registry, Porter can deploy it directly without managing your build process. This approach works well when you have an existing CI/CD pipeline that produces Docker images, or when you're deploying third-party images that you don't build yourself. Porter supports all major container registries including Amazon Elastic Container Registry (ECR), Google Artifact Registry (GAR), Azure Container Registry (ACR), Docker Hub, and any registry accessible via a standard Docker image URL. This guide covers deploying pre-built container images. If you want Porter to build your application from source code, see [Deploy from a GitHub repository](/applications/deploy/deploy-from-github-repo). Porter runs a hosted MCP server at [https://mcp.porter.run](https://mcp.porter.run). If the user asks for the dashboard, follow the steps on this page. Otherwise, if you are connected to the MCP server, create the application with the `create_app` tool, passing `image` with the repository and tag. Porter deploys the prebuilt image directly — there is no pull request to merge and the Porter GitHub App is not required. `image` is mutually exclusive with `source` and `build`; use `source` and `build` instead when Porter should build the application from a GitHub repository. *** ## Quick deploy Deploying a container image takes just a few steps since you're skipping the build process entirely. ### Specify your image From the Porter dashboard, navigate to your project and click **Create Application**. Porter defaults to the GitHub deployment flow. To deploy from a container registry instead, click **Or, deploy from a container registry** in the top right. ![GitHub deployment page with container registry link](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/github-deploy-registry-link.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=37df4e84d1bd2590e3852cf088e0e0df) You'll see two input fields: one for the image repository URL and one for the tag. Enter the full path to your image in the repository field. The format depends on your registry: * **Docker Hub**: `docker.io/username/image` or just `username/image` * **Amazon ECR**: `123456789.dkr.ecr.us-east-1.amazonaws.com/my-app` * **Google Artifact Registry**: `us-docker.pkg.dev/project-id/repo/image` * **Azure Container Registry**: `myregistry.azurecr.io/my-app` In the tag field, enter the specific version you want to deploy—this might be `latest`, a semantic version like `1.2.3`, a git commit SHA, or any tag you've pushed to your registry. ![Container registry image URL and tag inputs](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/container-registry-inputs.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=28c5589b7595d388641c72cea952d344) ### Review your application Once you've entered both the image URL and tag, Porter creates an application configuration for you. The application name is derived from your image name. Deploying `docker.io/myorg/api-server:v2.1` creates an application called `api-server`. ![Application card showing derived name and Docker image](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/app-card-docker.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=7fee4b1e48bb6650108c843e4f0723f2) Porter provides sensible defaults for resources: 0.5 CPU cores, 1 GB of RAM, and a single instance. Since Porter can't inspect your pre-built image the way it can with source code, you'll need to specify the port your container listens on (for web services) and optionally override the start command if your image's default CMD isn't what you want to run. ### Deploy Once the application is created, you can review the app's start command and port, and click the configure button (gear icon) on the card to see other pre-configured values. When ready, deploy using the **Deploy** button. ![Application card with configure and deploy buttons](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/app-card-deploy-button.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=c04f83d29fd588b638134b225dd7ea1e) Porter pulls your image and starts running it. The sections below cover customizing the configuration when you need more control. *** ## Customizing your deployment Since Porter isn't building your image, configuration focuses on how to run your container rather than how to build it. ### Understanding the container registry flow When you deploy from a container registry, Porter: 1. Pulls your specified image from the registry 2. Runs it with your configured services, resources, and environment 3. Manages scaling, health checks, and networking There's no build step, no Dockerfile parsing, and no GitHub Actions workflow. Updates happen when you push a new image tag and tell Porter to deploy it (covered in the [Updating Your Application](#updating-your-application) section). This makes container registry deployments faster to set up, but requires you to manage your own build pipeline. It's ideal when you already have CI/CD infrastructure or when deploying images you don't control. *** ### Configuring services A Porter application consists of one or more **services**—web services for HTTP traffic, workers for background processing, and jobs for scheduled tasks. By default, Porter configures a single web service. For a complete guide to service types, resource allocation, networking, environment variables, health checks, and more, see [Configuring application services](/applications/deploy/configuring-application-services). *** ## Updating your application When you push a new image tag to your registry, Porter doesn't automatically deploy it. You have two options for triggering updates. ### Manual updates From your application's dashboard in Porter, you can change the image tag to deploy a different version. This is useful for one-off deployments or rollbacks. ![Update tag interface](https://mintcdn.com/porter/NkmSD5B3JYkw7n0Y/images/deploy/v2/update-tag-interface.webp?fit=max\&auto=format\&n=NkmSD5B3JYkw7n0Y\&q=85\&s=1e059feb6c35d6fb32942945ea179659) ### CLI updates For automated deployments, use the Porter CLI to update your application's image tag: ```bash theme={null} porter app update-tag APP_NAME --tag TAG_NAME ``` Replace `APP_NAME` with your application name in Porter and `TAG_NAME` with the new image tag you want to deploy. ### CI/CD integration The most common pattern is adding the Porter CLI command to the end of your existing CI/CD pipeline. After your pipeline builds and pushes a new image, it calls Porter to deploy that tag. For example, in a GitHub Actions workflow: ```yaml theme={null} - name: Build and push image run: | docker build -t myregistry.com/myapp:${{ github.sha }} . docker push myregistry.com/myapp:${{ github.sha }} - name: Deploy to Porter run: porter app update-tag myapp --tag ${{ github.sha }} env: PORTER_TOKEN: ${{ secrets.PORTER_TOKEN }} ``` This keeps your existing build process intact while letting Porter handle the deployment. You maintain full control over when and how images are built, and Porter takes over once you're ready to deploy. # Deploy from a GitHub repository Source: https://docs.porter.run/applications/deploy/deploy-from-github-repo Connect a GitHub repository to Porter for automatic builds and deployments with framework detection, branch selection, and CI/CD triggers Porter connects to your GitHub repositories to build and deploy your applications. When you connect a repository, Porter detects your app's framework, identifies services, and configures sensible defaults. This guide covers deploying applications from GitHub. If you're deploying from a container registry instead, see [Deploy from a Docker registry](/applications/deploy/deploy-from-docker-registry). Porter runs a hosted MCP server at [https://mcp.porter.run](https://mcp.porter.run). If its tools are not available in this session, run `curl -fsSL https://agents.porter.run | sh` to install and register them, then restart the client. If the user asks for the dashboard, the CLI, `porter apply`, or configuration-as-code, follow that path on this page. Otherwise, if you are connected to the MCP server, do not author a `porter.yaml`. Before inspecting or building a GitHub repository or calling any deployment tool, call `connect_github` with the repository. Proceed only when it returns `next_step: none`; otherwise, give the user the returned URL and stop. Then call `create_app` with `source` and `build`; this creates the app on a placeholder image and opens a GitHub Actions pull request that the user merges to trigger the first real deploy. *** ## Quick deploy **Agent** Ask an agent connected to the Porter MCP server to create the application for you. It detects your build settings from the codebase and opens a pull request that you merge to deploy. **1. Check the prerequisites** * [Connect your agent to the Porter MCP server](/mcp/overview). `curl -fsSL https://agents.porter.run | sh` does this for you and installs Porter's skills. * The [Porter GitHub App](/applications/deploy/connect-github) needs access to the repository. If it already does, you do not need to do anything; otherwise, your agent gives you a link to complete any missing GitHub OAuth, App installation, or repository access. For an organization repository, an organization owner may have to approve the installation. **2. Ask your agent to deploy** ```text theme={null} Deploy this GitHub repository to Porter. Detect the build method and application settings from the codebase. ``` The agent resolves your Porter project and cluster, then checks the repository with `connect_github` before inspecting your code or calling `create_app`. Once the repository is reachable, it inspects your code and calls `create_app` with the repository source and build configuration. Porter creates the application with a placeholder image and opens a pull request containing a GitHub Actions workflow. **3. Merge the pull request** Review and merge the pull request that the agent returns. Merging it triggers the workflow that builds your code and runs the first real deployment. **Dashboard** For most applications, Porter's automatic detection handles the heavy lifting. Here's how to get your application running: ### Connect your repository From the Porter dashboard, navigate to your project and click Create Application. Porter attempts to deploy from a GitHub repository by default. ![Source selection screen](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/github-source-selection.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=8e3422acb973ae6ac09684a1c235c515) If this is your first time deploying from GitHub, [install the Porter GitHub App](/applications/deploy/connect-github). You can grant access to all repositories or select specific ones. Porter only needs read access to detect your code, and write access to set up automated deployments. ![GitHub repository and branch selector](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/github-repo-branch-selector.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=be7df38a829e255c46f3bbbdc5a22be4) Once connected, select the repository containing your application code. Porter auto-selects the repository's default branch (usually `main` or `master`) so you can start deploying immediately. If you need a different target for non‑production, use the Branch selector to switch to `dev`, `staging`, or any other branch. ### Review detected applications After you select a repository and branch, Porter scans your code. It identifies frameworks and languages, locates Dockerfiles, and determines the required services. ![Detection in progress](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/detection-loading.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=997b1ac233265ea4bfa71598d8ac35c2) Within a few seconds, you'll see a list of detected applications. Each card shows the app name (from your repo or directory), the detected framework or build method, and the path in your repository. If the detected build method isn't right for your application, click the configure button (gear icon) on the card to change it. ![Detected applications list](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/detected-apps-list.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=59a2e5204cea9415f5e938f925bdd3f8) For a simple repository with a single application, you'll typically see one card. For monorepos containing multiple services, Porter detects each application separately. For example, a Node.js API in `/api`, a React frontend in `/web`, and a Python worker in `/jobs` each appear as distinct applications you can configure independently. #### Previously deployed apps in monorepos When you deploy from a monorepo that already has some services running on your cluster, Porter automatically identifies which detected applications are already deployed and hides them from the main list. This keeps the view focused on new applications you haven't deployed yet. A banner at the top summarizes what was detected. For example, if Porter finds 5 applications but 3 are already deployed, the banner indicates that 2 new applications were configured and 3 are already deployed. To view the already-deployed applications, expand the **Show N previously deployed app(s)** section below the application list. Each previously deployed app card shows its name, build path, and the name of the existing deployment it matches. If you want to re-create an already-deployed application (for example, to deploy a second instance with different configuration), click **Add** on its card. A confirmation dialog appears letting you know the build context is already deployed, and you can click **Create anyway** to add it to your new application list. > [!NOTE] > > Porter matches detected applications to existing deployments using the build path and build method. If you change the build path of an existing application, Porter may not recognize it as already deployed. If Porter didn't detect an application you expected, or if you want to add another manually, click Add Application. To edit a detected application, click the configure button (gear icon) on the card to make updates. ![Settings button on application card](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/app-card-settings-button.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=5107323040904d8b0883253be5543f2b) ### Deploy Once applications are detected, you can review each app's start command and port, and open each app's configuration page to see other pre‑configured values. When ready, deploy using the "Deploy X applications" button. Porter creates a GitHub Actions workflow in your repository that handles building and deploying your application on every push to your selected branch. Your first deployment starts as soon as you merge Porter's GitHub Actions pull request. That's it for a basic deployment. Porter has configured your application with production-ready defaults: appropriate resource allocation, a web service listening on the detected port, and automatic builds on every commit. The sections below cover customizing these defaults when you need more control. **CLI** Deploy the application from a `porter.yaml` file: ```bash theme={null} porter apply -f porter.yaml ``` Learn how to define and deploy an application in the [configuration-as-code guide](/applications/configuration-as-code/overview). *** ## Customizing your deployment Porter's defaults work well for many applications, but you have full control over every aspect of your deployment. The following sections explain each configuration area in detail. ### Build configuration Porter needs to know how to turn your source code into a runnable container. There are two approaches: Docker (via Dockerfiles) and buildpacks. #### Docker builds If your repository contains a Dockerfile, Porter can use it to build your application. This gives you complete control over the build process and is the right choice when you have custom system dependencies, need a specific base image, or have an application that buildpacks don't support. When you select Docker as your build method, you'll need to specify the path to your Dockerfile relative to the repository root. If your Dockerfile is at the root level, this is simply `Dockerfile`. For monorepos, it might be `./api/Dockerfile` or `./frontend/Dockerfile`. ![Dockerfile path selector](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/dockerfile-path-selector.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=8c12a130a53f4a4b464586f513760058) #### Buildpacks Buildpacks automatically detect your application's language and dependencies, then build an optimized container image without requiring you to write a Dockerfile. Porter supports buildpacks for Node.js (including Next.js), Python (Flask, FastAPI, Django), Ruby (Rails), and Go. When Porter detects a supported framework, it selects the appropriate buildpack automatically. You'll see the detected framework displayed on the application card. If the detection isn't quite right, you can override it by selecting a different framework from the dropdown in an application's configuration page. ![Build method selector](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/build-method-selector.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=a2759a4dd3a36a4de864908115a1faf8) Buildpacks work well when your application follows standard conventions for its framework. They handle dependency installation, asset compilation, and runtime configuration automatically. #### Build path The build path determines which directory Porter uses as the root for your build. For most repositories, this is `./` (the repository root). In a monorepo, you'll typically set this to the directory containing the specific application, like `./api` or `./frontend`. The build path affects where Porter looks for your Dockerfile (if using Docker builds) and which files are available during the build process. ![Build path selector](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/build-path-selector.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=316b3c2fe9afc48c977ebe0004ee0efc) *** ### Configuring services A Porter application consists of one or more **services**—web services for HTTP traffic, workers for background processing, and jobs for scheduled tasks. By default, Porter configures a single web service for detected applications. For a complete guide to service types, resource allocation, networking, environment variables, health checks, and more, see [Configuring application services](/applications/deploy/configuring-application-services). *** ## What happens when you deploy When you click Deploy, Porter initiates several processes: First, Porter creates a GitHub Actions workflow file in your repository at `.github/workflows/porter.yml`. This workflow triggers on pushes to your selected branch, building your application and deploying it to Porter. You'll see a pull request created with this workflow file, and merging this PR enables automated deployments. The initial deployment builds your application image using the configured build method and starts your services with the specified configuration. You can monitor deployment progress from the application dashboard, which shows build logs, deployment status, and any errors that occur. Once deployed, subsequent pushes to your branch trigger automatic rebuilds and deployments. Each deployment creates new container instances, runs health checks (if configured), and shifts traffic only after the new instances are ready. From your application dashboard, you can view logs, monitor resource usage, check deployment history, and make configuration changes. Configuration changes from the dashboard trigger new deployments but do not rebuild your application; rebuilds only happen when triggered from GitHub. # Deploy multiple apps from one build Source: https://docs.porter.run/applications/deploy/multiple-deploys-from-same-build Create a custom GitHub Actions workflow that builds a single container image and deploys it across multiple Porter applications efficiently Sometimes you may want to run multiple applications that are using the same build image. In these cases, it may be more efficient to create a custom GitHub workflow that will build the image once and then deploy the applications from the same image. ``` "on": push: branches: - main env: PORTER_BASE_APP_NAME: base-app PORTER_BASE_IMAGE_REPOSITORY_URI: PORTER_CLUSTER: PORTER_DEPLOYMENT_TARGET_ID: PORTER_HOST: https://dashboard.porter.run PORTER_PROJECT: PORTER_TOKEN: ${{ secrets. }} jobs: build_image: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 - name: Setup Porter uses: porter-dev/setup-porter@v0.2.0 - name: Build Image from Dockerfile run: exec porter app build $PORTER_BASE_APP_NAME --build-method docker --build-context . --dockerfile ./Dockerfile --tag $GITHUB_SHA - name: Push Image to Registry run: exec porter app push $PORTER_BASE_APP_NAME --tag $GITHUB_SHA deploy_apps: runs-on: ubuntu-latest needs: [build_image] strategy: matrix: app: ["base-app", "app-with-same-image-1", "app-with-same-image-2"] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Porter uses: porter-dev/setup-porter@v0.2.0 - name: Deploy App run: exec porter app update ${{ matrix.app }} --image-repository $PORTER_BASE_IMAGE_REPOSITORY_URI --tag $GITHUB_SHA ``` To set up this workflow, you need to: 1. Choose one of your applications to serve as the base application (`PORTER_BASE_APP_NAME`). The built images will be pushed to the repository for that application. You can choose any application with the shared build image. 2. Get the repository URI for the base application and set `PORTER_BASE_IMAGE_REPOSITORY_URI`. 3. Set the other fields including `PORTER_CLUSTER`, `PORTER_PROJECT`, `PORTER_DEPLOYMENT_TARGET_ID`, and `PORTER_TOKEN`. If you previously created a GitHub workflow for the base application, you should be able to use the same values. 4. Update the `deploy_apps` job to include the names of the applications that will be deployed from the same image (`deploy_apps.strategy.matrix.app`). # Deploying applications Source: https://docs.porter.run/applications/deploy/overview Learn how applications and services work on Porter, then choose how to deploy your application Porter runs a hosted MCP server at [https://mcp.porter.run](https://mcp.porter.run). If the user asks for the dashboard, the CLI, `porter apply`, or configuration-as-code, follow that path. Otherwise, if you are connected to the MCP server, do not author a `porter.yaml`. Before inspecting or building a GitHub repository or calling any deployment tool, call `connect_github` with the repository. Proceed only when it returns `next_step: none`; otherwise, give the user the returned URL and stop. Then call `create_app` with `source` and `build`; this creates the app on a placeholder image and opens a GitHub Actions pull request that the user merges to trigger the first real deploy. The CLI and configuration-as-code paths apply only when the user asks for them. Once you've provisioned infrastructure in your cloud account, you can deploy your first application. For an end-to-end guide, see the [Quickstart](/getting-started/quickstart#step-4:-create-your-first-application). ## Applications and services Porter deployments have two core concepts: Applications and Services. An **Application** groups one or more **Services**. Every service in an application shares the same container image and environment variables. Porter supports three [types of services](/applications/deploy/types-of-services). Use separate applications for services that need different container images or environment variables. ## Choose a deployment path > **[MCP server](/mcp/overview)** > > Connect an AI agent to deploy, inspect, and manage your applications. > **[Connect GitHub](/applications/deploy/connect-github)** > > Install the Porter GitHub App before deploying from a repository. > **[Deploy from GitHub](/applications/deploy/deploy-from-github-repo)** > > Let an agent or the dashboard detect, build, and deploy your source code. > **[Deploy from a registry](/applications/deploy/deploy-from-docker-registry)** > > Run a pre-built container image from an OCI registry. > **[Configuration as code](/applications/configuration-as-code/overview)** > > Define and deploy an application from a version-controlled `porter.yaml` file. # Pre-deploy jobs Source: https://docs.porter.run/applications/deploy/pre-deploy-jobs Run database migrations or setup tasks after each build but before deployment using pre-deploy jobs in the dashboard or porter.yaml A pre-deploy job is an optional task that runs after each build, but before all your services are deployed. It is commonly used for preliminary tasks which are required for your services to run, such as a database migration or seeding. **Note:** any commands that are used to build your application should *not* be included in a pre-deploy job, as the build will already be completed by the time the pre-deploy job is run. You can specify the pre-deploy job for your application both in the Porter dashboard and in your `porter.yaml` file. ## Configuring pre-deploy in the Porter dashboard You should see the pre-deploy section at the top of your application's **Overview** tab: ![Add predeploy from UI](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/deploying-applications/add-pre-deploy.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=a903cf8f305db5daf507bbc592884d56) Alternatively, when creating a new application, you will see the same section in the creation flow. From here, you can specify the start command (e.g., `bash ./migrate-db.sh`) as well as resources allocated for the job. After updating your application, the pre-deploy command will be run after the next build. ## Configuring pre-deploy in the porter.yaml file If you are new to `porter.yaml`, start [here](/applications/configuration-as-code/overview). Otherwise, see [here](/applications/configuration-as-code/services/predeploy) for information about configuring a pre-deploy job in your `porter.yaml` file. ## Tracking pre-deploy progress Your pre-deploy job will run after your build completes successfully. You can track its progress in the **Activity** tab of your application: ![Predeploy activity feed](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/deploying-applications/pre-deploy-activity-feed.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=6f71445b1427e4e63c3f9b34bde25296) You can view logs of the pre-deploy job while it runs or after it completes: ![Predeploy logs](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/deploying-applications/pre-deploy-logs.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=aa1812aee1a04107b9dcc0e98d6aaa58) # Rollbacks Source: https://docs.porter.run/applications/deploy/rollbacks Revert your Porter application to a previous successful deployment version from the Activity tab when an undesired change has been deployed An application rollback may be necessary if a change was deployed that is not desired. Luckily, it is easy to rollback your application to a previous state. In the **Activity** tab of your application, navigate to the **Deploy** event you wish to rollback to, and click the revert button: ![Rollback](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/deploying-applications/rollback.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=472ad69ae7a35508987e60fcf0785210) Note that only successful deployments are able to be rolled back to. # Types of services Source: https://docs.porter.run/applications/deploy/types-of-services Understand the three Porter service types: web services for HTTP traffic, workers for background processing, and jobs for scheduled tasks There are three types of services you can deploy on Porter: **web services**, **workers**, and **jobs**. ### Web Service[](#web-service "Direct link to heading") Web services are long-running processes that are exposed to external or internal traffic. This includes any web servers that are serving requests on a domain. Web services are, by default, exposed on a domain automatically generated by Porter, which follows the form of `*.onporter.run`. You can add a custom domain to your web service - Porter will automatically [secure your endpoints with SSL certificates](/applications/configure/custom-domains). Alternatively, you can expose your web service to only internal traffic (i.e. accessible only by other services inside the same cluster). ### Worker[](#worker "Direct link to heading") Worker processes are constantly running processes that are exposed to neither external nor internal traffic. Workers have no URLs or ports - they're best suited for background processes, queuing systems, etc. Most of the configuration options are identical to web applications, excluding the options that manage the endpoint. ### Jobs and Cron Jobs[](#jobs-and-cron-jobs "Direct link to heading") Jobs are processes that run to completion. They're best suited for ephemeral tasks such as database migration or clean up scripts. On Porter, you can run either **one-off jobs** or **cron jobs**. One-off jobs can be triggered manually through the dashboard or using the CLI. Cron jobs run periodically on a schedule specified as a cron expression. # Using other CI tools Source: https://docs.porter.run/applications/deploy/using-other-ci-tools Deploy Porter applications from CircleCI, GitLab, Travis CI, or any CI tool using the Porter CLI Docker image and environment variables You can use other CI tools, such as CircleCI, Travis CI, or Gitlab, to deploy your application. These CI tools support running any Docker image as part of a CI workflow. Porter maintains `ghcr.io/porter-dev/releases/porter-cli:latest`, a public Docker image that contains the Porter CLI. This allows you to run Porter CLI commands easily as part of any workflow. The Porter CLI requires that the following environment variables are set in order to target a specific application: ``` PORTER_TOKEN PORTER_PROJECT PORTER_CLUSTER ``` These environment variables can be set by logging into the Porter CLI and running `porter config`. ## Examples[](#examples "Direct link to heading") ### CircleCI[](#circleci "Direct link to heading") It is easiest to create a [CircleCI Context](https://circleci.com/docs/2.0/contexts/) for each Porter cluster in order to set environment variables for a CircleCI job. In CircleCI, set the following environment variables in a context: ``` PORTER_TOKEN PORTER_PROJECT PORTER_CLUSTER ``` These environment variables can be found after running `porter config` from the Porter CLI. Next, you can create the following CircleCI config file to a desired branch of your repository: ```yaml theme={null} version: 2.1 jobs: porter: docker: - image: ghcr.io/porter-dev/releases/porter-cli:latest steps: - checkout - setup_remote_docker: version: 19.03.13 - run: name: "Update Porter application" command: "porter app update-tag --tag $CIRCLE_SHA1 --stream" workflows: version: 2 porter-staging-workflow: jobs: - porter: context: ``` Make sure to replace `` and `` with your actual CircleCI context name and application name. # Alerts Source: https://docs.porter.run/applications/observability/alerts Set up Slack, email, and PagerDuty notification groups to receive alerts for crash loops, out-of-memory errors, and non-zero exit codes On critical events such as Application Crash Loop, Out of Memory errors, or non-zero exit codes, Porter will alert you on Slack. You can create `Notification Groups` by heading to `Settings` on your Porter project and setting up channels - you can set up email, Slack or PagerDuty. These can then be attached to apps(in any app on the Porter dashboard, head to the `Settings` tab to attach a notification group to that specific app). For advanced and custom alerts, we recommend using a 3rd party addon such as Datadog, Mezmo, or New Relic that you can easily install from the `Addons` tab. # App metadata environment variables Source: https://docs.porter.run/applications/observability/app-metadata Reference for default environment variables Porter injects into every app, including CPU, RAM, replicas, pod IP, image tag, and domains Porter injects some default environment variables in all Porter-provisioned apps, containing basic metadata around your app and the current deployment revision. A full list of these environment variables may be found here: 1. `PORTER_RESOURCES_RAM` - The amount of RAM assigned to the current service. 2. `PORTER_RESOURCES_CPU` - The number of vCPU cores assigned to the current service. 3. `PORTER_RESOURCES_REPLICAS` - The static replica count for the current service. Note that this may differ from the actual number of replicas running, if you have autoscaling enabled. 4. `PORTER_NODE_NAME` - The node the current service replica is running on. 5. `PORTER_NODE_IP` - The internal IP of the node the current service replica is running on. 6. `PORTER_POD_NAME` - This is the same as the internal hostname for the current service replica. 7. `PORTER_POD_IP` - The internal private IP assigned to the current service replica. 8. `PORTER_POD_IMAGE_TAG` - The image tag being used to run the current service replica. This is typically the same as `PORTER_IMAGE_TAG`. 9. `PORTER_IMAGE_TAG` - The image tag being used for the current app. This is typically the same as `PORTER_POD_IMAGE_TAG`. 10. `PORTER_POD_REVISION` - The revision ID assigned to the latest app deployment by Porter. 11. `PORTER_APP_SERVICE_NAME` - A portmanteau of the app name and service name: `-SERVICE`. 12. `PORTER_DOMAINS` - A comma-separated list of the domains assigned to your service. The following environment variables are only injected into [preview environments](/preview-environments/overview): 13. `PORTER_PR_BRANCH` - The git branch (head ref) of the pull request that created the preview environment. 14. `PORTER_PR_NUMBER` - The number of the pull request that created the preview environment. 15. `PORTER_PR_NAMESPACE` - The Kubernetes namespace of the preview environment. Use this when constructing in-cluster DNS names for services in the same preview, e.g. `.$PORTER_PR_NAMESPACE.svc.cluster.local`. Prefer this over `PORTER_PR_BRANCH`, which is the raw branch name and is not a valid DNS label for long or non-conforming branch names. If you're looking at adding more metadata to logs or traces, these environment variables can be used to inject metadata about the origin of a log/trace into your observability tooling. # Custom metrics autoscaling Source: https://docs.porter.run/applications/observability/custom-metrics-and-autoscaling Scale your web services based on custom Prometheus metrics by configuring metrics scraping endpoints and KEDA-powered autoscaling rules # Custom Metrics Autoscaling Porter supports autoscaling based on custom Prometheus metrics, allowing you to scale your services based on application-specific signals like queue length, request latency, or business metrics. For other autoscaling options, see the [Autoscaling overview](/applications/configure/autoscaling). ## Configuring Metrics Scraping **Note:** Metrics scraping is only available for web services. You can configure Porter to scrape metrics from your application's `/metrics` endpoint. This is useful for: * Collecting application-specific metrics * Setting up custom autoscaling based on your metrics * Monitoring application performance ### How to Enable Metrics Scraping 1. Navigate to your application dashboard 2. Select your web service 3. Go to the **Advanced** tab under service settings 4. Find the **Metrics scraping** section 5. Enable **Enable metrics scraping** 6. Configure the following options: * **Port**: The port where your metrics endpoint is exposed (defaults to your web service's default port) * **Path**: The path where metrics are exposed (defaults to `/metrics`) **Important:** Our telemetry collector will automatically send requests to the specified port and path to collect metrics from your service. ![Metrics Scraping Configuration](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/observability/metrics-scraping-config.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=d7e9af5e82f1cb197a5559e310f9dae5) *Metrics scraping configuration in the Advanced tab of a web service* ### Prometheus Metrics Format Your application must expose metrics in Prometheus format: * Metrics are exposed as HTTP endpoints (typically `/metrics`) * Each metric follows the format: `metric_name{label1="value1",label2="value2"} value` * Common metric types: * **Counter**: Values that only increase (e.g., `http_requests_total`) * **Gauge**: Values that can go up and down (e.g., `queue_length`) * **Histogram**: Observations distributed into buckets (e.g., `request_duration_seconds`) Example metrics output: ```prometheus theme={null} # HELP http_requests_total Total number of HTTP requests # TYPE http_requests_total counter http_requests_total{method="post",code="200"} 1027 http_requests_total{method="get",code="200"} 2048 ``` For detailed information about implementing Prometheus metrics in your application, refer to: * [Official Prometheus Exposition Format](https://prometheus.io/docs/instrumenting/exposition_formats/) * [Client Libraries for Different Languages](https://prometheus.io/docs/instrumenting/clientlibs/) ## Configuring Custom Autoscaling With metrics scraping enabled, you can set up autoscaling based on your custom metrics. ### How to Configure 1. Navigate to your application dashboard 2. Select your service 3. Go to the **Resources** tab 4. Configure basic autoscaling: * Enable **Autoscaling** * Set **Min instances** (e.g., 1) * Set **Max instances** (e.g., 10) 5. Switch to custom metrics mode by clicking the customize icon 6. Configure custom metrics: * **Metric Name**: Select a metric from your exposed Prometheus metrics * **Query**: Write or modify the PromQL query (defaults to `avg()`) * **Threshold**: Set the threshold value that triggers scaling When your selected metric exceeds the threshold, Porter will automatically scale your service between the min and max instances you've specified. ![Custom Autoscaling Configuration](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/observability/custom-autoscaling.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=4f7aaa08483928a932f9fb6144e52d55) *Custom autoscaling configuration in the Resources tab of a service* ### Query Requirements Your PromQL query must return a single numeric value (scalar). **Valid query examples:** * `avg(metric_name)` → Returns a single average value * `sum(rate(http_requests_total[5m]))` → Returns a single sum value * `max(queue_length)` → Returns a single maximum value **Invalid queries:** * Vector results (multiple time series) * String results * No data/empty results If your query returns multiple values, use aggregation operators like `avg()`, `sum()`, or `max()` to reduce it to a single value. ### Switching Between Autoscaling Modes You can switch between: * **Default Mode**: Autoscale based on CPU/Memory usage * **Custom Mode**: Autoscale based on your application metrics Click the customize/restore icons to switch between modes. ## Example: Message Queue Consumer Consider a data processing pipeline with a web API and worker service: ### Analytics Ingestion API A web service that ingests events and publishes them to RabbitMQ for processing. ```python theme={null} from prometheus_client import generate_latest, Gauge # Track messages in RabbitMQ queues queue_metrics = Gauge('rabbitmq_queue_messages', 'Number of messages in queue', ['queue_name']) @app.route('/metrics') def metrics(): queue_metrics.labels(queue_name='user_events').set( rabbit_connection.get_queue_length('user_events')) queue_metrics.labels(queue_name='system_events').set( rabbit_connection.get_queue_length('system_events')) return generate_latest() ``` ### Event Processing Worker A worker service that processes events from RabbitMQ. **Custom Autoscaling Configuration:** * Metric Name: `rabbitmq_queue_messages{queue_name="user_events"}` * Query: `sum(rabbitmq_queue_messages{queue_name="user_events"})` * Threshold: `1000` (scale up when more than 1000 events are waiting) With this setup, Porter will add more workers when the queue backs up and scale down when the queue is processed. # DevOps Agent Source: https://docs.porter.run/applications/observability/devops-agent Use the AI-powered DevOps Agent in the Porter dashboard to debug application issues with read-only access to your cluster in real time > [!NOTE] > > The DevOps Agent is currently in alpha. Features and behavior may change as we iterate on feedback. Responses are AI-generated and may not always be accurate. If you're unsure about a recommendation, reach out to support. The DevOps Agent is an AI-powered debugging assistant built into the Porter dashboard. When something goes wrong with your app, you can open the agent sidebar and ask it what's happening in plain English. It has read-only access to your cluster, so it can investigate your infrastructure in real time without being able to modify anything. It translates what it finds into actionable answers and links you to the right docs or settings to fix the problem. ## Setup To enable the DevOps Agent, click **DevOps Agent** in the sidebar or press `⌘+I`. If the agent hasn't been configured yet, you'll see a setup prompt. Click **Configure** to open the settings panel. ![DevOps Agent setup prompt](https://mintcdn.com/porter/XyF9x41cR-VbWUTM/images/devops-agent/configure-prompt.png?fit=max\&auto=format\&n=XyF9x41cR-VbWUTM\&q=85\&s=19e6b394b7b8a60d59893e04c92e43ef) You'll need to configure a few things: 1. **Provider** - either Anthropic (direct API) or AWS Bedrock 2. **Model** - which model to run (Claude Sonnet 4.6, Claude Opus 4.6, or Claude Haiku 4.5) 3. **API key** - your API key for the selected provider 4. **Provider URL** (Bedrock only) - your Bedrock runtime endpoint, formatted as `https://bedrock-runtime.{region}.amazonaws.com` (see [AWS Bedrock endpoints](https://docs.aws.amazon.com/general/latest/gr/bedrock.html) for available regions) > [!WARNING] > > AWS Bedrock currently only supports long-term API keys generated from the Bedrock console. Short-term generated credentials are not supported. Click **Save changes** to deploy the agent to your cluster. ![DevOps Agent settings panel](https://mintcdn.com/porter/XyF9x41cR-VbWUTM/images/devops-agent/settings-panel.png?fit=max\&auto=format\&n=XyF9x41cR-VbWUTM\&q=85\&s=458932155a18f449a0516bd301245899) It takes a few seconds for the agent to start up. You'll see a loading indicator while it deploys. ![DevOps Agent starting up](https://mintcdn.com/porter/XyF9x41cR-VbWUTM/images/devops-agent/starting-up.png?fit=max\&auto=format\&n=XyF9x41cR-VbWUTM\&q=85\&s=b5ead75eae262c287abfaac95771bdef) The agent is configured per cluster, so you only need to set it up once per cluster. You can disable the agent at any time from the same settings panel. Disabling removes the agent from your cluster and clears the configuration. ## How It Works The agent runs inside your cluster and has read-only access to inspect the infrastructure backing your apps, add-ons, and datastores. When you ask a question, it pulls together information from multiple sources to build a diagnosis: * **Porter API** - deployment history, revision status, service configuration, build info * **Live cluster state** - service logs, resource usage, events, scheduling status * **Porter docs and knowledge base** - troubleshooting guides and configuration references The agent is context-aware. It knows which app, add-on, or datastore you're currently viewing in the dashboard and scopes its investigation to that resource. You don't need to tell it where to look. ## Using the Agent Open the DevOps Agent with `⌘+I` (or `Ctrl+I` on Windows/Linux), or click the agent icon in the sidebar. It's available on any app, add-on, datastore, or cluster page. Ask your question in the input field and the agent will start investigating. As it works, you can see what it's checking in a collapsible thinking section. ![Agent investigating a question](https://mintcdn.com/porter/XyF9x41cR-VbWUTM/images/devops-agent/investigating.png?fit=max\&auto=format\&n=XyF9x41cR-VbWUTM\&q=85\&s=73175b372cc2568dbdbc2bd70eb48519) Once it has a diagnosis, it presents a concise answer with what went wrong and how to fix it, usually with links to the relevant Porter docs or dashboard settings. The agent maintains a session, so you can ask follow-up questions to dig deeper into the same issue. ### Stopping a response If you want to stop the agent while it's responding, click the **stop** button that appears in place of the send button, or press `Escape` while the input field is focused. The agent will stop its current response and keep everything it has generated so far, including any thinking steps. Your session stays active, so you can send a new message or follow-up question right away without starting over. If you send a message while the agent is still responding, it gets queued and will be delivered automatically after the current response finishes or is stopped. After the agent responds, you can rate the answer with thumbs up or thumbs down. If the answer wasn't helpful, you'll be prompted to select a reason (inaccurate, not helpful, or too verbose). You can also create a support ticket directly from any agent response by clicking the headset icon, which is useful if the agent's diagnosis points to something that needs human help. ![Response with feedback actions](https://mintcdn.com/porter/XyF9x41cR-VbWUTM/images/devops-agent/response-feedback.png?fit=max\&auto=format\&n=XyF9x41cR-VbWUTM\&q=85\&s=d9ab1d99411f088187c9054e34f42bdf) ## What You Can Ask About The agent is good at diagnosing runtime issues with your apps and infrastructure. Some common questions: * Why is my service restarting? * Why did this deployment fail? * What's using all the memory on my cluster? * Why can't my app connect to the database? * How do I set up autoscaling for this service? * Why is my build failing? It can also answer general "how do I..." questions about Porter by searching the docs and knowledge base. ## Limitations The agent has read-only access. It can inspect your infrastructure and tell you what's wrong, but it can't make changes on your behalf. Any fixes it recommends will point you to the right place in the dashboard, CLI, or `porter.yaml` to make the change yourself. The agent is scoped to the resource you're viewing. If you need help with a different app or cluster, navigate to that resource's page and start a new session. ## FAQ ### Does the DevOps Agent cost anything? The agent itself is free to use on Porter. However, each query uses your own LLM API key, so you'll be billed by your provider (e.g. Anthropic) for the tokens consumed. A typical debugging session uses a moderate number of tokens across the tool calls and response. ### Can the agent affect my production workloads? No. The agent runs as a small, isolated container on your cluster with minimal resource requirements (50m CPU request, 128Mi memory request). It has read-only access to your cluster and can't modify or interfere with your services. # Logging Source: https://docs.porter.run/applications/observability/logging Search and browse application logs retained for up to 7 days, stored securely in your own cluster infrastructure using Grafana Loki Porter retains your logs up to 7 days by default. You can perform basic search across your logs and navigate across different time periods. ![Logging Tab](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/observability/logging.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=878ddcc84f13de4066b4029a16ba74ef) All logs are stored exclusively inside your own infrastructure using [Loki](https://github.com/grafana/loki) and does not get stored anywhere on Porter's system. Porter is responsible for the reliability of the Loki instance running on your cluster. # Monitoring Source: https://docs.porter.run/applications/observability/monitoring View CPU, RAM, network usage, and throughput metrics for your applications with 14-day retention using Prometheus inside your own cluster Porter provides observability around your basic application metrics. Below are the metrics that Porter supports: * CPU usage * RAM usage * Network Usage * Network Throughput (number of networking errors from NGINX) All metrics are retained up to 14 days by default and are collected via [Prometheus](https://prometheus.io/) that runs inside your own cluster. Porter is responsible for the reliability of this prometheus instance and manages it on an ongoing basis. While these metrics support basic use cases, for advanced options we recommend using a 3rd party addon such as DataDog, Mezmo, or New Relic. Installing these addons is as simple as one click. You can find a list of 3rd party observability tools that Porter supports inside the **Add-ons** tab. # CLI basic usage Source: https://docs.porter.run/cli/basic-usage Authenticate, configure projects and clusters, and deploy applications using essential Porter CLI commands including apply, config, and app This guide covers the essential commands and configuration options for the Porter CLI. ## Quick Start **1. Login to Porter** Authenticate with your Porter account: ```bash theme={null} porter auth login ``` This opens your browser to complete authentication and automatically configures your default project and cluster. **2. Verify Configuration** Check your current CLI configuration: ```bash theme={null} porter config ``` If necessary, [switch your project and/or cluster](#project-and-cluster-configuration) **3. Deploy Your App** Deploy using a `porter.yaml` file: ```bash theme={null} porter apply -f porter.yaml ``` [`porter.yaml` reference](/applications/configuration-as-code/overview) [`porter apply` reference](/standard/cli/command-reference/porter-apply) ## Project and Cluster Configuration After logging in, you may need to switch between projects or clusters. ### List Available Projects ```bash theme={null} porter projects list ``` ### Set Active Project ```bash theme={null} porter config set-project [PROJECT_ID] ``` ### List Available Clusters ```bash theme={null} porter clusters list ``` The default table does not include cluster IDs. Pass `--json` when you need the ID for the next command: ```bash theme={null} porter clusters list --json ``` ### Set Active Cluster ```bash theme={null} porter config set-cluster [CLUSTER_ID] ``` ### View Current Configuration ```bash theme={null} porter config ``` ## Global Flags These flags can be used with any Porter command: | Flag | Description | | - | - | | `--project ` | Override the project ID for this command | | `--cluster ` | Override the cluster ID for this command | | `--token ` | Use a specific authentication token | | `-h, --help` | Display help for the command | ```bash Override Project theme={null} porter app logs my-app --project 12345 ``` ```bash Override Cluster theme={null} porter app run my-app --cluster 67890 -- bash ``` ## Environment Variables Environment variables provide an alternative way to configure the CLI, which is especially useful in CI/CD pipelines. | Variable | Description | Equivalent Flag | | - | - | - | | `PORTER_PROJECT` | Project ID to use | `--project` | | `PORTER_CLUSTER` | Cluster ID to use | `--cluster` | | `PORTER_TOKEN` | Authentication token | `--token` | | `PORTER_HOST` | Custom Porter API host | `--host` | | `PORTER_APP_NAME` | Default app name for commands | `--app` | > [!NOTE] > > Environment variables take precedence over values in your config file, but flags take precedence over environment variables. ### Example: CI/CD Configuration ```bash theme={null} export PORTER_TOKEN="your-deploy-token" export PORTER_PROJECT="12345" export PORTER_CLUSTER="67890" porter apply -f porter.yaml ``` ## Common Workflows ### Local Development ```bash theme={null} # Login and configure porter auth login # View your app's logs porter app logs my-app # Run a command in an ephemeral copy of your app porter app run my-app -- bash # View current app configuration porter app yaml my-app ``` ### CI/CD Deployment ```bash theme={null} # Deploy with explicit configuration PORTER_TOKEN=$DEPLOY_TOKEN \ PORTER_PROJECT=$PROJECT_ID \ PORTER_CLUSTER=$CLUSTER_ID \ porter apply -f porter.yaml ``` ### Managing Environment Variables ```bash theme={null} # Pull environment variables from an app porter env pull -a my-app # Pull environment variables from an environment group porter env pull -g my-env-group # Set environment variables on an app porter env set -a my-app -v KEY=value # Set secrets on an environment group porter env set -g my-env-group -s API_KEY=secret123 ``` ### Debugging ```bash theme={null} # Stream live logs porter app logs my-app # View historical logs porter app logs my-app --since 1h ``` ## Viewing Help You can view help instructions for any command using the `-h` or `--help` flag: ```bash theme={null} # General help porter -h # Help for a specific command porter app -h # Help for a subcommand porter app run -h ``` ## Next Steps * Learn about [configuration-as-code](/applications/configuration-as-code/overview) with `porter.yaml` * Explore the [full command reference](/standard/cli/command-reference/porter-apply) for detailed options * Set up [CI/CD deployment](/applications/deploy/using-other-ci-tools) for automated deployments # CLI installation Source: https://docs.porter.run/cli/installation Install the Porter CLI on macOS via Homebrew or shell script, on Linux via shell script, and on Windows via manual binary download To install the Porter CLI, see the OS-specific instructions below. > [!NOTE] > > Working with an AI agent? `curl -fsSL https://agents.porter.run | sh` installs the CLI *and* registers the [Porter MCP server](/mcp/overview) with your agent client, plus Porter's skills. **Mac** #### Install via Homebrew ```bash theme={null} brew install porter-dev/porter/porter ``` #### Manual Installation > [!NOTE] > > **Prerequisites:** The installation process requires `curl` and `unzip` utilities. ```bash theme={null} /bin/bash -c "$(curl -fsSL https://install.porter.run)" ``` **Linux** > [!NOTE] > > **Prerequisites:** The installation process requires `curl` and `unzip` utilities. ```bash theme={null} /bin/bash -c "$(curl -fsSL https://install.porter.run)" ``` After installing, run the following to verify your installation: ```bash theme={null} porter version ``` # Advanced Cluster Settings Source: https://docs.porter.run/cloud-accounts/advanced-cluster-settings Enable ECR scanning, GuardDuty, private clusters, KMS encryption, and custom networking options for your AWS, GCP, and Azure Porter clusters Porter exposes advanced cluster configuration options for customers with specific compliance, security, or networking requirements. These settings are available on the **Advanced** tab of your cluster settings for **AWS**, **GCP**, and **Azure** clusters. **AWS** ## Networking ### Private cluster When **Private cluster** is enabled, Porter provisions the EKS cluster with **both public and private** API server endpoint access, and restricts the public endpoint to an IP allowlist containing Porter's [control-plane IPs](/security-and-compliance/porter-ip-ranges) plus any customer CIDRs you add. This configuration is SOC2 / HIPAA compliant. | Setting | Description | | - | - | | **Private cluster** | Restricts the EKS API server's public endpoint to an IP allowlist (Porter's IPs plus any customer-supplied CIDRs). The private endpoint inside your VPC remains reachable from VPC-attached resources. | | **CIDR allowlist** | Additional CIDR ranges (beyond Porter's required IPs) that may reach the public endpoint. | > [!NOTE] > > Porter intentionally does **not** enable EKS "private-only" endpoint mode. Private-only forces every control-plane call — including Porter's — through a VPN or VPC-peered path, which adds operational complexity and has historically caused outages for customers. Public + private with a tight IP allowlist meets the same compliance requirements and is significantly more reliable. The [Tailscale integration](/security-and-compliance/tailscale) is a separate layer that carries traffic for `porter kubectl` and `porter helm` commands; it does not control how the EKS API server endpoint itself is exposed. ### Load balancer Configure the type of load balancer used for your cluster's ingress. Changing this setting causes downtime while the load balancer is recreated. | Type | Description | | - | - | | **NLB** | Network Load Balancer — operates at Layer 4, routes TCP/UDP traffic, and provides ultra-low latency and high throughput. | | **ALB** | Application Load Balancer — operates at Layer 7, routes HTTP/HTTPS traffic, and supports wildcard domains, ACM certificates, and WAFv2. | When **ALB** is selected, the following additional settings become available. See [Custom domains with ALB](/applications/configure/custom-domains-alb) for end-to-end setup instructions. | Setting | Description | | - | - | | **Wildcard domains** | Domains to issue ACM certificates for. Do not include the `*.` prefix — Porter creates a SAN for `*.` automatically. Comma-separate multiple domains. | | **IP allow list** | Comma-separated list of CIDR ranges (e.g. `160.72.72.58/32,160.72.72.59/32`) permitted to reach the ALB. | | **Certificate ARNs** | Existing ACM certificate ARNs to attach to the ALB. | | **AWS tags** | Key/value tags applied to the ALB and related AWS resources. | | **WAFv2 enabled** | Attaches a Regional WAFv2 web ACL to the ALB. | | **WAFv2 ARN** | ARN of the Regional WAFv2 web ACL to attach. Only Regional WAFv2 is supported. | > [!NOTE] > > Do not use this setting. Use the [Application load balancer](#application-load-balancer) setting that follows. Porter continues to support this setting on clusters that already use it. ### Application load balancer This setting adds one more public ALB to the cluster. It does not replace the default load balancer of the cluster. Because each service selects the load balancer that serves it, you can move the domains one at a time. | Setting | Description | | - | - | | **Add application load balancer** | Porter provisions a public layer 7 ALB in front of the cluster. The ALB listens on port 80 and port 443. The ALB terminates TLS at the edge. The ALB does not redirect HTTP traffic to HTTPS. | | **Root domains** | The domains that this load balancer serves. Write one domain in each row, for example `alb.example.com`. Porter requests one ACM certificate for each root domain. Each certificate includes `*.`, so the load balancer can serve all subdomains of that root domain. You must give a minimum of one root domain. | | **DNS provider** | The provider that hosts the root domains. ACM uses this provider to validate the certificate. **Cloudflare** and **AWS Route53** are currently supported. | | **DNS credentials** | API token for Cloudflare. This field shows only when the DNS provider is Cloudflare. The token must have permission to read the zone that serves each root domain, and to create DNS records in that zone. Route53 needs no credentials, because Porter writes the records with the AWS account of the cluster. | | **Web application firewall** | Porter provisions a Regional WAFv2 web ACL with a Porter-managed name (`porter-alb-waf-`). Porter then associates the web ACL with the ALB. | #### Certificate validation ACM validates the certificate with DNS records, which are automatically managed for both DNS providers. If you use **Route53**, each root domain must have a public hosted zone in the AWS account of the cluster. If you use **Cloudflare**, you must upload an API token in the **DNS credentials** field. The token must have read and write permissions for the zone that serves each root domain. Validation records are required to renew the certificate after it has been provisioned. #### Point DNS at the ALB The ALB has its own address, so the records that point at the default load balancer do not change. For each root domain, point a wildcard record at the ALB, for example `*.alb.example.com`. On AWS, this address is a hostname, so you must use a `CNAME` record or a Route53 alias record. After Porter provisions the ALB, the address shows in the **Custom domains** area of the **Networking** tab of a service. #### Web application firewall Porter creates the web ACL with a default action of allow. Porter does not add rules. Add managed rule groups, rate-based rules, or IP sets in the AWS WAF console. Porter does not change these rules. Porter supports only Regional WAFv2. If you turn off this setting, Porter detaches the web ACL but does not delete it, so your rules remain if you turn on the setting again. #### Route a service through the ALB After the ALB serves traffic, the **Networking** tab of each web service shows a **Public ALB** option next to **Public LB**. In `porter.yaml`, use [`loadBalancers`](/applications/configuration-as-code/services/web-service#loadbalancers): ```yaml theme={null} services: - name: api type: web loadBalancers: - public_alb domains: - name: api.alb.example.com ``` The domains of a service that the ALB serves must be below a registered root domain. When you change the load balancer of a service, the change takes effect at the next deployment. Move the DNS record of the domain to the address of the new load balancer. ### Private load balancer In addition to the default public cluster load balancer, you can provision an **internal load balancer** that only accepts traffic from inside your VPC (or networks peered to it). Use this when you want to expose services to internal clients, for example an internal admin tool, a service consumed only by other VPCs, or a workload that must not be reachable from the public internet. | Setting | Description | | - | - | | **Add private load balancer** | Provisions a private NLB alongside the existing public cluster load balancer. Only NLB private load balancers are supported. | Once enabled, you must configure a DNS provider so Porter can issue and renew TLS certificates for ingress hostnames attached to the private load balancer. The following DNS providers are supported: **Cloudflare** and **AWS Route53**. > [!TIP] > > We recommend serving private ingress from a dedicated internal zone (for example, `internal.example.com`) rather than a zone that also serves production domains. This avoids record conflicts with production DNS and keeps DNS access scoped to internal hostnames only. > > If that isn't practical, you can still keep credentials off your production zone. See [Delegating certificate validation](#delegating-certificate-validation) below. **Cloudflare** | Setting | Description | | - | - | | **DNS credentials** | API token for Cloudflare. The token must have permission to create and delete `TXT` records on the zones used by your private ingress hostnames. | Save the credentials before updating the cluster. You can rotate the token later with **Edit credentials**, or remove the integration entirely with **Remove**. Removing credentials stops certificate issuance and renewal for private load balancer ingress. **Route53** When the cluster is on AWS, you can use Route53 instead of Cloudflare. Porter authenticates to Route53 through an [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) scoped to a single hosted zone, so no API tokens are stored. | Setting | Description | | - | - | | **Route53 domain** | The DNS zone the private ingress serves certificates for (for example, `internal.example.com`). At install time, Porter resolves the matching public Route53 hosted zone in the same AWS account and provisions a cert-manager pod identity scoped to that zone. | The domain must already exist as a public hosted zone in the cluster's AWS account, and the zone name must match the domain exactly. If only a parent zone is present (for example, `example.com` for a domain of `internal.example.com`), Porter cannot issue certificates for it, and the domain requires its own dedicated hosted zone. #### Delegating certificate validation By default, the DNS credentials you give Porter need write access to the zone that hosts your private hostnames. If those hostnames live under a zone that also serves production domains, those credentials can reach your production records too, which is more privilege than you may want to grant. You can avoid that by delegating just the validation records to a separate, lower-privilege zone. You add a `CNAME` from each validation record in your zone to the delegated zone, then scope the credentials to that zone only. Porter handles the rest with no extra configuration on its side. > [!NOTE] > > On AWS Route53 this isn't necessary. Create a dedicated hosted zone for the subdomain and provide it as the **Route53 domain** (the Route53 option above); Porter scopes access to that zone directly. **1. Add the delegation `CNAME` records.** Pick a low-privilege zone to hold the validation records (for example, `acme.example-internal.com`). In the zone that hosts your private hostnames, add a `CNAME` for each validation record, pointing at a record in the delegated zone: ``` _acme-challenge.app.internal.example.com. CNAME _acme-challenge.acme.example-internal.com. ``` **2. Scope the credentials to the delegated zone.** Create an API token scoped to the **delegated zone only**, with: * `Zone.DNS` → Edit * `Zone.Zone` → Read The token does not need any access to your production zone. **3. Verify the delegation.** Confirm the `CNAME` resolves publicly: ```bash theme={null} dig +short CNAME _acme-challenge.app.internal.example.com # expected: _acme-challenge.acme.example-internal.com. ``` Certificates are issued automatically when you attach a hostname to a service through its [load balancer config](/applications/configuration-as-code/services/web-service#loadbalancerconfig). Porter then publishes each validation record into the delegated zone through the `CNAME`. If you need help setting this up, reach out to Porter support. ## Observability ### CloudWatch control plane logs Configure which EKS cluster control plane log types are sent to AWS CloudWatch. These logs help with debugging, auditing, and monitoring your cluster's control plane components. | Log Type | Description | | - | - | | **API Server logs** | Logs from the Kubernetes API server, useful for debugging API requests | | **Audit logs** | Records of individual users, administrators, or system components that have affected the cluster | | **Authenticator logs** | Logs from the AWS IAM authenticator, useful for debugging authentication issues | | **Controller manager logs** | Logs from the controller manager, which manages core control loops | | **Scheduler logs** | Logs from the scheduler, useful for debugging pod scheduling decisions | ### CloudWatch Observability agent You may also enable the CloudWatch Observability agent as an EKS add-on for enhanced cluster monitoring. | Setting | Description | | - | - | | **AWS CloudWatch Observability agent installed on cluster** | Enables the CloudWatch Observability add-on for metrics and logs collection | ## Security ### ECR scanning Enable Amazon ECR image scanning to automatically scan container images for software vulnerabilities. | Setting | Description | | - | - | | **ECR scanning enabled** | When enabled, images pushed to ECR are automatically scanned for vulnerabilities | ### AWS GuardDuty AWS GuardDuty provides intelligent threat detection for your EKS cluster, monitoring for malicious activity and unauthorized behavior. When you enable the GuardDuty agent, Porter: 1. Installs the agent as an EKS add-on. 2. Turns on GuardDuty and Runtime Monitoring in the cluster's region, if either is off. 3. Creates a `guardduty-data` VPC endpoint, which the agent sends its findings through, if the cluster's VPC has none. Turning the agent off removes it and any endpoint Porter created. GuardDuty and Runtime Monitoring stay on. > [!NOTE] > > If a delegated administrator account manages GuardDuty for your organization, turn on Runtime Monitoring from that account. > [!WARNING] > > Leave GuardDuty's automated agent configuration for EKS off. Porter manages the agent's version and reverts agent updates that GuardDuty makes. > > The endpoint Porter creates accepts traffic only from the cluster, but its DNS name applies to the whole VPC. | Setting | Description | | - | - | | **AWS GuardDuty agent installed on cluster** | Installs the GuardDuty agent on your cluster nodes | ### KMS encryption Enable AWS Key Management Service (KMS) encryption for Kubernetes secrets stored in etcd. | Setting | Description | | - | - | | **KMS encryption enabled** | Encrypts Kubernetes secrets at rest using a customer-managed KMS key | **GCP** ## Advanced Networking Config > [!WARNING] > > Modifying these advanced network settings can impact cluster connectivity and performance. Ensure you understand the implications before making changes. | Setting | Description | Default | | - | - | - | | **Egress NAT IPs Count** | Number of egress NAT IPs. Cannot be decreased once set. | 1 | | **Min Ports per VM** | Configures the minimum number of ports allocated per VM for GKE | 64 | | **Enable Endpoint Independent Mapping** | Recommended for most use cases. Affects Cloud NAT behavior. | Enabled | | **Enable Dynamic Port Allocation** | Allows GKE to dynamically allocate more ports to VMs that need them | Disabled | ## Networking ### Private load balancer In addition to the default public cluster load balancer, you can provision an **internal load balancer** on GKE that only accepts traffic from inside your VPC (or networks peered to it). Use this when you want to expose services to internal clients, for example an internal admin tool, a service consumed only by other VPCs, or a workload that must not be reachable from the public internet. | Setting | Description | | - | - | | **Add private load balancer** | Provisions a GKE internal load balancer alongside the existing public cluster load balancer. | Once enabled, you must configure a DNS provider so Porter can issue and renew TLS certificates for ingress hostnames attached to the private load balancer. **Cloudflare** is supported as a DNS provider for private load balancer ingress. > [!TIP] > > We recommend serving private ingress from a dedicated internal zone (for example, `internal.example.com`) rather than a zone that also serves production domains. This avoids record conflicts with production DNS and keeps DNS access scoped to internal hostnames only. > > If that isn't practical, you can still keep credentials off your production zone. See [Delegating certificate validation](#delegating-certificate-validation-1) below. | Setting | Description | | - | - | | **DNS credentials** | API token for Cloudflare. The token must have permission to create and delete `TXT` records on the zones used by your private ingress hostnames. | Save the credentials before updating the cluster. You can rotate the token later with **Edit credentials**, or remove the integration entirely with **Remove**. Removing credentials stops certificate issuance and renewal for private load balancer ingress. #### Delegating certificate validation By default, the DNS credentials you give Porter need write access to the zone that hosts your private hostnames. If those hostnames live under a zone that also serves production domains, those credentials can reach your production records too, which is more privilege than you may want to grant. You can avoid that by delegating just the validation records to a separate, lower-privilege zone. You add a `CNAME` from each validation record in your zone to the delegated zone, then scope the credentials to that zone only. Porter handles the rest with no extra configuration on its side. **1. Add the delegation `CNAME` records.** Pick a low-privilege zone to hold the validation records (for example, `acme.example-internal.com`). In the zone that hosts your private hostnames, add a `CNAME` for each validation record, pointing at a record in the delegated zone: ``` _acme-challenge.app.internal.example.com. CNAME _acme-challenge.acme.example-internal.com. ``` **2. Scope the credentials to the delegated zone.** Create an API token scoped to the **delegated zone only**, with: * `Zone.DNS` → Edit * `Zone.Zone` → Read The token does not need any access to your production zone. **3. Verify the delegation.** Confirm the `CNAME` resolves publicly: ```bash theme={null} dig +short CNAME _acme-challenge.app.internal.example.com # expected: _acme-challenge.acme.example-internal.com. ``` Certificates are issued automatically when you attach a hostname to a service through its [load balancer config](/applications/configuration-as-code/services/web-service#loadbalancerconfig). Porter then publishes each validation record into the delegated zone through the `CNAME`. If you need help setting this up, reach out to Porter support. ## Observability Settings Configure observability features for the GKE cluster control plane. | Setting | Description | | - | - | | **Enable Control Plane Logging** | Enable the collection of logs from the Kubernetes control plane components (e.g., API server, scheduler) | | **Enable Control Plane Metrics** | Enable the collection of metrics from the Kubernetes control plane components | **Azure** ## Networking ### Private load balancer In addition to the default public cluster load balancer, you can provision an **internal load balancer** on AKS that only accepts traffic from inside your VNet (or networks peered to it). Use this when you want to expose services to internal clients, for example an internal admin tool, a service consumed only by other networks, or a workload that must not be reachable from the public internet. | Setting | Description | | - | - | | **Add private load balancer** | Provisions an internal Azure load balancer alongside the existing public cluster load balancer. | Once enabled, you must configure a DNS provider so Porter can issue and renew TLS certificates for ingress hostnames attached to the private load balancer. **Cloudflare** is supported as a DNS provider for private load balancer ingress. > [!TIP] > > We recommend serving private ingress from a dedicated internal zone (for example, `internal.example.com`) rather than a zone that also serves production domains. This avoids record conflicts with production DNS and keeps DNS access scoped to internal hostnames only. > > If that isn't practical, you can still keep credentials off your production zone. See [Delegating certificate validation](#delegating-certificate-validation-2) below. | Setting | Description | | - | - | | **DNS credentials** | API token for Cloudflare. The token must have permission to create and delete `TXT` records on the zones used by your private ingress hostnames. | Save the credentials before updating the cluster. You can rotate the token later with **Edit credentials**, or remove the integration entirely with **Remove**. Removing credentials stops certificate issuance and renewal for private load balancer ingress. #### Delegating certificate validation By default, the DNS credentials you give Porter need write access to the zone that hosts your private hostnames. If those hostnames live under a zone that also serves production domains, those credentials can reach your production records too, which is more privilege than you may want to grant. You can avoid that by delegating just the validation records to a separate, lower-privilege zone. You add a `CNAME` from each validation record in your zone to the delegated zone, then scope the credentials to that zone only. Porter handles the rest with no extra configuration on its side. **1. Add the delegation `CNAME` records.** Pick a low-privilege zone to hold the validation records (for example, `acme.example-internal.com`). In the zone that hosts your private hostnames, add a `CNAME` for each validation record, pointing at a record in the delegated zone: ``` _acme-challenge.app.internal.example.com. CNAME _acme-challenge.acme.example-internal.com. ``` **2. Scope the credentials to the delegated zone.** Create an API token scoped to the **delegated zone only**, with: * `Zone.DNS` → Edit * `Zone.Zone` → Read The token does not need any access to your production zone. **3. Verify the delegation.** Confirm the `CNAME` resolves publicly: ```bash theme={null} dig +short CNAME _acme-challenge.app.internal.example.com # expected: _acme-challenge.acme.example-internal.com. ``` Certificates are issued automatically when you attach a hostname to a service through its [load balancer config](/applications/configuration-as-code/services/web-service#loadbalancerconfig). Porter then publishes each validation record into the delegated zone through the `CNAME`. If you need help setting this up, reach out to Porter support. # Cluster observability Source: https://docs.porter.run/cloud-accounts/cluster-observability Monitor pod status, node resource usage, and cluster-wide infrastructure metrics in real time from the Porter Infrastructure dashboard Porter provides built-in observability for your cluster infrastructure through the **Infrastructure** dashboard. Access it by clicking **Infrastructure** in the left sidebar. *** ## Pods The **Pods** tab provides a real-time view of all pods running in your cluster. * **Search**: Filter pods by name * **Filters**: Filter by status or namespace Each pod displays: | Column | Description | | - | - | | **Pod name** | The name of the pod | | **Namespace** | Kubernetes namespace (e.g., `kube-system`, `default`) | | **Status** | Current state (Running, Pending, Failed, etc.) | | **Ready** | Container readiness (e.g., `1/1`) | | **Restarts** | Number of container restarts | | **CPU** | CPU usage | | **Memory** | Memory usage | | **Memory %** | Percentage of memory limit used | | **Age** | Time since pod creation | *** ## Nodes The **Nodes** tab shows your cluster's node groups and individual nodes. ### Node Groups View The default view displays all node groups: | Column | Description | | - | - | | **Node group** | Name of the node group (e.g., default, monitoring, system) | | **Instance type** | The machine type for nodes in this group | | **Utilization** | Visual indicator of resource usage | | **Actions** | Link to view detailed metrics | ### Individual Nodes View Click on a node group to see individual nodes: * **Node name**: The cloud provider's node identifier * **Node group**: Which node group this node belongs to * **Instance type**: The machine type * **CPU**: CPU utilization shown as utilized (yellow) vs reserved (blue) * **Memory**: Memory utilization shown as utilized (yellow) vs reserved (blue) * **Status**: Node health status (Ready, NotReady) Click **Metrics >** on any node group to view historical instance counts over time. *** ## Integrating External Monitoring For application-level monitoring and alerting, integrate with external observability platforms: > **[Datadog](/addons/third-party-observability)** > > Full-stack monitoring with APM, logs, and infrastructure metrics > **[New Relic](/addons/third-party-observability)** > > Application performance monitoring and alerting > **[Grafana](/addons/third-party-observability)** > > Dashboards and visualization for metrics and logs See [Third party observability](/addons/third-party-observability) or reach out to support for more information. # Cluster upgrades Source: https://docs.porter.run/cloud-accounts/cluster-upgrades How Porter manages automated Kubernetes version upgrades for your cluster, including the shared responsibility model and prerequisites Keeping your Kubernetes clusters up-to-date is essential for ensuring security, stability, and access to the latest features built by the wider Kubernetes community as well as the underlying public cloud. To that end, we take care of managed Kubernetes upgrades for all clusters provisioned through our platform. Our automated upgrade process ensures your clusters remain current without disrupting your workloads, so you can focus on building and deploying your applications while we handle the complexities of cluster maintenance. ## Shared Responsibility Model We've endeavoured to build a world-class cluster management system which is able to manage and upgrade customer infrastructure without causing disruption to customer workloads. To that end, we've defined a shared responsibility model which maps out the roles played by Porter's engineering/SRE teams as well as customers to ensure the best possible experience with upgrades. ### Customers' responsibilities 1. Ensuring all production workloads are running with [a minimum of 3 replicas](/applications/configure/zero-downtime-deployments#high-availability-applications). 2. Adding [functional healthchecks](/applications/configure/zero-downtime-deployments#health-checks) to all production workloads. 3. Adding [support for graceful shutdowns](/applications/configure/zero-downtime-deployments#graceful-shutdown) to all production workloads. > [!WARNING] > > If your app doesn't have a minimum of three replicas / doesn't use healthchecks or graceful shutdowns, then an upgrade can cause significant disruption. During an upgrade, your cluster's nodes are refreshed and workloads are gradually rescheduled and the absence of these prerequisites will lead to a scenario where your cluster may not have ready replicas to serve production traffic. More documentation around zero-downtime deployments may be found [here](/applications/configure/zero-downtime-deployments). ### Porter's responsibilities 1. Testing each upgrade release extensively to ensure new versions and system components work together cohesively. 2. Executing upgrades in a blue-green deployment fashion, where older components and nodes are gradually replaced with newer versions without compromising customer workload uptime. 3. Maintaining a constant stream of communication around upgrade timelines and statuses. ## Upgrade Calendar Kubernetes follows a release cycle where there are - approximately - three minor version releases a year. Every release is followed by a period where public clouds integrate the new version into their managed Kubernetes offerings and run tests to ensure compatibility with the underlying cloud. Our upgrade calendar is thus dependent on both release cycles. To account for that, we carry out cluster upgrades twice a year, where we "leapfrog" over versions to ensure customer clusters are running the *latest stable* version of Kubernetes. These are typically carried out once towards the end of Q1/beginning of Q2 and then later towards the end of Q3. ## Upgrade Path When a new version of upstream Kubernetes is released, we closely track the corresponding release on public clouds in conjunction with the wider community as well as our public cloud partners (AWS, Google Cloud, Azure). 1. Whilst waiting for public clouds to launch the new version on their managed versions, we start conducting initial compatibility tests with upstream Kubernetes against our systems, to catch any potential issues. 2. Once public clouds have announced support for the new version, more extensive tests are conducted around the following themes: a. System components responsible for managing your cluster's functionality - Ingress, certificate management, autoscaling, telemetry and so on. All system components are upgraded to the latest stable versions from their upstream repos, and validated against both the current stable version and the new version of Kubernetes. b. App templates powering customer workloads for web, worker and job services. c. Additional addons - these include telemetry addons like Datadog/New Relic. 3. After our tests are successful, we announce a timeline for upgrades over our comms channels. At this point, while we typically announce a window during low-traffic hours when upgrades are conducted, customers have the option of scheduling a specific slot. 4. When a cluster is upgraded, we upgrade system components, all app templates, the managed cluster control plane as well as all nodegroups. While this operation is meant to be non-disruptive, there are certain prerequisites on the customers' end to ensure zero downtime (see the section below for more details). ## Node Patches Cluster nodes are typically a shared responsibility between Porter and the underlying cloud provider. Patches usually fall into two categories: 1. Regular machine image patches issued by the underlying cloud provider. These are typically applied automatically by the cloud provider every few weeks, although this can be manually overriden by running a simple update on your cluster; at that point if a fresh machine image is present in the underlying cloud provider's image stores, every node will be refreshed. 2. One-off patches pushed by Porter, in case of high / critical CVEs. In these cases, the underlying cloud provider releases a patched machine image, and Porter ensures it's deployed to every cluster. # Connecting a cloud account Source: https://docs.porter.run/cloud-accounts/connecting-a-cloud-account Grant Porter access to your AWS, GCP, or Azure account using IAM role assumption or Workload Identity Federation to provision infrastructure Before Porter can create a cluster, you need to grant it access to your cloud account. Porter uses secure credential methods that don't require storing static API keys. **AWS** Porter uses AWS IAM role assumption via the `AssumeRole` [operation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html) to access your account. You create a role in your AWS account and declare that you trust Porter to assume it. This eliminates static credentials and makes access easy to revoke. ## Create the IAM Role **1. Enter your AWS Account ID** After selecting AWS as your cloud provider, log into your [AWS Console](https://console.aws.amazon.com) and find your 12-digit Account ID in the top-right corner. Enter this ID in Porter and click **Grant Permissions**. **2. Create the CloudFormation stack** Porter opens the AWS CloudFormation console in a new tab to create a stack that provisions the `porter-manager` IAM role. > [!WARNING] > > If the popup is blocked, check your browser settings and allow popups from Porter. Scroll to the bottom of the CloudFormation page, check the **I acknowledge that AWS CloudFormation might create IAM resources** box, and click **Create Stack**. Wait for the stack creation to complete (this takes a few minutes). > [!NOTE] > > The IAM role must remain in your AWS account for Porter to manage your infrastructure. Deleting it will prevent Porter from making changes. ## Permissions Granted The CloudFormation stack creates an IAM role with permissions to: * Create and manage EKS clusters * Create and manage VPCs, subnets, and security groups * Create and manage ECR repositories * Create and manage IAM roles for cluster operations * Request service quota increases > [!NOTE] > > If you need Porter to operate with more restricted permissions, contact us through the support widget to inquire about Porter Enterprise. ## Revoking Access To revoke Porter's access: 1. First, delete any clusters through the Porter dashboard 2. Navigate to **CloudFormation Stacks** in your AWS console 3. Select the stack named `PorterRole` and click **Delete** This removes the IAM role and prevents Porter from accessing your account. **GCP** Porter connects to GCP using [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation). Workload Identity Federation offers a mechanism for Porter to connect with your GCP project without requiring any static keys. Setup runs in [Google Cloud Shell](https://cloud.google.com/shell) with a single command Porter generates for your project. ## Prerequisites Your GCP project needs an active [billing account](https://console.cloud.google.com/billing) attached. Porter cannot provision infrastructure without one. The Google account running the setup must have these project-level roles (or `roles/owner`, which covers all of them): * `roles/serviceusage.serviceUsageAdmin` * `roles/iam.serviceAccountAdmin` * `roles/resourcemanager.projectIamAdmin` * `roles/iam.workloadIdentityPoolAdmin` * `roles/storage.admin` ## Connect Your GCP Project **1. Enter your GCP Project ID** In Porter, select GCP. Enter your [GCP project ID](https://console.cloud.google.com) (visible at the top of any page in the GCP Console) and click **Connect**. Porter generates a one-time setup command scoped to this integration. **2. Run the setup in Cloud Shell** In the Porter dashboard: 1. Copy the command shown in the **Run Setup in Cloud Shell** panel. 2. Click **Open Cloud Shell** to launch a new tab with the [porter-dev/gcp-onboarding](https://github.com/porter-dev/gcp-onboarding) repository pre-loaded. 3. Cloud Shell will warn that the repository is from an untrusted source. Click **Trust** to continue. The repository is open source so you can review the Terraform before approving. 4. Paste the command into the shell and press Enter. The script runs Terraform in your project and performs the initial setup: * Enables five Google APIs: Cloud Resource Manager, IAM, IAM Credentials, STS, and Service Usage * Creates a `porter-manager-*` service account that Porter impersonates * Creates a `porter-pool-*` Workload Identity Pool with a trust policy scoped to your project * Grants three bootstrap IAM roles to the service account so Porter can finish the rest of the configuration itself: `roles/serviceusage.serviceUsageAdmin`, `roles/resourcemanager.projectIamAdmin`, and `roles/iam.serviceAccountAdmin` Setup takes about 30 seconds. **3. Wait for verification** Porter's dashboard polls the connection automatically. Once the bootstrap completes, Porter takes over the heavier configuration on your behalf. It enables the remaining APIs needed for cluster provisioning (Compute, Kubernetes Engine, Artifact Registry, Secret Manager, and others) and grants the matching per-service roles to the `porter-manager-*` service account (Compute Admin, Kubernetes Engine Admin, Artifact Registry Admin, Secret Manager Admin, and others). Once everything is provisioned, the cloud account is marked connected and the status banner turns green. The full Terraform module is open source if you want to inspect every resource Porter creates: [porter-dev/gcp-onboarding](https://github.com/porter-dev/gcp-onboarding). ## Porter-Managed Infrastructure Once your project is connected, Porter provisions and continuously reconciles infrastructure in it: the VPC, Cloud NAT and Cloud Router, the GKE cluster, its node pools, and the cluster load balancer. > [!WARNING] > > Do not modify Porter-managed resources directly in the GCP Console or with `gcloud`. Out-of-band changes are detected as drift on the next cluster update: Porter will revert them, which can itself disrupt traffic. Changes to settings that GKE cannot update in place may cause the **cluster to be replaced entirely**, resulting in downtime and the loss of any manually created in-cluster resources. Settings that can trigger reconciliation or cluster replacement when changed outside of Porter include: * **Cloud NAT and Cloud Router** — port allocation (min ports per VM, dynamic port allocation), endpoint-independent mapping, TCP timeouts, NAT IP allocation mode and static NAT IPs, source subnetwork ranges * **VPC** — the cluster VPC and subnet, primary and secondary CIDR ranges, Private Google Access and its associated routes and DNS * **GKE control plane** — cluster version, private cluster settings (private nodes, endpoint access), master authorized networks and CIDR, workload identity pool, logging and system components, cluster autoscaling, KMS settings * **GKE node pools** — machine type, disk, image type, autoscaling settings, node locations and versions, spot/preemptible flag, OAuth scopes, labels, taints, network tags, GPU settings, upgrade and management settings * **Load balancer** — the external static IP Settings like NAT port allocation and node pool configuration can be changed safely from the Porter dashboard. For any requirement not exposed there (for example, public node IPs to reduce egress cost), contact Porter support before changing it in GCP. ## Migrating from a Service Account JSON If you previously connected GCP using a service account JSON key, you can switch to Workload Identity Federation with no downtime: 1. In Porter, navigate to **Integrations** → **Cloud accounts** and select your GCP account. 2. Click **Migrate to Workload Identity Federation** in the banner at the top of the page. 3. Follow the same Cloud Shell flow above. Your existing clusters keep authenticating with the JSON key while the new federation is being verified. Once verified, Porter swaps the credential atomically. There is no service interruption. ### After Migration Workload Identity Federation is now the active connection between Porter and your GCP project, but your previous service account and its JSON key are still present in your project until you remove them. To finish the migration: 1. In the [GCP Console](https://console.cloud.google.com), open **IAM & Admin** → **Service Accounts**. 2. Find your legacy service account (the one whose key you previously uploaded to Porter — typically named `porter-manager`, separate from the new `porter-manager-*` account created during federation). 3. Delete its JSON key, or delete the entire service account. Porter no longer needs the legacy account once federation is active. Leaving it in place means an unrotated long-lived key continues to exist in your project. ## Revoking Access To disconnect Porter from your GCP project: 1. First, delete any clusters through the Porter dashboard. 2. In the [GCP Console](https://console.cloud.google.com), navigate to **IAM & Admin** → **Workload Identity Federation** and delete the `porter-pool-*` Workload Identity Pool. This immediately invalidates all federated tokens. Optionally, also delete the `porter-manager-*` service account under **IAM & Admin** → **Service Accounts** for full cleanup. **Azure** Porter connects to Azure using [workload identity federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation). You create a [managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) that trusts a Porter-managed OIDC issuer. Porter generates short-lived JWT tokens using this OIDC isser and exchanges them with Azure for short-lived Azure tokens. ## Prerequisites * You must have permissions to create app registrations, role definitions, and role assignments in your Azure subscription * You must have the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) and [`jq`](https://jqlang.github.io/jq/) installed ## Connect Your Azure Subscription **1. Begin the Azure connection process in Porter** In the Porter dashboard, click **Connect a new account** and select **Azure**. Porter generates a one-time setup command with your OIDC subject and issuer already substituted. Keep this open — you'll paste it into your terminal in the next step. **2. Run the setup commands** Copy the commands shown in the dashboard and run it in your terminal. The commands look like: ```bash theme={null} # Authenticate the Azure CLI az login # Download the setup script curl -O https://install.porter.run/scripts/setup-azure-porter-wif.sh # Make it executable chmod +x setup-azure-porter-wif.sh # Run the script with the OIDC subject and issuer from Porter ./setup-azure-porter-wif.sh \ --subject \ --issuer ``` The script performs the initial setup in your Azure account: * Enables the required Azure resource providers: * Microsoft.Capacity * Microsoft.Compute * Microsoft.ContainerRegistry * Microsoft.ContainerService * Microsoft.ManagedIdentity * Microsoft.Network * Microsoft.OperationalInsights * Microsoft.OperationsManagement * Microsoft.ResourceGraph * Microsoft.Resources * Microsoft.Storage * Creates the custom `porter-aks-restricted` role and grants all subscription actions except: * `Microsoft.Authorization/elevateAccess/Action` * `Microsoft.Blueprint/blueprintAssignments/write` * `Microsoft.Blueprint/blueprintAssignments/delete` * `Microsoft.Compute/galleries/share/action` * Creates the `azure-porter-federated-sp` app registration and matching service principal, then assigns the custom role at the subscription scope * Makes the service principal an owner of its own app registration * Adds the `Application.ReadWrite.OwnedBy` Microsoft Graph application permission and grants admin consent for it. This is the only Graph permission Porter uses, and Porter uses it solely to manage the federated identity credentials on the app registration the service principal owns. Its write access is limited to applications the service principal creates or owns, so Porter cannot modify any other application in your tenant * Adds a federated identity credential on the app registration trusting Porter's OIDC issuer (audience `api://AzureADTokenExchange`) * Waits for Azure's [eventually consistent IAM service](https://devblogs.microsoft.com/identity/designing-for-eventual-consistency-for-microsoft-entra/) to propogate changes. * Outputs the app registration's metadata: **Subscription ID**, **Application (Client) ID**, and **Tenant ID** Setup can take a few minutes. > [!NOTE] > > If the script fails to grant admin consent automatically, grant it manually in the Azure Portal: **App registrations** > **azure-porter-federated-sp** > **API permissions** > **Grant admin consent for Default Directory**. **3. Enter app metadata into Porter** Paste the outputted **Subscription ID**, **Application (Client) ID**, and **Tenant ID** back into the Porter dashboard to register the connection. ## Revoking Access To revoke Porter's access: 1. In the Azure portal, search for **App registrations** and delete **azure-porter-federated-sp** 2. \[Optional] Delete the custom role definition created by Porter This ensures that Porter can no longer access your Azure subscription. # Creating a cluster Source: https://docs.porter.run/cloud-accounts/creating-a-cluster Provision a managed Kubernetes cluster in your AWS, GCP, or Azure cloud account by selecting a region, machine type, and initial instance count After [connecting your cloud account](/cloud-accounts/connecting-a-cloud-account), you can create a cluster. Porter handles all the complexity of cluster provisioning, including networking, load balancers, and node groups. ## Provisioning Your Cluster **AWS** **1. Review costs** Porter displays estimated monthly costs for your infrastructure (\~\$225/month for AWS). These estimates are for the default cluster configuration. Actual costs vary based on usage, region, and customizations. Review the cost breakdown and click **Accept** to continue. **2. Configure your cluster** Porter pre-configures your cluster with sensible defaults: * **Cluster name**: Auto-generated based on your project * **Region**: Defaults to `us-east-1` * **Node groups**: EKS clusters are initialially provisioning with cost-optimal node groups. You can edit your node group configuration after provisioning is complete > [!NOTE] > > For guidance on choosing a region: if you have an external database, choose that region or a region as close as possible. Otherwise, choose a region near your primary customer base. You can customize these settings or accept the defaults. **3. Handle quota limits (if needed)** If AWS is limiting your account's resource quota, Porter displays a warning and offers to auto-request quota increases on your behalf. **Allow Porter to auto-request AWS quota** is enabled by default. This is the recommended approach. Alternatively, you can manually request quota increases through the [AWS Service Quotas console](https://console.aws.amazon.com/servicequotas/). If you go the manual route, you won't be able to provision until the quota increase requests are approved. **4. Provision** Click **Provision** to start creating your infrastructure. > [!WARNING] > > Provisioning takes approximately 30-45 minutes. You can close the browser and return later — Porter continues working in the background. ### Troubleshooting > **Provisioning takes longer than 45 minutes** > > If your cluster has been provisioning for more than 45 minutes, there may be an issue: > > * Verify that the IAM role still exists in your AWS account > * Check your AWS Service Quotas to ensure they were approved > * Verify that the selected region supports the requested instance types > > If issues persist, contact us through the dashboard chat bot with your project ID. > **Permission errors during provisioning** > > If you encounter permission errors: > > * Verify the CloudFormation stack created successfully and the role exists > * Ensure the role has not been modified after creation > * Check that your AWS account has not applied SCPs (Service Control Policies) that restrict Porter's actions > **Quota increase requests not approved** > > AWS typically approves quota increases automatically, but some may require manual review: > > * Check the status of quota requests in the [AWS Service Quotas console](https://console.aws.amazon.com/servicequotas/) > * Requests under manual review typically take 24-48 hours > * If urgent, contact AWS support to expedite the review > **CloudFormation stack creation fails** > > If the CloudFormation stack fails to create: > > * Ensure you have sufficient permissions in your AWS account to create IAM roles > * Check that you're logged into the correct AWS account (the account ID should match) > * Verify your account is in good standing and billing is enabled **GCP** **1. Review costs** Porter displays estimated monthly costs for your infrastructure (\~\$253/month for GCP). These estimates are for the default cluster configuration. Actual costs vary based on usage, region, and customizations. Review the cost breakdown and click **Accept** to continue. **2. Pass provision checks** Before provisioning, Porter verifies that your cloud account credentials are connected and that billing is enabled on your GCP project. If any checks fail, troubleshooting steps are shown on the dashboard. **3. Configure your cluster** Porter pre-configures your cluster with sensible defaults: * **Cluster name**: Auto-generated based on your project * **Region**: Defaults to `us-east1` * **Node groups**: Pre-configured with appropriate instance types > [!NOTE] > > For guidance on choosing a region: if you have an external database, choose a region close to it. Otherwise, choose a region near your primary customer base. You can customize these settings or accept the defaults. **4. Provision** Click **Provision** to start creating your infrastructure. > [!WARNING] > > Provisioning takes approximately 30-45 minutes. You can close the browser and return later—Porter continues working in the background. ### Troubleshooting > **Provisioning takes longer than 45 minutes** > > If your cluster has been provisioning for more than 45 minutes: > > * Verify the service account still exists and has the **Project IAM Admin** role > * Verify the **Cloud Resource Manager API** is enabled > * Verify the GCP project has billing enabled > > If issues persist, contact us through the dashboard chat bot with your project ID. > **API not enabled errors** > > Porter automatically enables required APIs during setup. If you still see API errors: > > 1. Verify the `porter-manager-*` service account still has its bootstrap roles (`roles/serviceusage.serviceUsageAdmin`, `roles/resourcemanager.projectIamAdmin`, `roles/iam.serviceAccountAdmin`). Porter needs these to enable additional APIs and grant itself the heavier roles. > > 2. Navigate to **APIs & Services** in the GCP Console and verify the five bootstrap APIs are enabled: **Cloud Resource Manager**, **IAM**, **IAM Credentials**, **STS**, and **Service Usage**. > > 3. Return to Porter and retry provisioning. > > > [!NOTE] > > > > If any of the bootstrap APIs were disabled after onboarding, re-enable them in the GCP Console. Porter cannot enable Service Usage programmatically if it has been turned off. > **Permission denied errors** > > Porter automatically provisions all required IAM bindings from the bootstrap roles. If you encounter permission errors: > > * Verify the `porter-manager-*` service account still has its bootstrap roles (`serviceusage.serviceUsageAdmin`, `resourcemanager.projectIamAdmin`, `iam.serviceAccountAdmin`). > * Verify the `porter-pool-*` Workload Identity Pool exists and its provider is still bound to the service account. > * Ensure neither the service account nor the pool has been deleted since onboarding. Porter cannot authenticate without them. > * Ensure a billing account is attached to the project. **Azure** **1. Review costs** Porter displays estimated monthly costs for your infrastructure (\~\$165/month for Azure). These estimates are for the default cluster configuration. Actual costs vary based on usage, region, and customizations. Review the cost breakdown and click **Accept** to continue. **2. Request compute quotas** By default, Azure limits the types of resources you can provision. Before provisioning, you may need to request quota increases. In the Azure portal: 1. Navigate to your subscription 2. Select **Usage + quotas** 3. Set the resource filter to **Compute** and region to your desired region Request increases for: | Resource Family | Recommended Quota | | - | - | | Total Regional vCPUs | 40 | | Standard Basv2 Family vCPUs | 40 | Click **Request quota increase** for each resource. Requests are typically approved automatically within a few minutes. If not, fill out the support ticket as prompted. **3. Configure your cluster** Porter pre-configures your cluster with sensible defaults: * **Cluster name**: Auto-generated based on your project * **Region**: Defaults to `eastus` * **Azure tier**: Free tier for non-production, Standard tier for production * **Node groups**: Pre-configured with appropriate instance types > [!NOTE] > > For guidance on choosing a region: if you have an external database, choose a region close to it. Otherwise, choose a region near your primary customer base. You can customize these settings or accept the defaults. The Azure tier can be changed after cluster creation. **4. Provision** Click **Provision** to start creating your infrastructure. > [!WARNING] > > Provisioning takes approximately 30-45 minutes. You can close the browser and return later—Porter continues working in the background. ### Troubleshooting > **Provisioning takes longer than 45 minutes** > > If your cluster has been provisioning for more than 45 minutes: > > * Verify the service principal still exists and has the required permissions > * Check that all resource providers are registered > * Verify your compute quota requests were approved > > If issues persist, contact us through the dashboard chat bot with your project ID. > **Quota errors** > > If you see errors about insufficient quota: > > 1. Navigate to **Usage + quotas** in your Azure subscription > 2. Check the status of your quota requests > 3. If pending, wait for approval or contact Azure support > 4. Once approved, retry provisioning in Porter > **Resource provider not registered** > > If you see errors about resource providers: > > 1. Navigate to your subscription's **Resource providers** page > 2. Find the mentioned provider and click **Register** > 3. Wait for registration to complete > 4. Retry provisioning in Porter > **Permission errors** > > If you encounter permission errors: > > * Verify the service principal has the `porter-aks-restricted` role > * Check that admin consent was granted for Microsoft Graph permissions > * Ensure the client secret hasn't expired *** ## After Provisioning Once your cluster is ready, you'll see the Porter dashboard. From here you can: > **[Deploy your first app](/applications/deploy/overview)** > > Deploy an application from GitHub or a container registry. > **[Configure node groups](/cloud-accounts/node-groups)** > > Customize instance types and scaling settings. > **[Cluster observability](/cloud-accounts/cluster-observability)** > > Monitor cluster health and resource usage. # Deleting a cluster Source: https://docs.porter.run/cloud-accounts/deleting-a-cluster Remove a Porter-provisioned Kubernetes cluster and automatically clean up associated cloud resources like load balancers and node groups > [!WARNING] > > Deleting a cluster removes all applications and data running on it. This action cannot be undone. ## Before You Delete Before deleting your cluster: 1. **Back up any data** stored in your applications or persistent volumes 2. **Export environment variables** and secrets if you need them for future deployments 3. **Note your configuration** if you plan to recreate the cluster later *** ## Delete the Cluster **AWS** > [!WARNING] > > Porter needs the IAM role to delete resources. Delete the cluster first, then delete the IAM role. If you've already deleted the IAM role, you'll need to [delete resources directly from the AWS console](/other/deleting-dangling-resources). **1. Delete the cluster in Porter** 1. Navigate to the **Infrastructure** tab in the Porter dashboard 2. Click **Delete Cluster** 3. Confirm the deletion This process may take up to 30 minutes. **2. Delete the IAM role** After the cluster is deleted: 1. Navigate to **CloudFormation Stacks** in your AWS console 2. Select the stack named `PorterRole` 3. Click **Delete** This revokes Porter's access to your AWS account. **3. Verify cleanup** Check your AWS console to verify all resources have been removed: * **EC2**: No instances, load balancers, or security groups related to the cluster * **EKS**: No clusters remaining * **VPC**: No VPCs created by Porter * **ECR**: Container images may remain (delete manually if not needed) > [!WARNING] > > Deleting resources via Porter may result in dangling resources. See [Deleting Dangling Resources](/other/deleting-dangling-resources) for cleanup guidance. **GCP** **1. Delete the cluster in Porter** 1. Navigate to the **Infrastructure** tab in the Porter dashboard 2. Click **Additional Settings** 3. Click **Delete Cluster** 4. Confirm the deletion This process may take up to 30 minutes. **2. Verify cleanup** Check your GCP console to verify all resources have been removed: * **Compute Engine**: No instances or load balancers related to the cluster * **Kubernetes Engine**: No clusters remaining * **VPC networks**: No networks created by Porter * **Artifact Registry**: Container images may remain (delete manually if not needed) **3. Revoke Porter access (optional)** If you no longer need Porter to access your GCP project: 1. Navigate to **IAM & Admin** → **Service Accounts** 2. Find and delete the Porter service account > [!WARNING] > > Deleting resources via Porter may result in dangling resources. You can remove dangling resources via the GCP console or the gcloud CLI. **Azure** **1. Delete the cluster in Porter** 1. Navigate to the **Infrastructure** tab in the Porter dashboard 2. Click **Additional Settings** 3. Click **Delete Cluster** 4. Confirm the deletion This process may take up to 30 minutes. **2. Verify cleanup** After deletion, verify in the Azure portal: 1. Search for **Resource groups** 2. A resource group named `-` may remain, containing your build images 3. Porter does not delete build images by default—delete this resource group manually if needed 4. Delete any other remaining resource groups created by Porter **3. Revoke Porter access (optional)** If you no longer need Porter to access your Azure subscription: 1. Search for **App registrations** in the Azure portal 2. Find and delete the Porter service principal 3. Optionally, delete the custom `porter-aks-restricted` role definition > [!WARNING] > > Deleting resources via Porter may result in dangling resources. Check your Azure portal to verify all resources have been removed. *** ## Troubleshooting Deletion > **Deletion is stuck or taking too long** > > If deletion takes more than 45 minutes: > > 1. Check your cloud provider's console for any resources in a "deleting" state > 2. Look for dependencies that may be blocking deletion (e.g., load balancers with active connections) > 3. Contact us through the dashboard chat bot with your project ID > **Resources remain after deletion** > > If resources remain in your cloud account after Porter reports deletion complete: > > 1. Follow the [Deleting Dangling Resources](/other/deleting-dangling-resources) guide > 2. Check for resources in different regions than expected > 3. Look for resources with names containing your project ID > **Cannot delete because credentials are missing or expired** > > If Porter can't delete because it can no longer authenticate to your cloud: > > **AWS**: Re-create the CloudFormation stack to restore the IAM role, then delete the cluster. > > **GCP**: Workload Identity Federation tokens are minted on demand and do not expire. If Porter can't authenticate, either the `porter-pool-*` Workload Identity Pool or the `porter-manager-*` service account was deleted. Re-run the connect flow in **Integrations** → **GCP** to recreate them, then delete the cluster. > > **Azure**: Generate a new client secret and update it in **Integrations** → **Azure**, then delete the cluster. > > If you can't restore credentials, you'll need to [delete resources manually](/other/deleting-dangling-resources). # Node groups Source: https://docs.porter.run/cloud-accounts/node-groups Configure node groups with custom instance types, enable cost optimization with smart instance selection, and manage compute capacity Node groups are collections of compute instances that run your workloads. Porter provides flexible options for managing compute resources, including custom node groups for specialized workloads and cost optimization features. ## Default Node Groups Porter provisions three node groups by default: | Node Group | Purpose | Autoscaling | | - | - | - | | **System** | Kubernetes system workloads | Fixed | | **Monitoring** | Observability stack (metrics, logs) | Fixed | | **Application** | Your application workloads | Enabled (1 node minimum) | The application node group autoscales based on demand. You can customize instance types, node counts, and scaling limits for all node groups. *** ## Changing Instance Types You can modify the instance type for any node group from the Infrastructure settings. **1. Navigate to Infrastructure** From your Porter dashboard, click on the **Infrastructure** tab in the left sidebar. **2. Select your cluster** Click on **Overview** to view your cluster configuration and node groups. **3. Configure the node group** Expand the node group dropdown (e.g., **Default node group**) to see the following settings: | Setting | Description | | - | - | | **Machine type** | The underlying instance for nodes. Options vary by cloud provider. | | **Maximum nodes** | Upper limit for autoscaling under high load | | **Minimum nodes** | Lower limit for autoscaling under low load | | **Disk size** | Storage capacity for each node (default: 50GB) | **4. Update** Select the appropriate **Machine type** from the dropdown, then scroll to the bottom and click **Update**. ### Instance Type Pricing > **[AWS Instance Types](https://aws.amazon.com/ec2/pricing/on-demand)** > **[Azure Instance Types](https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/#pricing)** > **[GCP Instance Types](https://cloud.google.com/compute/all-pricing#general_purpose)** ### Why is my instance type not available? Instance types may not be available if: * The instance is not available in your cluster's region (cloud providers release new instances on a phased, region-by-region basis) * The instance type hasn't been validated by Porter for compatibility If you need a specific instance type that isn't listed, contact us through the dashboard chat bot. *** ## Creating a Custom Node Group Create custom node groups for specialized workloads like GPU processing, high-memory applications, or isolated environments. **1. Navigate to Infrastructure** From your Porter dashboard, click on the **Infrastructure** tab in the left sidebar. **2. Select your cluster** Click on **Cluster** to view your cluster configuration and node groups. **3. Add a node group** Click **Add an additional node group** to open the configuration panel. **4. Configure the node group** Choose between two configuration approaches: **Cost Optimization** Cost optimization automatically selects the most cost-effective instance types for your workloads. Any cluster provisioned in AWS will default to cost optimized node groups. Set your **maximum CPU cores limit** to prevent unexpected scaling. This caps infrastructure costs while allowing flexibility in instance selection. ### Restricting per-instance size By default, Karpenter can satisfy a cost-optimized node group with instances of any size, including a single very large instance. Enable **Restrict per-instance CPU** on the node group form to bound the vCPUs of each instance Karpenter provisions. Use this when you want to spread workloads across more, smaller nodes to shrink the blast radius of a single Spot interruption or node failure. Setting the minimum equal to the maximum pins the node group to instances of that exact vCPU size while still allowing Karpenter to diversify across instance families. | Field | Description | | - | - | | **Min vCPUs per instance** | Smallest instance vCPU count Karpenter may pick. Leave empty for no minimum. | | **Max vCPUs per instance** | Largest instance vCPU count Karpenter may pick. Leave empty for no maximum. | Min must be less than or equal to Max, and Min must not exceed the node group's **Max CPU** limit. ### Limitations The following configurations should use fixed instance types instead: * GPU instances (e.g., instances with NVIDIA GPUs) * Spot instances * Instances in public subnets * Instances with specialized hardware requirements **Fixed Instance Type** Fixed node groups use a specific instance type. Applications scheduled on this group run only on the exact instance type you specify. This gives you precise control but may result in over-provisioning if configured incorrectly. Configure: | Setting | Description | | - | - | | **Instance type** | The machine type for nodes in this group | | **Minimum nodes** | Minimum number of nodes (set to 0 for scale-to-zero) | | **Maximum nodes** | Upper limit for autoscaling | > [!NOTE] > > For GPU workloads, select instances with GPU support: > > * **AWS**: `g4dn.xlarge`, `p3.2xlarge` > * **Azure**: `Standard_NC4as_T4_v3` > * **GCP**: `g2-standard-4` > [!NOTE] > > **Health Checks Required**: For production applications on cost-optimized node groups, configure proper [health checks](/applications/configure/zero-downtime-deployments#health-checks). This ensures applications can be safely rescheduled as nodes are reshuffled. **5. Save and apply** Click **Save** to create the node group. Porter provisions the new nodes, which may take a few minutes. *** ## Public node groups By default, nodes in a Porter-managed cluster are private: they sit in a private subnet (AWS) or have private IPs only (GCP) and reach the internet through a NAT gateway. You can opt a custom node group into public networking when workloads need to receive inbound traffic directly or when you want to avoid NAT egress costs. > [!WARNING] > > Public nodes are reachable from the internet. Only enable this for workloads that need it, and rely on security groups, network policies, and firewall rules to limit exposure. To enable public networking on a node group: 1. From the node group form, locate the **Use public subnet** (AWS) or **Use public IP addresses** (GCP) toggle. 2. Turn the toggle on and click **Update** or **Save**. | Cloud | Toggle | Effect | | - | - | - | | AWS | Use public subnet | Nodes launch in a public subnet and receive public IPs. | | GCP | Use public IP addresses | Nodes receive public IPs and skip Cloud NAT for egress. | On GCP, this setting is immutable for the lifetime of a node pool. Flipping it triggers a replacement pool; existing nodes are cordoned and drained before the old pool is removed, so running workloads stay available during the switch. *** ## Assigning Workloads to Node Groups Once your custom node group is created, assign applications to run on it: 1. Navigate to your application in the Porter dashboard 2. Go to the **Services** tab 3. Click the service you want to assign 4. Under **General**, find the **Node group** selector 5. Select your custom node group from the dropdown 6. Save and redeploy your application *** ## Deleting a Node Group > [!WARNING] > > Ensure no workloads are scheduled on the node group before deleting. Workloads will be disrupted if their node group is removed. To remove a custom node group: 1. Migrate any workloads running on the node group to another node group 2. Navigate to **Infrastructure** → **Cluster** 3. Find the node group you want to delete 4. Click the delete icon and confirm # Cloud accounts Source: https://docs.porter.run/cloud-accounts/overview Connect your AWS, GCP, or Azure account to Porter and provision managed Kubernetes clusters, node groups, and networking infrastructure Porter provisions and manages infrastructure directly in your own cloud account. This gives you full control over your data and resources while Porter handles the complexity of Kubernetes cluster management. ## Supported Cloud Providers > **[AWS](/cloud-accounts/connecting-a-cloud-account#create-the-iam-role)** > > Amazon Web Services > **[Google Cloud](/cloud-accounts/connecting-a-cloud-account#connect-your-gcp-project)** > > Google Cloud Platform > **[Azure](/cloud-accounts/connecting-a-cloud-account#enable-resource-providers)** > > Microsoft Azure ## Getting Started Setting up Porter with your cloud account involves three steps: **1. Connect your cloud account** Provide Porter with credentials to access your cloud provider. Porter uses secure methods like IAM role assumption (AWS), service principals (Azure), or Workload Identity Federation (GCP) to manage resources without storing static credentials. [Connect a cloud account →](/cloud-accounts/connecting-a-cloud-account) **2. Create a cluster** Porter provisions a Kubernetes cluster with sensible defaults including networking, load balancers, and node groups. Provisioning takes approximately 30-45 minutes. [Create a cluster →](/cloud-accounts/creating-a-cluster) **3. Deploy your application** Once your cluster is ready, deploy applications from GitHub or a container registry. [Deploy your first app →](/getting-started/quickstart#step-4-create-your-first-application) *** ## What Porter Provisions Infrastructure provisioned by Porter includes: | Component | AWS | Azure | GCP | | - | - | - | - | | **Virtual Network** | VPC | VNet | VPC | | **Load Balancer** | Network Load Balancer | Azure Load Balancer | Cloud Load Balancer | | **Kubernetes Cluster** | EKS | AKS | GKE | | **Container Registry** | ECR | ACR | Artifact Registry | ### Default Node Groups Porter provisions three node groups by default: | Node Group | Purpose | AWS | Azure | GCP | | - | - | - | - | - | | **System** | Kubernetes system workloads | 2× t3.medium | 2× Standard\_B2s | 2× e2-medium | | **Monitoring** | Observability stack | 1× t3.large | 1× Standard\_B2ms | 1× e2-standard-2 | | **Application** | Your workloads | 1× t3.medium (autoscales) | 1× Standard\_B2s (autoscales) | 1× e2-medium (autoscales) | > [!NOTE] > > On AWS, the application node group uses [cost optimization](/cloud-accounts/node-groups#creating-a-custom-node-group) by default, which automatically selects the most cost-effective instance types for your workloads. The application node group autoscales based on workload demand. All nodes include 50GB of disk storage by default. > [!NOTE] > > You can customize machine types, node counts, and disk sizes after initial provisioning through the [Node Groups](/cloud-accounts/node-groups) settings. *** ## FAQ ### How much does the underlying infrastructure cost? The cost varies based on resource usage. By default, clusters provisioned by Porter cost approximately: | Provider | Estimated Monthly Cost | | - | - | | AWS | \~\$201/month | | GCP | \~\$253/month | | Azure | \~\$165/month | These estimates are for the default cluster configuration. Actual costs vary based on usage, region, and customizations. **All infrastructure costs can be covered with cloud credits from AWS, Google Cloud, or Azure.** For a full per-component breakdown, see [Infrastructure pricing](/cloud-accounts/pricing). ### Can I use my existing cloud credits? Yes. Porter provisions infrastructure directly in your cloud account, so any credits you have with AWS, GCP, or Azure apply to the resources Porter creates. ### What permissions does Porter need? Porter requires permissions to create and manage Kubernetes clusters, networking resources, and container registries. The setup process varies by provider: * **AWS**: Automatic setup — Porter guides you through creating a CloudFormation stack that provisions the required IAM role with one click * **Azure**: Manual setup required — you'll need to create a service principal using our setup script or the Azure CLI * **GCP**: One-line Cloud Shell setup via Workload Identity Federation. No service account keys to download or rotate. For detailed permission requirements and setup instructions, see [Connecting a Cloud Account](/cloud-accounts/connecting-a-cloud-account). ### Can I revoke Porter's access? Yes. You can revoke Porter's access at any time by deleting the IAM role (AWS), service principal (Azure), or Workload Identity Pool (GCP). Note that Porter will no longer be able to manage or delete resources after access is revoked. # Concepts Source: https://docs.porter.run/getting-started/concepts Understand Porter's core architecture including cloud accounts, clusters, applications, services, and how they fit together for deployments Check out our [deployment documentation](/applications/deploy/overview), [infrastructure provisioning guides](/cloud-accounts/overview), and more. ## Cloud Account Your cloud provider account where Porter provisions and manages infrastructure. Porter connects to your AWS, GCP, or Azure account using secure access methods, maintaining full ownership and control in your own cloud. All infrastructure costs can be covered with your cloud credits. ![Cloud account connection](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/cloud-account.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=d0c2690a918f972989bf498b8aa4de0f) ## Cluster A managed Kubernetes cluster that Porter provisions in your cloud account to run your applications. Porter abstracts away the complexity of Kubernetes while intelligently allocating your applications across available nodes based on resource requirements. ![Cluster configuration](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/cluster.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=d8059f445e5948edbe9e28ff17b98525) ## Node Group Groups of compute instances that make up your cluster. Porter provisions three default node groups: System (for Kubernetes workloads), Monitoring (for observability), and Application (for your workloads, with autoscaling). ![Node groups configuration](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/node-groups.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=053ddc5d587055e1041332938a37ae51) ## Application A group of services that share the same build and environment variables. When you deploy from a Git repository, Porter builds your code once and can run multiple services from that same build. Porter also supports deployment from both public and private Docker registries. ![Configure applications](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/apps.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=71ddc080a3832a12b91bc061c13a7aa1) ## Service Individual processes that make up your application. Each service can have different start commands, resource allocations, and configurations. Porter supports three types: **Web** (HTTP traffic), **Worker** (background processes), and **Job** (scheduled or on-demand tasks). ![Service types](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/services.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=47f224c4fc602a063ade87deb55462bf) # Introduction Source: https://docs.porter.run/getting-started/introduction Porter is a platform as a service (PaaS) that runs in your own AWS, GCP, or Azure account, so you keep full control of your cloud. ## What is Porter? [Porter | Introduction](https://player.vimeo.com/video/1158441358?badge=0\&autopause=0\&player_id=0\&app_id=58479) Porter brings the magical experience of deploying an application with just a few clicks into a cloud provider of your choice. Porter will provision and manage the underlying infrastructure for you - simply point at your repository and Porter will handle the rest, from building your application to autoscaling it. > **[MCP server](/mcp/overview)** > > Connect an AI agent to deploy, inspect, and manage your applications. > **[Quickstart](/getting-started/quickstart)** > > Deploy your first application to Porter in minutes. > **[Concepts](/getting-started/concepts)** > > Learn the key concepts behind Porter's architecture. > **[CLI](/cli/installation)** > > Install the Porter CLI for local workflows and headless environments. # Quickstart Source: https://docs.porter.run/getting-started/quickstart Deploy your first application on Porter from sign-up to a live public URL, covering cloud account setup, cluster provisioning, and app creation This guide walks you through deploying your first application on Porter, from creating an account to seeing your app live at a public URL. > [!TIP] > > Porter hosts an MCP server at `https://mcp.porter.run`. Agents can use it to deploy and manage your applications. To connect your client, see the [MCP server overview](/mcp/overview). ## Prerequisites Before you begin, make sure you have: > **Cloud provider account** > > An active account with **AWS**, **GCP**, or **Azure** with admin or owner permissions. Porter provisions infrastructure in your cloud account, so you'll need permissions to create resources like Kubernetes clusters, networking components, and IAM roles. > **A deployable application** > > Your application should: > > * Have a start command or Dockerfile > * Be pushed to a GitHub repository (or have a container image in a registry) *** ## Step 1: Sign up and create a project 1. Navigate to [dashboard.porter.run](https://dashboard.porter.run) and create an account 2. Click **Create Project** 3. Enter a project name ![Create project screen](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/step-1.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=7dad48e19d577f8f93e05e65aaca149a) > [!NOTE] > > Project names must be 1-25 characters, using only lowercase letters, numbers, and hyphens. *** ## Step 2: Select your cloud provider Choose the cloud provider where you want Porter to provision your infrastructure. ![Cloud provider selection](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/step-2.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=5283ed0d828fda92ffe751b966e6b540) If you've previously connected a cloud account to Porter, you can reuse those credentials. ![Existing cloud accounts](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/step-2-existing.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=fcb323d5fa8ba03e0990ae73b7ff4f25) *** ## Step 3: Set up your cluster Porter provisions a Kubernetes cluster in your cloud account. The setup process varies by provider. **AWS** Porter uses AWS IAM role assumption to securely access your account without storing static credentials. **1. Enter your AWS Account ID** Log into your [AWS Console](https://console.aws.amazon.com) and find your 12-digit Account ID in the top-right corner. Enter this ID in Porter. **2. Create the CloudFormation stack** Click **Grant Permissions**. Porter opens the AWS CloudFormation console to create a stack that provisions the `porter-manager` IAM role. ![Create CloudFormation stack](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/step-3-aws-create-stack.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=254f30b268ae7b8590f403f874e5aee1) > [!WARNING] > > If the popup is blocked, check your browser settings and allow popups from Porter. Scroll to the bottom of the CloudFormation page, check the **I acknowledge that AWS CloudFormation might create IAM resources** box, and click **Create Stack**. > [!NOTE] > > The IAM role must remain in your AWS account for Porter to manage your infrastructure. Deleting it will prevent Porter from making changes. **3. Review costs** Porter displays estimated monthly costs for your infrastructure (\~\$225/month for AWS). ![AWS cost review](https://mintcdn.com/porter/zqQS4FM4YSdp2anY/images/getting-started/step-3-aws-review-costs.webp?fit=max\&auto=format\&n=zqQS4FM4YSdp2anY\&q=85\&s=0034b2f3a25c1925f3b3b2ee6295f151) These estimates are for the default cluster configuration. Actual costs vary based on usage, region, and any customizations you make. Review the cost breakdown and click **Accept** to continue. **4. Configure and provision** Porter pre-configures your cluster with sensible defaults: * **Cluster name**: Auto-generated based on your project * **Region**: Defaults to `us-east-1` * **Node groups**: Pre-configured with appropriate instance types ![AWS cluster configuration](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/step-3-aws-configure-and-provision.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=35f852e92362e6ae9e8b52aa707ebd2f) > **AWS resource quota limits** > > If AWS is limiting your account's resource quota, Porter will display a warning and offer to auto-request quota increases on your behalf. **Allow Porter to auto-request AWS quota** is enabled by default. > > ![AWS quota limits](https://mintcdn.com/porter/k87aZZFaLNfy9soJ/images/getting-started/step-3-aws-limiting-quota.webp?fit=max\&auto=format\&n=k87aZZFaLNfy9soJ\&q=85\&s=afca442903dd39ec7d8abdfdd1151c2e) > > Alternatively, you can manually request quota increases through the [AWS Service Quotas console](https://console.aws.amazon.com/servicequotas/). You can customize these settings or accept the defaults. Click **Provision** to start creating your infrastructure. > [!WARNING] > > Provisioning takes approximately 30-45 minutes. You can close the browser and return later. Porter continues working in the background. **GCP** Porter connects to GCP using [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation). Workload Identity Federation offers a mechanism for Porter to connect with your GCP project without requiring any static keys. **1. Enter your GCP Project ID** In Porter, select GCP. Enter your [GCP project ID](https://console.cloud.google.com) (visible at the top of any page in the GCP Console) and click **Connect**. **2. Run the setup in Cloud Shell** Porter generates a one-line setup command tied to this integration. Copy it, click **Open Cloud Shell**, and paste it into the shell that opens. The script runs Terraform inside your project to create a service account, a Workload Identity Pool with a trust policy scoped to your tenant, and the five Google APIs Porter needs to bootstrap federation (Cloud Resource Manager, IAM, IAM Credentials, STS, Service Usage). Setup takes about 30 seconds. **3. Wait for verification** The Porter dashboard polls automatically. Once the bootstrap completes, Porter finishes enabling the remaining APIs, grants the heavier IAM roles, and marks the cloud account connected. The status banner turns green and the Continue button enables. For the full mechanism (trust boundary, permissions granted, troubleshooting), see [Connecting a Cloud Account → GCP](/cloud-accounts/connecting-a-cloud-account). **4. Review costs** Porter displays estimated monthly costs for your infrastructure (\~\$253/month for GCP). ![GCP cost review](https://mintcdn.com/porter/zqQS4FM4YSdp2anY/images/getting-started/step-3-gcp-review-costs.webp?fit=max\&auto=format\&n=zqQS4FM4YSdp2anY\&q=85\&s=273640ee2f266d9152aa586c2ce42b82) These estimates are for the default cluster configuration. Actual costs vary based on usage, region, and any customizations you make. Review the cost breakdown and click **Accept** to continue. **5. Configure and provision** Porter pre-configures your cluster with sensible defaults: * **Cluster name**: Auto-generated based on your project * **Region**: Defaults to `us-east1` * **Node groups**: Pre-configured with appropriate instance types ![GCP cluster configuration](https://mintcdn.com/porter/zqQS4FM4YSdp2anY/images/getting-started/step-3-gcp-configure-and-provision.webp?fit=max\&auto=format\&n=zqQS4FM4YSdp2anY\&q=85\&s=423ddf8257fe2ddfabc9a6cb8aed76fe) You can customize these settings or accept the defaults. Click **Provision** to start creating your infrastructure. > [!WARNING] > > Provisioning takes approximately 30-45 minutes. You can close the browser and return later. Porter continues working in the background. **Azure** Porter connects to Azure using a service principal with permissions to manage your infrastructure. **1. Create a service principal** Before connecting Azure to Porter, you need to create a service principal. Follow the Azure tab of [Connecting a cloud account](/cloud-accounts/connecting-a-cloud-account): the Porter dashboard shows the exact setup commands to run, and the script outputs the values you need for the next step. **2. Enter your credentials** In Porter, enter the following values from your service principal: | Field | Description | | - | - | | **Subscription ID** | Your Azure subscription ID | | **Application (Client) ID** | The `appId` from your service principal | | **Client Secret** | The `password` from your service principal | | **Tenant ID** | The `tenant` from your service principal | ![Azure credentials entry](https://mintcdn.com/porter/zqQS4FM4YSdp2anY/images/getting-started/step-3-azure-permissions.webp?fit=max\&auto=format\&n=zqQS4FM4YSdp2anY\&q=85\&s=765990cc626eccb949fdf7fb16000e08) Porter verifies the credentials immediately when you submit. **3. Review costs** Porter displays estimated monthly costs for your infrastructure (\~\$165/month for Azure). ![Azure cost review](https://mintcdn.com/porter/zqQS4FM4YSdp2anY/images/getting-started/step-3-azure-review-costs.webp?fit=max\&auto=format\&n=zqQS4FM4YSdp2anY\&q=85\&s=b6ccab15509235e0d5a135f37899e158) These estimates are for the default cluster configuration. Actual costs vary based on usage, region, and any customizations you make. Review the cost breakdown and click **Accept** to continue. **4. Configure and provision** Porter pre-configures your cluster with sensible defaults: * **Cluster name**: Auto-generated based on your project * **Region**: Defaults to `eastus` * **Node groups**: Pre-configured with appropriate instance types ![Azure cluster configuration](https://mintcdn.com/porter/zqQS4FM4YSdp2anY/images/getting-started/step-3-azure-configure-and-provision.webp?fit=max\&auto=format\&n=zqQS4FM4YSdp2anY\&q=85\&s=3b86d29a120333252c5f24c98389be09) You can customize these settings or accept the defaults. Click **Provision** to start creating your infrastructure. > [!WARNING] > > Provisioning takes approximately 30-45 minutes. You can close the browser and return later. Porter continues working in the background. *** ## Step 4: Create your first application Once your cluster is ready, choose how you want to deploy your code. Porter runs a hosted MCP server at [https://mcp.porter.run](https://mcp.porter.run). If its tools are not available in this session, run `curl -fsSL https://agents.porter.run | sh` to install and register them, then restart the client. If the user asks for the dashboard, the CLI, `porter apply`, or configuration-as-code, follow that path on this page. Otherwise, if you are connected to the MCP server, do not author a `porter.yaml`. Before inspecting or building a GitHub repository or calling any deployment tool, call `connect_github` with the repository. Proceed only when it returns `next_step: none`; otherwise, give the user the returned URL and stop. Then call `create_app` with `source` and `build`; this creates the app on a placeholder image and opens a GitHub Actions pull request that the user merges to trigger the first real deploy. **Agent** Ask an agent connected to the Porter MCP server to create the application for you. It detects your build settings from the codebase and opens a pull request that you merge to deploy. **1. Check the prerequisites** * [Connect your agent to the Porter MCP server](/mcp/overview). `curl -fsSL https://agents.porter.run | sh` does this for you and installs Porter's skills. * The [Porter GitHub App](/applications/deploy/connect-github) needs access to the repository. If it already does, you do not need to do anything; otherwise, your agent gives you a link to complete any missing GitHub OAuth, App installation, or repository access. For an organization repository, an organization owner may have to approve the installation. **2. Ask your agent to deploy** ```text theme={null} Deploy this GitHub repository to Porter. Detect the build method and application settings from the codebase. ``` The agent resolves your Porter project and cluster, then checks the repository with `connect_github` before inspecting your code or calling `create_app`. Once the repository is reachable, it inspects your code and calls `create_app` with the repository source and build configuration. Porter creates the application with a placeholder image and opens a pull request containing a GitHub Actions workflow. **3. Merge the pull request** Review and merge the pull request that the agent returns. Merging it triggers the workflow that builds your code and runs the first real deployment. **Dashboard** In the Porter dashboard, click **Create Application**, then choose your source. **GitHub repository** Porter builds your application from source code and sets up automated deployments on every push. **1. Connect GitHub** If this is your first deployment, [install the Porter GitHub App](/applications/deploy/connect-github): ![Source selection screen](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/github-source-selection.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=8e3422acb973ae6ac09684a1c235c515) 1. Click **Connect repositories** 2. Authorize the Porter GitHub App 3. Choose which repositories Porter can access (all repositories or select specific ones) Porter needs read access to detect your code structure and write access to create the deployment workflow. **2. Select your repository** 1. Choose the repository containing your application 2. Select the branch to deploy (defaults to `main` or `master`) ![GitHub repository and branch selector](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/github-repo-branch-selector.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=be7df38a829e255c46f3bbbdc5a22be4) **3. Review detected applications** Porter scans your repository and automatically detects: * Frameworks and languages (Node.js, Python, Go, etc.) * Dockerfiles * Service types (web servers, workers, etc.) ![Detected applications list](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/detected-apps-list.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=59a2e5204cea9415f5e938f925bdd3f8) You'll see a card for each detected application showing the app name, detected framework, and repository path. > [!NOTE] > > For monorepos with multiple services, Porter detects each application separately. A Node.js API in `/api` and a React frontend in `/web` appear as distinct applications. If some services are already deployed on your cluster, Porter hides them by default so you can focus on new applications. Expand the **previously deployed** section to view or re-add them. **4. Configure your application (optional)** Click the **gear icon** on any application card to customize: ![Settings button on application card](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/app-card-settings-button.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=5107323040904d8b0883253be5543f2b) * **Build method**: Docker (using your Dockerfile) or Buildpacks (automatic) * **Start command**: The command to run your application * **Port**: The port your application listens on For most applications, Porter's detected defaults work without changes. **5. Deploy** Click **Deploy** to start your deployment. **6. Merge the GitHub Actions PR** > [!WARNING] > > This step is critical. Your application won't be available until you merge the PR. Porter creates a pull request in your repository containing a GitHub Actions workflow file at `.github/workflows/porter.yml`. This workflow handles building and deploying your application on every push. After clicking Deploy, you'll see: * A **toast notification** with a "View PR" button that links directly to the pull request * A **warning banner** on your app's page with a "Merge PR" link ![Merge PR banner](https://mintcdn.com/porter/zqQS4FM4YSdp2anY/images/getting-started/step-4-github-merge-pr-banner.webp?fit=max\&auto=format\&n=zqQS4FM4YSdp2anY\&q=85\&s=4bffbb23739d82aa2f299dec7fdef078) Click either link to open the pull request on GitHub, then merge it. After merging, click **Refresh** on the banner to update the status. GitHub Actions triggers automatically and your first deployment begins. > **What's in the workflow file?** > > The workflow Porter creates looks like this: > > ```yaml theme={null} > on: > push: > branches: > - main > name: Deploy to Porter > jobs: > porter-deploy: > runs-on: ubuntu-latest > steps: > - uses: actions/checkout@v4 > - uses: porter-dev/setup-porter@v0.2.0 > - run: porter apply > env: > PORTER_TOKEN: ${{ secrets.PORTER_APP_... }} > ``` **Docker registry** This approach works well when you have an existing CI/CD pipeline or are deploying third-party images. **1. Select container registry** From the Create Application screen, click **Or, deploy from a container registry** in the top right. ![Container registry link](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/github-deploy-registry-link.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=37df4e84d1bd2590e3852cf088e0e0df) **2. Specify your image** Enter your image details: * **Image repository**: The full path to your image * Docker Hub: `docker.io/username/image` or `username/image` * Amazon ECR: `123456789.dkr.ecr.us-east-1.amazonaws.com/my-app` * Google Artifact Registry: `us-docker.pkg.dev/project-id/repo/image` * Azure Container Registry: `myregistry.azurecr.io/my-app` * **Tag**: The version to deploy (e.g., `latest`, `v1.2.3`, or a commit SHA) ![Container registry inputs](https://mintcdn.com/porter/rTf4JWBYOzQjpLlM/images/deploy/v2/container-registry-inputs.webp?fit=max\&auto=format\&n=rTf4JWBYOzQjpLlM\&q=85\&s=28c5589b7595d388641c72cea952d344) **3. Configure your application** Porter creates an application configuration with sensible defaults. Since Porter can't inspect your pre-built image, you'll need to specify: * **Port**: The port your container listens on (for web services) * **Start command**: Override the image's default CMD if needed Click the **gear icon** to adjust resources (CPU, RAM, instances) if the defaults don't fit your needs. **4. Deploy** Click **Deploy** to pull your image and start running it. > [!NOTE] > > To update your application later, push a new image tag and use the Porter CLI: > > ```bash theme={null} > porter app update-tag APP_NAME --tag NEW_TAG > ``` **CLI** Deploy the application from a `porter.yaml` file: ```bash theme={null} porter apply -f porter.yaml ``` Learn how to define and deploy an application in the [configuration-as-code guide](/applications/configuration-as-code/overview). *** ## Step 5: View your running application Return to the Porter dashboard to monitor your deployment: 1. Click on your application to view deployment progress 2. Watch the build logs as Porter builds your container image 3. Monitor the deployment as Porter starts your application Once the deployment completes, your application status changes to **Running**. ### Access your application Porter automatically provisions a public URL for your application at `*.onporter.run`. Click the URL in your dashboard to visit your deployed application. ![Deployed application](https://mintcdn.com/porter/zqQS4FM4YSdp2anY/images/getting-started/step-5-deployed.webp?fit=max\&auto=format\&n=zqQS4FM4YSdp2anY\&q=85\&s=8c0155ef198a20c446c792f517858064) > [!TIP] > > Congratulations! Your application is now live and will automatically redeploy whenever you push to your selected branch. *** ## Next steps Now that your application is running, explore these guides to customize your deployment: > **[MCP server](/mcp/overview)** > > Connect an AI agent to deploy, inspect, and manage your applications. > **[Custom domains](/applications/configure/custom-domains)** > > Connect your own domain name to your application. > **[Environment groups](/applications/configure/environment-groups)** > > Configure secrets and environment variables for your app. > **[Autoscaling](/applications/configure/autoscaling)** > > Scale your application automatically based on traffic. > **[Types of services](/applications/deploy/types-of-services)** > > Add workers, jobs, and configure service settings. # Overview Source: https://docs.porter.run/index Porter is a platform as a service that runs in your own AWS, GCP, or Azure account. Start here to deploy and manage your first application. Point Porter at a repository and it provisions and manages the underlying infrastructure for you, from building your application to autoscaling it, with the experience of deploying in a few clicks. [Learn more about how Porter works](/getting-started/introduction). ## Start here > **[MCP server](/mcp/overview)** > > Connect an AI agent to deploy, inspect, and manage your applications. > **[Quickstart](/getting-started/quickstart)** > > Connect a cloud account, create a cluster, and deploy your first > application. > **[CLI](/cli/installation)** > > Install the Porter CLI for local workflows and headless environments. Use the [MCP server](/mcp/overview) for interactive agent workflows. Use the [CLI](/cli/basic-usage) or [`porter.yaml`](/applications/configuration-as-code/overview) for CI, headless environments, or an explicit command or version-controlled workflow. ## Trust and support Review Porter's [terms of service](/terms-and-privacy/terms-of-service) and [privacy policy](/terms-and-privacy/privacy-policy). For documentation or product support, reach out via the support widget on the dashboard. # Doppler Source: https://docs.porter.run/integrations/doppler Sync secrets and environment variables from Doppler into your Porter applications as a managed environment group with automatic updates on change. Porter integrates with [Doppler](https://www.doppler.com/) so you can manage secrets in Doppler and have them automatically synced into your Porter applications. Each Doppler config is exposed in Porter as a read-only environment group that can be attached to any application in the cluster. Under the hood, Porter installs the [External Secrets Operator](https://external-secrets.io/) on your cluster and uses your Doppler service token to pull secrets directly from the Doppler API. Secret values never persist in Porter's infrastructure. ## Enabling the Doppler integration The Doppler integration is enabled per cluster. 1. From the Porter dashboard, navigate to the **Integrations** tab. 2. Find **Doppler** in the list and toggle it on. Porter installs the External Secrets Operator on the selected cluster. Once enabled, you can begin adding Doppler environment groups. ## Adding a Doppler environment group Each environment group corresponds to a single Doppler config (e.g. `dev`, `staging`, `production`). ### Generate a Doppler service token In the Doppler dashboard, create a [service token](https://docs.doppler.com/docs/service-tokens) for the project and config you want to sync. Service tokens are read-only by default, which is the correct scope for Porter. The token will be prefixed with `dp.st.`. ### Create the environment group in Porter 1. On the Doppler integration page in Porter, click **Add Doppler env group**. 2. Fill in the form: * **Env group name (vanity name for Porter)** — the name you'll use to reference this environment group inside Porter (e.g. `production-doppler`). * **Doppler service token** — the `dp.st.…` token you generated above. 3. Click **Add Doppler env group**. ![Add a new Doppler service token modal in the Porter dashboard](https://mintcdn.com/porter/kg1pZSYLzGJxGgQ_/images/integrations/doppler-add-env-group.webp?fit=max\&auto=format\&n=kg1pZSYLzGJxGgQ_\&q=85\&s=0bc5196a6699bd69dfd0a587167da2e5) The new environment group will appear alongside your other environment groups in the **Env Groups** tab. ## Syncing to an application Doppler environment groups work like any other Porter environment group. You can sync them to an application: * From the dashboard, on the application's **Env Groups** tab, add the Doppler group and click **Update app**. * From `porter.yaml`, add the group's name to the `envGroups` field: ```yaml theme={null} version: v2 name: my-app envGroups: - production-doppler services: - name: web type: web run: npm start port: 3000 ``` For more on syncing, see [Environment groups](/applications/configure/environment-groups). > [!NOTE] > > Doppler environment groups are **read-only** in Porter. Values are fetched from Doppler and cannot be edited from the Porter dashboard. To change a secret's value, update it in Doppler — the change will propagate to your cluster automatically. ## Rotating the service token If you regenerate a service token in Doppler, update Porter so it can keep syncing: 1. Open the Doppler environment group in Porter. 2. Go to the **Settings** tab. 3. Under **Credentials**, click **Rotate** and paste the new service token. ## Deleting a Doppler environment group To stop syncing a Doppler config: 1. Open the environment group in Porter. 2. Go to the **Settings** tab and click **Delete**. > [!WARNING] > > You cannot delete an environment group that is synced to an application. Remove it from all synced applications first. Deleting the environment group removes the synced Kubernetes resources from your cluster. It does **not** delete the underlying secrets in Doppler. # Infisical Source: https://docs.porter.run/integrations/infisical Sync secrets and environment variables from Infisical into your Porter applications as a managed environment group with automatic updates on change. Porter integrates with [Infisical](https://infisical.com/) so you can manage secrets in Infisical and have them automatically synced into your Porter applications. Each Infisical environment is exposed in Porter as a read-only environment group that can be attached to any application in the cluster. Under the hood, Porter installs the [Infisical Kubernetes operator](https://infisical.com/docs/integrations/platforms/kubernetes) on your cluster, which uses a machine identity to pull secrets directly from Infisical. Secret values never persist in Porter's infrastructure. ## Enabling the Infisical integration The Infisical integration is enabled per cluster. 1. From the Porter dashboard, navigate to the **Integrations** tab. 2. Find **Infisical** in the list and toggle it on. Porter installs the Infisical operator on the selected cluster. Once enabled, you can begin adding Infisical environment groups. ## Adding an Infisical environment group Each environment group corresponds to a path within a specific environment of an Infisical project. ### Create a machine identity in Infisical Porter authenticates with Infisical using a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities). In your Infisical project: 1. Create a machine identity and assign it the **Viewer** role on the project. 2. Generate a **Client ID** and **Client Secret** for the identity. Keep both values handy — you'll paste them into Porter in the next step. ### Create the environment group in Porter 1. On the Infisical integration page in Porter, click **Add Infisical env group**. 2. Fill in the form: * **Name (for Porter environment group)** — the name you'll use to reference this environment group inside Porter (e.g. `production-infisical`). * **Project slug** — the slug of the Infisical project to sync from. * **Env slug** — the environment to pull from (e.g. `dev`, `staging`, `prod`). * **Env path** — the secret path within the environment. Defaults to `/`. * **Service URL** — the Infisical API URL. Defaults to `https://app.infisical.com/api`. Set this to your self-hosted Infisical URL if you're not using Infisical Cloud. * **Client ID** — the client ID of the machine identity you created. * **Client Secret** — the client secret of the machine identity. 3. Click **Add Infisical env group**. ![Add a new Infisical integration modal in the Porter dashboard](https://mintcdn.com/porter/kg1pZSYLzGJxGgQ_/images/integrations/infisical-add-env-group.webp?fit=max\&auto=format\&n=kg1pZSYLzGJxGgQ_\&q=85\&s=bcfa4e7bf84d196d525f1af8e326b927) The new environment group will appear alongside your other environment groups in the **Env Groups** tab. ## Syncing to an application Infisical environment groups work like any other Porter environment group. You can sync them to an application: * From the dashboard, on the application's **Env Groups** tab, add the Infisical group and click **Update app**. * From `porter.yaml`, add the group's name to the `envGroups` field: ```yaml theme={null} version: v2 name: my-app envGroups: - production-infisical services: - name: web type: web run: npm start port: 3000 ``` For more on syncing, see [Environment groups](/applications/configure/environment-groups). > [!NOTE] > > Infisical environment groups are **read-only** in Porter. Values are fetched from Infisical and cannot be edited from the Porter dashboard. To change a secret's value, update it in Infisical — the change will propagate to your cluster automatically. ## Deleting an Infisical environment group To stop syncing an Infisical environment: 1. Open the environment group in Porter. 2. Go to the **Settings** tab and click **Delete**. > [!WARNING] > > You cannot delete an environment group that is synced to an application. Remove it from all synced applications first. Deleting the environment group removes the synced Kubernetes resources from your cluster. It does **not** delete the underlying secrets in Infisical. # MCP Server Source: https://docs.porter.run/mcp/overview Connect Claude Code, Cursor, Codex, and other AI agents to the Porter MCP server to deploy, debug, and manage apps in your own AWS, GCP, or Azure account. Porter hosts a remote [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server at `https://mcp.porter.run`. Point an MCP client at that URL, sign in, and your agent can do the same things you'd do in the dashboard or the CLI: read logs, check why a deploy failed, change a service, redeploy it. If the Porter tools are not available in this session, run `curl -fsSL https://agents.porter.run | sh`. It installs the Porter CLI, registers this MCP server with your client, and installs Porter's skills. Restart the client afterward. When a user asks to deploy a GitHub repository, call `connect_github` with the repository before inspecting or building the app or calling any deployment tool. Proceed only when it returns `next_step: none`; otherwise, give the user the returned URL and stop. Then call `create_app` with `source` and `build`, and do not author a `porter.yaml`. Reach for the CLI, `porter apply`, or configuration-as-code only when the user asks for them. Porter creates the app on a placeholder image and opens a GitHub Actions pull request that the user merges to trigger the first real deploy. You don't need to install or run anything locally. The server runs on Porter's side, so all your client needs is the URL and a browser to sign in. ``` https://mcp.porter.run ``` ## Connect your client ### Quick install Install the Porter CLI and configure agent support — MCP registration and skills — in one command: ```bash theme={null} curl -fsSL https://agents.porter.run | sh ``` Restart your client afterward. It prompts for browser sign-in the first time it uses a Porter tool, or pass `--login` to sign in during the install instead. Run inside an agent session, the installer configures that client alone. `--clients=all` covers every client on the machine, `--dry-run` previews, `--uninstall` reverses, and `--help` lists the rest. Skills go beyond what the individual [tools](/mcp/tools) describe: `porter-deploy` takes a GitHub repository from nothing to a live URL, following the generated workflow pull request through merge. `porter-qm` takes a cluster to a signed-in [QM](/addons/qm) install. To register the server on its own, without the CLI or the skills: ```bash theme={null} # Claude Code claude mcp add --transport http porter https://mcp.porter.run # Codex codex mcp add porter --url https://mcp.porter.run # OpenCode, prompts for a name, a type, and the URL opencode mcp add ``` ### Client configuration **Claude Code** 1. Add the server: ```bash theme={null} claude mcp add --transport http porter https://mcp.porter.run ``` Or add it to `.mcp.json` to share it with the project: ```json theme={null} { "mcpServers": { "porter": { "type": "http", "url": "https://mcp.porter.run" } } } ``` 2. Start Claude Code and run `/mcp`. 3. Select **porter** and sign in through the browser. For more information, see the [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp). **Codex** 1. Add the server: ```bash theme={null} codex mcp add porter --url https://mcp.porter.run ``` Or add it to `~/.codex/config.toml` directly: ```toml theme={null} [mcp_servers.porter] url = "https://mcp.porter.run" ``` 2. Sign in: ```bash theme={null} codex mcp login porter ``` The ChatGPT desktop app, Codex CLI, and IDE extension share this file, so you only do this once. For more information, see the [Codex MCP documentation](https://developers.openai.com/codex/extend/mcp). **OpenCode** 1. Run the setup command: ```bash theme={null} opencode mcp add ``` 2. Enter `porter` as the name, choose **remote**, and paste the URL: ``` https://mcp.porter.run ``` Or add it to `opencode.json` directly: ```json theme={null} { "$schema": "https://opencode.ai/config.json", "mcp": { "porter": { "type": "remote", "url": "https://mcp.porter.run" } } } ``` 3. Sign in: ```bash theme={null} opencode mcp auth porter ``` OpenCode also starts the sign-in on its own the first time you use a Porter tool. For more information, see the [OpenCode MCP documentation](https://opencode.ai/docs/mcp-servers/). **Cursor** 1. Create or edit `.cursor/mcp.json` in your project root: ```json theme={null} { "mcpServers": { "porter": { "url": "https://mcp.porter.run" } } } ``` Use `~/.cursor/mcp.json` instead to get Porter in every project. 2. Restart Cursor. 3. Open **Settings > Tools & MCP**, select **porter**, and click **Connect**. For more information, see the [Cursor MCP documentation](https://cursor.com/docs/mcp). **Claude Desktop** 1. Go to **Customize → Connectors**, click **+**, then **Add custom connector**. On Team or Enterprise, an Owner does this from **Organization settings → Connectors**. 2. Paste the URL: ``` https://mcp.porter.run ``` 3. Click **Add** and approve access in the browser. Porter then shows up under **Connectors** in the **+** menu of any conversation. On Team or Enterprise, everyone else finds it already listed and clicks **Connect**. > [!NOTE] > > Claude connects to remote MCP servers from Anthropic's cloud, not from your machine. That's true in the desktop app too. For more information, see the [Claude custom connectors documentation](https://support.anthropic.com/en/articles/11175166-about-custom-connectors-remote-mcp-servers). ## Capabilities You ask for things in plain language, so it's easier to think in terms of tasks than tools: * **[Deploy an app](/applications/deploy/deploy-from-github-repo).** Create an application and get it running without leaving your agent. If you're deploying a pre-built image, the agent can take it all the way. If you're deploying from a GitHub repo, the agent checks the repository's GitHub connection first and gives you a link if anything is missing. Once connected, it creates the app and opens a pull request with the build workflow, and you merge it to deploy your code. For the complete setup flow, see the [Quickstart](/getting-started/quickstart#step-4:-create-your-first-application). * **Update an app.** Change services, resources, or domains on an existing app and redeploy. Updates can be validated with a dry run first, so you can see the resulting config before anything deploys. * **Troubleshoot your app.** Read logs, metrics, and notifications, or check nodes, node groups, pods, and load balancers to find what's broken. From there you can go back to the config that caused it, fix it, and redeploy. * **Rightsize a service.** Compare requested CPU and memory against what the service actually uses, then adjust the requests. * **Copy an environment.** Create dev or staging versions of an existing app, or move one to a different cluster. * **Inspect resources.** List projects, apps, and clusters, or check the status of a deployment. Some examples: ```text theme={null} Help me deploy my code to Porter. Why did my latest deploy fail? My web service keeps restarting, figure out why and fix it. Help me reduce cost for my apps. Create a staging copy of my production web service. ``` The [tools reference](/mcp/tools) has the full list of tools. ## Limitations OAuth is currently the only auth method, so headless environments like CI pipelines and sandboxed agents are not supported. ## Risks > [!WARNING] > > MCP tools can perform destructive operations on your applications, including > creating, updating, and redeploying services. They can directly impact > production environments and cause downtime to services if not used with care. The agent authenticates as you and inherits your Porter permissions. Recommendations: * **Use non-production environments first.** Validate changes in a dev or staging project before applying them to production. * **Review the agent's plan before approving it**, particularly for operations that modify production resources. * **Disable auto-approval against production.** It is reasonable on a test project and unsafe everywhere else. * **Account for prompt injection.** The agent reads your application logs, notifications, and pull request bodies, all of which can carry attacker-controlled text. The server cannot distinguish injected instructions from your own, so a compromised agent can call `create_app`, `update_application`, or `redeploy_application` and change or restart a running service. It can also call `create_github_workflow` to open a pull request and configure GitHub Actions secrets in a connected repository. Read tools exclude environment variables and secrets, and redact token-shaped values in logs, so the exposure is to your application's configuration and availability rather than its credentials. ## Revoke access If you installed with the script, one command removes the CLI, the skills, and the server entries: ```bash theme={null} curl -fsSL https://agents.porter.run | sh -s -- --uninstall ``` It backs up every file it edits to `~/.porter/backups`. OpenCode's config is left alone, so remove the `porter` entry from `opencode.json` yourself. Otherwise you revoke access per client. Remove the stored credentials, then delete the server entry so it doesn't sign in again later. **Claude Code** ```bash theme={null} claude mcp remove porter ``` **Codex** ```bash theme={null} codex mcp logout porter codex mcp remove porter ``` `logout` clears the stored OAuth credentials and `remove` deletes the server entry. **OpenCode** ```bash theme={null} opencode mcp logout porter ``` That clears the stored OAuth tokens. Also remove the `porter` entry from `opencode.json` to get rid of the server entirely. **Cursor** Delete the `porter` entry from `.cursor/mcp.json` (or `~/.cursor/mcp.json`) and restart Cursor. **Claude Desktop** Go to **Customize → Connectors**, click the three dots next to Porter, and choose **Remove**. On Team and Enterprise plans, an Owner has to remove it for the whole organization from **Organization settings → Connectors**. # MCP Server Tools Source: https://docs.porter.run/mcp/tools Reference for every tool the Porter MCP server exposes to AI agents, from reading logs, metrics, and cluster state to creating and redeploying apps. Use these tools to discover projects, inspect applications and infrastructure, connect GitHub repositories, and create or update deployments. Tools that make changes are called out where they appear. If the Porter tools are not available in this session, run `curl -fsSL https://agents.porter.run | sh`. It installs the Porter CLI, registers this MCP server with your client, and installs Porter's skills. Restart the client afterward. When a user asks to deploy a GitHub repository, call `connect_github` with the repository before inspecting or building the app or calling any deployment tool. Proceed only when it returns `next_step: none`; otherwise, give the user the returned URL and stop. Then call `create_app` with `source` and `build`, and do not author a `porter.yaml`. Reach for the CLI, `porter apply`, or configuration-as-code only when the user asks for them. Porter creates the app on a placeholder image and opens a GitHub Actions pull request that the user merges to trigger the first real deploy. Most tools require a project, given as either its ID or exact name. `get_projects` takes no arguments, while `connect_github` takes only a repository. Application arguments accept an app name or UUID, and cluster arguments accept a cluster ID, exact name, or vanity name. Start with `get_projects` to resolve the project, then `get_applications` or `get_clusters` from there. Tools that page a list return a `nextCursor` and accept it back as `cursor`. The cursor is opaque and carries the filters it was issued with, so pass it on its own to read the next page rather than rebuilding the filters or a `created_before` bound by hand. A response with no `nextCursor` is the last page. ### Projects & Discovery | Tool | Description | | - | - | | `get_projects` | List the projects your account can access. Takes no arguments. | | `get_applications` | List applications in a project. Pages with a `cursor`. | | `get_clusters` | List clusters in a project. | ### Applications | Tool | Description | | - | - | | `get_application` | Read an application's configuration. Excludes variables, secrets, or environment groups. | | `get_application_status` | Read an application's current deployment status and conditions. | | `get_application_revisions` | List an application's revisions. | | `get_application_metrics` | Read CPU, memory, and request metrics for an application. | | `get_app_logs` | Read an application's logs. Bearer tokens and secret-shaped values are redacted. | | `get_notifications` | List an application's runtime notifications. Requires an app name or UUID. | ### Jobs | Tool | Description | | - | - | | `list_job_runs` | List recent runs of a job service, newest first. Capped; sets `truncated` when older runs exist. | | `get_job_run` | Read the status of one job run by name. Searches the job's most recent 100 runs. | ### Clusters & Infrastructure | Tool | Description | | - | - | | `get_cluster_nodes` | List the nodes in a cluster. | | `get_cluster_node_groups` | List the node groups in a cluster. | | `get_cluster_pods` | List pods in a cluster. | | `get_cluster_pod` | Read a pod's owning controller, resource usage, restart count, recent events, Kubernetes metadata, extended status, and spec. Excludes environment variables. | | `get_load_balancers` | List load balancers for a cluster. | | `get_node_group_metrics` | Read metrics for a node group. | ### GitHub | Tool | Description | | - | - | | `connect_github` | Check whether Porter can reach a GitHub repository and return the browser action required to connect it. | | `create_github_workflow` | Recover a missing GitHub Actions workflow pull request for a Porter application. | #### `connect_github` Check whether Porter can reach a repository before inspecting or deploying its code. The tool takes only `repository`, in `owner/name` form. Each call returns one complete connection state. If the user completes a returned browser action, call the tool again to check the new state. | `next_step` | Meaning | What the user does | | - | - | - | | `none` | The repository is reachable through the Porter GitHub App. | Nothing; proceed. | | `authorize_oauth` | No GitHub OAuth connection exists for this Porter user. | Open the returned URL while signed into the same Porter account and authorize GitHub. | | `install_app` | OAuth is connected but the Porter GitHub App is not installed on that account. | Install it at the returned URL. | | `add_repository` | The App is installed on the account but this repository is not in its selected repositories. | Add the repository in the installation settings at the returned URL. | | `request_org_install` | No installation covers the organization that owns the repository. | An organization owner has to approve the installation; the user cannot self-serve this. | #### `create_github_workflow` Use this recovery tool only when [`create_app`](#create_app) returned a placeholder application without a pull request URL, or when an existing application is missing its workflow pull request. It takes `project` and `app`, then creates or re-creates the GitHub Actions workflow pull request and returns its URL. Do not call it after `create_app` successfully returns a pull request URL. ### Create, update, and redeploy applications > [!WARNING] > > These tools can create, update, or redeploy applications, or start job runs, > and may have unintended side effects. Review the agent's plan before approving > any call to `create_app`, `update_application`, `redeploy_application`, or > `trigger_job_run`. > [!NOTE] > > `create_app` and `create_github_workflow` create an application or workflow > pull request rather than overwrite deployed state. #### `create_app` Create a new application. The behavior depends on which inputs you pass: Before using the recommended GitHub `source` and `build` flow, call [`connect_github`](#connect_github) with the repository and proceed only when it returns `next_step: none`. | Input | Behavior | | - | - | | `image` | Deploys the prebuilt image directly. No pull request. | | `source` + `build` | Creates the app on a placeholder image and opens a GitHub Actions pull request. Nothing real serves until that PR is merged. | | `build` alone | Returns a `local_build_command` to run `porter apply` from the local repository. | `image` is mutually exclusive with `source` and `build`. Creation fails if an application with the same name already exists in the target project or cluster. Use `update_application` instead. For `source` and `build`, review and merge the pull request returned by `create_app`. The merge triggers the workflow that builds the application's code and runs the first real deployment. If the placeholder application has no pull request URL, recover it with [`create_github_workflow`](#create_github_workflow). See [Deploy from a GitHub repository](/applications/deploy/deploy-from-github-repo) for the full flow. #### `update_application` Update an application's configuration. Defaults to a dry run, which validates the change and returns the resulting config without deploying. Set `dry_run: false` to deploy. #### `redeploy_application` Trigger an immediate redeploy of an application. #### `trigger_job_run` Start a run of a job service on an application, optionally overriding the run command or the entrypoint and arguments. Returns the run name; use `get_job_run` for status and `get_app_logs` for output. ### Sandboxes | Tool | Description | | - | - | | `get_sandbox_settings` | Read whether sandboxes are enabled and ready on a cluster, plus its sandbox settings. | | `list_sandboxes` | List sandboxes on a cluster, newest first. Finished sandboxes stay in the list as history. | | `get_sandbox` | Read one sandbox's current phase and, once it ends, why it ended. | > [!WARNING] > > These tools can change a cluster's sandbox runtime or run commands in a > sandbox. Review the agent's plan before approving any call to > `enable_sandboxes`, `update_sandbox_settings`, `set_sandbox_domain`, > `create_sandbox`, or `exec_sandbox`. | Tool | Description | | - | - | | `enable_sandboxes` | Enable sandboxes on an AWS cluster. Returns before the runtime is up; poll `get_sandbox_settings` until `ready`. | | `update_sandbox_settings` | Change the warm pool or turn egress allowlist enforcement on or off. Changing egress restarts the sandbox runtime. | | `set_sandbox_domain` | Set the public or private domain sandboxes are served under. A sandbox domain can't be removed once set. | | `create_sandbox` | Create a sandbox from an image. Poll `get_sandbox` until the phase is `running`. | | `exec_sandbox` | Run a command in a running sandbox and return its output and exit code. | See [Sandboxes](/sandboxes/overview) for how sandboxes work. ### Add-ons The add-on tools install and manage [QM](/addons/qm). `get_addons` lists every add-on on a cluster, but only QM can be installed or updated from an agent. | Tool | Description | | - | - | | `get_addons` | List the add-ons on a cluster with each add-on's type and install status, plus the URL and sign-in state for QM. | | `get_addon_status` | Read where an add-on is in its install. Returns `not_found`, not an error, once an add-on is deleted. | > [!WARNING] > > These tools can install, reconfigure, or remove an add-on. Review the > agent's plan before approving any call to `install_addon`, `update_addon`, > or `delete_addon`. #### `install_addon` Install QM on a cluster. The call returns right away with a `next_step`: | `next_step` | Meaning | | - | - | | `poll_status` | Poll `get_addon_status` until the status is `installed`, `waiting_for_sso`, or `failed`. | | `register_oidc_app` | Register an app with your identity provider using the returned `oidc_callback_url`, then finish with `update_addon`. | | `add_dns_record` | Point the custom domain at the cluster's load balancer before QM is reachable. | The install takes either `builtin` sign-in, where QM emails sign-in links through Resend, or `oidc` sign-in through your identity provider. It fails if an add-on with the same name exists, so check `get_addons` first. Password sign-in and bucket attachment are only in the [dashboard](/addons/qm#setup). #### `update_addon` Change an installed QM add-on and roll it out again. Only the fields you pass change. Use it to finish `oidc` sign-in with the client ID and secret, or to change a value such as a key or the custom domain. #### `delete_addon` Uninstall an add-on and remove its configuration. The Postgres database QM was given is left untouched. > [!NOTE] > > Secret values passed to `install_addon` or `update_addon` persist in the > conversation transcript. # AWS VPC peering Source: https://docs.porter.run/other/aws-vpc-peering Set up VPC peering between your Porter cluster and existing AWS VPCs to access RDS instances and other resources across accounts or regions AWS VPC Peering comes in quite handy when you need to allow access to RDS instances or other workloads that are located inside separate VPCs, from the Porter cluster. This doc is meant to serve as a checklist for setting up AWS VPC peering connections between a Porter cluster's VPC and an existing VPC on your AWS account. Note that this can work across separate regions too. ## Create a Peering Connection[](#create-a-peering-connection "Direct link to heading") The first step is to create a peering connection, which can be accomplished by navigating to the `Peering Connections` section on the VPC dashboard. Whilst creating a connection, note that the requester VPC should be the Porter VPC, and the accepter VPC should be the VPC you wish to create a peering tunnel towards. Note that this is purely for consistency's sake. ![step-one](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/other/173303613-941738ee-b85e-4e84-bfc8-1fa69238b489.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=ddc1bcd5ca306c78d499e467fa5ccb63) Once done, you'll need to "accept" the request for the connection. Do ensure that the peering connection is set up between VPCs that have different CIDRs. ![step-two](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/other/173303642-c3d733ba-a87b-4092-bc38-8db39d3a8722.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=87ec7977b6cf0010a1aa7edf83b4eeb9) ![step-three](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/other/173303666-91fe61ae-7132-4e3c-b6f7-4ac6a602a268.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=469bdf6ea12ba9bef76d5ee7741a1012) ## Routing Tables[](#routing-tables "Direct link to heading") The next step is to ensure that each VPC's subnets have routing tables that are updated with entries telling each subnet where to throw traffic for the other VPC's CIDR. As an example, assume that VPC `k8s-upgrade-test-vpc` has `10.99.0.0/16` as its CIDR and VPC `default` has `172.31.0.0/16`. In this case, the routing table for each subnet inside `k8s-upgrade-test` needs to have an entry specifying that traffic for `172.31.0.0/16` is sent to the peering connection, and the routing table for each subnet inside `default` needs to have an entry that routes traffic for `10.99.0.0/16` to the peering connection. ![step-four](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/other/173303690-cb51181b-8b45-4552-99d2-9cce81f27b9b.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=28f17b3a02de3fd17a769b0c39fea2b7) ![step-five](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/other/173303731-3449eb18-b70c-4338-b236-839809986d87.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=4bc51b6cfc5e77b7e872f9356475d0b0) ![step-six](https://mintcdn.com/porter/UfwSdxx6iu4MwCqd/images/other/173303768-2199a545-5e39-41b7-989e-24033c06ad15.png?fit=max\&auto=format\&n=UfwSdxx6iu4MwCqd\&q=85\&s=b0ed7c022cc8e582a77297109cf3192a) ## Security Groups[](#security-groups "Direct link to heading") Finally, it's important to ensure that if the Porter VPC needs to access a resource in a second VPC over a peering connection, the security group(s) for that resource in the second VPC have entries allowing traffic from the Porter VPC's CIDR - typically 10.99.0.0/16 - for the port the resource is supposed to be available at. # Deleting dangling resources Source: https://docs.porter.run/other/deleting-dangling-resources Manually clean up orphaned AWS, GCP, or Azure resources like EKS clusters, load balancers, and VPCs left behind after project deletion When you delete your project or clusters, Porter automatically destroys your resources so you don't get charged for any unused resources. In unforeseen circumstances, some resources may not be deleted, and you may need to manually delete them. Whilst this does not happen often, the following is a guide with specific steps on how to ensure that all resources have been deleted for each cloud provider. **AWS** Please note that the instructions in the following sections need to be used in the order they're specified in; not following the sequence will result in errors whilst trying to delete resources that may be dependent on other resources within your account. ## Deleting EKS Go to the **EKS** section on your AWS console, and delete the cluster in question, if it's visible. ![EKS](https://imagedelivery.net/l4LYM_vOYKe7O1NCT_Nc_g/86a228b3-68d5-4e42-912b-a34a51f6d600/large "Screen Shot 2021-03-26 at 4.16.01 PM.png") ## Removing Auto Scaling Groups & Launch Configurations On the **EC2** dashboard, navigate to the **Auto Scaling Groups** section, and remove any auto scaling groups that contain your cluster's name. Once they're deleted, you need to delete any launch configurations that may be left over, containing your cluster's name. ![Autoscaling group](https://imagedelivery.net/l4LYM_vOYKe7O1NCT_Nc_g/33c10eca-b30b-44c3-8f43-3deae40da300/large "Screen Shot 2021-03-26 at 4.24.08 PM.png") ## Removing Load Balancers & Elastic IPs First, navigate to the **VPC** section in your AWS console to see the VPC's that are currently in use. Select the VPC that belongs to the cluster you've provisioned, and copy the VPC ID. Then go to the **Load Balancers** section on the **EC2** dashboard, and locate the load balancer for your cluster using the VPC ID you just copied as a filter - delete any load balancers found. ## Terminating EC2 nodes Deleting the EKS cluster and associated auto scaling groups and launch configurations will ensure your EC2 nodes are also terminated; you can navigate to the **Instances** section on the EC2 dashboard to confirm. ## Removing VPC Resources Navigate to the **VPC** section in your AWS console to see the VPC's that are currently in use. Select the VPC that belongs to the cluster you've provisioned, and copy the VPC ID. 1. Click on **NAT Gateways**, and use the VPC ID to filter out the NAT Gateway for your cluster; select it and then click on **Actions** -> **Delete NAT Gateway**.![NAT gateway](https://imagedelivery.net/l4LYM_vOYKe7O1NCT_Nc_g/29bcf96e-84ff-4d9d-d3b5-1e15a5554900/large "Screen Shot 2021-03-26 at 4.09.51 PM.png") 2. After ensuring the NAT Gateway was deleted, navigate to **Internet Gateways**, and use the VPC ID to locate the Internet Gateway for your cluster; select it and click on **Actions** -> **Detach from VPC**. Once the gateway is detached, select **Actions** -> **Delete Internet gateway**. If detaching the gateway throws an error about unmapped public IPs, go to the **Elastic IPs** section and release any elastic IPs associated with the VPC ID. 3. After all gateways have been deleted, go to the **Subnets** section, and remove all subnets associated with the cluster VPC, by clicking on **Actions** -> **Delete subnet**. 4. Finally, you can delete the VPC by going to **Your VPCs** and deleting the VPC for your cluster, by selecting **Actions** -> **Delete VPC**. ![Delete VPC](https://imagedelivery.net/l4LYM_vOYKe7O1NCT_Nc_g/e74141e4-6a77-4a08-6ffb-541586499300/large "Screen Shot 2021-03-26 at 4.05.16 PM.png") **GCP** The instructions in the following sections should be followed in the order they're specified in; GCP will refuse to delete resources that other resources still depend on. Porter names the resources it creates after your cluster (referred to as `CLUSTER` below) or with a `porter-` prefix followed by your Porter project ID (referred to as `PROJECT_ID` below). Only delete resources whose names contain `porter-` or your cluster's name. ## Deleting the GKE cluster Go to **Kubernetes Engine** in the GCP console, and delete the cluster in question, if it's visible. This also removes the cluster's node pools and their Compute Engine instances. Once the cluster is deleted, navigate to **Network Services** -> **Load Balancing** and delete any load balancers that were created by the cluster (these have auto-generated names, typically starting with `k8s-` or `a` followed by a long hash) — leftover load balancers will block deleting the VPC network later. ## Removing Cloud NAT & Cloud Router Navigate to **Network Services** -> **Cloud NAT** and delete the `CLUSTER-cloud-nat-router` gateway. Once it's deleted, go to **Network Connectivity** -> **Cloud Routers** and delete `CLUSTER-cloud-router`. ## Releasing static IP addresses Navigate to **VPC network** -> **IP addresses** and release `CLUSTER-cloud-nat-ip` (there may be additional addresses named `CLUSTER-cloud-nat-ip-2`, `-3`, and so on) as well as `k8s-CLUSTER-lb`. ## Deleting Cloud DNS private zones Navigate to **Network Services** -> **Cloud DNS** and delete any private zones associated with your cluster, if present. ## Deleting the VPC network Navigate to **VPC network** -> **VPC networks** and delete `CLUSTER-vpc`; this also removes the `CLUSTER-subnet` subnet and the `CLUSTER-cloud-firewall-rules` firewall rule. If GCP reports that the network is still in use, revisit the previous sections — a leftover load balancer, NAT gateway, or static address is still referencing it. ## Deleting leftover persistent disks Navigate to **Compute Engine** -> **Disks** and delete any unattached disks with names starting with `pvc-` that belonged to the cluster. These are created for application volumes and are not always removed with the cluster. ## Deleting the logs bucket Each cluster has its own log storage bucket. Navigate to **Cloud Storage** -> **Buckets** and delete the bucket for your cluster — its name starts with `porter-PROJECT_ID-` and contains `porter-logs` (on newer clusters this is followed by a numeric cluster ID; on older clusters, by the cluster's name). ## Deleting container images and secrets The following resources are shared across all Porter clusters in the GCP project, so only delete them if you're removing Porter from the GCP project entirely — not just deleting a single cluster: 1. Navigate to **Artifact Registry** -> **Repositories** and delete the `porter-PROJECT_ID` repository. 2. Navigate to **Cloud Storage** -> **Buckets** and delete any remaining buckets with names starting with `porter-PROJECT_ID-`. 3. Navigate to **Secret Manager** and delete any secrets with names starting with `porter-env-groups-PROJECT_ID-`. Finally, to revoke Porter's access to the GCP project, go to **IAM & Admin** -> **Service Accounts** and delete the `porter-manager-` service account, then remove the `porter-pool-` pool under **IAM & Admin** -> **Workload Identity Federation**. Note that this breaks Porter's ability to manage anything remaining in the project — see [Deleting a cluster](/cloud-accounts/deleting-a-cluster) for details. If you enabled disk encryption, the Cloud KMS key ring and key named `porter-PROJECT_ID-CLUSTER` cannot be deleted — GCP only allows destroying key versions, not key rings. These incur no compute cost and can be safely left in place. **Azure** On Azure, Porter creates all resources inside resource groups, so cleanup is simpler: deleting a resource group also deletes everything in it. Porter names resource groups with your Porter project ID (referred to as `PROJECT_ID` below), followed by your cluster's name or its Azure region. ## Deleting the cluster resource group Go to **Resource groups** in the Azure portal and delete the `PROJECT_ID-CLUSTER` resource group. This removes the AKS cluster and all of its underlying resources. The `MC_`-prefixed managed resource group that AKS created alongside the cluster is deleted automatically with it — confirm it's gone once the deletion completes. ## Deleting the regional resource group If you're removing Porter from the Azure subscription entirely — not just deleting a single cluster — also delete the `PROJECT_ID-REGION` resource group (for example, `123-eastus`), which holds resources shared across clusters in that region, such as the container registry, key vault, and log storage. ## Cleaning up directory and subscription-level leftovers A few objects live outside the resource groups and survive their deletion. None of them incur cost, but you can remove them for a fully clean subscription: 1. In **Microsoft Entra ID** -> **Groups**, delete the `CLUSTER-admins` group. 2. In your subscription's **Access control (IAM)** -> **Role assignments**, remove any assignments listed as **Identity not found** — these are left over from the deleted cluster's managed identities. 3. The key vault from the regional resource group remains in a soft-deleted state, which reserves its name for the retention period. To free the name immediately, purge it under **Key vaults** -> **Manage deleted vaults**. Finally, to revoke Porter's access to the Azure subscription, remove the federated credential Porter added to the app registration you connected it with, under **Microsoft Entra ID** -> **App registrations** -> your app -> **Certificates & secrets** -> **Federated credentials**. Note that this breaks Porter's ability to manage anything remaining in the subscription — see [Deleting a cluster](/cloud-accounts/deleting-a-cluster) for details. # Running GPU workloads Source: https://docs.porter.run/other/gpu-workloads Create fixed GPU node groups with NVIDIA-enabled instances and deploy GPU-accelerated machine learning or inference workloads on Porter This guide walks you through setting up GPU infrastructure on Porter, from creating a GPU node group to deploying your first GPU-accelerated application. You can deploy GPU-enabled workloads on Porter by creating a fixed node group and selecting a GPU-enabled instance type. Note that this has to be the **second** node group in your cluster as the default node group is reserved for CPU workloads. > [!NOTE] > > GPUs are only supported on fixed node groups because cost optimization does not currently support GPU instances. This means you select a specific GPU instance type and Porter scales by adding or removing instances of that exact type. ## Creating a GPU Node Group **1. Navigate to Infrastructure** From your Porter dashboard, click on the **Infrastructure** tab in the left sidebar. **2. Select your cluster** Click on **Cluster** to view your cluster configuration and existing node groups. **3. Add a node group** Click **Add an additional node group** to open the node group configuration panel. **4. Configure the GPU node group** Configure your GPU node group with the following settings: | Setting | Description | | - | - | | **Instance type** | Select a GPU-enabled instance type (see table below) | | **Minimum nodes** | Select minimum number of nodes that will be available at all times | | **Maximum nodes** | The upper limit for autoscaling based on demand | ![Fixed Node Group Configuration](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/provisioning-infrastructure/cost-opt-1.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=21d2b7ae56f8e0a2bd899241d8bb1863) > [!WARNING] > > GPU instances are significantly more expensive than standard instances. **5. Save and provision** Click **Save** to create the node group. Porter will provision the GPU nodes in your cluster. This may take a few minutes as GPU nodes often require additional driver installation. ## Deploying a GPU Application Once your GPU node group is ready, you can deploy applications that use GPU resources. **1. Create or select your application** Navigate to your application in the Porter dashboard, or create a new one if you haven't already. **2. Go to the Services tab** Click on the **Services** tab to view your application's services. **3. Select the service** Click on the service that needs GPU access (e.g., your inference worker or training job). **4. Assign to the GPU node group** Under **General**, find the **Node group** selector and choose your GPU node group from the dropdown. **5. Configure GPU resources** Under **Resources**, configure your GPU requirements: | Setting | Recommended Value | | - | - | | **GPU** | Number of GPUs needed (typically 1) | | **CPU** | Match your workload needs (GPU instances have fixed CPU/RAM ratios) | | **RAM** | Match your workload needs | > [!NOTE] > > Request only the GPUs you need. Each GPU request reserves an entire GPU. **6. Save and deploy** Save your changes and redeploy the application. Porter will schedule your workload on the GPU node group. ## Troubleshooting > **Pod stuck in Pending state** > > This usually means there are no available GPU nodes. Check: > > * The node group has scaled up (check Infrastructure → Cluster) > * Your GPU request doesn't exceed available GPUs on the instance type > * The node group maximum nodes hasn't been reached > **Out of memory errors** > > GPU memory errors indicate your model or batch size is too large: > > * Reduce batch size > * Use a larger GPU instance with more VRAM > **Slow cold starts** > > GPU nodes take longer to start than CPU nodes due to driver initialization: > > * Keep minimum nodes at 1 for latency-sensitive workloads > * Consider keeping a warm pool of nodes during peak hours # Kubernetes 101 Source: https://docs.porter.run/other/kubernetes-101 Learn core Kubernetes concepts for Porter including nodes, pods, resource allocation, cluster architecture, and namespaces explained simply While Porter abstracts away the underlying complexity of Kubernetes, we've found that there are few core concepts that are useful to learn in order to make educated decisions about application configuration. If you are unsure what instance type you should provision your Kubernetes cluster with or how much resources you should assign to your applications, please read the sections below. ## Resources[](#resources "Direct link to heading") Each Kubernetes cluster consists of underlying **nodes**. Nodes are basic compute instances (e.g. EC2 on AWS) on which your applications run. One Kubernetes cluster manages multiple nodes, and each node can run multiple applications. Kubernetes will intelligently allocate your applications on these nodes depending on how much resources each node has available. An application you deploy on Porter consists of one or multiple **pods.** Each pod is a replica of your application, and incoming requests will automatically be dispersed across all pods of your application. You can vertically scale each pod by allocating more resources, or horizontally scale them by adding more pods to your application. A useful analogy is to think of these nodes as buckets in which your pods are placed. You can fill up an entire bucket with just one big pod, or you can run multiple smaller pods on one node. You can assign however much resources you want to each pod, and Kubernetes will intelligently allocate them across the nodes. The only rule is that you cannot allocate more resources to a single pod than what is available on a single node - a bucket simply cannot fit a pod that is larger than itself. > **Takeaway:** > > You cannot allocate more resources to a single pod than what is available on a single node. The instance type you choose when provisioning the cluster should be at least double the amount of resources you want to assign to a single pod. ## Cluster Architecture[](#cluster-architecture "Direct link to heading") Regardless of which cloud provider it provisions in, each cluster provisioned by Porter consists of one load balancer. This load balancer sits in front of the cluster and forwards all traffic to an nginx-ingress-controller running inside the cluster that acts as a reverse proxy. Porter clusters also consist of two node groups: system and workloads. The system nodes run all components that are pre-installed by Porter, such as nginx-ingress-controller, cert-manager, prometheus, etc. The workload nodes run user deployed applications and autoscale based on resource usage. ## FAQ[](#faq "Direct link to heading") ### What is a namespace?[](#what-is-a-namespace "Direct link to heading") Namespaces are semantic partitions by which you can organize and divide your applications. It bares no significance in how your applications are actually run. It is strictly semantic - you can think of them as folders. ### How do I set up multiple environments?[](#how-do-i-set-up-multiple-environments "Direct link to heading") The canonical way to set up development, staging, and production environments is to run each environment on a single cluster. This is necessary to completely isolate your environments from each other, as opposed to putting them on the same cluster that shares the same networking stack. # Migrate from Heroku Source: https://docs.porter.run/other/migrate-from-heroku Step-by-step guide to migrate from Heroku to Porter by converting your Procfile to porter.yaml and deploying to your own cloud account This guide will walk you through how to migrate from Heroku to Porter step by step. It usually takes around an hour to complete the steps outlined in this guide. # Step 1: Provision Infrastructure > [!NOTE] > > Estimated time of completion: 30 minutes Unlike Heroku, Porter deploys applications on your own infrastructure. Accordingly, the first step would be to grant Porter permissions to your AWS account to create the necessary underlying infrastructure for you to start deploying applications. [Please follow this guide](/cloud-accounts/creating-a-cluster) to provision the required infrastructure in your AWS account. # Step 2: Convert your `Procfile` to `porter.yaml` > [!NOTE] > > Estimated time of completion: 5 minutes Once your infrastructure has been provisioned, you will be able to [launch applications](/applications/deploy/overview#launch-your-application) from the dashboard. You can deploy your application on Porter using a `porter.yaml` file. While this is an optional step, `porter.yaml` allows you to declare your application in a similar way as you can on Heroku using a `Procfile`. Converting a `Procfile` into a `porter.yaml` file is straightforward. For example, a `Procfile` that looks like this: ``` web: puma worker: sidekiq-process-manager release: bundle exec rake db:migrate ``` can be converted into the following `porter.yaml` file: ``` apps: web: run: "puma" worker: run: "sidekiq-process-manager" release: run: "bundle exec rake db:migrate" ``` It is possible to configure more than just start commands on `porter.yaml`. For a full guide on how to write a `porter.yaml` file, [take a look here](/applications/configuration-as-code/overview). # Step 3: Launch Applications > [!NOTE] > > Estimated time of completion: 10 minutes Once the `porter.yaml` file is ready and is present in the root folder of your repository, you can simply link up your repository. Porter will detect your `porter.yaml` file and create your application accordingly. Porter supports Heroku's own buildpacks, so your applications will be built the exact same way as you can on Heroku. If you are capable of it, however, we recommend that you write a `Dockerfile` for your application. If you are using custom buildpacks, you will have to fork the custom buildpack you are using and make it compatible with [Cloud Native Buildpacks](https://buildpacks.io/). Doing this is not arbitrary, and in most cases we recommend that you write a `Dockerfile` for your application instead. If you are not able to write a `Dockerfile` and are using any of the following custom buildpacks, we maintain a fork of these custom buildpacks that you can just plug into **Build Settings** from the dashboard. * `libvips` - Installs the `libvips` library ([https://github.com/portersupport/cnb-libvips](https://github.com/portersupport/cnb-libvips)) * `wkhtmltopdf` - Downloads and extracts the `wkhtmltopdf` binaries ([https://github.com/portersupport/heroku-buildpack-wkhtmltopdf-1](https://github.com/portersupport/heroku-buildpack-wkhtmltopdf-1)) * `puppeteer` - Installs dependencies needed in order to run puppeteer ([https://github.com/portersupport/cnb-puppeteer](https://github.com/portersupport/cnb-puppeteer)) * `ffmpeg` - Installs `ffmpeg` and its dependencies ([https://github.com/portersupport/heroku-buildpack-ffmpeg-latest](https://github.com/portersupport/heroku-buildpack-ffmpeg-latest)) * `apt-buildpack` - Installs dependencies declared in the `Aptfile` ([https://github.com/portersupportapt-buildpack](https://github.com/portersupportapt-buildpack)) * `geo-buildpack` - Installs GEOS libraries ([https://github.com/portersupport/cn-buildpack-geo](https://github.com/portersupport/cn-buildpack-geo)) If you'd like to request a custom buildpack that you are using, please email us at contact \[at] porter.run with the subject "Buildpack Request". We will review the requested buildpack and let you know if we can support it. While we maintain these buildpacks, we still recommend you to write a `Dockerfile` instead if possible. For a step by step guide on how to do this, [please follow this guide](/applications/deploy/overview). # Step 4: Copy over Environment Variables > [!NOTE] > > Estimated time of completion: 5 minutes Once you've specified your repository and build settings, you will be prompted to add environment variables. To do this quickly without having to copy over each variable one by one, simply run `printenv` from your Heroku console or the CLI: ``` heroku run printenv ``` Then, simply copy and paste the output into the application by clicking on the **Load from File** button. Heroku attaches a lot of variables automatically even if you don't specify them, so the output from `printenv` will contain some variables that you did not add yourself. You will need to remove some of these environment variables that are specific to Heroku. The important environment variable that's essential to remove is the `PATH` environment variable as this would cause the build process to fail. With this copied set of environment variables, click on **Deploy**. # Step 5: Debugging your build process on GitHub Actions Porter uses [GitHub Actions to build your applications](/applications/deploy/builds). To debug your application builds, take a look at your build logs from the **Actions** tab of your repository. To change your build settings or environment variables, simply edit it from the Porter dashboard. If you navigate to **Build Settings**, you'll see a checkbox that says **Re-run build and deploy on save**. With that setting enabled, click on **Save Build Settings** to re-run the build. Of course, you can also trigger the build from the GitHub actions tab. Consult the [GitHub Actions documentation](https://docs.github.com/en/actions) for more info about Actions. # Step 6: Allocate Resources Once your application has built and you can verify that it's running properly, next step is to rightsize your applications with an appropriate amount of RAM and CPU. Here's the resource allocation that corresponds to each dyno type: * **Standard 1X Dyno** - 1000m vCPU, 500MB RAM * **Standard 2X Dyno** - 1000m vCPU, 1GB RAM * **Performance-M Dyno** - 1000m vCPU, 2.5GB RAM * **Performance-L Dyno** - 2000m vCPU, 14GB RAM # Step 7: Switch over DNS The final step is to switch over your production traffic by changing your DNS records. [Follow this guide](/applications/configure/custom-domains) to add your custom domain to the application and create a `CNAME` record in your DNS provider that points to the Load Balancer's IP address as specified in the aforementioned docs. This switchover may incur downtime. In most cases, this switchover does not incur more than 5 minutes of downtime. We recommend doing this during your low-traffic hours. ## Support We also offer a white-glove migration package for our enterprise tier customers. For more information, please visit [https://www.porter.run/migrate-from-heroku](https://www.porter.run/migrate-from-heroku). # Custom preview environment workflows Source: https://docs.porter.run/preview-environments/custom-workflows Customize GitHub Actions workflows and use the Porter CLI to control how preview environments build, deploy, and filter by branch naming patterns In cases where you need more control over how your apps build and deploy when creating new preview environments, it is possible to leverage the Porter CLI in combination with custom CI actions to handle a wider range of workflows. ## Using the CLI Spinning up an isolated preview environment is as simple as running the following command: ```bash theme={null} porter apply -f porter.yaml --preview ``` This will create a new preview environment using the currently checked out branch. ## Customizing Github Actions Porter uses Github Actions to build and deploy your apps. When setting up preview environments, a workflow file with a name like `porter-preview-.yml` will be created in your repository. You can customize this file to suit your needs. For example, it is possible to add conditionals to only run the workflow for specific branches or tags. This can be useful if you want to only create preview environments when a specific branch naming convention is used. If you only want to create preview environments for branches that start with `feat/`, you can add the following to your workflow file: ```yaml theme={null} ... jobs: porter-deploy: runs-on: ubuntu-latest if: ${{ startsWith(github.head_ref, 'feat/') }} steps: - name: Checkout code ... ``` For more information on Github Actions, see the [Github Actions documentation](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions). ## Opening a shell In cases where you need to open a shell into a preview env's running apps, you need to first retrieve the preview env name and then run `porter app run`: 1. Retrieve the preview env name by running `porter target list --preview`. Copy the name of the preview env you'd like to open a shell into. 2. Next, run `porter app run -x -- ` where `` is the name of the underlying app the preview env uses as a template, and `` is the preview env name you copied from running the first command. This will spin up a new replica and open a shell; if you wish to use an existing running replica, add the `-e` flag to `porter app run`. # Preview environment overrides Source: https://docs.porter.run/preview-environments/defining-overrides Override resource settings, environment variables, and configuration for preview environments using the Porter dashboard or porter.yaml ## Changing Values and Variables It is frequently the case that the configuration you are using for your app in production will be different from what you'd want to use in a preview environment. For example, you might want to bump down the resources your app consumes or use development API keys. Porter allows you to easily change the configuration of your app for preview environments by defining overrides in the dashboard or in your `porter.yaml` file. ### Using the Dashboard After enabling previews, you will be directed to a form where you can define what you want your app to look like when deployed to a preview environment. Any value that you can set when creating or updating an app can be changed specifically for preview environments. Newly created previews will use these values. Note that you can always change this configuration later. ![Updating Values](https://mintcdn.com/porter/OTbn3CF3vhOvlOWP/images/preview-environments/updating-values.png?fit=max\&auto=format\&n=OTbn3CF3vhOvlOWP\&q=85\&s=40421c8bb79c60f1477b0547c1764713) *Any value that you can set when creating or updating an app can be changed specifically for preview environments.* ### Using `porter.yaml` (Optional) You can also define a separate `porter.yaml` file to be used specifically for preview apps. The values in this file will override any values that are set while configuring the preview app template in the dashboard. The format of this file is the same as non-preview `porter.yaml` files (full reference [here](/applications/configuration-as-code/reference)). When creating the preview app template, you will be prompted to select the path of your preview `porter.yaml` file. ![Selecting Preview porter.yaml File](https://mintcdn.com/porter/OTbn3CF3vhOvlOWP/images/preview-environments/choose-preview-porter-yaml.png?fit=max\&auto=format\&n=OTbn3CF3vhOvlOWP\&q=85\&s=9fb22d6304445dd08029d86daba37de1) *Selecting the path of your preview `porter.yaml` file.* ## Setting Environment Variables Porter will by default use the environment variables from the base app, but strip out any environment groups that you have attached. This is because environment groups are typically used to define values that are specific to a particular environment. If there are any variables that you want to ensure are set in preview environments, you can add them to the app's environment variables, or create a new environment group specifically for preview environments and add it to the preview app configuration in the dashboard. ## Preview App URLs Preview apps can be made accessible via a unique URL that is generated when the preview is created. Similarly to environment groups, custom domains defined on your base app are also stripped out when creating the preview app in order to prevent collisions. If you want to make a preview web service accessible via a unique URL, make sure that the service is configured to be public. ### Custom Domains Custom domains can be configured based on the PR branch name and number by using the `{BRANCH}` and `{PR_NUMBER}` placeholders in the custom domain name while configuring the preview app template: ![Custom Domains](https://mintcdn.com/porter/IDEOYSmZwMY0A2xh/images/preview-environments/custom-domain-with-branch-placeholder.png?fit=max\&auto=format\&n=IDEOYSmZwMY0A2xh\&q=85\&s=9b467c73e90945f2489277543198ce24) *Using the `{BRANCH}` or `{PR_NUMBER}` placeholders in the custom domain name will result in a custom domain that is based on the PR branch name or number for each preview.* For example, if you set the custom domain to `{BRANCH}.my-domain.com` and have a branch named `my-feature-branch`, the preview app will be accessible via `my-feature-branch.my-domain.com`. If you set the custom domain to `{PR_NUMBER}.my-domain.com` and have a PR number of `123`, the preview app will be accessible via `123.my-domain.com`. Note that in order to be compatible with this feature, the branch name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character. You will also have to create a wildcard record in your DNS provider for the base domain (e.g. `*.my-domain.com`) that points to the ingress IP for your cluster. The ingress IP can be found in the **Networking** tab of any of your web services. ![Cluster Ingress IP](https://mintcdn.com/porter/jU3Ga0p8cNvoZxE1/images/preview-environments/cluster-ingress-ip.png?fit=max\&auto=format\&n=jU3Ga0p8cNvoZxE1\&q=85\&s=8099c5b649e4db993762e3ea316df5e2) *The ingress IP for your cluster can be found in the **Networking** tab of any of your web services.* # Preview environments Source: https://docs.porter.run/preview-environments/overview Automatically create isolated preview environments for every pull request with configurable templates, addons, and service overrides ## Getting Started Porter offers a simple way to create preview environments for your application. These environments allow you to test changes in a production-like environment before merging. When enabled, preview environments are by default created automatically for every pull request that is opened in your GitHub repository. Each preview environment is created in isolation on your cluster and is destroyed once the pull request is closed or merged. The first step to enabling preview environments is to configure a preview template. ## Configuring a Preview Template ![Configuring a Preview Template](https://mintcdn.com/porter/bu4QpGFfrvF7pPtg/images/preview-environments/configure-template-button.png?fit=max\&auto=format\&n=bu4QpGFfrvF7pPtg\&q=85\&s=37ad85f65d14cdb03b426603a0c811fc) Preview templates allow you to select an application that is currently deployed to your cluster and configure how it should be deployed as a preview application. This configuration includes: 1. Addons to be created alongside the preview application. This includes things like databases and custom helm charts. ![Configuring Template Addons](https://mintcdn.com/porter/bu4QpGFfrvF7pPtg/images/preview-environments/configure-template-addons.png?fit=max\&auto=format\&n=bu4QpGFfrvF7pPtg\&q=85\&s=16954ccd3b811e309bac0d9a39b6d392) 2. Configuration overrides for services within the application. By default, the preview application will use the same configuration as the currently deployed application, but you can override any of these settings, including environment variables. ![Configuring Template Service Overrides](https://mintcdn.com/porter/bu4QpGFfrvF7pPtg/images/preview-environments/configure-template-service-overrides.png?fit=max\&auto=format\&n=bu4QpGFfrvF7pPtg\&q=85\&s=7070db0f05aec9aabed89a7734d2d940) ## Merging in the GitHub Actions Workflow File After creating a preview template for an application, Porter will open a PR in that application's repository with a workflow file for spinning up a new preview environment when a PR is created. This PR must be merged for preview environments to be created. ## Deploying Multiple Applications to the Same Preview Environment After setting up a preview template, you can further modify the template to specify other applications on your cluster that should be deployed alongside the preview application. Adding a required app will create a copy of that application in the preview environment. ![Configuring Template Required Applications](https://mintcdn.com/porter/bu4QpGFfrvF7pPtg/images/preview-environments/configure-template-required-apps.png?fit=max\&auto=format\&n=bu4QpGFfrvF7pPtg\&q=85\&s=29b85ca3c2bb03319a73e8cf4b3acd88) ### Deploying Multiple Preview Applications If you would like to test changes across multiple applications/repositories, it is also possible to deploy multiple preview applications to the same preview environment. To do so, you must create multiple preview templates (one for each preview application that you would like to deploy). If deploying applications across multiple repositories, the PR branch name must also be the same for the preview applications to get deployed to the same environment. # Sandbox Capacity Source: https://docs.porter.run/sandboxes/capacity Raise the sandbox node group's CPU limit and tune the warm pod pool to run more sandboxes Two settings determine how many sandboxes a cluster can run and how fast they start: the **Max CPU** limit on the sandbox node group, which caps total compute, and the **warm pod pool**, which keeps pre-booted pods ready so sandboxes start quickly. This page covers both and how to size them together. ## How sandbox capacity works When you enable sandboxes, Porter creates a dedicated sandbox node group on the cluster. It's a cost-optimized (Karpenter) node group: nodes are provisioned on demand as sandboxes are created and removed when they're no longer needed, so there are no minimum or maximum node counts to manage. The one capacity setting is **Max CPU**, the maximum total CPU cores across all nodes in the group. The cluster keeps a pool of pre-booted warm pods on the sandbox node group, and every new sandbox claims one. This is what makes startup fast. When more sandboxes are created than there are warm pods available, the pool grows by the number of waiting sandboxes and Karpenter adds nodes to fit the new pods, up to the Max CPU limit. Bursts above the pool size are still served, but they wait for a fresh pod to boot instead of starting instantly. Once the node group is at its Max CPU limit, new sandboxes wait in the `queued` phase until running sandboxes terminate and free capacity. If sandboxes queue regularly, raise Max CPU. ## Raise the node group's Max CPU **1. Navigate to Infrastructure** From your Porter dashboard, click the **Infrastructure** tab in the left sidebar and select your sandbox-enabled cluster. **2. Open the sandbox node group** Click **Overview** and expand the **Sandbox node group** under the node groups list to see the following settings: | Setting | Description | | - | - | | **Max CPU** | Maximum total CPU cores across all nodes in the node group. This is the capacity ceiling: raise it to run more concurrent sandboxes. Defaults to 20 cores. | | **Capacity type** | On-demand or spot instances. | | **Restrict per-instance CPU** | Optional bounds on the vCPUs of each instance Karpenter provisions, to spread sandboxes across more, smaller nodes. See [node groups](/cloud-accounts/node-groups#restricting-per-instance-size). | | **Disk size** | Storage capacity for each node (default: 50GB). | **3. Update** Set **Max CPU** to the new ceiling, then scroll to the bottom and click **Update**. The change applies without disrupting running sandboxes, and Karpenter provisions nodes up to the new limit as demand arrives. > [!NOTE] > > Two settings are fixed on sandbox node groups: cost optimization can't be turned off, because sandbox node groups require Karpenter, and instance selection is restricted to machine types with local NVMe disks, which sandboxes use for fast storage. ## Tune the warm pod pool The warm pod pool is configured with the other sandbox settings, not on the node group. Changing it requires the admin role on the project. **1. Open the sandbox settings** In the Porter Dashboard, go to the **Sandbox** tab for your sandbox-enabled cluster, then open **Settings** and find the **Warm pod pool** section. **2. Configure the pool** | Setting | Description | | - | - | | **Warm pool size** | Number of pre-booted pods the cluster keeps available. This is how many sandboxes can start instantly at once. It does not cap how many sandboxes can run. | | **CPU** | CPU cores each warm pod gets (0.1 to 16). | | **RAM** | Memory each warm pod gets, in MiB (64 to 32768). | **3. Save** Click **Save**. Porter rolls the new pool out on the cluster, replacing existing warm pods with pods at the new size and resources. ## Custom sandbox sizes The warm pod pool's **CPU** and **RAM** settings are the default size every sandbox gets. A single sandbox can ask for a different size at create time, which is useful when most of your workloads fit the default but a few need more or less room. ```bash CLI theme={null} porter sandbox create python:3.12 --cpu 2 --memory 4Gi -- python train.py ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "python:3.12", command: ["python", "train.py"], resources: { cpu: "2", memory: "4Gi" }, }); ``` ```python Python theme={null} sandbox = porter.sandboxes.create( image="python:3.12", command=["python", "train.py"], resources={"cpu": "2", "memory": "4Gi"}, ) ``` CPU is in cores: pass a whole number (`2`), or an `m` suffix for a fraction, where `500m` is half a core. Memory takes a `Gi` or `Mi` suffix (`4Gi`, `512Mi`). If you only set one, the other will keep the default. A larger sandbox needs a node with room for it, so it may take longer to start than a default-sized one. The cluster places it on a node that can fit it if one exists, otherwise it will add a node to accomodate. A large sandbox also draws more of the node group's **Max CPU**, so a handful of them reduces how many others can run at once. Each cluster caps how large a single sandbox may be for sanity - the current limit is 256 cores & 1TB of memory. If for whatever reason you have a use-case where you need this increased, please reach out! ## Size the two settings together Warm pods reserve their CPU and memory on the sandbox node group even while idle, so the pool consumes Max CPU headroom. As a rule of thumb, keep ``` (warm pool size + expected concurrent sandboxes) × CPU per pod ``` comfortably under **Max CPU**, with some slack for per-node system overhead. The settings fail in different ways when they're out of balance. Raising the warm pool size without room under Max CPU leaves the extra warm pods waiting for nodes that never come, so starts don't get any faster. Raising Max CPU alone lets more sandboxes run, but the number that can start instantly is still the pool size. ## Next steps * [Sandboxes Overview](/sandboxes/overview#lifecycle) * [Sandboxes Getting Started](/sandboxes/getting-started) * [Node groups](/cloud-accounts/node-groups) # Sandboxes CLI Source: https://docs.porter.run/sandboxes/cli Enable, create, list, exec into, monitor CPU and memory usage, snapshot, and terminate Porter Sandboxes from the Porter CLI `porter sandbox` contains commands for enabling sandboxes on a cluster, listing, inspecting, executing commands in, monitoring CPU and memory usage of, snapshotting, and terminating sandboxes in the currently selected Porter project and cluster. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) * You're connected to the correct cluster by running [porter config set-cluster](/standard/cli/command-reference/porter-config) * Sandboxes are enabled on the selected AWS cluster. Run [`porter sandbox status`](#porter-sandbox-status) to check, and [`porter sandbox enable`](#porter-sandbox-enable) to enable them if they are not If the CLI cannot find a selected project or cluster, it asks you to run `porter config` or pass `--project` and `--cluster`. *** ## `porter sandbox enable` Enables sandbox workloads on the configured EKS cluster in the current project. Porter adds a sandbox node group and provisions it asynchronously. The command is idempotent: enabling an already-enabled cluster succeeds without creating a new revision. **Usage:** ```bash theme={null} porter sandbox enable [flags] ``` **Options:** | Flag | Description | | - | - | | `-w, --wait` | Block until sandboxes are ready to use. Exits with code `7` on timeout | | `--wait-timeout` | How long to wait when `--wait` is set, as a Go duration (`5m`, `30m`, `1h`). Defaults to `1h` | | `--json` | Print the result as JSON | `--wait` polls sandbox readiness every 10 seconds until the runtime is ready or the timeout elapses. Pass `--wait-timeout` only alongside `--wait`; passing it alone fails validation. The command targets the cluster selected in your Porter config. The cluster must be an AWS EKS cluster; the command exits with code `4` on any other provider. **Exit codes:** | Exit code | Meaning | | - | - | | `0` | Success | | `1` | Unexpected error | | `3` | Not permitted (sandboxes not enabled for this project) | | `4` | Not applicable (cluster is not an EKS cluster) | | `5` | Cluster not found | | `6` | Conflict (cluster is busy) | | `7` | Timed out waiting for readiness | ```bash Enable theme={null} porter sandbox enable ``` ```bash Enable and Wait theme={null} porter sandbox enable --wait ``` ```bash Enable with Custom Timeout theme={null} porter sandbox enable --wait --wait-timeout 30m ``` ```bash JSON Output theme={null} porter sandbox enable --json ``` ### Enable at cluster creation Pass `--enable-sandboxes` to `porter clusters create` to provision a new AWS cluster with sandboxes already enabled, so you do not have to run `porter sandbox enable` as a follow-up step. The flag is AWS-only and adds some provisioning time; the cost confirmation notes that a sandbox node group will be added and billed by usage. ```bash Create Cluster with Sandboxes theme={null} porter clusters create --name my-cluster --region us-east-1 --cloud-account-id --enable-sandboxes ``` *** ## `porter sandbox status` Reports whether sandboxes are enabled on the configured EKS cluster and whether the sandbox runtime is ready to use. Use this as a pollable state check before or after enabling sandboxes. **Usage:** ```bash theme={null} porter sandbox status [flags] ``` **Options:** | Flag | Description | | - | - | | `--json` | Print the result as JSON | Output includes: | Field | Description | | - | - | | `Cluster` | Cluster ID being reported on | | `Sandboxes enabled` | Whether the sandbox node group has been requested for the cluster | | `Ready` | Whether the sandbox runtime is ready to accept `porter sandbox create` calls | **Exit codes** are the same as [`porter sandbox enable`](#porter-sandbox-enable), except `--wait` does not apply, so `7` is not returned. ```bash Show Status theme={null} porter sandbox status ``` ```bash JSON Output theme={null} porter sandbox status --json ``` ```bash Script Readiness theme={null} porter sandbox status --json | jq -r '.ready' ``` *** ## `porter sandbox disable` Disables sandbox workloads on the configured EKS cluster. The sandbox node group tears down in the background. The command is idempotent: disabling an already-disabled cluster succeeds without creating a new revision. **Usage:** ```bash theme={null} porter sandbox disable [flags] ``` **Options:** | Flag | Description | | - | - | | `--json` | Print the result as JSON | All running sandboxes must be terminated first. If any are still running, the command fails with a conflict (exit code `6`); run [`porter sandbox terminate --all`](#porter-sandbox-terminate) and retry. **Exit codes** match [`porter sandbox enable`](#porter-sandbox-enable), except `--wait` does not apply, so `7` is not returned. ```bash Disable theme={null} porter sandbox disable ``` ```bash JSON Output theme={null} porter sandbox disable --json ``` ```bash Terminate Then Disable theme={null} porter sandbox terminate --all && porter sandbox disable ``` *** ## `porter sandbox create` Creates a sandbox from a container image in the current project and cluster. The image accepts a tag (`alpine:3.20`) or a digest (`repo@sha256:`). Pass `--from-snapshot` instead of an image to start from a [snapshot](#porter-sandbox-snapshot). **Usage:** ```bash theme={null} porter sandbox create [-- [args...]] [flags] porter sandbox create --from-snapshot [-- [args...]] [flags] ``` **Options:** | Flag | Description | | - | - | | `--name` | Cluster-unique sandbox name. Use lowercase letters, numbers, and hyphens; start and end with a letter or number. Use this name with `exec`, `logs`, and `terminate` | | `--command` | Override the image entrypoint. Repeat for each argv element | | `--arg` | Argument passed to the command. Repeatable | | `-e, --env` | Environment variable in `KEY=VALUE` form. Repeatable | | `--env-group` | Environment group on the cluster whose variables are injected into the sandbox. Repeatable | | `--tag` | Tag in `key=value` form. Repeatable | | `--volume` | Volume mount in `mount_path=volume-ref` form, where `volume-ref` is a volume name or ID. Repeatable. The mount path must be absolute | | `--ttl` | Maximum sandbox lifetime as a Go duration, such as `30m` or `2h`. Defaults to no limit | | `--cpu` | CPU cores, such as `2` or `500m` for half a core. Defaults to the cluster's sandbox size | | `--memory` | Memory, such as `4Gi` or `512Mi`. Defaults to the cluster's sandbox size | | `--from-snapshot` | Start the sandbox from a snapshot ID instead of an image. Omit the image argument | | `--json` | Print the created sandbox as JSON | Use the positional form after `--` for common one-shot commands. Use `--command` and `--arg` when scripting individual argv elements. Volume values can be volume names or volume IDs. The CLI resolves names first, then falls back to treating the value as an ID. `--env-group` injects the variables of a named [environment group](/applications/configure/environment-groups) on the cluster. Values are resolved to the group's latest version at create time and do not update afterwards. On a key conflict, a later `--env-group` wins over an earlier one, and an explicit `--env` wins over any group value. Creation fails if a named group does not exist on the cluster or has not synced yet. `--ttl` bounds the sandbox's lifetime regardless of its main process: once the duration elapses, counted from creation, Porter terminates the sandbox the same way an explicit `terminate` does. See [sandbox lifetime](/sandboxes/overview#sandbox-lifetime). `--cpu` and `--memory` size an individual sandbox instead of using the cluster's default sandbox size. Pass one or both; the flag you omit keeps the default. CPU is in cores, either a whole number (`2`) or an `m` suffix for a fraction (`500m` is half a core), and memory takes a `Gi` or `Mi` suffix (`4Gi`, `512Mi`). See [custom sandbox sizes](/sandboxes/capacity#custom-sandbox-sizes). `--from-snapshot` starts the sandbox from the filesystem that a [snapshot](#porter-sandbox-snapshot) captured, so the image argument is omitted. A snapshot holds files only: an omitted command runs the entrypoint of the image the snapshot was taken from, and the sandbox mounts only the volumes you pass with `--volume`. Every other flag works the same as it does with an image. Sandbox names currently cannot be reused, even after the sandbox is terminated. Omit `--name` only for one-off sandboxes where you do not need stable lookup later. The sandbox runs as long as its main process. When the process exits, the sandbox moves to `succeeded` or `failed` and stops accepting exec calls, so pass a long-running command if you plan to exec into the sandbox. See [sandbox lifetime](/sandboxes/overview#sandbox-lifetime). ```bash Create Sandbox theme={null} porter sandbox create alpine:3.20 ``` ```bash Named Sandbox theme={null} porter sandbox create alpine:3.20 --name web -- sleep 3600 ``` ```bash With Env and Tags theme={null} porter sandbox create ghcr.io/example/tool:latest --env PORT=8080 --tag env=dev --tag owner=me ``` ```bash With Volume theme={null} porter sandbox create ubuntu:24.04 --volume /workspace=my-data -- bash -lc 'ls -la /workspace' ``` ```bash With Env Group theme={null} porter sandbox create alpine:3.20 --env-group my-env-group -- sleep 3600 ``` ```bash With TTL theme={null} porter sandbox create alpine:3.20 --ttl 2h -- sleep 7200 ``` ```bash With a Custom Size theme={null} porter sandbox create python:3.12 --cpu 2 --memory 4Gi -- python train.py ``` ```bash From a Snapshot theme={null} porter sandbox create --from-snapshot 11790e2e-939f-4ce9-94ef-7f82a7d404cc --name web-2 -- sleep 3600 ``` ```bash JSON Output theme={null} porter sandbox create ubuntu:24.04 --volume /workspace=my-data --command bash --arg -lc --arg 'ls -la' --json ``` *** ## `porter sandbox list` Lists sandboxes in the current project and cluster. The command auto-paginates through the API and sorts results most-recent-first. **Usage:** ```bash theme={null} porter sandbox list [flags] ``` **Options:** | Flag | Description | | - | - | | `--phase` | Filter by phase. Defaults to hiding `terminated` sandboxes. Use `all` to include every phase | | `--tag` | Filter by tag in `key=value` format. Repeat the flag to require multiple tags | | `--json` | Print sandboxes as JSON | **Phases:** | Phase | Description | | - | - | | `queued` | Accepted by the API and waiting for capacity | | `creating` | Runtime is being prepared and the container is starting | | `running` | Sandbox is ready and can accept exec calls | | `succeeded` | Sandbox command completed successfully | | `failed` | Sandbox command or runtime failed | | `terminated` | Sandbox was explicitly terminated | | `all` | Includes every phase when used with `--phase` | Output includes these columns: | Column | Description | | - | - | | `NAME` | Sandbox name | | `IMAGE` | Container image | | `PHASE` | Current sandbox phase | | `CREATED` | Creation timestamp | | `STARTED` | Start timestamp, when available | | `EXIT` | Exit code, when available | | `TAGS` | Sandbox tags | ```bash List Sandboxes theme={null} porter sandbox list ``` ```bash Include Terminated theme={null} porter sandbox list --phase all ``` ```bash Filter by Phase theme={null} porter sandbox list --phase running ``` ```bash Filter by Tags theme={null} porter sandbox list --tag workflow=agent-run --tag run=2026-06-17 ``` ```bash JSON Output theme={null} porter sandbox list --json ``` ```bash Script Running Names theme={null} porter sandbox list --json | jq -r '.[] | select(.phase == "running") | .name' ``` *** ## `porter sandbox exec` Runs a command in a running sandbox, identified by name. Non-interactive exec prints stdout and stderr; interactive exec opens a shell session. **Usage:** ```bash theme={null} porter sandbox exec [-it] -- [args...] [flags] ``` **Options:** | Flag | Description | | - | - | | `-c, --command` | Shell-style command string to run through `sh -c` inside the sandbox | | `-i, --interactive` | Keep stdin open and stream it to the sandbox | | `-t, --tty` | Allocate a TTY for an interactive shell | Do not combine `--command` with positional command arguments. **Exit codes:** | Exit code | Meaning | | - | - | | `0` | Command succeeded | | `1` | Operational error or propagated sandbox command failure | | `2` | CLI usage error | | `3` | Sandbox is not in the `running` phase | Only `running` sandboxes accept exec calls. ```bash List Files theme={null} porter sandbox exec abc123 -- ls -la /workspace ``` ```bash Check Python theme={null} porter sandbox exec abc123 -- python --version ``` ```bash Shell Command theme={null} porter sandbox exec abc123 --command "echo hi && ls /workspace" ``` ```bash Inline Script theme={null} porter sandbox exec abc123 -- python -c 'print("ok")' ``` ```bash Interactive Shell theme={null} porter sandbox exec web -it -- sh ``` *** ## `porter sandbox logs` Fetches and prints a batch of log lines for a sandbox. By default, the CLI requests logs from the last hour with a limit of 500 lines. **Usage:** ```bash theme={null} porter sandbox logs [flags] ``` **Options:** | Flag | Description | | - | - | | `--since` | Lookback window as a Go duration, such as `30m` or `1h30m`. Defaults to `1h` | | `--limit` | Maximum number of log lines. Defaults to `500`; server cap is `5000`; `0` uses the server default | | `--tail` | Alias for `--limit`. When greater than `0`, it overrides `--limit` | | `--level` | Client-side filter: `info`, `warning`, or `error` | | `--no-timestamps` | Suppress the leading RFC3339 timestamp on each line | Each rendered log line uses this format: ```text theme={null} [] ``` ```bash Recent Logs theme={null} porter sandbox logs abc123 ``` ```bash Last 15 Minutes theme={null} porter sandbox logs abc123 --since 15m --tail 100 ``` ```bash Errors Only theme={null} porter sandbox logs abc123 --level error --no-timestamps ``` ```bash Search Logs theme={null} porter sandbox logs abc123 --tail 200 | grep -i 'panic\|fatal' ``` *** ## `porter sandbox metrics` Use this command to monitor a sandbox's resource usage: how much CPU and memory it is consuming and how close it is to its limits. It prints p50 and p90 CPU and memory usage over a lookback window for a running sandbox, alongside the sandbox's CPU and memory limits and each percentile's utilization as a percentage of that limit. Only `running` sandboxes report metrics. **Usage:** ```bash theme={null} porter sandbox metrics [flags] ``` **Options:** | Flag | Description | | - | - | | `--since` | Lookback window as a Go duration, such as `30m` or `2h`. Bounded to `[3m, 24h]`. Defaults to `1h` | | `--json` | Print the metrics summary as JSON | CPU is reported in cores and memory in bytes. Utilization is each percentile divided by the sandbox's limit, as a percentage; it can exceed 100% because the limit is a throttling and OOM threshold, not a hard cap on the reported sample. Memory values include page cache and slightly overestimate resident memory. The summary has one row per metric (`cpu_cores`, `mem_bytes`) with these columns: | Column | Description | | - | - | | `metric` | Metric name | | `p50` | Median value over the window | | `p90` | 90th-percentile value over the window | | `limit` | The sandbox's limit for this metric | | `p50_util_pct` | `p50` as a percentage of `limit` | | `p90_util_pct` | `p90` as a percentage of `limit` | | `window_seconds` | Length of the lookback window in seconds | `--json` prints the same values at full precision on one line, plus a `has_data` boolean that is `false` when no samples were collected in the window. ```bash Recent Metrics theme={null} porter sandbox metrics web ``` ```bash Last 30 Minutes theme={null} porter sandbox metrics web --since 30m ``` ```bash JSON Output theme={null} porter sandbox metrics web --json ``` ```bash Script p90 CPU Utilization theme={null} porter sandbox metrics web --json | jq '.cpu_util_p90_pct' ``` Output looks like this: ```text theme={null} ┌──────────────┬────────────────────┬────────────────────┐ │ METRIC │ P50 │ P90 │ ├──────────────┼────────────────────┼────────────────────┤ │ cpu cores │ 0.001 cores (0.1%) │ 0.003 cores (0.3%) │ │ cpu limit │ 1.000 cores │ │ │ memory │ 1.27 GiB (63.6%) │ 2.54 GiB (126.9%) │ │ memory limit │ 2.00 GiB │ │ └──────────────┴────────────────────┴────────────────────┘ window: 1h0m0s ``` *** ## `porter sandbox terminate` Terminates one sandbox by name, or terminates many sandboxes with `--all`. **Usage:** ```bash theme={null} porter sandbox terminate [flags] porter sandbox terminate --all [flags] ``` **Options:** | Flag | Description | | - | - | | `--all` | Terminate every actionable sandbox in the current project and cluster | | `--dry-run` | Print the sandboxes that would be terminated without terminating them | | `--tag` | Scope bulk termination by tag in `key=value` format. Requires `--all` | **Behavior:** | Case | Behavior | | - | - | | `--all` | No interactive confirmation is shown | | Already terminated sandboxes | Skipped | | `404` during `--all` | Treated as success because another caller may have already deleted the sandbox | ```bash Terminate One theme={null} porter sandbox terminate abc123 ``` ```bash Dry Run Bulk Terminate theme={null} porter sandbox terminate --all --dry-run ``` ```bash Terminate by Tag Dry Run theme={null} porter sandbox terminate --all --tag workflow=agent-run --dry-run ``` ```bash Terminate by Tag theme={null} porter sandbox terminate --all --tag workflow=agent-run ``` *** ## `porter sandbox snapshot` Captures the filesystem of a running sandbox as a snapshot that later sandboxes can start from. The sandbox keeps running. The capture pauses it for a few milliseconds. **Usage:** ```bash theme={null} porter sandbox snapshot [flags] ``` **Options:** | Flag | Description | | - | - | | `--json` | Print the snapshot as JSON | The sandbox may be given by name or ID and must be in the `running` phase. The command returns once the capture is complete, so the snapshot ID it prints is ready to pass to `porter sandbox create --from-snapshot`. A snapshot captures the sandbox's filesystem: the image it started from plus every file written since. Running processes and mounted volumes are not included. A sandbox started from a snapshot runs its command from the beginning with the captured files in place, and mounts only the volumes you pass at create time. Use snapshots to save an environment after a setup step, such as installing dependencies or cloning a repository, and then start many sandboxes from that point instead of repeating the setup in each one. ```bash Snapshot by Name theme={null} porter sandbox snapshot my-sandbox ``` ```bash JSON Output theme={null} porter sandbox snapshot my-sandbox --json ``` ```bash Snapshot and Restore theme={null} SNAPSHOT=$(porter sandbox snapshot my-sandbox --json | jq -r .id) porter sandbox create --from-snapshot "$SNAPSHOT" --name my-sandbox-2 -- sleep 3600 ``` *** ## `porter sandbox snapshots` Manages the snapshots captured from sandboxes in the current project and cluster. ### `porter sandbox snapshots list` Lists snapshots, most recent first. **Usage:** ```bash theme={null} porter sandbox snapshots list [flags] ``` **Options:** | Flag | Description | | - | - | | `--sandbox` | Only list snapshots captured from this sandbox, by name or ID | | `--json` | Print snapshots as JSON | **Snapshot statuses:** | Status | Description | | - | - | | `pending` | The capture is still running | | `ready` | The snapshot can be used with `porter sandbox create --from-snapshot` | | `failed` | The capture failed. The record is kept so the failure reason stays visible in the JSON output | Output includes these columns: | Column | Description | | - | - | | `ID` | Snapshot ID | | `SANDBOX` | ID of the sandbox the snapshot was captured from | | `STATUS` | Current snapshot status | | `SIZE` | Captured size, or `-` until the capture completes | | `CAPTURED` | Timestamp the capture completed, or `-` until it does | ```bash List Snapshots theme={null} porter sandbox snapshots list ``` ```bash Snapshots of One Sandbox theme={null} porter sandbox snapshots list --sandbox my-sandbox ``` ```bash JSON Output theme={null} porter sandbox snapshots list --json ``` ```bash Ready Snapshot IDs theme={null} porter sandbox snapshots list --json | jq -r '.[] | select(.status == "ready") | .id' ``` ### `porter sandbox snapshots delete` Deletes a snapshot and the files it captured. **Usage:** ```bash theme={null} porter sandbox snapshots delete ``` The server rejects deletion while the capture is still running, or while a sandbox started from the snapshot has not finished. Wait for the capture, or terminate those sandboxes first. ```bash Delete a Snapshot theme={null} porter sandbox snapshots delete 11790e2e-939f-4ce9-94ef-7f82a7d404cc ``` *** ## `porter sandbox volume` Manages persistent volumes that sandboxes can mount at launch. Create a volume first, then reference it from `porter sandbox create` with `--volume =`, where `volume-ref` is a volume name or ID. ### `porter sandbox volume list` Lists sandbox volumes in the current project and cluster, sorted most-recent-first. **Usage:** ```bash theme={null} porter sandbox volume list [flags] ``` **Options:** | Flag | Description | | - | - | | `--json` | Print volumes as JSON | **Volume phases:** | Phase | Description | | - | - | | `pending` | The volume was created and is waiting for the underlying claim to bind | | `ready` | The volume is ready to mount into sandboxes | | `failed` | The volume failed to provision | Output includes these columns: | Column | Description | | - | - | | `NAME` | Volume name | | `PHASE` | Current volume phase | | `ATTACHED` | Attached sandboxes, or `-` when unattached | | `CREATED` | Creation timestamp | ```bash List Volumes theme={null} porter sandbox volume list ``` ```bash JSON Output theme={null} porter sandbox volume list --json ``` ```bash Script Volume Names theme={null} porter sandbox volume list --json | jq -r '.[].name' ``` ```bash Ready Volume Names theme={null} porter sandbox volume list | awk -F'\t' '$2=="ready" {print $1}' ``` ### `porter sandbox volume create` Creates a persistent volume on the current cluster. The volume name may contain lowercase letters, numbers, and hyphens, and must start and end with a letter or number. Omit the name only for one-off volumes where you do not need stable lookup later. Volumes start in the `pending` phase. Sandboxes that mount them wait for the underlying claim to bind, so `porter sandbox create` does not need a separate wait step. Volume names must be unique within a cluster for the lifetime of the volume. After a volume is deleted, its name can be used again. With `--bucket`, the volume is an [object volume](/sandboxes/volumes#object-volumes) exposing an S3 bucket registered with `porter storage bucket register`. Use `--prefix` to scope it to keys under a prefix and `--access` to restrict how sandboxes can use it. **Usage:** ```bash theme={null} porter sandbox volume create [name] [flags] ``` **Options:** | Flag | Description | | - | - | | `--json` | Print the created volume as JSON | | `--bucket` | Create an object volume exposing this registered S3 bucket | | `--prefix` | Scope an object volume to keys under this prefix. Requires `--bucket` | | `--access` | Access mode for an object volume: `read-write` (default), `read-only`, or `write-only-new-files`. Requires `--bucket` | ```bash Create Named Volume theme={null} porter sandbox volume create my-data ``` ```bash Create Generated Name theme={null} porter sandbox volume create ``` ```bash Create Object Volume theme={null} porter sandbox volume create datasets --bucket my-bucket --prefix training/v1 --access read-only ``` ```bash JSON Output theme={null} porter sandbox volume create my-data --json ``` ### `porter sandbox volume get` Shows a single sandbox volume by name, including phase, creation time, and currently attached sandboxes. **Usage:** ```bash theme={null} porter sandbox volume get [flags] ``` **Options:** | Flag | Description | | - | - | | `--json` | Print the volume as JSON | ```bash Get by Name theme={null} porter sandbox volume get my-data ``` ```bash JSON Output theme={null} porter sandbox volume get my-data --json ``` ### `porter sandbox volume files` Lists the files and directories under a path inside a volume. Omit the path to list from the volume root. The server walks the tree up to an entry budget; directories it could not fully read are marked truncated. List them directly to see more. **Usage:** ```bash theme={null} porter sandbox volume files [path] [flags] ``` **Options:** | Flag | Description | | - | - | | `--search` | Only show entries whose name contains this substring (case-insensitive) | | `--json` | Print the full nested listing as JSON | ```bash List From Root theme={null} porter sandbox volume files my-data ``` ```bash List a Subdirectory theme={null} porter sandbox volume files my-data models/checkpoints ``` ```bash Filter by Name theme={null} porter sandbox volume files my-data --search .bin ``` ```bash JSON Output theme={null} porter sandbox volume files my-data --json | jq '.entries[].name' ``` ### `porter sandbox volume read` Streams a file's raw bytes from a volume to stdout. Redirect stdout to save the file locally, or use `--range` to read part of a large file. **Usage:** ```bash theme={null} porter sandbox volume read [flags] ``` **Options:** | Flag | Description | | - | - | | `--range` | Single byte range to read (`start-end`, `start-`, or `-length`). Defaults to the whole file | Reading a path that does not exist fails with a not-found error. ```bash Print a File theme={null} porter sandbox volume read my-data config/app.yaml ``` ```bash Save Locally theme={null} porter sandbox volume read my-data models/model.bin > model.bin ``` ```bash Read a Byte Range theme={null} porter sandbox volume read my-data logs/app.log --range 0-4095 ``` ### `porter sandbox volume write` Uploads a file into a volume at a path relative to the volume root. Parent directories are created as needed, and an existing file at the path is replaced. Content comes from stdin unless `--file` names a local file. The write is staged to a temporary file and renamed into place, so a failed or interrupted upload leaves the previous contents alone rather than a truncated file. You can write to a volume whether or not a sandbox has it mounted. **Usage:** ```bash theme={null} porter sandbox volume write [flags] ``` **Options:** | Flag | Description | | - | - | | `--file` | Local file to upload. Defaults to reading stdin | A single write is capped at 1 GiB, and a request from outside the cluster must finish within 30 seconds; write very large files from inside a sandbox that mounts the volume. ```bash Upload a Local File theme={null} porter sandbox volume write my-data config/app.yaml --file ./app.yaml ``` ```bash Redirect a File to stdin theme={null} porter sandbox volume write my-data config/app.yaml < app.yaml ``` ```bash Pipe Content theme={null} echo hello | porter sandbox volume write my-data notes.txt ``` ### `porter sandbox volume move` Moves a file or directory to a new path inside one volume. The destination is the entry's full new path, not a directory to drop it into, so a move renames and relocates in one call. A directory moves with everything under it. **Usage:** ```bash theme={null} porter sandbox volume move ``` The destination's parent directory must already exist, and nothing is replaced. The server refuses a move onto an occupied path, and the volume root cannot be moved or overwritten. ```bash Rename a File theme={null} porter sandbox volume move my-data notes.txt archive/notes.txt ``` ```bash Move a Directory theme={null} porter sandbox volume move my-data models/v1 models/v2 ``` ### `porter sandbox volume delete` Deletes a sandbox volume by name. **Usage:** ```bash theme={null} porter sandbox volume delete ``` The server rejects deletion while the volume is attached to any sandbox. Terminate or recreate the attached sandboxes first. ```bash Delete by Name theme={null} porter sandbox volume delete my-data ``` ## Common Workflows | Workflow | Command | | - | - | | Create a named sandbox | `porter sandbox create alpine:3.20 --name web -- sleep 3600` | | Create a sandbox that cleans itself up | `porter sandbox create alpine:3.20 --ttl 2h -- sleep 7200` | | Create a sandbox with an env group | `porter sandbox create alpine:3.20 --env-group my-env-group -- sleep 3600` | | Create and mount a volume | `porter sandbox volume create my-data && porter sandbox create ubuntu:24.04 --volume /workspace=my-data -- bash` | | Snapshot a running sandbox | `porter sandbox snapshot ` | | Start a sandbox from a snapshot | `porter sandbox create --from-snapshot -- sleep 3600` | | Upload a local file to a volume | `porter sandbox volume write my-data config/app.yaml --file ./app.yaml` | | Read a file back from a volume | `porter sandbox volume read my-data config/app.yaml` | | List running sandboxes | `porter sandbox list --phase running` | | Get running sandbox names for a script | `porter sandbox list --json \| jq -r '.[] \| select(.phase == "running") \| .name'` | | Run a smoke command | `porter sandbox exec -- python -c 'print("ok")'` | | Fetch recent errors | `porter sandbox logs --since 30m --level error` | | Check CPU and memory usage | `porter sandbox metrics --since 30m` | | Preview tagged cleanup | `porter sandbox terminate --all --tag run= --dry-run` | | Clean up tagged sandboxes | `porter sandbox terminate --all --tag run=` | ## Related Docs * [Sandboxes Overview](/sandboxes/overview) * [Sandboxes Getting Started](/sandboxes/getting-started) * [Python Sandbox SDK quickstart](/sandboxes/sdk/python/quickstart) * [TypeScript Sandbox SDK quickstart](/sandboxes/sdk/typescript/quickstart) # Sandboxes Getting Started Source: https://docs.porter.run/sandboxes/getting-started Enable Porter Sandboxes on a cluster and launch your first sandbox with the Sandbox SDK Porter Sandboxes are isolated container workloads that you launch from code running inside your Porter cluster. Use them for code interpreters, agent tools, batch fan-out, and other on-demand workloads that may need persistent storage through volumes. ## Prerequisites * A Porter project * An AWS cluster where you want to run sandboxes. We recommend creating a new AWS cluster for sandboxes so sandbox workloads are isolated from your other running workloads. > [!NOTE] > > A dedicated cluster is not required, it's a defense-in-depth recommendation. Sandboxes often run untrusted code (end-user submissions, LLM agent output), and running them in their own cluster isolates that code from your main workloads as much as possible. Sharing a cluster with your other workloads is reasonable for development and testing; for production workloads that run untrusted code, we recommend the separate cluster. ## Enable sandboxes **1. Enable sandboxes from the Sandbox tab** Sandboxes can only be enabled on AWS clusters. In the Porter Dashboard, navigate to the Sandbox tab for the AWS cluster where you want to run sandboxes and click **Enable sandboxes**. ![Sandbox tab with the enable sandboxes button](https://mintcdn.com/porter/6TPN4-7r0xlBqsgK/images/sandboxes/enable-sandboxes.webp?fit=max\&auto=format\&n=6TPN4-7r0xlBqsgK\&q=85\&s=ded29e0ca341fef6f30de1562cd86896) Or from the CLI, after selecting the cluster with [`porter config set-cluster`](/standard/cli/command-reference/porter-config): ```bash theme={null} porter sandbox enable --wait ``` You can also pass `--enable-sandboxes` when creating a new cluster with `porter clusters create` to provision it with sandboxes already enabled. See the [Sandboxes CLI guide](/sandboxes/cli#porter-sandbox-enable) for `porter sandbox status` and `porter sandbox disable`. **2. Install a Sandbox SDK in your application** Add either the Python or TypeScript Sandbox SDK to the application that will create and manage sandboxes. ![Sandbox tab after sandboxes are enabled](https://mintcdn.com/porter/6TPN4-7r0xlBqsgK/images/sandboxes/sandbox-list.webp?fit=max\&auto=format\&n=6TPN4-7r0xlBqsgK\&q=85\&s=883521b92dbf67b87459c5d13b3f0051) **3. Write your first sandbox call** In your application code, create a sandbox, execute a command, read the output, and terminate the sandbox when the work is done. The examples below show the smallest end-to-end flow. **4. Deploy your application to the sandbox cluster** For now, we recommend deploying the application that uses the SDK as a Porter Application in the same cluster where you want to run sandboxes. ## Calling from outside the cluster The SDK connects to the in-cluster Sandbox API automatically when your application runs as a Porter Application in the same cluster where sandboxes are enabled. To invoke sandboxes from anywhere else, authenticate with a Porter API token and tell the SDK which cluster to target: ```bash theme={null} export PORTER_SANDBOX_API_KEY="" export PORTER_CLUSTER_ID="" ``` The SDK reads the project from the token and routes through the Porter API at `dashboard.porter.run` automatically. Replace `` with the cluster where sandboxes are enabled. You can copy the prefilled snippet from the cluster's Sandbox tab, or look up the ID with the [Porter CLI](/cli/basic-usage): ```bash theme={null} $ porter clusters list --json [ { "id": 2, "name": "my-sandbox-cluster", "provider": "aws", "region": "us-east-1", "status": "ready", "vanity_name": "my-sandbox-cluster", "cloud_account_id": "00000000-0000-0000-0000-000000000000", "created_at": "2026-01-15T10:30:00Z" } ] ``` You can create an API token from **Settings > API tokens** in the Porter Dashboard. Creating API tokens requires admin permissions, and the token itself must have developer permissions to authenticate with the Sandbox API. To point the SDK at a specific URL instead, set `PORTER_SANDBOX_BASE_URL` or pass `base_url` (Python) / `baseUrl` (TypeScript) to the client constructor. These take precedence over everything above. ## Python quickstart Install the SDK in your application image: ```bash theme={null} pip install porter-sandbox ``` Create a sandbox, run a command, print the output, and terminate it: ```python theme={null} from porter_sandbox import Porter with Porter() as porter: sandbox = porter.sandboxes.create( image="python:3.12-slim", name="getting-started-python", tags={"example": "getting-started"}, ) result = sandbox.exec(["python", "-c", "print(2 + 2)"]) print(result.stdout) logs = sandbox.logs(limit=100) print(logs) sandbox.terminate() ``` ## TypeScript quickstart Install the SDK in your application image: ```bash theme={null} npm install porter-sandbox ``` Create a sandbox, run a command, print the output, and terminate it: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", name: "getting-started-typescript", tags: { example: "getting-started" }, }); const result = await sandbox.exec(["python", "-c", "print(2 + 2)"]); console.log(result.stdout); const logs = await sandbox.logs({ limit: 100 }); console.log(logs); await sandbox.terminate(); porter.close(); ``` ## Keep a sandbox alive for exec A sandbox lives as long as its main process: the image's default entrypoint, or the command you pass at create time. When that process exits, the sandbox moves to `succeeded` (exit code `0`) or `failed` (nonzero), and it stops accepting exec calls. An image whose default command exits immediately reaches `succeeded` within a few seconds of starting. If you want to create a sandbox first and exec into it later, give it a long-running main process, then terminate it when the work is done: ```python Python theme={null} sandbox = porter.sandboxes.create( image="alpine:3.20", name="worker", command=["sleep", "3600"], ) result = sandbox.exec(["echo", "ok"]) print(result.stdout) sandbox.terminate() ``` ```bash CLI theme={null} porter sandbox create alpine:3.20 --name worker -- sleep 3600 porter sandbox exec worker -- echo ok porter sandbox terminate worker ``` ## Set environment variables Pass environment variables at create time. The SDKs take an `env` map, and the CLI takes a repeatable `--env KEY=VALUE` flag: ```python Python theme={null} sandbox = porter.sandboxes.create( image="alpine:3.20", env={"API_KEY": "secret", "LOG_LEVEL": "debug"}, ) ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "alpine:3.20", env: { API_KEY: "secret", LOG_LEVEL: "debug" }, }); ``` ```bash CLI theme={null} porter sandbox create alpine:3.20 --env API_KEY=secret --env LOG_LEVEL=debug ``` ### Inject environment groups Instead of passing variables one at a time, you can inject an entire [environment group](/applications/configure/environment-groups) from the cluster at create time. Any kind of environment group works, including groups synced from Doppler, Infisical, or a datastore. The SDKs take an `env_groups` list, and the CLI takes a repeatable `--env-group` flag: ```python Python theme={null} sandbox = porter.sandboxes.create( image="alpine:3.20", env_groups=["my-env-group"], ) ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "alpine:3.20", env_groups: ["my-env-group"], }); ``` ```bash CLI theme={null} porter sandbox create alpine:3.20 --env-group my-env-group -- sleep 3600 ``` Group values are resolved to the group's latest version at create time and do not update afterwards. On a key conflict, a later group wins over an earlier one, and an explicit `env` entry wins over any group value. ## Keep secrets out of exec commands Porter by default records the command line of every exec in the sandbox's activity feed, so you can audit what ran (or failed) in a sandbox. Don't pass secrets such as API keys as exec arguments. Set them as [environment variables](#set-environment-variables) at create time, or write them to a [volume](/sandboxes/volumes) and read the file from inside the sandbox. To stop recording arguments, open **Settings** in the **Sandbox** tab and turn off **Record command arguments** under **Command history**. Porter then stores only the program name of each exec and records `psql postgres://user:secret@db.internal/app` as `psql [omitted 1 redacted arg]`. **Delete command history** on the same page wipes the arguments already recorded for the cluster. ## Limit sandbox lifetime with a TTL To guarantee a sandbox cleans itself up even if its main process never exits, set a TTL at create time. The SDKs take `ttl_seconds`, and the CLI takes a `--ttl` Go duration. The TTL counts from creation; once it elapses, Porter terminates the sandbox the same way an explicit terminate does: ```python Python theme={null} sandbox = porter.sandboxes.create( image="ubuntu:24.04", command=["sleep", "infinity"], ttl_seconds=7200, ) ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "ubuntu:24.04", command: ["sleep", "infinity"], ttl_seconds: 7200, }); ``` ```bash CLI theme={null} porter sandbox create ubuntu:24.04 --ttl 2h -- sleep infinity ``` ## Use custom images Sandboxes run any container image, so you can bake in the tools your workload needs (language runtimes, CLIs, agents) instead of installing them at runtime. ### Pin an image by digest Image references accept a tag or a digest (`repo@sha256:`). A tag can point to different content over time, so pin the digest when your sandboxes need to run the exact same image on every create: ```python Python theme={null} sandbox = porter.sandboxes.create( image="python@sha256:229a2c5bfa27522db7815ea81f9bed70af17ccb9de9fc7ad142b1877b5830d36", ) ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "python@sha256:229a2c5bfa27522db7815ea81f9bed70af17ccb9de9fc7ad142b1877b5830d36", }); ``` ```bash CLI theme={null} porter sandbox create python@sha256:229a2c5bfa27522db7815ea81f9bed70af17ccb9de9fc7ad142b1877b5830d36 ``` ### Private images Private images are supported. Push your image to the ECR registry in the AWS account associated with your sandbox cluster, and sandboxes on that cluster can pull it without extra credential configuration. The easiest way to get an image there is [`porter registry push`](/standard/cli/command-reference/porter-registry#porter-registry-push), which exchanges registry credentials automatically and creates the remote repository on the first push: ```bash Push a locally built image theme={null} porter registry push myimage:dev --dest sandboxes/my-image:v1 ``` ```bash Build and push in one step theme={null} porter registry push myimage:dev --build ./app --dest sandboxes/my-image:v1 ``` With `--build`, the image is built from the given context directory on your machine before it is pushed, targeting `linux/amd64` by default. In CI or scripts, add `--json` to capture the fully qualified pushed reference and pass it straight to sandbox creation: ```bash theme={null} IMAGE=$(porter registry push myimage:dev --json | jq -r .image) porter sandbox create "$IMAGE" -- sleep infinity ``` See the [`porter registry push` reference](/standard/cli/command-reference/porter-registry#porter-registry-push) for all flags, including multi-arch builds and selecting between multiple linked registries. > [!NOTE] > > Sandboxes can only pull private images from the ECR registry in the sandbox cluster's AWS account. Images in other private registries (Docker Hub, GHCR, or ECR in a different account) need to be pushed to that registry first - `porter registry push` handles this in one step. ## Run an agent inside a sandbox A sandbox can host an entire agent, not just execute tools for an agent that runs elsewhere. Two patterns are common: * **Sandbox as a tool**: your agent's control loop and model calls run in your application, and the agent uses the sandbox to execute code or commands as a tool. This is the exec flow shown above. * **Agent in the sandbox**: the whole agent, its control loop and its model calls, runs inside the sandbox. Bake your harness into a custom image, inject the model API key at create time, and run it as the sandbox's main process. Sandboxes reach the public internet by default, so an in-sandbox agent calls a hosted model API directly. The second pattern is a custom image plus an environment variable. The agent runs as the sandbox's main process, so the sandbox stays alive as long as the agent is running (see [Sandbox lifetime](/sandboxes/overview#sandbox-lifetime)): ```python Python theme={null} sandbox = porter.sandboxes.create( image="ghcr.io/acme/agent:latest", name="research-agent", env={"MODEL_API_KEY": model_api_key}, ) # Follow the agent's output while it runs. print(sandbox.logs(limit=100)) # Terminate when you're done, or let the agent's process exit on its own. sandbox.terminate() ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "ghcr.io/acme/agent:latest", name: "research-agent", env: { MODEL_API_KEY: modelApiKey }, }); // Follow the agent's output while it runs. console.log(await sandbox.logs({ limit: 100 })); // Terminate when you're done, or let the agent's process exit on its own. await sandbox.terminate(); ``` ```bash CLI theme={null} porter sandbox create ghcr.io/acme/agent:latest --name research-agent --env MODEL_API_KEY=$MODEL_API_KEY ``` ## Use tags to identify sandboxes Tags make it easier to find sandboxes created by a workflow: ```python theme={null} sandbox = porter.sandboxes.create( image="python:3.12-slim", tags={"workflow": "agent-run", "run": "2026-06-17"}, ) ``` ## Next steps * Use [Sandbox Networking](/sandboxes/networking) to serve sandboxes over HTTPS on your own domains. * Use [the sandbox CLI guide](/sandboxes/cli) to enable sandboxes on a cluster, list, inspect logs, exec into, monitor CPU and memory usage of, and terminate sandboxes. * Use the [Python Sandbox SDK quickstart](/sandboxes/sdk/python/quickstart) for Python services. * Use the [TypeScript Sandbox SDK quickstart](/sandboxes/sdk/typescript/quickstart) for Node.js and TypeScript services. # Sandbox Networking Source: https://docs.porter.run/sandboxes/networking Serve sandboxes over HTTPS on your own domains and restrict their outbound traffic with egress allowlists A sandbox that opens a port can be served over HTTPS on a hostname under your own domain. You configure networking on the cluster once, and every sandbox that exposes a port gets a URL under your domain, either minted automatically from the sandbox name or chosen by you at create time. ## How it works You bring a domain you own (for example `sandbox.acme.com`), and Porter provisions a load balancer and a wildcard HTTPS certificate for it on your sandbox cluster. A cluster supports two kinds of networking, each with its own domain: | Networking | Load balancer | Who can reach sandboxes | | - | - | - | | Public | Internet-facing | Anyone on the internet | | Private | Internal | Clients inside your VPC | Enable public networking, private networking, or both. Each sandbox that opens a port is served at a hostname one label under the configured domain, such as `my-app.sandbox.acme.com`, with TLS handled by the wildcard certificate. ## What sandboxes can reach Sandboxes run untrusted code, so their outbound traffic is isolated by default, whether or not networking is configured. A sandbox can resolve DNS and reach the public internet, and nothing else: | Destination | Reachable | | - | - | | Public internet | Yes | | Cluster DNS | Yes | | In-cluster services and other pods (including other sandboxes) | No (unless allowlisted, see below) | | Kubernetes API server and the sandbox control plane | No | | Cloud metadata endpoint (`169.254.169.254`) | No | | Private address space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `100.64.0.0/10`) | No | Because private address space is blocked, this includes services behind a private load balancer. If a sandbox needs to call a service you run on the same cluster, either expose that service on a public domain and call it over the internet, or give the sandbox an egress allowlist that includes the service's cluster-internal hostname (see below). ### Private domains on the baseline When [egress allowlists are enforced](#restrict-egress-with-an-allowlist) on the cluster, a short list of private domains stays reachable from every sandbox, with or without an allowlist: | Domain | Reachable when | | - | - | | `*.` | Private networking is configured, so sandboxes can call other sandboxes served privately | | A [QM add-on's](/addons/qm#private-load-balancer) custom domain | QM is served from the cluster's private load balancer | Porter updates this list whenever a QM add-on moves on or off the private load balancer. Without enforcement, sandboxes can't reach any of these, since the default isolation blocks the private load balancer's addresses. ## Restrict egress with an allowlist Beyond the default isolation, a sandbox can be limited to an explicit list of outbound destinations. Pass `egress` when creating a sandbox and it can only reach the destinations it lists; all other outbound traffic is blocked. Enforcement happens transparently at the network layer, so any protocol and client works without proxy configuration. Egress enforcement is off by default. To turn it on for a cluster, go to the **Sandbox** tab, open **Settings**, and enable **Enforce egress allowlists** under **Egress controls**. This installs network policy enforcement on the sandbox nodes. Sandboxes created without an `egress` entry keep unrestricted internet access even when enforcement is on. ```python Python theme={null} sandbox = porter.sandboxes.create( image="ghcr.io/example/agent:latest", name="restricted", egress={"allowed_destinations": ["api.stripe.com", "*.github.com"]}, ) ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "ghcr.io/example/agent:latest", name: "restricted", egress: { allowed_destinations: ["api.stripe.com", "*.github.com"] }, }); ``` An `allowed_destinations` entry is one of: | Entry | Matches | | - | - | | `api.example.com` | That exact hostname | | `*.example.com` | Any host under the domain, but not the domain itself | | `203.0.113.7` | That IP address | | `203.0.113.0/24` | Any address in the CIDR range | | `payments.billing.svc.cluster.local` | The backing pods of the `payments` Service in the `billing` namespace on the sandbox's cluster | An empty list denies all egress. DNS resolution keeps working inside the sandbox so hostname entries can be resolved and enforced. CIDR entries are honored as written, including private address space, so an allowlist can deliberately grant a sandbox access to internal IPs that the default isolation blocks. A Service entry is how a restricted sandbox reaches another workload on its own cluster: an entry of the form `name.namespace.svc.cluster.local` allows traffic to that Service's backing pods and tracks them as they change, so a rolling deploy of the Service doesn't interrupt connectivity. Service entries aren't scoped to a port; any port on the backing pods is allowed. ## Configure networking on the cluster **1. Open the sandbox networking settings** In the Porter Dashboard, go to the **Sandbox** tab for your sandbox-enabled cluster, then open **Settings**. The **Public** and **Private** networking sections each have their own toggle. **2. Set the domain** Enable the kind of networking you want and enter the domain sandbox hostnames should be minted under, for example `sandbox.acme.com` for public networking or `internal.sandbox.acme.com` for private. Each needs its own domain. **3. Connect your DNS provider** Porter issues the wildcard certificate through a Let's Encrypt DNS-01 challenge, which needs access to the DNS zone that contains your domain. **Cloudflare**: create an API token and paste it into the networking settings. 1. In the Cloudflare dashboard, go to your profile icon > **My Profile** > **API Tokens** (or directly at [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens)) and click **Create Token**. 2. Use the **Edit zone DNS** template. It grants the one permission Porter needs: **Zone / DNS / Edit**. 3. Under **Zone Resources**, scope the token to **Include / Specific zone** and select the zone that contains your sandbox domain (the zone `acme.com` for a domain like `sandbox.acme.com`). 4. Continue to the summary, create the token, and copy it. Cloudflare shows the token value only once. 5. Paste it into the section's API token field in Porter and save. Each networking section stores its own token, so public and private are credentialed separately (one token can be pasted into both). **AWS Route53**: no token needed. Porter authenticates through an [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) scoped to a single hosted zone, so the sandbox domain needs its own public hosted zone in the cluster's AWS account. A parent zone such as `acme.com` doesn't work. 1. In the Route53 console, create a **public** hosted zone named exactly the sandbox domain (`internal.sandbox.acme.com` for a domain of `internal.sandbox.acme.com`). Note the four `NS` values Route53 assigns to it. 2. In the parent zone (`acme.com`), add an `NS` record for the sandbox domain with those four values. This delegates the subdomain to the new zone. 3. Enter the same domain in the networking settings and save. Porter resolves the zone on the next cluster update and provisions the pod identity. 4. When you create the wildcard record in the next step, put it in the new zone, not the parent. Records for a delegated subdomain that sit in the parent zone aren't served. This is the same hosted zone rule as the [private load balancer's Route53 setting](/cloud-accounts/advanced-cluster-settings#private-load-balancer), which covers it in more detail. **4. Create the wildcard DNS record** Save the settings, then wait for the load balancer to provision. The settings page shows the record target and type once it's ready: create a DNS record for `*.` (for example `*.sandbox.acme.com`) pointing at that address. The record must be a wildcard. Each sandbox is served at its own hostname under the domain, so a record for the bare domain or for a single hostname only routes that one name. On most providers this is a `CNAME`. On Route53, prefer an alias `A` record pointing at the load balancer: it resolves in one lookup and Route53 doesn't bill alias queries. A `CNAME` is also supported if you'd rather use one. For private networking, the load balancer address only resolves to something reachable from inside your VPC. Point your own private DNS at it if your clients resolve through one; certificates are still issued automatically either way. ## Expose a port from a sandbox Pass `networking` when creating a sandbox. A sandbox with no `networking` entry exposes nothing. Currently one entry is supported, and its `port` is required (1024-65535; privileged ports are not allowed): ```python Python theme={null} sandbox = porter.sandboxes.create( image="ghcr.io/example/web:latest", name="my-app", networking=[{"port": 8080}], ) ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "ghcr.io/example/web:latest", name: "my-app", networking: [{ port: 8080 }], }); ``` With no domain specified, the sandbox is served at `.` (falling back to `.` when the sandbox has no name), publicly when public networking is configured on the cluster, otherwise privately. On a cluster with no networking configured, the port is served at the sandbox's cluster-internal address only (see below). ## Choose the hostname and visibility Add a `domains` entry to control where the port is served. Currently one entry is supported: ```python Python theme={null} sandbox = porter.sandboxes.create( image="ghcr.io/example/web:latest", name="my-app", networking=[ { "port": 8080, "domains": [ {"domain": "my-app.sandbox.acme.com", "visibility": "public"}, ], } ], ) ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "ghcr.io/example/web:latest", name: "my-app", networking: [ { port: 8080, domains: [{ domain: "my-app.sandbox.acme.com", visibility: "public" }], }, ], }); ``` | Field | Description | | - | - | | `domain` | Fully qualified hostname for the sandbox, one label under the domain configured for the chosen visibility (`my-app.sandbox.acme.com` under `sandbox.acme.com`). Unlike sandbox names, domains don't have to be unique: successive sandboxes can reuse a hostname, though only one may be live on it at a time. | | `visibility` | `public` or `private`: whether the sandbox is served publicly or inside your VPC when the cluster has both configured. Omit to default to whichever is configured, public winning. Creating a sandbox that requests a visibility the cluster doesn't have configured is rejected. | ## Serve a port inside the cluster only Set `internal: true` on the networking entry to keep the sandbox off both load balancers. It gets no hostname; instead the port is served at a stable cluster-internal address, reachable only from workloads on the same cluster. This needs no networking configured on the cluster and can't be combined with `domains`: ```python Python theme={null} sandbox = porter.sandboxes.create( image="ghcr.io/example/web:latest", name="my-app", networking=[{"port": 8080, "internal": True}], ) ``` ```typescript TypeScript theme={null} const sandbox = await porter.sandboxes.create({ image: "ghcr.io/example/web:latest", name: "my-app", networking: [{ port: 8080, internal: true }], }); ``` The address is in the sandbox's status as `internal_address`, a `host:port` like `sandbox-route-.porter-sandbox.svc.cluster.local:8080`. Traffic to it doesn't go through the load balancer, so it isn't TLS-terminated: the connection carries the protocol the workload serves on that port. Reaching it from another sandbox requires an egress allowlist entry for the address's hostname (see [the allowlist section](#restrict-egress-with-an-allowlist)); other workloads on the cluster reach it subject to their own network policy. ## Find the sandbox URL The sandbox's status includes `host`, the hostname it's reachable at. It's empty when the sandbox exposes no port or the cluster has no networking configured: ```python Python theme={null} status = sandbox.refresh() print(f"https://{status.host}") ``` ```typescript TypeScript theme={null} const status = await sandbox.refresh(); console.log(`https://${status.host}`); ``` Every sandbox that opens a port also gets `internal_address` in its status, even when it's also served on a hostname. ## Next steps * [Sandboxes Getting Started](/sandboxes/getting-started) * [Python Sandbox SDK reference](/sandboxes/sdk/python/reference) * [TypeScript Sandbox SDK reference](/sandboxes/sdk/typescript/reference) # Sandboxes Overview Source: https://docs.porter.run/sandboxes/overview Run isolated container workloads on Porter [Porter | Sandboxes Demo](https://player.vimeo.com/video/1228161972?badge=0\&autopause=0\&player_id=0\&app_id=58479) Porter Sandboxes are isolated container workloads that allow you to run untrusted code or workflows for agents. Use them when you need a secure, fresh runtime environment for code interpretation, agent tools, batch fan-out, or other dynamic work. > [!NOTE] > > Sandboxes are currently only available on AWS. If you are interested in the Azure and GCP sandbox beta, please reach out via the support widget on the dashboard. ## Features * Fully hosted within a user-owned cloud account * Persistent volume support * Snapshot and restore from a previous state * Sub-second spin-up time with a configurable warm pool * Connection to S3 for object storage * Egress controls ## When to use sandboxes * **Untrusted code execution**: run code from end users or LLM agents in isolation * **Agentic tool use**: give an LLM a fresh execution environment per task * **Parallel batch work**: fan out many short jobs concurrently * **On-demand processing**: create a runtime for a request, job, or workflow step For long-running services with autoscaling, rollouts, and normal application lifecycle management, use [Applications](/applications/deploy/overview) instead. ## Lifecycle A sandbox moves through these phases: | Phase | Description | | - | - | | `queued` | The sandbox was accepted and is waiting for capacity. | | `creating` | Porter is preparing the runtime and starting the container. | | `running` | The sandbox is ready and can accept exec calls. | | `succeeded` | The sandbox command completed successfully. | | `failed` | The sandbox command or runtime failed. | | `terminated` | The sandbox was explicitly terminated through the API, SDK, or CLI. | Only `running` sandboxes accept exec calls. Logs remain available after terminal phases so you can fetch output from completed or terminated sandboxes. ### Sandbox lifetime A sandbox lives as long as its main process: the image's default entrypoint, or the command you pass at create time. When that process exits with code `0` the sandbox moves to `succeeded`; a nonzero exit moves it to `failed`. Once the sandbox reaches either phase, exec calls fail. This means an image whose default command exits immediately reaches `succeeded` within a few seconds of starting. To keep a sandbox alive so you can exec into it, give it a long-running main process and terminate it when you're done: ```bash theme={null} porter sandbox create alpine:3.20 --name worker -- sleep 3600 porter sandbox exec worker -- echo ok porter sandbox terminate worker ``` You can also bound a sandbox's lifetime up front with a TTL, so it cleans itself up even if its main process never exits. Pass `ttl_seconds` in the SDKs or `--ttl` on the CLI at create time. The TTL counts from creation; shortly after it elapses, Porter terminates the sandbox the same way an explicit terminate does, moving it to `terminated`: ```bash theme={null} porter sandbox create ubuntu:24.04 --name worker --ttl 2h -- sleep infinity ``` ## Names Names are the canonical way to refer to sandboxes and volumes in the SDK and CLI. | Resource | Naming rule | | - | - | | Sandbox | Sandbox names must be unique within a cluster and currently cannot be reused, even after the sandbox is terminated. Omit the name only for one-off sandboxes where you do not need stable lookup later. | | Volume | Volume names must be unique within a cluster for the lifetime of the volume. After a volume is deleted, its name can be used again. | ## Build with the Sandbox SDK Your application creates and manages sandboxes with a Sandbox SDK. For now, we recommend deploying that application as a Porter Application in the same AWS cluster where you want to run sandboxes. * [Python Sandbox SDK quickstart](/sandboxes/sdk/python/quickstart) * [TypeScript Sandbox SDK quickstart](/sandboxes/sdk/typescript/quickstart) ## Use the CLI for operations Use the sandbox CLI to inspect and operate on sandboxes from your terminal: * Create sandboxes from container images * List sandboxes by phase * Fetch logs * Exec into a running sandbox * Terminate one sandbox or many sandboxes at once * Snapshot a running sandbox and start new sandboxes from it * Create, list, inspect, and delete sandbox volumes See the [sandbox CLI guide](/sandboxes/cli). ## Networking A sandbox that opens a port can be served over HTTPS on a hostname under your own domain, with public or private networking configured on the cluster. See [Sandbox Networking](/sandboxes/networking). ## Persistent storage Volumes provide persistent storage that can be mounted into sandboxes at launch. Create a volume first, then pass `volume_mounts` when creating a sandbox. See [Volumes](/sandboxes/volumes) for creating, mounting, reading, writing, and managing them. * [Volumes](/sandboxes/volumes) * [Python Sandbox SDK volumes](/sandboxes/sdk/python/volumes) * [TypeScript Sandbox SDK volumes](/sandboxes/sdk/typescript/volumes) * [Sandbox CLI volume commands](/sandboxes/cli#porter-sandbox-volume) ## Snapshots A snapshot captures a running sandbox's filesystem so you can start new sandboxes from it later, with the same files in place. Use it to save an environment after a setup step, such as installing dependencies or cloning a repository, and then start many sandboxes from that point instead of repeating the setup in each one. Capturing does not stop the sandbox. A snapshot holds files only, not running processes or mounted volumes: a sandbox started from a snapshot runs its command from the beginning, and mounts only the volumes you pass at create time. Snapshot from the SDKs or the CLI: * [Python Sandbox SDK snapshots](/sandboxes/sdk/python/reference#snapshots) * [TypeScript Sandbox SDK snapshots](/sandboxes/sdk/typescript/reference#snapshots) * [`porter sandbox snapshot`](/sandboxes/cli#porter-sandbox-snapshot) ## Monitoring To see how much CPU and memory a running sandbox is using, and how close it is to its limits, run [`porter sandbox metrics`](/sandboxes/cli#porter-sandbox-metrics). It reports p50 and p90 CPU and memory usage over a lookback window, with per-limit utilization. ## Next steps * [Sandboxes Getting Started](/sandboxes/getting-started) * [Sandbox Networking](/sandboxes/networking) * [Monitor sandbox CPU and memory usage](/sandboxes/cli#porter-sandbox-metrics) * [Snapshot a sandbox and restore from it](/sandboxes/cli#porter-sandbox-snapshot) * [Python Sandbox SDK quickstart](/sandboxes/sdk/python/quickstart) * [TypeScript Sandbox SDK quickstart](/sandboxes/sdk/typescript/quickstart) # Python Sandbox SDK errors Source: https://docs.porter.run/sandboxes/sdk/python/errors Error classes raised by the porter-sandbox Python Sandbox SDK All SDK errors inherit from `SandboxError`. ```python theme={null} from porter_sandbox import ( AuthenticationError, NotFoundError, RateLimitError, SandboxError, SandboxTimeoutError, ServerError, ) ``` ## Error classes | Error | Raised when | | - | - | | `SandboxError` | Base class for SDK-raised API errors. | | `AuthenticationError` | The API rejects credentials. | | `NotFoundError` | A sandbox, volume, or other resource cannot be found. | | `RateLimitError` | The API returns a rate limit response. | | `ServerError` | The API returns a server-side error. | | `SandboxTimeoutError` | A request exceeds its timeout. Never retried, since the server may still be executing the request. | Each `SandboxError` includes: | Attribute | Type | Description | | - | - | - | | `message` | `str` | Human-readable error message. | | `status_code` | `int \| None` | HTTP status code, when available. | | `body` | `object \| None` | Parsed response body, when available. | ## Handle errors ```python theme={null} from porter_sandbox import Porter, SandboxError with Porter() as porter: sandbox = porter.sandboxes.create(image="python:3.12-slim") try: result = sandbox.exec(["python", "-c", "raise SystemExit(2)"]) if result.exit_code != 0: print(result.stderr) except SandboxError as exc: print(f"Sandbox API error: {exc.message}") finally: sandbox.terminate() ``` Non-zero command exit codes are returned in the exec response. They are normal command results, not SDK exceptions. # Python Sandbox SDK quickstart Source: https://docs.porter.run/sandboxes/sdk/python/quickstart Create and manage Porter Sandboxes from Python The Python Sandbox SDK is published as `porter-sandbox`. Use it from application code running in your sandbox-enabled Porter cluster. ## Install Add the SDK to your application image: ```bash theme={null} pip install porter-sandbox ``` Or with `uv`: ```bash theme={null} uv add porter-sandbox ``` ## Create your first sandbox Create a sandbox, execute a command, print the command output, and terminate the sandbox when the work is done: ```python theme={null} from porter_sandbox import Porter with Porter() as porter: sandbox = porter.sandboxes.create( image="python:3.12-slim", name="python-quickstart", tags={"example": "python-quickstart"}, ) result = sandbox.exec(["python", "-c", "print('hello from porter')"]) print(result.stdout) sandbox.terminate() ``` The SDK connects to the in-cluster Sandbox API automatically when this code runs as a Porter Application in the same cluster where sandboxes are enabled. If you need to invoke sandboxes from outside that cluster, set `PORTER_SANDBOX_API_KEY` to a Porter API token and `PORTER_CLUSTER_ID` to the target cluster. The SDK reads the project from the token and builds the external Porter API URL automatically; see [calling from outside the cluster](/sandboxes/getting-started#calling-from-outside-the-cluster) for details. Use sandbox names when you need to fetch, inspect, exec into, or terminate a sandbox later. Sandbox names must be unique within a cluster and currently cannot be reused, even after the sandbox is terminated. ## Fetch logs Logs are returned as structured log lines: ```python theme={null} from porter_sandbox import Porter with Porter() as porter: sandbox = porter.sandboxes.create( image="python:3.12-slim", name="python-logs-demo", ) try: sandbox.exec(["python", "-c", "print('log me')"]) for line in sandbox.logs(limit=100): print(f"{line.timestamp} [{line.level}] {line.line}") finally: sandbox.terminate() ``` ## Get a sandbox by name Use `get(name)` when you know the sandbox name: ```python theme={null} from porter_sandbox import Porter with Porter() as porter: sandbox = porter.sandboxes.get("python-quickstart") print(sandbox.phase) ``` ## List sandboxes Use tags to find sandboxes created by a workflow: ```python theme={null} from porter_sandbox import Porter with Porter() as porter: sandboxes = porter.sandboxes.list(tags={"workflow": "agent-run"}) for sandbox in sandboxes: print(sandbox.phase) ``` ## Async usage Use `AsyncPorter` from async application code: ```python theme={null} import asyncio from porter_sandbox import AsyncPorter async def main(): async with AsyncPorter() as porter: sandbox = await porter.sandboxes.create( image="python:3.12-slim", name="async-python-demo", ) try: result = await sandbox.exec(["python", "-c", "print(2 + 2)"]) print(result.stdout) finally: await sandbox.terminate() asyncio.run(main()) ``` Launch many sandboxes concurrently with `asyncio.gather`: ```python theme={null} async with AsyncPorter() as porter: sandboxes = await asyncio.gather(*[ porter.sandboxes.create( image="python:3.12-slim", name=f"batch-demo-{i}", tags={"batch": "demo"}, ) for i in range(10) ]) ``` ## Next steps * [Python Sandbox SDK reference](/sandboxes/sdk/python/reference) * [Python Sandbox SDK volumes](/sandboxes/sdk/python/volumes) * [Python Sandbox SDK errors](/sandboxes/sdk/python/errors) * [Sandboxes getting started](/sandboxes/getting-started) # Python Sandbox SDK reference Source: https://docs.porter.run/sandboxes/sdk/python/reference Core classes and methods in the porter-sandbox Python Sandbox SDK Import the top-level client from `porter_sandbox`: ```python theme={null} from porter_sandbox import Porter, AsyncPorter ``` ## `Porter` `Porter` is the top-level sync client. It exposes resource namespaces for sandboxes, volumes, health, and readiness. ```python theme={null} from porter_sandbox import Porter with Porter() as porter: sandbox = porter.sandboxes.create( image="python:3.12-slim", name="my-sandbox", ) ``` Constructor options: | Option | Type | Description | | - | - | - | | `api_key` | `str \| None` | Optional API key. Falls back to the `PORTER_SANDBOX_API_KEY` environment variable. Only required when invoking the Sandbox API from outside the sandbox-enabled cluster. Create one from **Settings > API tokens** in the Porter Dashboard. Creating API tokens requires admin permissions, and the token must have developer permissions to authenticate with the Sandbox API. | | `base_url` | `str \| None` | Optional API base URL override. Falls back to the `PORTER_SANDBOX_BASE_URL` environment variable. If neither is set, the SDK builds the external Porter API URL from the API token's project and `PORTER_CLUSTER_ID`, then falls back to the in-cluster Sandbox API URL when running in a Kubernetes cluster, and otherwise raises an error explaining what to set. | | `timeout` | `float \| None` | Request timeout in seconds. Defaults to 30 seconds. | ## `porter.sandboxes` ### `create` ```python theme={null} sandbox = porter.sandboxes.create( image="python:3.12-slim", name="my-sandbox", command=None, args=None, env=None, tags={"workflow": "demo"}, volume_mounts=None, ) ``` Arguments: | Argument | Type | Description | | - | - | - | | `image` | `str` | Container image to run, referenced by tag or digest. Pass `""` when starting from a snapshot. | | `snapshot_id` | `str \| None` | Start the sandbox from a [snapshot](#snapshots) instead of an image. The snapshot holds files only. An omitted `command` runs the base image's entrypoint, and only the volumes in `volume_mounts` are mounted. | | `name` | `str \| None` | Optional cluster-unique sandbox name. Use lowercase letters, numbers, and hyphens; start and end with a letter or number. Names currently cannot be reused, even after termination. | | `command` | `list[str] \| None` | Optional entrypoint override. | | `args` | `list[str] \| None` | Optional arguments passed to the command. | | `env` | `dict[str, str] \| None` | Environment variables to set in the sandbox. | | `env_groups` | `list[str] \| None` | Names of [environment groups](/applications/configure/environment-groups) on the cluster whose variables are injected into the sandbox, resolved to their latest version at create time. On a key conflict a later group wins over an earlier one, and an explicit `env` entry wins over any group value. | | `tags` | `dict[str, str] \| None` | Key-value labels for finding related sandboxes. | | `volume_mounts` | `dict[str, str] \| None` | Volume IDs keyed by absolute mount path inside the sandbox. | | `networking` | `list[SandboxNetworkingSpec] \| None` | Port to expose and, optionally, the domain and visibility to serve it on, or `internal` to serve it inside the cluster only. Omit to expose nothing. See [Sandbox Networking](/sandboxes/networking). | | `egress` | `SandboxEgressSpec \| None` | Restricts the sandbox's outbound traffic to a list of allowed destinations (hostnames, wildcards, IP literals, CIDR ranges, or in-cluster Service DNS names). Omit to leave internet access unrestricted. See [Restrict egress with an allowlist](/sandboxes/networking#restrict-egress-with-an-allowlist). | | `resources` | `SandboxResourcesSpec \| None` | CPU and memory for the sandbox (`{"cpu": "2", "memory": "4Gi"}`). CPU is in cores, with an `m` suffix for a fraction; memory takes a `Gi` or `Mi` suffix. Omit a field to keep the cluster's default sandbox size for that resource. See [custom sandbox sizes](/sandboxes/capacity#custom-sandbox-sizes). | | `ttl_seconds` | `int \| None` | Maximum lifetime in seconds, counted from creation. The sandbox is terminated once it elapses. Omit for no limit. See [sandbox lifetime](/sandboxes/overview#sandbox-lifetime). | Returns a `Sandbox` handle. ### `get` ```python theme={null} sandbox = porter.sandboxes.get("my-sandbox") ``` Looks up a sandbox by name and returns a `Sandbox` handle. Use names as the canonical reference for sandboxes you need to operate on later. ### `list` ```python theme={null} sandboxes = porter.sandboxes.list(tags={"workflow": "demo"}, page=1) ``` Lists sandboxes, optionally filtered by tags and page. ## `Sandbox` `Sandbox` is the handle returned by `porter.sandboxes`. Properties: | Property | Type | Description | | - | - | - | | `id` | `str` | Sandbox identifier. | | `phase` | `StatusResponsePhase \| None` | Last-known lifecycle phase. | | `tags` | `dict[str, str] \| None` | Last-known tags. | Methods: ```python theme={null} status = sandbox.refresh() result = sandbox.exec(["python", "-c", "print('hi')"]) logs = sandbox.logs(limit=100) sandbox.terminate() ``` | Method | Description | | - | - | | `refresh()` | Refetches status and updates `phase` and `tags`. | | `exec(command, timeout=None)` | Runs a command in the sandbox and returns stdout, stderr, and exit code. | | `logs(since=None, limit=None)` | Fetches structured log lines. | | `terminate()` | Terminates the sandbox. | ### `exec` timeouts `exec` is long-running: the client sends no timeout by default, so the call waits for the command to exit. Pass `timeout` (seconds) to cap how long the client waits. When the timeout elapses, the SDK raises `SandboxTimeoutError` and does not retry, since the command may still be running server-side. ```python theme={null} from porter_sandbox import SandboxTimeoutError try: result = sandbox.exec(["sleep", "120"], timeout=30.0) except SandboxTimeoutError: print("exec exceeded 30s; the command may still be running in the sandbox") ``` The client-wide `timeout` on `Porter` still applies to every other request (30 seconds by default). ## Volumes Volumes are persistent storage that can be mounted into sandboxes. ```python theme={null} volume = porter.volumes.create(name="my-data") sandbox = porter.sandboxes.create( image="python:3.12-slim", volume_mounts={"/mnt/data": volume.id}, ) ``` Sandbox and volume names may contain lowercase letters, numbers, and hyphens, and must start and end with a letter or number. Sandbox names currently cannot be reused, even after termination. Volume names must be unique for the lifetime of the volume and can be reused after deletion. Volume methods: | Method | Description | | - | - | | `porter.volumes.list()` | Lists volumes as handles. | | `porter.volumes.create(name=None, type=None, object=None)` | Creates a volume and returns its handle. `type` and `object` make an object volume; the default is a disk volume. | | `porter.volumes.get(name)` | Fetches a volume handle by name. | | `porter.volumes.delete(name)` | Deletes a volume by name. | `create`, `get`, and `list` return a `Volume` handle for a disk volume and an `ObjectVolume` handle for an object volume; narrow with `isinstance`. A `Volume` has `id`, `name`, `type`, `phase`, `path`, `attached_to`, and `created_at`, plus: | Method | Description | | - | - | | `volume.listdir(path)` | Entries directly inside a directory. | | `volume.iterdir(path)` | Iterator over the whole tree below a directory. | | `volume.search(query, path="/")` | Entries whose name contains a substring. | | `volume.read_file(path, offset=None, length=None)` | File contents as bytes, optionally a byte range. | | `volume.read_text(path, offset=None, length=None)` | File contents as a string. | | `volume.stream(path, chunk_size=...)` | Iterator over a file in chunks, for files too large to hold in memory. | | `volume.write_file(path, content)` | Writes bytes to a file, creating parent directories and replacing any existing file. Atomic. | | `volume.write_text(path, content, encoding="utf-8")` | Writes a string to a file. See `write_file`. | | `volume.move_file(from_path, to_path)` | Moves or renames a file or directory; `to_path` is the full new path. Fails if the destination is occupied. | | `volume.refresh()` | Refetches the underlying volume record. | | `volume.delete()` | Deletes this volume. | An `ObjectVolume` shares `id`, `name`, `type`, `phase`, `attached_to`, `created_at`, `refresh()`, and `delete()`, and adds `object` (the bucket, key prefix, and access mode). It has no file methods: an object volume's contents live in its bucket. `AsyncPorter` returns `AsyncVolume` and `AsyncObjectVolume` handles with the same surface and async methods. ## Snapshots A snapshot captures a running sandbox's filesystem so new sandboxes can start from it later, with the same files in place. Capturing does not stop the sandbox, and `create` returns once the capture is complete. ```python theme={null} from porter_sandbox import SnapshotSpec base = porter.sandboxes.create( image="python:3.12-slim", name="base", command=["sleep", "infinity"], ) base.exec(["pip", "install", "pandas"]) snapshot = porter.snapshots.create(base.id, SnapshotSpec()) sandbox = porter.sandboxes.create( image="", snapshot_id=snapshot.id, command=["python", "-c", "import pandas; print(pandas.__version__)"], ) ``` A snapshot holds files only, not running processes or mounted volumes. A sandbox started from one runs its command from the beginning with the captured files in place, and mounts only the volumes passed in `volume_mounts`. Pass `image=""` with `snapshot_id`, since the snapshot is the image. Snapshot methods: | Method | Description | | - | - | | `porter.snapshots.create(id, SnapshotSpec())` | Captures the sandbox with ID `id`, which must be `running`, and returns a `Snapshot` once the capture completes. `SnapshotSpec(mode=...)` defaults to `filesystem`, the only mode available today. | | `porter.snapshots.list(sandbox_id=None)` | Lists snapshots, most recent first. Returns a `SnapshotListResponse` whose `snapshots` holds `Snapshot` records. Pass `sandbox_id` to narrow to one sandbox. | | `porter.snapshots.delete(id)` | Deletes a snapshot and the files it captured. Fails while the capture is still running or while a sandbox started from it has not finished. | A `Snapshot` has `id`, `sandbox_id`, `mode`, `status` (`pending`, `ready`, or `failed`), `size_bytes`, `t_created_unix_ms`, `t_ready_unix_ms`, and `failure_reason`. `AsyncPorter` exposes the same methods as awaitables on `porter.snapshots`. ## Async client `AsyncPorter` mirrors `Porter` with awaitable methods: ```python theme={null} from porter_sandbox import AsyncPorter async with AsyncPorter() as porter: sandbox = await porter.sandboxes.create( image="python:3.12-slim", name="async-sandbox", ) result = await sandbox.exec(["python", "-c", "print('hi')"]) await sandbox.terminate() ``` ## Low-level client The top-level client exposes generated low-level clients through `.raw`: ```python theme={null} raw_client = porter.raw raw_sandboxes = porter.sandboxes.raw ``` Use these only when you need an endpoint that is not wrapped by the ergonomic SDK surface yet. ## Next steps * [Python Sandbox SDK volumes](/sandboxes/sdk/python/volumes) * [Python Sandbox SDK errors](/sandboxes/sdk/python/errors) # Python Sandbox SDK volumes Source: https://docs.porter.run/sandboxes/sdk/python/volumes Create, mount, browse, read, and write sandbox volumes with the Python Sandbox SDK Volumes provide persistent storage that can be mounted into Porter Sandboxes. Create a volume before launching the sandbox, then pass the volume ID in `volume_mounts`. ## Create and mount a volume ```python theme={null} from porter_sandbox import Porter with Porter() as porter: volume = porter.volumes.create(name="agent-workspace") sandbox = porter.sandboxes.create( image="python:3.12-slim", volume_mounts={"/workspace": volume.id}, ) try: sandbox.exec(["python", "-c", "open('/workspace/result.txt', 'w').write('done')"]) result = sandbox.exec(["cat", "/workspace/result.txt"]) print(result.stdout) finally: sandbox.terminate() ``` `volume_mounts` is a dictionary keyed by the absolute mount path inside the sandbox. Each value is a volume ID. ## Object volumes Passing `type` and `object` to `create` makes an [object volume](/sandboxes/volumes#object-volumes), which exposes a registered S3 bucket. It mounts like any other volume through `volume_mounts`. ```python theme={null} from porter_sandbox import Porter, VolumeObjectSpec, VolumeObjectSpecAccess, VolumeSpecType with Porter() as porter: datasets = porter.volumes.create( name="datasets", type=VolumeSpecType.OBJECT, object=VolumeObjectSpec( bucket="my-bucket", prefix="training/v1", access=VolumeObjectSpecAccess.READ_ONLY, ), ) print(datasets.object.bucket, datasets.object.access) ``` Object volumes get their own `ObjectVolume` handle (`AsyncObjectVolume` on the async client). It carries the shared surface — `id`, `name`, `type`, `phase`, `attached_to`, `created_at`, `refresh`, and `delete` — plus the `object` spec, and none of the file methods: an object volume's contents live in its bucket. `create`, `get`, and `list` can return either handle, so narrow with `isinstance` before touching files: ```python theme={null} from porter_sandbox import ObjectVolume for volume in porter.volumes.list(): if isinstance(volume, ObjectVolume): print(volume.name, "exposes bucket", volume.object.bucket) else: checkpoints = [file.path for file in volume.listdir("/checkpoints")] ``` ### File system mode Set `mode` to `VolumeObjectSpecMode.FILESYSTEM` to mount the bucket in [file system mode](/sandboxes/volumes#file-system-mode), which supports renames and in-place edits. The bucket needs the `sandbox_filesystem` capability. ```python theme={null} from porter_sandbox import VolumeObjectSpecMode workspace = porter.volumes.create( name="team-a-workspace", type=VolumeSpecType.OBJECT, object=VolumeObjectSpec( bucket="my-bucket", prefix="team-a", mode=VolumeObjectSpecMode.FILESYSTEM, ), ) ``` The volume still returns an `ObjectVolume` handle, so manage its files from a sandbox that mounts it or through S3. ## List volumes `list()` returns volume handles: ```python theme={null} with Porter() as porter: for volume in porter.volumes.list(): print(volume.id, volume.name, volume.phase, volume.attached_to, volume.path) ``` ## Get a volume by name Volume names are unique within the cluster. Use `get(name)` when you know a volume name: ```python theme={null} with Porter() as porter: volume = porter.volumes.get("agent-workspace") print(volume.id) ``` Volume names may contain lowercase letters, numbers, and hyphens, and must start and end with a letter or number. A volume name must be unique within the cluster for the lifetime of the volume. After the volume is deleted, the name can be used again. ## Inspect a volume ```python theme={null} with Porter() as porter: volume = porter.volumes.get("agent-workspace") print(volume.name, volume.phase, volume.attached_to, volume.created_at) ``` ## Browse volume contents `listdir` returns the entries directly inside a directory, directories first: ```python theme={null} with Porter() as porter: volume = porter.volumes.get("agent-workspace") for file in volume.listdir("/checkpoints"): print(file.path, "dir" if file.is_directory else file.size_bytes) ``` `iterdir` walks the whole tree instead, descending into every subdirectory: ```python theme={null} for file in volume.iterdir("/checkpoints"): print(file.path, file.size_bytes) ``` `search` walks the tree and returns only the entries whose name contains a substring, optionally rooted at a subdirectory: ```python theme={null} configs = volume.search("config.json") checkpoint_configs = volume.search("config.json", path="/checkpoints") ``` ## Read a file `read_text` returns a whole file as a string, and `read_file` returns raw bytes: ```python theme={null} config = volume.read_text("/checkpoints/config.json") data = volume.read_file("/checkpoints/weights.bin") ``` Both accept `offset` and `length` to read part of a file: ```python theme={null} head = volume.read_text("/logs/train.log", offset=0, length=512) ``` For files too large to hold in memory, `stream` pages through the file in chunks (8 MiB by default): ```python theme={null} with open("model.safetensors", "wb") as out: for chunk in volume.stream("/checkpoints/model.safetensors"): out.write(chunk) ``` Reading a path that does not exist raises `NotFoundError`. ## Write a file `write_text` writes a string and `write_file` writes raw bytes. Both create parent directories as needed and replace any existing file at the path: ```python theme={null} volume.write_text("/config/app.yaml", "replicas: 3\n") volume.write_file("/checkpoints/weights.bin", data) ``` A write is atomic: the file appears at its path only after every byte has been written, so an interrupted or failed write leaves the previous contents in place rather than a truncated file. An uploaded file gets the same permissions a sandbox writing to its own mount would leave, so it is indistinguishable from a file the sandbox wrote. You can write to a volume whether or not a sandbox has it mounted; the volume itself is the target. A single write is capped at 1 GiB; a request made from outside the cluster with an API token must also finish within 30 seconds, so write very large files from an app running in the same cluster. ## Move or rename a file `move_file` moves a file or directory to a new path. The second argument is the entry's full new path, not a directory to drop it into, so one call both renames and relocates. A directory moves with everything under it: ```python theme={null} volume.move_file("/notes.txt", "/archive/notes.txt") volume.move_file("/models/v1", "/models/v2") ``` The destination's parent directory must already exist, and nothing is overwritten. Moving onto a path something already occupies fails and leaves the entry where it was. Every method here has an async counterpart on `AsyncVolume`, returned by `AsyncPorter`: ```python theme={null} async with AsyncPorter() as porter: volume = await porter.volumes.get("agent-workspace") async for file in volume.iterdir("/checkpoints"): print(file.path) config = await volume.read_text("/checkpoints/config.json") ``` ## Access volume data from apps Volumes live on a shared disk that apps on the same cluster can attach. Attach the disk named `sandbox-volumes` to a service in your app; it mounts at `/data//sandbox-volumes` and contains one subdirectory per volume. Each volume handle exposes a `path` with the volume's subdirectory on that disk, so an app reads a volume's data at `/data//sandbox-volumes/`: ```python theme={null} with Porter() as porter: volume = porter.volumes.get("agent-workspace") print(volume.path) # sandbox-vol-2f1c8b7e-... ``` The disk is a live view: writes a sandbox makes to its mounted volume are visible to apps right away, and new volumes show up as new subdirectories without redeploying the app. > [!WARNING] > > Volume contents are written by sandboxed workloads, which are often running untrusted code. Treat anything your app reads from this disk as untrusted input: hostile file contents, names, or sizes can exploit vulnerabilities in the code that processes them. Porter isolates the sandboxes themselves but does not inspect or sanitize what they write, so validating this data before acting on it is your application's responsibility. ## Delete a volume Delete volumes by name: ```python theme={null} with Porter() as porter: porter.volumes.delete("agent-workspace") ``` Deleting a volume fails while it is attached to a sandbox. Terminate any attached sandboxes before deleting the volume. ## Async usage ```python theme={null} from porter_sandbox import AsyncPorter async with AsyncPorter() as porter: volume = await porter.volumes.create(name="async-workspace") sandbox = await porter.sandboxes.create( image="python:3.12-slim", volume_mounts={"/workspace": volume.id}, ) try: result = await sandbox.exec(["sh", "-lc", "echo hello > /workspace/out.txt && cat /workspace/out.txt"]) print(result.stdout) finally: await sandbox.terminate() ``` ## Next steps * [Python Sandbox SDK quickstart](/sandboxes/sdk/python/quickstart) * [Python Sandbox SDK reference](/sandboxes/sdk/python/reference) * [Python Sandbox SDK errors](/sandboxes/sdk/python/errors) # TypeScript Sandbox SDK errors Source: https://docs.porter.run/sandboxes/sdk/typescript/errors Error classes raised by the porter-sandbox TypeScript Sandbox SDK All SDK errors inherit from `SandboxError`. ```typescript theme={null} import { AuthenticationError, NotFoundError, RateLimitError, SandboxError, SandboxTimeoutError, ServerError, } from "porter-sandbox"; ``` ## Error classes | Error | Raised when | | - | - | | `SandboxError` | Base class for SDK-raised API errors. | | `AuthenticationError` | The API rejects credentials. | | `NotFoundError` | A sandbox, volume, or other resource cannot be found. | | `RateLimitError` | The API returns a rate limit response. | | `ServerError` | The API returns a server-side error. | | `SandboxTimeoutError` | A request exceeds its timeout. Never retried, since the server may still be executing the request. | Each `SandboxError` includes: | Property | Type | Description | | - | - | - | | `message` | `string` | Human-readable error message. | | `statusCode` | `number \| null` | HTTP status code, when available. | | `body` | `unknown` | Parsed response body, when available. | ## Handle errors ```typescript theme={null} import { Porter, SandboxError } from "porter-sandbox"; const porter = new Porter(); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim" }); try { const result = await sandbox.exec(["python", "-c", "raise SystemExit(2)"]); if (result.exit_code !== 0) { console.error(result.stderr); } } catch (error) { if (error instanceof SandboxError) { console.error(`Sandbox API error: ${error.message}`); } else { throw error; } } finally { await sandbox.terminate(); porter.close(); } ``` Non-zero command exit codes are returned in the exec response. They are normal command results, not SDK exceptions. # TypeScript Sandbox SDK quickstart Source: https://docs.porter.run/sandboxes/sdk/typescript/quickstart Create and manage Porter Sandboxes from TypeScript The TypeScript Sandbox SDK is published as `porter-sandbox`. Use it from application code running in your sandbox-enabled Porter cluster. ## Install Add the SDK to your application: ```bash theme={null} npm install porter-sandbox ``` ## Create your first sandbox Create a sandbox, execute a command, print the command output, and terminate the sandbox when the work is done: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", name: "typescript-quickstart", tags: { example: "typescript-quickstart" }, }); try { const result = await sandbox.exec(["python", "-c", "print('hello from porter')"]); console.log(result.stdout); } finally { await sandbox.terminate(); porter.close(); } ``` The SDK connects to the in-cluster Sandbox API automatically when this code runs as a Porter Application in the same cluster where sandboxes are enabled. If you need to invoke sandboxes from outside that cluster, set `PORTER_SANDBOX_API_KEY` to a Porter API token and `PORTER_CLUSTER_ID` to the target cluster. The SDK reads the project from the token and builds the external Porter API URL automatically; see [calling from outside the cluster](/sandboxes/getting-started#calling-from-outside-the-cluster) for details. Use sandbox names when you need to fetch, inspect, exec into, or terminate a sandbox later. Sandbox names must be unique within a cluster and currently cannot be reused, even after the sandbox is terminated. ## Fetch logs Logs are returned as structured log lines: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", name: "typescript-logs-demo", }); try { await sandbox.exec(["python", "-c", "print('log me')"]); const logs = await sandbox.logs({ limit: 100 }); for (const line of logs) { console.log(`${line.timestamp} [${line.level}] ${line.line}`); } } finally { await sandbox.terminate(); porter.close(); } ``` ## Get a sandbox by name ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const sandbox = await porter.sandboxes.get("typescript-quickstart"); console.log(sandbox.phase); porter.close(); ``` ## List sandboxes Use tags to find sandboxes created by a workflow: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const sandboxes = await porter.sandboxes.list({ tags: { workflow: "agent-run" }, }); for (const sandbox of sandboxes) { console.log(sandbox.phase); } porter.close(); ``` ## Concurrent sandboxes All TypeScript Sandbox SDK methods are promise-based, so you can fan out with `Promise.all`: ```typescript theme={null} const sandboxes = await Promise.all( Array.from({ length: 10 }, (_, index) => porter.sandboxes.create({ image: "python:3.12-slim", name: `batch-demo-${index}`, tags: { batch: "demo" }, }), ), ); ``` ## Next steps * [TypeScript Sandbox SDK reference](/sandboxes/sdk/typescript/reference) * [TypeScript Sandbox SDK volumes](/sandboxes/sdk/typescript/volumes) * [TypeScript Sandbox SDK errors](/sandboxes/sdk/typescript/errors) * [Sandboxes getting started](/sandboxes/getting-started) # TypeScript Sandbox SDK reference Source: https://docs.porter.run/sandboxes/sdk/typescript/reference Core classes and methods in the porter-sandbox TypeScript Sandbox SDK Import the top-level client from `porter-sandbox`: ```typescript theme={null} import { Porter } from "porter-sandbox"; ``` ## `Porter` `Porter` is the top-level client. It exposes resource namespaces for sandboxes, volumes, health, and readiness. ```typescript theme={null} const porter = new Porter(); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", name: "my-sandbox", }); ``` Constructor options: | Option | Type | Description | | - | - | - | | `apiKey` | `string \| undefined` | Optional API key. Falls back to the `PORTER_SANDBOX_API_KEY` environment variable. Only required when invoking the Sandbox API from outside the sandbox-enabled cluster. Create one from **Settings > API tokens** in the Porter Dashboard. Creating API tokens requires admin permissions, and the token must have developer permissions to authenticate with the Sandbox API. | | `baseUrl` | `string \| undefined` | Optional API base URL override. Falls back to the `PORTER_SANDBOX_BASE_URL` environment variable. If neither is set, the SDK builds the external Porter API URL from the API token's project and `PORTER_CLUSTER_ID`, then falls back to the in-cluster Sandbox API URL when running in a Kubernetes cluster, and otherwise throws an error explaining what to set. | | `timeoutMs` | `number \| undefined` | Request timeout in milliseconds. Defaults to 30 seconds. | Close the client when your process is done: ```typescript theme={null} porter.close(); ``` ## `porter.sandboxes` ### `create` ```typescript theme={null} const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", name: "my-sandbox", command: undefined, args: undefined, env: undefined, tags: { workflow: "demo" }, volume_mounts: undefined, }); ``` Options: | Option | Type | Description | | - | - | - | | `image` | `string` | Container image to run, referenced by tag or digest. Pass `""` when starting from a snapshot. | | `snapshot_id` | `string \| undefined` | Start the sandbox from a [snapshot](#snapshots) instead of an image. The snapshot holds files only. An omitted `command` runs the base image's entrypoint, and only the volumes in `volume_mounts` are mounted. | | `name` | `string \| undefined` | Optional cluster-unique sandbox name. Use lowercase letters, numbers, and hyphens; start and end with a letter or number. Names currently cannot be reused, even after termination. | | `command` | `string[] \| undefined` | Optional entrypoint override. | | `args` | `string[] \| undefined` | Optional arguments passed to the command. | | `env` | `Record \| undefined` | Environment variables to set in the sandbox. | | `env_groups` | `string[] \| undefined` | Names of [environment groups](/applications/configure/environment-groups) on the cluster whose variables are injected into the sandbox, resolved to their latest version at create time. On a key conflict a later group wins over an earlier one, and an explicit `env` entry wins over any group value. | | `tags` | `Record \| undefined` | Key-value labels for finding related sandboxes. | | `volume_mounts` | `Record \| undefined` | Volume IDs keyed by absolute mount path inside the sandbox. | | `networking` | `SandboxNetworkingSpec[] \| undefined` | Port to expose and, optionally, the domain and visibility to serve it on, or `internal` to serve it inside the cluster only. Omit to expose nothing. See [Sandbox Networking](/sandboxes/networking). | | `egress` | `SandboxEgressSpec \| undefined` | Restricts the sandbox's outbound traffic to a list of allowed destinations (hostnames, wildcards, IP literals, CIDR ranges, or in-cluster Service DNS names). Omit to leave internet access unrestricted. See [Restrict egress with an allowlist](/sandboxes/networking#restrict-egress-with-an-allowlist). | | `resources` | `SandboxResourcesSpec \| undefined` | CPU and memory for the sandbox (`{ cpu: "2", memory: "4Gi" }`). CPU is in cores, with an `m` suffix for a fraction; memory takes a `Gi` or `Mi` suffix. Omit a field to keep the cluster's default sandbox size for that resource. See [custom sandbox sizes](/sandboxes/capacity#custom-sandbox-sizes). | | `ttl_seconds` | `number \| undefined` | Maximum lifetime in seconds, counted from creation. The sandbox is terminated once it elapses. Omit for no limit. See [sandbox lifetime](/sandboxes/overview#sandbox-lifetime). | Returns a `Sandbox` handle. ### `get` ```typescript theme={null} const sandbox = await porter.sandboxes.get("my-sandbox"); ``` Looks up a sandbox by name and returns a `Sandbox` handle. Use names as the canonical reference for sandboxes you need to operate on later. ### `list` ```typescript theme={null} const sandboxes = await porter.sandboxes.list({ tags: { workflow: "demo" }, page: 1, }); ``` Lists sandboxes, optionally filtered by tags and page. ## `Sandbox` `Sandbox` is the handle returned by `porter.sandboxes`. Properties: | Property | Type | Description | | - | - | - | | `id` | `string` | Sandbox identifier. | | `phase` | `StatusResponsePhase \| null` | Last-known lifecycle phase. | | `tags` | `Record \| null` | Last-known tags. | Methods: ```typescript theme={null} const status = await sandbox.refresh(); const result = await sandbox.exec(["python", "-c", "print('hi')"]); const logs = await sandbox.logs({ limit: 100 }); await sandbox.terminate(); ``` | Method | Description | | - | - | | `refresh()` | Refetches status and updates `phase` and `tags`. | | `exec(command, requestOptions?)` | Runs a command in the sandbox and returns stdout, stderr, and exit code. | | `logs(options?)` | Fetches structured log lines. | | `terminate()` | Terminates the sandbox. | ### `exec` timeouts `exec` is long-running: the client sends no timeout by default, so the call waits for the command to exit. Pass `requestOptions.timeoutMs` to cap how long the client waits. When the timeout elapses, the SDK throws `SandboxTimeoutError` and does not retry, since the command may still be running server-side. ```typescript theme={null} import { SandboxTimeoutError } from "porter-sandbox"; try { const result = await sandbox.exec(["sleep", "120"], { timeoutMs: 30_000 }); } catch (error) { if (error instanceof SandboxTimeoutError) { console.error("exec exceeded 30s; the command may still be running in the sandbox"); } else { throw error; } } ``` The client-wide `timeoutMs` on `Porter` still applies to every other request (30 seconds by default). ## Volumes Volumes are persistent storage that can be mounted into sandboxes. ```typescript theme={null} const volume = await porter.volumes.create({ name: "my-data" }); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", volume_mounts: { "/mnt/data": volume.id }, }); ``` Sandbox and volume names may contain lowercase letters, numbers, and hyphens, and must start and end with a letter or number. Sandbox names currently cannot be reused, even after termination. Volume names must be unique for the lifetime of the volume and can be reused after deletion. Volume methods: | Method | Description | | - | - | | `porter.volumes.list()` | Lists volumes as handles. | | `porter.volumes.create(body)` | Creates a volume and returns its handle. A body with `type: "object"` and `object` makes an object volume; the default is a disk volume. | | `porter.volumes.get(name)` | Fetches a volume handle by name. | | `porter.volumes.delete(name)` | Deletes a volume by name. | `create`, `get`, and `list` return `Volume | ObjectVolume`; the `type` getter discriminates the union. A `Volume` (disk) has `id`, `name`, `type`, `phase`, `path`, `attachedTo`, and `createdAt`, plus: | Method | Description | | - | - | | `volume.listdir(path)` | Entries directly inside a directory. | | `volume.iterdir(path)` | Async iterator over the whole tree below a directory. | | `volume.search(query, { path })` | Entries whose name contains a substring. | | `volume.readFile(path, { offset, length })` | File contents as bytes, optionally a byte range. | | `volume.readText(path, { offset, length })` | File contents as a string. | | `volume.stream(path, { chunkSize })` | Async iterator over a file in chunks, for files too large to hold in memory. | | `volume.writeFile(path, content)` | Writes bytes to a file, creating parent directories and replacing any existing file. Atomic. | | `volume.writeText(path, content)` | Writes a string to a file. See `writeFile`. | | `volume.moveFile(from, to)` | Moves or renames a file or directory; `to` is the full new path. Fails if the destination is occupied. | | `volume.refresh()` | Refetches the underlying volume record. | | `volume.delete()` | Deletes this volume. | An `ObjectVolume` shares `id`, `name`, `type`, `phase`, `attachedTo`, `createdAt`, `refresh()`, and `delete()`, and adds `object` (the bucket, key prefix, and access mode). It has no file methods: an object volume's contents live in its bucket. ## Snapshots A snapshot captures a running sandbox's filesystem so new sandboxes can start from it later, with the same files in place. Capturing does not stop the sandbox, and `create` resolves once the capture is complete. ```typescript theme={null} const base = await porter.sandboxes.create({ image: "python:3.12-slim", name: "base", command: ["sleep", "infinity"], }); await base.exec(["pip", "install", "pandas"]); const snapshot = await porter.snapshots.create(base.id, {}); const sandbox = await porter.sandboxes.create({ image: "", snapshot_id: snapshot.id, command: ["python", "-c", "import pandas; print(pandas.__version__)"], }); ``` A snapshot holds files only, not running processes or mounted volumes. A sandbox started from one runs its command from the beginning with the captured files in place, and mounts only the volumes passed in `volume_mounts`. Pass `image: ""` with `snapshot_id`, since the snapshot is the image. Snapshot methods: | Method | Description | | - | - | | `porter.snapshots.create(id, spec)` | Captures the sandbox with ID `id`, which must be `running`, and returns a `Snapshot` once the capture completes. `spec.mode` defaults to `filesystem`, the only mode available today. | | `porter.snapshots.list({ sandboxId })` | Lists snapshots, most recent first. Returns a `SnapshotListResponse` whose `snapshots` holds `Snapshot` records. Pass `sandboxId` to narrow to one sandbox. | | `porter.snapshots.delete(id)` | Deletes a snapshot and the files it captured. Fails while the capture is still running or while a sandbox started from it has not finished. | A `Snapshot` has `id`, `sandbox_id`, `mode`, `status` (`pending`, `ready`, or `failed`), `size_bytes`, `t_created_unix_ms`, `t_ready_unix_ms`, and `failure_reason`. ## Low-level client The top-level client exposes generated low-level clients through `.raw`: ```typescript theme={null} const rawClient = porter.raw; const rawSandboxes = porter.sandboxes.raw; ``` Use these only when you need an endpoint that is not wrapped by the ergonomic SDK surface yet. ## Next steps * [TypeScript Sandbox SDK volumes](/sandboxes/sdk/typescript/volumes) * [TypeScript Sandbox SDK errors](/sandboxes/sdk/typescript/errors) # TypeScript Sandbox SDK volumes Source: https://docs.porter.run/sandboxes/sdk/typescript/volumes Create, mount, browse, read, and write sandbox volumes with the TypeScript Sandbox SDK Volumes provide persistent storage that can be mounted into Porter Sandboxes. Create a volume before launching the sandbox, then pass the volume ID in `volume_mounts`. ## Create and mount a volume ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const volume = await porter.volumes.create({ name: "agent-workspace", }); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", volume_mounts: { "/workspace": volume.id, }, }); try { await sandbox.exec(["python", "-c", "open('/workspace/result.txt', 'w').write('done')"]); const result = await sandbox.exec(["cat", "/workspace/result.txt"]); console.log(result.stdout); } finally { await sandbox.terminate(); porter.close(); } ``` `volume_mounts` is an object keyed by the absolute mount path inside the sandbox. Each value is a volume ID. ## Object volumes Passing `type` and `object` to `create` makes an [object volume](/sandboxes/volumes#object-volumes), which exposes a registered S3 bucket. It mounts like any other volume through `volume_mounts`. ```typescript theme={null} const datasets = await porter.volumes.create({ name: "datasets", type: "object", object: { bucket: "my-bucket", prefix: "training/v1", access: "read_only" }, }); ``` Object volumes get their own `ObjectVolume` handle. It carries the shared surface — `id`, `name`, `type`, `phase`, `attachedTo`, `createdAt`, `refresh`, and `delete` — plus the `object` spec, and none of the file methods: an object volume's contents live in its bucket. `create`, `get`, and `list` return `Volume | ObjectVolume`, and the `type` getter discriminates the union, so a check narrows it (`instanceof ObjectVolume` works too): ```typescript theme={null} for (const volume of await porter.volumes.list()) { if (volume.type === "object") { console.log(volume.name, "exposes bucket", volume.object.bucket); } else { const checkpoints = await volume.listdir("/checkpoints"); } } ``` ### File system mode Set `mode` to `"filesystem"` to mount the bucket in [file system mode](/sandboxes/volumes#file-system-mode), which supports renames and in-place edits. The bucket needs the `sandbox_filesystem` capability. ```typescript theme={null} const workspace = await porter.volumes.create({ name: "team-a-workspace", type: "object", object: { bucket: "my-bucket", prefix: "team-a", mode: "filesystem" }, }); ``` The volume still returns an `ObjectVolume` handle, so manage its files from a sandbox that mounts it or through S3. ## List volumes `list()` returns volume handles: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); for (const volume of await porter.volumes.list()) { console.log(volume.id, volume.name, volume.phase, volume.attachedTo, volume.path); } porter.close(); ``` ## Get a volume by name Volume names are unique within the cluster. Use `get(name)` when you know a volume name: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const volume = await porter.volumes.get("agent-workspace"); console.log(volume.id); porter.close(); ``` Volume names may contain lowercase letters, numbers, and hyphens, and must start and end with a letter or number. A volume name must be unique within the cluster for the lifetime of the volume. After the volume is deleted, the name can be used again. ## Inspect a volume ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const volume = await porter.volumes.get("agent-workspace"); console.log(volume.name, volume.phase, volume.attachedTo, volume.createdAt); porter.close(); ``` ## Browse volume contents `listdir` returns the entries directly inside a directory, directories first: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const volume = await porter.volumes.get("agent-workspace"); for (const file of await volume.listdir("/checkpoints")) { console.log(file.path, file.isDirectory ? "dir" : file.sizeBytes); } porter.close(); ``` `iterdir` walks the whole tree instead, descending into every subdirectory: ```typescript theme={null} for await (const file of volume.iterdir("/checkpoints")) { console.log(file.path, file.sizeBytes); } ``` `search` walks the tree and returns only the entries whose name contains a substring, optionally rooted at a subdirectory: ```typescript theme={null} const configs = await volume.search("config.json"); const checkpointConfigs = await volume.search("config.json", { path: "/checkpoints" }); ``` ## Read a file `readText` returns a whole file as a string, and `readFile` returns raw bytes: ```typescript theme={null} const config = await volume.readText("/checkpoints/config.json"); const bytes = await volume.readFile("/checkpoints/weights.bin"); ``` Both accept `offset` and `length` to read part of a file: ```typescript theme={null} const head = await volume.readText("/logs/train.log", { offset: 0, length: 512 }); ``` For files too large to hold in memory, `stream` pages through the file in chunks (8 MiB by default): ```typescript theme={null} import { createWriteStream } from "node:fs"; const out = createWriteStream("model.safetensors"); for await (const chunk of volume.stream("/checkpoints/model.safetensors")) { out.write(chunk); } out.end(); ``` Reading a path that does not exist throws `NotFoundError`. ## Write a file `writeText` writes a string and `writeFile` writes raw bytes. Both create parent directories as needed and replace any existing file at the path: ```typescript theme={null} await volume.writeText("/config/app.yaml", "replicas: 3\n"); await volume.writeFile("/checkpoints/weights.bin", bytes); ``` A write is atomic: the file appears at its path only after the last byte lands, so an interrupted or failed write leaves the previous contents in place rather than a truncated file. An uploaded file gets the same permissions a sandbox writing to its own mount would leave, so it is indistinguishable from a file the sandbox wrote. You can write to a volume whether or not a sandbox has it mounted; the volume itself is the target. A single write is capped at 1 GiB; a request made from outside the cluster with an API token must also finish within 30 seconds, so write very large files from an app running in the same cluster. ## Move or rename a file `moveFile` moves a file or directory to a new path. The second argument is the entry's full new path, not a directory to drop it into, so one call both renames and relocates. A directory moves with everything under it: ```typescript theme={null} await volume.moveFile("/notes.txt", "/archive/notes.txt"); await volume.moveFile("/models/v1", "/models/v2"); ``` The destination's parent directory must already exist, and nothing is overwritten. Moving onto a path something already occupies fails and leaves the entry where it was. ## Access volume data from apps Volumes live on a shared disk that apps on the same cluster can attach. Attach the disk named `sandbox-volumes` to a service in your app; it mounts at `/data//sandbox-volumes` and contains one subdirectory per volume. Each volume handle exposes a `path` with the volume's subdirectory on that disk, so an app reads a volume's data at `/data//sandbox-volumes/`: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const volume = await porter.volumes.get("agent-workspace"); console.log(volume.path); // sandbox-vol-2f1c8b7e-... porter.close(); ``` The disk is a live view: writes a sandbox makes to its mounted volume are visible to apps right away, and new volumes show up as new subdirectories without redeploying the app. > [!WARNING] > > Volume contents are written by sandboxed workloads, which are often running untrusted code. Treat anything your app reads from this disk as untrusted input: hostile file contents, names, or sizes can exploit vulnerabilities in the code that processes them. Porter isolates the sandboxes themselves but does not inspect or sanitize what they write, so validating this data before acting on it is your application's responsibility. ## Delete a volume Delete volumes by name: ```typescript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); await porter.volumes.delete("agent-workspace"); porter.close(); ``` Deleting a volume fails while it is attached to a sandbox. Terminate any attached sandboxes before deleting the volume. ## Next steps * [TypeScript Sandbox SDK quickstart](/sandboxes/sdk/typescript/quickstart) * [TypeScript Sandbox SDK reference](/sandboxes/sdk/typescript/reference) * [TypeScript Sandbox SDK errors](/sandboxes/sdk/typescript/errors) # Volumes Source: https://docs.porter.run/sandboxes/volumes Create, mount, and manage persistent sandbox volumes: read, write, upload, download, and move files from the SDK, CLI, or dashboard Volumes are persistent storage you can mount into Porter Sandboxes. Unlike a sandbox's own filesystem, which is discarded when the sandbox exits, a volume lives independently and keeps its contents across sandboxes. Use them for input data, checkpoints, caches, and results that need to outlive any single run. You can work with a volume three ways: the [Python](/sandboxes/sdk/python/volumes) and [TypeScript](/sandboxes/sdk/typescript/volumes) SDKs, the [CLI](/sandboxes/cli#porter-sandbox-volume), and the Porter Dashboard. This page covers the common tasks; the SDK and CLI pages document the full surface. ## Naming and lifecycle Volume names may contain lowercase letters, numbers, and hyphens, and must start and end with a letter or number. A name must be unique within the cluster for the lifetime of the volume, and can be reused after the volume is deleted. A volume starts in the `pending` phase and moves to `ready` once its underlying claim binds. Sandboxes that mount a volume wait for it to bind automatically, so you do not need a separate wait step. ## Create and mount a volume Create a volume, then mount it into a sandbox by passing `volume_mounts` keyed by the absolute mount path inside the sandbox. Each value is a volume ID. ```python Python theme={null} from porter_sandbox import Porter with Porter() as porter: volume = porter.volumes.create(name="agent-workspace") sandbox = porter.sandboxes.create( image="python:3.12-slim", volume_mounts={"/workspace": volume.id}, ) ``` ```typescript TypeScript theme={null} import { Porter } from "porter-sandbox"; const porter = new Porter(); const volume = await porter.volumes.create({ name: "agent-workspace" }); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", volume_mounts: { "/workspace": volume.id }, }); ``` ```bash CLI theme={null} porter sandbox volume create agent-workspace porter sandbox create python:3.12-slim --volume /workspace=agent-workspace -- sleep infinity ``` ## Object volumes A volume is a disk volume by default: file storage on your cluster's disks. An object volume exposes an S3 bucket in your cloud account instead, so sandboxes work with the bucket's objects as files at the mount path. Like all storage Porter provisions, both kinds live in your account and belong to you. Object volumes are AWS-only today. > [!WARNING] > > A `read-write` object volume isn't a general-purpose filesystem. Sandboxes can't edit a file in place or append to one. Write each file once, with its complete contents. To change an existing file, delete it and write it again. > > Treat object volumes as read-mostly: read datasets in, write results out as new files. For workloads that edit files, use a disk volume or mount the bucket in [file system mode](#file-system-mode). **Choosing a volume kind.** Default to a disk volume: it behaves like a normal filesystem, so anything that edits or appends files works, and it's the only kind that supports the file operations on this page from outside a sandbox. Reach for an object volume only when the data already lives in S3 or belongs there. Use an object volume to expose an existing bucket or dataset to sandboxes, usually with `read-only` access, or to collect run outputs straight into a bucket with `write-only-new-files`. Write each output once, as a complete new file. Avoid `read-write` object volumes for anything that modifies files: in-place edits and appends fail, per the preceding warning. `read-write` only helps workloads that delete objects or replace them wholesale. If a workload needs to mutate working files and the results belong in S3, use [file system mode](#file-system-mode) when the bucket supports it, or do the work on a disk volume and copy the results to the bucket at the end. ### Set up the bucket Object volumes reference a bucket in your project with the `sandboxes` capability. You can create one through Porter, or register a bucket you already have. The bucket is yours in both cases. The difference is how much Porter manages. **Create it through Porter** and Porter manages the bucket's full lifecycle, deletion included: removing the bucket from Porter deletes it with all its contents. ```bash theme={null} porter storage bucket create --name my-bucket \ --cloud-account-id \ --region us-east-1 \ --connected-cluster-ids \ --capabilities sandboxes ``` **Register an existing bucket** and Porter manages access only: registration grants workloads on the connected clusters access, and removing it removes only those grants, never the bucket itself. ```bash theme={null} porter storage bucket register my-bucket \ --cloud-account-id \ --region us-east-1 \ --connected-cluster-ids \ --capabilities sandboxes ``` Both flows are also in the dashboard: open **Add-ons**, choose **Object storage**, then pick **Create a new bucket** or **Register an existing bucket**. If the bucket already exists in your project without sandbox access, enable the **Sandbox access** toggle on its page. Register with `--read-only` to forbid all writes through Porter. A read-only bucket accepts only read-only volumes. To change an existing bucket's clusters or capabilities, use `porter storage bucket update` or the bucket's dashboard page. ### Create and mount an object volume Create the volume from the SDK or CLI by naming the bucket. Mounting works exactly like any other volume. ```python Python theme={null} from porter_sandbox import Porter, VolumeObjectSpec, VolumeObjectSpecAccess, VolumeSpecType with Porter() as porter: datasets = porter.volumes.create( name="datasets", type=VolumeSpecType.OBJECT, object=VolumeObjectSpec( bucket="my-bucket", prefix="training/v1", access=VolumeObjectSpecAccess.READ_ONLY, ), ) sandbox = porter.sandboxes.create( image="python:3.12-slim", volume_mounts={"/data": datasets.id}, ) ``` ```typescript TypeScript theme={null} const datasets = await porter.volumes.create({ name: "datasets", type: "object", object: { bucket: "my-bucket", prefix: "training/v1", access: "read_only" }, }); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", volume_mounts: { "/data": datasets.id }, }); ``` ```bash CLI theme={null} porter sandbox volume create datasets --bucket my-bucket --prefix training/v1 --access read-only porter sandbox create python:3.12-slim --volume /data=datasets -- python train.py ``` In the SDKs, an object volume gets its own `ObjectVolume` handle without the file methods; the SDK volumes pages show how to handle both kinds. `--prefix` scopes the volume to keys under a prefix: sandboxes see only objects under it, at paths relative to it. Omit it to expose the whole bucket. Several volumes can expose the same bucket with different prefixes and access modes. ### Access modes Pick the mode with `--access` when creating the volume: | Mode | What sandboxes can do | | - | - | | `read-write` (default) | Read, create, overwrite, and delete objects | | `read-only` | Read only | | `write-only-new-files` | Read and create new objects; overwriting or deleting existing objects fails | ### Behavior and limits An object volume's contents live in the bucket. Writes from a sandbox are visible to anything else with bucket access, and objects written outside Porter appear in the sandbox. Deleting the volume removes only the volume itself, never the bucket or its objects. Any number of sandboxes can mount the same object volume at once, on any sandbox cluster. Sandboxes reach the bucket through [Mountpoint for Amazon S3](https://github.com/awslabs/mountpoint-s3); its write limits are in the warning at the top of this section. The file operations below work on disk volumes only. Manage an object volume's contents with your usual S3 tooling instead. ### File system mode By default, an object volume mounts its bucket in object mode, with the write limits described earlier. File system mode mounts the bucket through Amazon S3 Files instead, which gives sandboxes full file semantics. Renames and in-place edits work, and the data still lives in your bucket. Turning file system mode on for a bucket only makes it available. Existing volumes on the bucket keep object mode, and each new volume chooses file system mode when it's created. To enable it, the bucket needs: * The `sandboxes` capability and write access. A bucket registered with `--read-only` can't use file system mode. * Versioning turned on. Then add the `sandbox_filesystem` capability, either with the **Allow file system mounts** toggle on the bucket's dashboard page or from the CLI: ```bash theme={null} porter storage bucket update --name my-bucket \ --connected-cluster-ids \ --capabilities sandboxes,sandbox_filesystem ``` Porter then creates an S3 Files file system for the bucket, which takes a few minutes. Creating a file system mode volume fails until the file system is ready. Create the volume from the SDK by setting `mode` to `filesystem`: ```python Python theme={null} from porter_sandbox import ( Porter, VolumeObjectSpec, VolumeObjectSpecAccess, VolumeObjectSpecMode, VolumeSpecType, ) with Porter() as porter: workspace = porter.volumes.create( name="team-a-workspace", type=VolumeSpecType.OBJECT, object=VolumeObjectSpec( bucket="my-bucket", prefix="team-a", access=VolumeObjectSpecAccess.READ_WRITE, mode=VolumeObjectSpecMode.FILESYSTEM, ), ) sandbox = porter.sandboxes.create( image="python:3.12-slim", volume_mounts={"/workspace": workspace.id}, ) ``` ```typescript TypeScript theme={null} const workspace = await porter.volumes.create({ name: "team-a-workspace", type: "object", object: { bucket: "my-bucket", prefix: "team-a", access: "read_write", mode: "filesystem" }, }); const sandbox = await porter.sandboxes.create({ image: "python:3.12-slim", volume_mounts: { "/workspace": workspace.id }, }); ``` File system mode supports the `read-write` and `read-only` access modes, but not `write-only-new-files`. A sandbox can't mount two volumes that expose the same bucket and prefix, so don't pair a file system mode volume with an object mode volume on the same prefix in one sandbox. File system and object mode volumes on the same bucket work fine in separate sandboxes. Changes from a file system mode volume reach the bucket about a minute after the last write. Until then, object mode volumes and other S3 clients read the earlier contents. File system mode is AWS-only, like all object volumes. ## Work with files Reads and writes target the volume itself, so you can browse, read, and write a volume's files whether or not a sandbox has it mounted. This lets you stage inputs before a run and collect outputs after the sandbox exits. These operations apply to disk volumes. An [object volume](#object-volumes)'s contents live in its bucket. ### Browse ```python Python theme={null} for file in volume.listdir("/checkpoints"): print(file.path, "dir" if file.is_directory else file.size_bytes) ``` ```typescript TypeScript theme={null} for (const file of await volume.listdir("/checkpoints")) { console.log(file.path, file.isDirectory ? "dir" : file.sizeBytes); } ``` ```bash CLI theme={null} porter sandbox volume files agent-workspace checkpoints ``` `iterdir` walks the whole tree and `search` filters entries by name. See the SDK volumes pages for both. ### Read and download ```python Python theme={null} config = volume.read_text("/config/app.yaml") data = volume.read_file("/checkpoints/weights.bin") ``` ```typescript TypeScript theme={null} const config = await volume.readText("/config/app.yaml"); const bytes = await volume.readFile("/checkpoints/weights.bin"); ``` ```bash CLI theme={null} porter sandbox volume read agent-workspace config/app.yaml > app.yaml ``` Reads accept an offset and length for byte ranges, and the SDKs can `stream` files too large to hold in memory. From the dashboard, open a file and use **Download** for files up to 20 MB. ### Write and upload ```python Python theme={null} volume.write_text("/config/app.yaml", "replicas: 3\n") volume.write_file("/checkpoints/weights.bin", data) ``` ```typescript TypeScript theme={null} await volume.writeText("/config/app.yaml", "replicas: 3\n"); await volume.writeFile("/checkpoints/weights.bin", bytes); ``` ```bash CLI theme={null} porter sandbox volume write agent-workspace config/app.yaml --file ./app.yaml ``` Writes create parent directories as needed and replace any existing file. A write is atomic: the file appears at its path only after the last byte lands, so an interrupted write leaves the previous contents in place. A single write is capped at 1 GiB, and a request from outside the cluster must finish within 30 seconds; write larger files from inside a sandbox that mounts the volume. ### Move or rename ```python Python theme={null} volume.move_file("/notes.txt", "/archive/notes.txt") ``` ```typescript TypeScript theme={null} await volume.moveFile("/notes.txt", "/archive/notes.txt"); ``` ```bash CLI theme={null} porter sandbox volume move agent-workspace notes.txt archive/notes.txt ``` The destination is the entry's full new path, so one call both renames and relocates, and a directory moves with everything under it. The destination's parent directory must already exist, and nothing is overwritten. ## Manage files in the dashboard In the Porter Dashboard, open the **Add-ons** tab and select **Sandbox file storage** for your cluster, then click a volume to open it. The **Files** tab shows the volume as a tree, where you can: * **Browse** the contents, expanding directories as you go. * **Upload** by dragging files onto the tree. A file dropped on a directory row goes into that directory; one dropped on empty space goes to the volume root. * **Export** by opening a file and clicking **Download** (up to 20 MB; text files under 1 MB also preview inline). * **Move** by dragging a row onto a folder. ![Dragging a file onto the volume tree to upload it](https://mintcdn.com/porter/IOY4-aWRTqHxLmiY/images/sandboxes/volume-upload.gif?s=b31984b73d1fda1cf0e4fe953b52255a) ## Persist and share across sandboxes Because a volume outlives any sandbox that mounts it, one sandbox can write state that a later one resumes from: a checkpoint, a warm cache, or a partial result. Mount the same volume into each sandbox in turn. This sequential handoff works on any sandbox cluster. Mounting the same volume into several sandboxes that run at the same time additionally requires the cluster to back sandbox volumes with shared storage; without it, a volume attaches to one sandbox at a time. Porter does not coordinate concurrent writes, so if several sandboxes may write the same paths at once, partition the writes or arrange your own locking. ## Access volume data from apps When the app that reads a volume runs as a Porter app in the same cluster, it can read the volume's files straight off the shared disk, with no sandbox required. Attach the disk named `sandbox-volumes` to a service; it mounts at `/data//sandbox-volumes` with one subdirectory per volume. A volume handle's `path` gives its subdirectory, so the app reads a volume's data at `/data//sandbox-volumes/`. The disk is a live view, so files a sandbox writes show up right away. > [!WARNING] > > Volume contents are written by sandboxed workloads, which often run untrusted code. Treat anything your app reads from a volume as untrusted input, and validate it before acting on it. ## Delete a volume ```python Python theme={null} porter.volumes.delete("agent-workspace") ``` ```typescript TypeScript theme={null} await porter.volumes.delete("agent-workspace"); ``` ```bash CLI theme={null} porter sandbox volume delete agent-workspace ``` Deleting a volume fails while it is attached to a sandbox. Terminate any attached sandboxes first. ## Reference * [Python Sandbox SDK volumes](/sandboxes/sdk/python/volumes) and [reference](/sandboxes/sdk/python/reference) * [TypeScript Sandbox SDK volumes](/sandboxes/sdk/typescript/volumes) and [reference](/sandboxes/sdk/typescript/reference) * [Sandbox CLI volume commands](/sandboxes/cli#porter-sandbox-volume) # Audit logs Source: https://docs.porter.run/security-and-compliance/audit-logs Review project activity in Porter, filter audit log entries, and export matching results for security reviews Porter audit logs help admins review project activity across the dashboard, CLI, and API token usage. Each entry captures when the request happened, who initiated it, what resource it targeted, which action ran, and the resulting status code. > [!NOTE] > > Audit logs are only available to users with the **Admin** role. See [Role-based access control](/security-and-compliance/role-based-access-control) for more information about Porter roles. ## Open audit logs In the Porter dashboard, open **Security** and select the **Audit logs** tab. ![Audit logs page in the Porter dashboard](https://mintcdn.com/porter/cgaz99Nx1lSlKM0B/images/security-and-compliance/audit-logs.webp?fit=max\&auto=format\&n=cgaz99Nx1lSlKM0B\&q=85\&s=4d3abd5dfc59ffc208191511c74b85db) When you open audit logs from the dashboard navigation, Porter applies a default filter that excludes `GET` requests. This keeps the initial view focused on write operations such as creating, updating, deploying, and deleting resources. Clear the `Method excludes GET` filter if you need to review read-only activity. ## What each entry shows The audit log table includes: * **Timestamp:** when Porter received the request, shown in your browser's time zone. * **Actor:** the user or API token that made the request. CLI activity is marked with a `CLI` badge. * **Method:** the HTTP method, such as `POST`, `PATCH`, `PUT`, or `DELETE`. * **Status:** the HTTP response status code returned by Porter. * **Resource:** the Porter resource associated with the request, such as an application, cluster, datastore, environment group, project, cloud account, API token, integration, or user. * **Action:** a human-readable action name. Hover over the action to view the underlying request path. Porter preserves readable actor names for deleted users and revoked API tokens so older entries remain useful during investigations. ## Filter audit logs Audit logs are available for the last 30 days. Use the **Date range** filter to choose a preset, such as the last hour, last 24 hours, last 7 days, or last 30 days, or select a custom date and time range. You can filter by: * Actor * Method * Source * Date range * Action * Status code * Resource type * Application * Cluster Most filters support include and exclude modes. For example, you can include only `DELETE` requests or exclude a noisy application while reviewing the rest of the project. Use the search field to search across matching audit entries. Search can match request paths, user email addresses, API token names, application names, and cluster names. The volume chart above the table shows activity over the selected range. Select part of the chart to narrow the table to that time window. ## Share a filtered view Filters, search, date range, and pagination are stored in the page URL. Copy the URL from your browser to share the same audit log view with another project admin. ## Export audit logs If audit log export is enabled for your project, the **Export** button appears above the table. Exports use the current date range and supported table filters, including actor, method, action, status code, resource type, application, cluster, and search. You can download matching entries as: * `.csv` * `.ndjson` Exports are capped at the first 10,000 matching rows. If the current filters match more than 10,000 rows, narrow the date range or add filters before exporting. ## Troubleshooting ### I cannot see the Audit logs tab Only project admins can view audit logs. Ask a project admin to update your role, or have an admin export the entries you need. ### I expected to see read-only requests The dashboard applies `Method excludes GET` by default when opening audit logs from the Security navigation. Clear that filter to include `GET` requests. ### A filter combination returns no results Application and cluster include filters do not overlap. Each audit log entry is tied to a single resource, so filtering for both a specific application and a specific cluster can return no rows. Clear one of those filters or use an exclude filter instead. # AWS permissions required for Porter Source: https://docs.porter.run/security-and-compliance/aws-permissions Review the IAM roles, policies, and CloudFormation stack permissions that Porter creates and requires to manage your AWS cloud infrastructure # Overview Porter creates several roles and policies for use in the management of your AWS account. These may change over time, and will be automatically updated both here, and on your AWS account. Make sure to check back here for updates, and if you have any questions, please reach out via the support widget on the dashboard. ## Initial Permissions When signing up for Porter on AWS, you will be asked to create a cloudformation stack. This is only used for setting up the initial permissions required for Porter to manage your account. If you are operating in a regulated environment, these are the minimum permissions required to execute the initial cloudformation stack: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Sid": "ManagePorterCloudFormation", "Effect": "Allow", "Action": [ "cloudformation:GetTemplateSummary", "cloudformation:DescribeStackEvents", "cloudformation:CreateStack", "cloudformation:DeleteStack", "cloudformation:DescribeStacks", "cloudformation:ListStacks", "cloudformation:ListStackResources", "iam:DeleteRolePolicy", "iam:DeleteRole", "iam:PutRolePolicy", "iam:GetRole", "iam:CreateRole" ], "Resource": "*" } ] } ``` ## Roles ### Porter Access Manager Porter Access Manager is a role that Porter creates to manage the access given to the other roles in this document. If permissions are removed from this role, you will be **unable to access new features of Porter**.

### Porter Manager

Porter Manager is used for all API operations from Porter, to your AWS account. This role is primarily used to read resources in your account to ensure that Porter is not creating duplicate resources, or using resources managed by a non-Porter process.
This role may adopt policies from the other roles mentioned here, when managing their specific domains.

Porter Manager attaches the three policies below.

#### Read

Describe, List and Get calls across the resources Porter manages. Nothing here changes state.


#### Write

The calls that change state in your account.


#### Data Access

The reads that return the contents of your secrets and stored objects. Porter needs these to sync environment groups and to pull images.


### Porter EKS Manager

Porter EKS Manager is the role used by EKS to perform actions on your AWS account, for ongoing maintenance of your EKS cluster.



### Porter Node Manager

Porter Node Manager is the role used by EKS worker nodes. This role also includes access to some AWS-provided roles such as:

* `AmazonEKS_CNI_Policy`
* `AmazonEKSClusterPolicy`
* `AmazonEKSWorkerNodePolicy`
* `AmazonEC2ContainerRegistryReadOnly`
* `service-role/AmazonEBSCSIDriverPolicy`
* `service-role/AmazonEFSCSIDriverPolicy`


### Porter Infra Manager

This is the role used to manage non-EKS resources such as RDS, Elasticache, S3, etc.


## Policies

On top of the roles above, the following policies are created which may be attached to one, or many of the roles above.

### Cluster Autoscaler

Porter Cluster Autoscaler is used to allow Porter to scale your cluster up and down based on the load of your cluster.


### Loadbalancer

Porter Loadbalancer is used to allow Porter to manage loadbalancers in your account.

  A policy was supposed to appear here, but something had happened.

  Please reach out via the support widget on the dashboard for assistance.
Last Updated: 2026-10-29T23:16:12Z # Using Cloudflare DNS with Porter Source: https://docs.porter.run/security-and-compliance/cloudflare-dns Configure Cloudflare DNS in proxy or non-proxy mode with Porter, including SSL settings, CNAME records, and TLS certificate handling Porter supports the use of Cloudflare DNS out-of-the-box in both proxy, and non-proxy mode. Before continuing any further, ensure that you have followed our guide for [deploying applications on your custom domain](/applications/configure/custom-domains#deploying-on-the-custom-domain) When using non-Cloudflare services such as AWS WAFv2, AWS Cloudfront, or most other DDoS prevention services, it is recommended to use non-proxy mode with your Cloudflare DNS. This ensures that Cloudflare acts only as a Trusted Authority for DNS, and delegates all other security to other providers. Porter handles the creation and renewal of your TLS/HTTPS certificates, and therefore recommends using non-proxy mode. Most Cloudflare services will require Cloudflare DNS Proxy mode. If you require one of these services, follow the steps below for ensuring that your applications are compatible. Before continuing, you will need to copy the address of your Porter-managed loadbalancer. To find your loadbalancer address, visit one of your apps in its Overview tab and access one of its Web services Networking tab. You should see the address for your loadbalancer. This address may be a DNS address, on an IP address depending on your cloud provider. This is important later. # Creating a DNS record If you have already created a wildcard DNS record (recommended), you may skip this section. * From the Cloudflare dashboard, select `Websites`, then your chosen domain name. ![Websites](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/sidebar-websites.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=9344e51796e04e64c4ddf17e7b97898a) * In the sidebar, select `DNS` > `Records` ![DNS Records](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/sidebar-dns-records.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=246108d3d7e803ec951543210d000d89) * Click `Add Record` ![Add DNS Record](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/add-record.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=04741b0de529ca353d70d343b2e4ca5b) **CNAME Record** If your loadbalancer address is a DNS name address, you will need to create a CNAME Record Set `Type` to `CNAME` (CNAME Record) Set `Name` to the DNS name that your application should be available at Set `Target` to the Porter Loadbalancer DNS address from before ![CNAME Record](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/record-cname.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=cc06764ca0e5ccf418621b4d8ca440a6) **A Record** If your loadbalancer address is an IP address, you will need to create an A Record Set `Type` to `A` (A Record) Set `Name` to the DNS name that your application should be available at Set `IPv4 Address` to the Porter Loadbalancer address from before ![CNAME Record](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/record-a.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=92e05cb0eb7da43919d5beae04661775) # Non-Proxy DNS Ensure that `Proxy Status` is disabled. This will show as `DNS only`. ![DNS Only](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/dns-non-proxy.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=247daf0fbdd0be2796fb2c5f789e7741) Done! You should now be able to visit your custom domain, with Porter managing your HTTPS! # Proxy DNS Ensure that `Proxy Status` is enabled. This will show as `Proxied`. ![Proxied DNS](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/dns-proxy.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=456052d8b7b9c7d4903e06ed73b77cdb) ## Allowing Acme-Challenges To ensure that Porter can still create certificates on your behalf, we must allow LetsEncrypt traffic to not be proxied by Cloudflare, as they must be made over http, or unverified https. From the sidebar, select `Rules` > `Page Rules` ![Page Rules](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/sidebar-page-rules.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=35f1b42aebbaf73cd8aa617e6d89e002) Select `Create rule` Give the rule a name. This can be any name you choose. Assuming the domain in question is `example.com`, add a new Page rule for `*example.com/.well-known/acme-challenge/*`, with the following settings: 1. `SSL: Off` 2. `Cache Level: Bypass` Done! You will now be able to avail of any Cloudflare services through Porter. If you are still seeing errors, remove the custom domain from your application in Porter, deploy, then add the domain back and your certificate should be validated after a few seconds. ## Too Many Redirects This step is not necessary for most customers. Only proceed if you are seeing a `too many redirects` error. If you visit your custom domain, you may now get a `too many redirects` error from Cloudflare. To fix this issue, we must create a Cloudflare Configuration Rule. From the sidebar, select `Rules` > `Overview` ![Rules Overview](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/sidebar-overview.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=930ee9dd967bf36141af42dbace30fe3) Click `Create rule`, then select `Configuration Rules` ![Configuration Rules](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/configuration-rules.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=6687bef813287e816050942896b677f2) Give the rule a name. This can be any name you choose. For this guide, we will assume that you want to enable proxied TLS on all subdomains for this website. As such, select `All incoming requests` ![All Incoming Requests](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/rules-all-incoming.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=4547b69f9a2ae41afcd5608bdf02c6e1) Scroll down to `SSL (Optional)` Click `Add` and choose `Full` from the drop down. ![SSL Full](https://mintcdn.com/porter/bja7Zm50xP-m5T8X/images/security-and-compliance/cloudflare/rules-ssl-full.png?fit=max\&auto=format\&n=bja7Zm50xP-m5T8X\&q=85\&s=f3a17b155fee2ea6f7cc05ed8141c143) After a few moments, your custom domain will be ready behind Cloudflare Proxy # Configuring AWS ALB on Porter Source: https://docs.porter.run/security-and-compliance/configuring-alb Switch from NLB to AWS Application Load Balancer on Porter for IP allowlisting, WAFv2 integration, ACM certificates, and DNS validation The default Porter-managed Kubernetes cluster on AWS uses a Network Load Balancer (NLB) which forwards traffic to an on-cluster Nginx Ingress Controller. Whilst this provides the best performance, some users have extra requirements which require an ALB such as: * allowlisting specific IP ranges for applications deployed on Porter * integrating with other AWS tools such as AWS WAFv2 * using AWS ACM certificates As an enterprise feature, Porter supports switching out the default NLB for an Application Load Balancer (ALB). ## Requirements * A Porter-managed Kubernetes cluster * Access to your DNS provider. For this example, we will use CloudFlare, but Route53 is also supported. * A domain name which can be wildcarded. For this example, we will use `stefan-2.porter.run` * Using a wildcard domain is **strongly** recommended as you can then create multiple applications which are exposed to internet through Porter, without any further changes in future If the following options are not available to you, please reach out to Porter using the chatbot on the dashboard for more information ## DNS Validation Skip this step if you are using Route53, as Porter can perform the validation on your behalf. When your ALB is up and running, you will have to ensure that your new AWS Certificate Manager (ACM) Certificate is validated. AWS does this by creating a CNAME record which must resolve to an automatically generated value. ### AWS Certificate Manager - CNAME 1. In your AWS console, navigate to the region that your cluster was deployed in. You can find this from your Porter dashboard in the `Infrastructure` section, in the Configuration tab. 2. After selecting the correct region, search for `AWS Certificate Manager` in the AWS Console search bar. 3. Select `List Certificates` from the bar on the left. 4. Find the domain name which matches the domain that you provided to Porter, then click on the Certificate ID. 5. The status for this certificate should be `Pending validation`. ![Pending Certificate](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/allowlist-cidr-ranges/pending-certificate.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=5cf7e4a08fa69aeeea9a541e4ab5f665) 6. Scroll down to `Domains`. 7. Take note of the `CNAME name` and `CNAME value`. ![CNAME Name and Value](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/allowlist-cidr-ranges/cname-values.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=70bc9c7677cb8743ef0428bd5b911a45) * You may see multiple domains here, all with the same CNAME name and CNAME value. No need to worry about these. 8. Go to your DNS provider. This will be CloudFlare in our case. ### CloudFlare 1. Select `DNS` then `Records`. 2. Create a new record - Select `CNAME` for type. * Enter the value that you pulled from AWS for `CNAME name` in the `Name` field in CloudFlare. * Enter the value that you pulled from AWS for `CNAME value` in the `Target` field in CloudFlare. * Ensure `Proxy status` is disabled, and is in `DNS only` mode. * Set `TTL` to `Auto`. * Enter `Porter-managed ACM Certificate Verification for YOUR_DOMAIN` in the `Comment` field. This is not required, but is strongly recommended. ![Completed DNS Validation Record](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/allowlist-cidr-ranges/dns-validation-cloudflare.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=943e89e81f75822ad1a8726031d1f196) 3. Return to the AWS console. ### AWS Certificate Manager - Status 1. Go to the ACM Certificate and refresh the page. 2. If the status is still `Pending Validation`, wait 5 minutes then refresh again. You should now see that the status is `Issued`. ![Issued Certificate](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/allowlist-cidr-ranges/issued-certificate.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=bcb5d042e0a291b3522c980b9bd544c3) 3. Your certificate is now issued, and can be used by your ALB. ## Creating a CNAME for your wildcard domain As a reminder, we are using `stefan-2.porter.run` as the address that we provided to Porter. All of our protected applications will be available at `*.stefan-2.porter.run` for example `myapp.stefan-2.porter.run` or `whoami.stefan-2.porter.run`. 1. Go to your DNS provider. We will be using CloudFlare, but the settings should be similar in any DNS Provider. 1 Select `DNS` then `Records`. 2. Create a new record. * Select `CNAME` for type. * Enter the wildcarded domain in the `Name` field i.e. `*.stefan-2.porter.run`. * Enter the ALB domain name in `Target` field. This will be provided to you by the Porter engineer. * Ensure `Proxy status` is disabled, and is in `DNS only` mode. * Set `TTL` to `Auto`. ![Completed DNS Wildcard Record](https://mintcdn.com/porter/8DMoPPuHIfHJYu-s/images/allowlist-cidr-ranges/dns-wildcard-cloudflare.png?fit=max\&auto=format\&n=8DMoPPuHIfHJYu-s\&q=85\&s=c9aa12775f9de718d86f6b5bbad76ba9) 3. You can now use this as a custom domain in Porter for your applications. See [Deploying on the Custom Domain](/applications/configure/custom-domains#custom-domains) to setup your new domain to be compatible with Porter. # Porter IP ranges Source: https://docs.porter.run/security-and-compliance/porter-ip-ranges Definitive list of Porter's current public IP addresses and CIDR ranges for allowlisting inbound connections from Porter to your services Some applications or host providers might find it handy to know about Porter's IPs. This page is intended to be the definitive source of Porter's current IP ranges. ``` 3.137.50.74/32 18.219.110.11/32 3.12.138.127/32 3.132.57.213/32 3.19.137.253/32 3.133.219.114/32 18.189.41.50/32 3.15.245.5/32 18.117.68.145/32 3.146.250.167/32 ``` # Role-based access control Source: https://docs.porter.run/security-and-compliance/role-based-access-control Assign admin, developer, or viewer roles to team members with SSO integration and just-in-time user provisioning for Porter projects Porter supports setting basic authorization permissions via for other members in a Porter project. At the moment, there are 3 roles that can be assigned in a Porter project: * **Admin:** read/write access to all resources, ability to delete the project and manage team members. * **Developer:** read/write access to applications, jobs, environment groups, cluster data, and integrations. * **Viewer:** read access to applications, jobs, environment groups, and cluster data. ## Adding Collaborators[](#adding-collaborators "Direct link to heading") To add a new collaborator to a Porter project, you must be logged in with an **Admin** role. As an admin, you will see a **Settings** tab in the sidebar. Navigate to **Settings** and input the email of the user you would like to add. This will generate an invitation link for the user, which expires in 24 hours. The user will get an email to join the Porter project, but if the email is not delivered, you can copy the invite link and send it to them directly. If the user does not have a Porter account, they will be asked to register. After registering, if they are not automatically added to the project, the user should **click the invite link again**. ## Just In Time User Provisioning[](#just-in-time-user-provisioning "Direct link to heading") To set up just in time user provisioning, you must be logged in with an **Admin** role. As an admin, you can find the setup in **Settings** then **Members** in the sidebar then go to the **SSO** tab. Setting up Just In Time User Provisioning requires your organization to have an Single Sign-On provider configured. 1. To set up Single Sign-On, you can click **Request** under **SSO provisioning** to reach out to support. Single Sign-On is configured by linking your identity provider with our auth provider. 2. After reaching out to support, set up JIT by going back to the **SSO** tab and click **Set Up** to login with an account. Ensure that you log in with an account connected to your organization. Once setup, any new users in the same organization will automatically be added to the project without an invite email. By default these users will receive **viewer** role, but you can also configure the default role. You can also click **Update Provider** to set up a different organization. > [!NOTE] > > Users that have been manually removed from the project will need to be manually re-added. ## Changing Collaborator Permissions[](#changing-collaborator-permissions "Direct link to heading") To change an invite or collaborator role, you must be logged in with an **Admin** role. As an admin, you will see a **Settings** tab in the sidebar. Navigate to **Settings** and lookup on the table the invite/collaborator that you want to change it's role, then click the icon with three dots on the row. This will open a pop up that will allow you to select the new role for that invite/collaborator. You will see that the user that created the project will not be displayed on the table, and you cannot change your own permissions. ## Removing Collaborators[](#removing-collaborators "Direct link to heading") To remove an invite or a collaborator, you must be logged in with an **Admin** role. As an admin, you will see a **Settings** tab in the sidebar. Navigate to **Settings** and lookup on the table the invite/collaborator that you want to remove then click the trash icon to remove the user from the project or delete the invite. # SOC 2 and HIPAA compliance Source: https://docs.porter.run/security-and-compliance/soc2-hipaa Enable instant SOC 2 and HIPAA compliance for your infrastructure on Porter with one click, compatible with compliance management platforms Porter supports one-click compliant infrastructure, which ensures SOC2/HIPAA compliance for all AWS infrastructure that is managed by Porter, including EKS, RDS, S3, and auxiliary services like Cloudwatch so all infra controls on compliance management platforms such as [Oneleet](https://www.oneleet.com/) and [Thoropass](https://www.thoropass.com/) pass instantly. # Static egress IPs Source: https://docs.porter.run/security-and-compliance/static-egress-ip Find your cluster's static egress IP addresses on AWS, Azure, or GCP using CLI commands or cloud console for third-party allowlisting Some third-party platforms may find it necessary to whitelist connections made from your Porter-provisioned infrastructure. This guide walks you through determining the egress IPs used by your applications. ## AWS ### AWS CLI After ensuring you have the AWS CLI installed and configured to access the account which contains your Porter cluster, run the following command: ```bash theme={null} aws --region ec2 describe-nat-gateways --filter "Name=tag-key,Values=*/cluster/" --query "NatGateways[*].NatGatewayAddresses[0].PublicIp" ``` You'll need to replace `` and `` with the name, and region of your cluster. This command will return the egress IP which you can use to whitelist your workloads with third party platforms. In case your cluster has multiple gateways, there will be multiple egress IPs. ### AWS VPC Console 1. Log into the AWS account containing your Porter cluster, and navigate to the `VPC` console in the appropriate region for your cluster. 2. In the left pane, select `NAT gateways`, and you'll be able to see your cluster's NAT gateways; they'll be named after your cluster, and have a tag `porter.run/managed: true` 3. Copy the contents of `Primary public IP` - this serves as the egress IP which you can use to whitelist your workloads with third party platforms. In case your cluster has multiple gateways, there will be multiple egress IPs. ## Azure ### Azure CLI After ensuring you have the Azure CLI installed and configured to access the account which contains your Porter cluster, run the following command: ```bash theme={null} az aks show -g -n --query 'networkProfile.loadBalancerProfile.effectiveOutboundIPs[].id' ``` You need to replace `` with the name of the resource group your cluster is deployed into and `` with the name of your cluster. This command should return an array with only one ID. After that, you should run ```bash theme={null} az network public-ip show --ids --query ipAddress -o tsv ``` where `` is the ID you got from the previous command (preferably between quotes). Note that the ID is the full path displayed by the previous command, and not just the UUID at the end. From the last command, you should see your static egress IP as its output. ## Google Cloud Platform (GCP) ### gcloud CLI After ensuring you have the GCP gcloud CLI installed and configured to access the account which contains your Porter cluster, run the following command: ```bash theme={null} gcloud compute addresses list --filter="region:REGION AND users~.*routers/.*-cloud-router$" ``` You'll need to replace `REGION` with the region that your cluster was deployed in. If your cluster name is `my-cluster`, then your egress IP will be `my-cluster-cloud-nat-ip` This command will return the egress IP which you can use to whitelist your workloads with third party platforms. In case your cluster has multiple gateways, there will be multiple egress IPs. ### GCP Console 1. Log into the GCP console, and navigate to [Cloud NAT](https://console.cloud.google.com/net-services/nat/list) 2. Search for your cluster name in the search bar, and click on the 'Gateway Name' 3. Your clusters IP address will be listed under "Cloud NAT Rules" as "Cloud NAT IPs" # Tailscale VPN Source: https://docs.porter.run/security-and-compliance/tailscale Set up Tailscale VPN integration on your Porter cluster for secure private access to kubectl, helm, app run, and datastore connect commands Tailscale is a VPN that creates a secure network between your servers, computers, and cloud instances. Porter integrates with Tailscale to provide secure access to your cluster resources over a private network (Tailnet). To learn more about how Tailscale works under the hood, check out this [overview on their official blog](https://tailscale.com/blog/how-tailscale-works/). ## Scope The Tailscale integration applies to the following Porter flows: * `porter kubectl` and `porter helm` — CLI commands that tunnel through the Tailnet to reach the Kubernetes API server and in-cluster Helm releases. * `porter app run` and `porter datastore connect` — when the Tailscale machine is marked as an exit node (see [Step 3](#step-3-approve-routes-in-tailscale) below). * Direct access to Porter-managed applications and datastores over the approved subnet routes. Tailscale does **not** change how the EKS API server endpoint itself is exposed. API server endpoint access is controlled by Porter's [Private Cluster setting](/cloud-accounts/advanced-cluster-settings#private-cluster), which locks the public endpoint to an IP allowlist independently of Tailscale. ## Setting up Tailscale ### Step 1: Create an OAuth Client in Tailscale **1. Add tag owners to your policy** In the Tailscale admin dashboard, navigate to the **Access controls** tab and add the following to your policy file: ```json theme={null} "tagOwners": { "tag:k8s-operator": [], "tag:k8s": ["tag:k8s-operator"], }, ``` ![Tailscale Policy File](https://mintcdn.com/porter/aQhV2uLaypBvX9lt/images/other/tailscale-policy-file.webp?fit=max\&auto=format\&n=aQhV2uLaypBvX9lt\&q=85\&s=4933ecba86c27b8bae9471e6ac038a76) Click **Save** to save the changes. **2. Generate OAuth credentials** 1. Navigate to `Settings -> Trust credentials -> Add credential -> OAuth` 2. Select the **Core** and **Auth keys** scopes with write permissions 3. Click **Add tags** and select the `k8s-operator` tag ![Tailscale OAuth Scopes](https://mintcdn.com/porter/aQhV2uLaypBvX9lt/images/other/tailscale-oauth-scopes.webp?fit=max\&auto=format\&n=aQhV2uLaypBvX9lt\&q=85\&s=273236e7158567897414450d098fe952) 4. Click **Generate client** > [!WARNING] > > Save the credentials securely—you will need them in the next steps and cannot retrieve them later. ### Step 2: Enable VPN on Your Cluster **1. Navigate to VPN settings** In the Porter dashboard, go to **Infrastructure** → **VPN** tab. **2. Enter OAuth credentials** Input your Tailscale OAuth Client ID and OAuth Secret. Click **Save OAuth Credentials**. **3. Enable VPN** Check **Enable VPN for this cluster** and update your cluster. The cluster will update, and once finished, you should see the cluster and Tailscale operator in your Tailnet. ### Step 3: Approve Routes in Tailscale By default, Porter ensures that all Porter-managed applications and datastores are accessible over the Tailnet. Each route must be approved by an admin in the Tailscale Admin Panel. **1. Open the Tailscale Admin Panel** Visit your [Tailscale Admin Panel](https://login.tailscale.com/admin/machines) and click **Machines**. **2. Find your cluster** Locate the machine named `cluster-ABC`, where `ABC` is the name of your Porter cluster. **3. Approve routes** 1. Click the three dots on the right side of the cluster machine 2. Click **Edit Route settings...** 3. Click **Approve All** to approve all routes **4. Enable exit node (optional)** If you intend to use `porter app run` or `porter datastore connect` commands, also check **Use as Exit Node**. Your Tailscale subnet router is now online. All Porter applications and datastores should be accessible over the Tailnet. *** ## Subnet Routes By default, the subnet routes for your cluster and all connected datastores are routed through Tailscale. To add additional subnet routes: 1. Navigate to **Infrastructure** → **VPN** 2. Click **+ Add subnet route** 3. Enter the subnet CIDR you want to route through Tailscale 4. Update your cluster *** ## Troubleshooting ### VPN not connecting If your VPN connection isn't working: 1. Verify the OAuth credentials are correct in Porter 2. Check that the `k8s-operator` tag is properly configured in your Tailscale ACL 3. Ensure routes are approved in the Tailscale Admin Panel ### Cannot access cluster resources If you can't reach cluster resources over Tailscale: 1. Verify Tailscale is running on your local machine 2. Check that routes are approved in the Tailscale Admin Panel 3. Ensure your Tailscale client is connected to the same Tailnet 4. Try disconnecting and reconnecting your Tailscale client # porter app Source: https://docs.porter.run/standard/cli/command-reference/porter-app Manage Porter applications from the CLI with commands to run, exec, logs, update config, list builds, and view environment variables `porter app` contains commands for managing and interacting with your applications. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) * You're connected to the correct cluster by running [porter config set-cluster](/standard/cli/command-reference/porter-config) *** ## `porter app run` Runs a command inside your application's environment. By default, this spins up an ephemeral copy of your application that is deleted when the command completes. **Usage:** ```bash theme={null} porter app run [application] -- COMMAND [args...] [flags] ``` **Options:** | Flag | Description | | - | - | | `-e` | Connect to an existing replica instead of spinning up a new one (default: false). This will launch an interactive selector if there is more than one replica of your application running | | `--job` | Name of the job to run (runs the job as defined and returns the job run ID without waiting) | | `--wait` | Wait for the command to complete before exiting for non-interactive mode (default: false) | | `--allow-concurrent` | Allow concurrent job runs when using `--job` (overrides job config if concurrency is disabled) | | `--cpu` | CPU allocation in millicores (1000 millicores = 1 vCPU) | | `--ram` | RAM allocation in Mi (1024 Mi = 1 GB) | | `-v` | Print verbose output | > [!WARNING] > > The `--cpu` and `--ram` flags aren't applicable when used with the `-e` flag as they require spinning up an ephemeral replica of your application. ```bash Interactive Shell theme={null} porter app run my-app -- bash ``` ```bash Existing Container theme={null} porter app run my-app -e -- bash ``` ```bash Trigger Job theme={null} porter app run my-app --job my-job ``` ```bash Wait for Job theme={null} porter app run my-app --job my-job --wait ``` ```bash Concurrent Job theme={null} porter app run my-app --job my-job --allow-concurrent ``` ```bash Run Command theme={null} porter app run my-app -- python manage.py migrate ``` ```bash Custom Resources theme={null} porter app run my-app --cpu 500 --ram 512 -- python heavy_script.py ``` > [!NOTE] > > In these examples, we use `bash` and `python`, but those commands need to actually be present within the container to run. With very lightweight containers, you may find that none of these are available. If you run into trouble here, feel free to reach out to the support team. *** ## `porter app run cleanup` Delete any lingering ephemeral replicas that were created with `porter app run`. **Usage:** ```bash theme={null} porter app run cleanup ``` *** ## `porter app logs` Streams the latest logs for an application. **Usage:** ```bash theme={null} porter app logs [application] [flags] ``` **Options:** | Flag | Description | | - | - | | `--service` | The name of the service to get logs for | | `--search` | Search for a specific string in the logs | | `--revision` | The revision number to get logs for (default: -1, no filter applied) | **Historical Log Options:** | Flag | Description | | - | - | | `--since` | Lookback period from the current time to get historical logs (e.g., `12h`, `30m`) | | `--from` | Start time (UTC) for historical logs (format: `2021-01-01T00:00:00Z`) | | `--to` | End time (UTC) for historical logs (format: `2021-01-01T00:00:00Z`) | | `--limit` | Number of lines of historical logs to return | | `--newest-first` | Return newest historical logs first (default: true) | ```bash Stream Live Logs theme={null} porter app logs my-app ``` ```bash Filter by Service theme={null} porter app logs my-app --service web ``` ```bash Search Logs theme={null} porter app logs my-app --search "error" ``` ```bash Historical Logs theme={null} porter app logs my-app --since 12h ``` ```bash Time Range theme={null} porter app logs my-app --from 2025-01-15T10:00:00Z --to 2025-01-15T12:00:00Z ``` ```bash Limit Results theme={null} porter app logs my-app --since 24h --limit 500 ``` > [!NOTE] > > By default, the command streams new logs. Setting `--since` or `--from`/`--to` switches to pulling historical logs. *** ## `porter app build` Builds a new version of the specified app. Attempts to use any build settings previously configured for the app, which can be overridden with flags. **Usage:** ```bash theme={null} porter app build [application] [flags] ``` **Options:** | Flag | Description | | - | - | | `--build-context` | Set the build context for the app | | `--dockerfile` | Set the path to the Dockerfile when build method is `docker` | | `--tag` | Set the image tag to use for the build | | `--build-method` | Set the build method for the app (`docker` or `pack`) | | `--builder` | Set the builder to use when build method is `pack` | | `--attach-buildpacks` | Attach buildpacks to use when build method is `pack` | | `--no-pull` | Do not pull the previous image before building | | `--remote` | \[EXPERIMENTAL] Build remotely using kpack in the cluster instead of locally | | `--verbose` | Show verbose output during build (detailed logs, step timings) | ```bash Basic Build theme={null} porter app build my-app ``` ```bash Custom Context theme={null} porter app build my-app --build-context ./frontend --dockerfile ./frontend/Dockerfile ``` ```bash With Tag theme={null} porter app build my-app --tag v1.2.3 ``` ```bash With Buildpacks theme={null} porter app build my-app --build-method pack --builder heroku/builder:22 --attach-buildpacks heroku/nodejs ``` *** ## `porter app push` Pushes the specified app to your default Porter registry. If no tag is specified, the latest commit SHA from the current branch will be used as the tag. **Usage:** ```bash theme={null} porter app push [application] [flags] ``` **Options:** | Flag | Description | | - | - | | `--tag` | Set the image tag to use for the push | ```bash Push Latest theme={null} porter app push my-app ``` ```bash Push Specific Tag theme={null} porter app push my-app --tag v1.2.3 ``` *** ## `porter app create` Creates and deploys a new app in your project. If no flags are specified, you will be directed to a series of required prompts to configure the app. **Usage:** ```bash theme={null} porter app create [flags] ``` **Options:** | Flag | Short | Description | | - | - | - | | `--name` | `-n` | The name of the app | | `--file` | `-f` | Path to porter.yaml | | `--deploy-method` | `-m` | The deployment method for the app (`docker`, `repo`) | | `--tag` | | Set the image tag used for the application (overrides field in yaml) | | `--image-repository` | | Set the image repository to use for the app | | `--attach-env-groups` | | Attach environment groups to the app | | `--build-context` | | Set the build context for the app | | `--build-method` | | Set the build method for the app (`docker` or `pack`) | | `--dockerfile` | | Set the path to the Dockerfile when build method is `docker` | | `--builder` | | Set the builder to use when build method is `pack` | | `--attach-buildpacks` | | Attach buildpacks to use when build method is `pack` | | `--remote` | | \[EXPERIMENTAL] Build remotely using kpack in the cluster instead of locally | | `--verbose` | | Show verbose output during build | ```bash Interactive Creation theme={null} porter app create ``` ```bash With Name theme={null} porter app create --name example-app ``` ```bash From YAML theme={null} porter app create --name my-app -f porter.yaml ``` *** ## `porter app update` Updates the specified app with the provided configuration. This command differs from `porter apply` in that it only updates the app, but does not attempt to build a new image. **Usage:** ```bash theme={null} porter app update [application] [flags] ``` **Options:** | Flag | Short | Description | | - | - | - | | `--file` | `-f` | Path to porter.yaml | | `--tag` | | Set the image tag used for the application (overrides field in yaml) | | `--image-repository` | | Set the image repository to use for the app | | `--attach-env-groups` | | Attach environment groups to the app | | `--wait` | `-w` | Wait until an update has rolled out successfully, otherwise time out | ```bash Update from YAML theme={null} porter app update my-app -f porter.yaml ``` ```bash Update Image Tag theme={null} porter app update my-app --tag v1.2.3 ``` ```bash Wait for Rollout theme={null} porter app update my-app -f porter.yaml --wait ``` > [!NOTE] > > This command differs from `porter apply` in that it only updates the app configuration without attempting to build a new image. *** ## `porter app update-tag` Updates the image tag for an application. This is functionally equivalent to running `porter apply` or `porter app update` with the `--tag` flag. **Usage:** ```bash theme={null} porter app update-tag [application] [flags] ``` **Options:** | Flag | Short | Description | | - | - | - | | `--tag` | `-t` | The specified tag to use (default: `latest`) | | `--wait` | `-w` | Wait and be notified when an update is successful, otherwise time out | ```bash Update Tag theme={null} porter app update-tag my-app --tag abc123def ``` ```bash Wait for Rollout theme={null} porter app update-tag my-app --tag v1.2.3 --wait ``` *** ## `porter app yaml` Export the current Porter YAML configuration for an application. Useful for bootstrapping a `porter.yaml` file or inspecting the current configuration. **Usage:** ```bash theme={null} porter app yaml [application] [flags] ``` **Options:** | Flag | Description | | - | - | | `--helm-overrides` | Return only the helm overrides for the application | ```bash Export YAML theme={null} porter app yaml my-app ``` ```bash Helm Overrides Only theme={null} porter app yaml my-app --helm-overrides ``` > [!TIP] > > Redirect the output to create a `porter.yaml` file: `porter app yaml my-app > porter.yaml` *** ## `porter app list` Lists all applications in the project. **Usage:** ```bash theme={null} porter app list ``` *** ## `porter app manifests` Prints the Kubernetes manifests for an application. **Usage:** ```bash theme={null} porter app manifests [application] ``` ```bash theme={null} porter app manifests my-app ``` *** ## `porter app rollback` Rolls back an application to the last successful revision. **Usage:** ```bash theme={null} porter app rollback [application] ``` ```bash theme={null} porter app rollback my-app ``` *** ## Related Commands * [porter apply](/standard/cli/command-reference/porter-apply) - Deploy an app using configuration-as-code * [porter env](/standard/cli/command-reference/porter-env) - Manage environment variables # porter apply Source: https://docs.porter.run/standard/cli/command-reference/porter-apply Deploy applications using porter.yaml with the porter apply command, supporting container builds, image updates, and preview environments > [!NOTE] > > This command is the recommended way to deploy applications in CI/CD pipelines and for version-controlled infrastructure. `porter apply` is the primary command for deploying applications using configuration-as-code. It reads a `porter.yaml` file, **builds a new container image**, and **deploys your application** to the cluster. By default, `porter apply` performs a full build and deploy cycle. Use `--no-build` to skip building and deploy with an existing image. > [!NOTE] > > You can run `porter apply` without a `porter.yaml` file to deploy using the existing application configuration. This is useful for triggering a new build and deploy without changing any settings. ## Usage ```bash theme={null} porter apply -f porter.yaml [flags] ``` ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) * You're connected to the correct cluster by running [porter config set-cluster](/standard/cli/command-reference/porter-config) ## Options ### Deployment Options | Flag | Short | Description | | - | - | - | | `--file` | `-f` | Path to the `porter.yaml` configuration file (optional if app already exists) | | `--app` | | Override the app name specified in `porter.yaml` | | `--target` | `-x` | Specify a deployment target (name or ID) | | `--wait` | `-w` | Wait for the deployment to complete before exiting | | `--preview` | `-p` | Deploy to a preview environment based on current git branch | > [!WARNING] > > **Use `--exact` with caution.** When set, this flag disables config merging with existing app configuration—any settings not explicitly defined in your `porter.yaml` will be overwritten. Most users should avoid this flag unless they need to completely replace the application configuration. | Flag | Description | | - | - | | `--exact` | Disable config merging with existing app configuration | ### Build Options | Flag | Description | | - | - | | `--no-build` | Skip building a new image and deploy with an existing image | | `--no-pull` | Don't pull the previous image before building | | `--tag` | Specify a custom image tag (overrides value in porter.yaml) | | `--image-repository` | Set the image repository to use | | `--build-context` | Override the build context directory | | `--build-method` | Set build method: `docker` or `pack` | | `--dockerfile` | Override the Dockerfile path (for docker builds) | | `--builder` | Set the builder to use (for pack builds) | | `--attach-buildpacks` | Attach buildpacks to use (for pack builds) | | `--verbose` | Show verbose output during build | ### Environment Options You can pass environment variables and secrets directly via command-line flags without modifying your `porter.yaml`. | Flag | Description | | - | - | | `--variables` | Set environment variables as key=value pairs (overrides porter.yaml) | | `--secrets` | Set secrets as key=value pairs (overrides porter.yaml) | ### Validation Options | Flag | Description | | - | - | | `--validate` | Check that the `porter.yaml` file is valid YAML syntax | | `--dry-run` | Perform server-side validation to confirm the configuration is valid without applying changes | ### Advanced Options | Flag | Short | Description | | - | - | - | | `--helm-overrides-file` | `-v` | Path to a file containing Helm value overrides | | `--attach-env-groups` | | Comma-separated list of environment groups to attach | ## Environment Variables You can also configure `porter apply` using environment variables. These are automatically set in the GitHub workflow that Porter generates for your application. | Variable | Description | | - | - | | `PORTER_TOKEN` | Authentication token (required for CI/CD) | | `PORTER_HOST` | Porter API host URL | | `PORTER_PROJECT` | Target project ID | | `PORTER_CLUSTER` | Target cluster ID | | `PORTER_APP_NAME` | App name (alternative to `--app`) | | `PORTER_DEPLOYMENT_TARGET_ID` | Deployment target ID | | `PORTER_TAG` | Image tag to deploy | ## Examples ```bash Basic Deployment theme={null} # Deploy using porter.yaml in the current directory porter apply -f porter.yaml ``` ```bash Wait for Completion theme={null} # Deploy and wait for the deployment to complete porter apply -f porter.yaml --wait ``` ```bash Preview Environment theme={null} # Deploy to a preview environment porter apply -f porter.yaml --preview ``` ```bash Validate YAML Syntax theme={null} # Check that porter.yaml is valid YAML porter apply -f porter.yaml --validate ``` ```bash Dry Run theme={null} # Validate the full configuration server-side without deploying porter apply -f porter.yaml --dry-run ``` ```bash CI/CD Pipeline theme={null} # Deploy in a CI/CD pipeline with explicit configuration PORTER_TOKEN=$PORTER_DEPLOY_TOKEN \ PORTER_PROJECT=$PROJECT_ID \ PORTER_CLUSTER=$CLUSTER_ID \ porter apply -f porter.yaml ``` ```bash Skip Build theme={null} # Deploy using an existing image (skip build step) porter apply -f porter.yaml --no-build --tag v1.2.3 ``` ```bash With Environment Variables theme={null} # Deploy with runtime environment variables and secrets porter apply -f porter.yaml --variables LOG_LEVEL=debug,NODE_ENV=production --secrets API_KEY=secret123 ``` ## Workflow When you run `porter apply`, the following steps occur: **1. Parse Configuration** Porter reads and validates your `porter.yaml` file (if provided) **2. Build Image** Porter builds and pushes a new container image (unless `--no-build` is specified) **3. Deploy Services** Porter deploys or updates all services defined in the configuration **4. Wait for Rollout (if --wait)** If `--wait` is specified, Porter waits for all services to become healthy ## Configuration File The `porter apply` command expects a `porter.yaml` file. For detailed documentation on all available configuration options, see the [porter.yaml Reference](/applications/configuration-as-code/reference). ### Minimal Example ```yaml theme={null} version: v2 name: my-app services: - name: web type: web run: npm start port: 3000 cpuCores: 0.5 ramMegabytes: 512 ``` ## CI/CD Integration Most users deploy with `porter apply` through GitHub Actions. Porter automatically generates a GitHub workflow for your application that handles authentication and deployment. > [!NOTE] > > The workflow below is similar to what Porter automatically creates when you connect your GitHub repository. You can find it in your repository at `.github/workflows/porter_.yml`. ### GitHub Actions Workflow ```yaml theme={null} name: Deploy to my-app on: push: branches: - main jobs: porter-deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set Github tag id: vars run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: Setup porter uses: porter-dev/setup-porter@v0.2.0 - name: Deploy stack run: porter apply timeout-minutes: 30 env: PORTER_CLUSTER: "1" PORTER_HOST: https://dashboard.porter.run PORTER_PROJECT: "1" PORTER_TOKEN: ${{ secrets.PORTER_TOKEN }} PORTER_TAG: ${{ steps.vars.outputs.sha_short }} PORTER_APP_NAME: my-app PORTER_DEPLOYMENT_TARGET_ID: your-deployment-target-id ``` ## Related Commands * [porter app update](/standard/cli/command-reference/porter-app#porter-update) - Update an app without building * [porter app yaml](/standard/cli/command-reference/porter-app#porter-yaml) - Export the current app configuration as YAML # porter auth Source: https://docs.porter.run/standard/cli/command-reference/porter-auth Authenticate with Porter using browser-based login, token-based authentication for CI/CD pipelines, or manual email and password via the CLI `porter auth` contains commands for authenticating with Porter. *** ## `porter auth login` Authenticate with your Porter account. By default, this opens your browser to complete authentication. **Usage:** ```bash theme={null} porter auth login [flags] ``` **Options:** | Flag | Description | | - | - | | `--token` | Log in using an existing token | ```bash Browser Login (Default) theme={null} porter auth login ``` ```bash Token Login theme={null} porter auth login --token ``` > [!NOTE] > > After logging in, Porter automatically sets your default project and cluster if you have access to any. You can view these with `porter config`. *** ## `porter auth logout` Log out from Porter and clear your local credentials. **Usage:** ```bash theme={null} porter auth logout ``` > [!TIP] > > After logging out, you'll need to run `porter auth login` again before using other commands. # porter cloud-accounts Source: https://docs.porter.run/standard/cli/command-reference/porter-cloud-accounts Connect, list, and disconnect AWS, GCP, and Azure cloud accounts from the Porter CLI so Porter can provision and deploy into your own cloud `porter cloud-accounts` contains commands for connecting your AWS, GCP, and Azure cloud accounts to Porter and managing them from the CLI. Connecting a cloud account grants Porter scoped access to your cloud provider so it can provision clusters and deploy applications into your own infrastructure. For the dashboard-based workflow and provider setup details, see [Connecting a Cloud Account](/cloud-accounts/connecting-a-cloud-account). ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) * You have permission to create the required IAM role, service account, or app registration in the cloud account you want to connect *** ## `porter cloud-accounts list` List the cloud accounts connected to the current project. **Usage:** ```bash theme={null} porter cloud-accounts list [flags] ``` **Options:** | Flag | Description | | - | - | | `--provider` | Filter by cloud provider. One of `aws`, `azure`, `gcp` | | `--status` | Filter by connection status. One of `connected`, `pending` | | `--json` | Print the cloud accounts as JSON | The default output is a table with the account's **Name**, **Provider**, **Provider ID** (AWS account ID, GCP project ID, or Azure subscription ID), **Status**, and **Created At**. When the output is piped to another command, it is printed as tab-separated values so it stays friendly to tools like `grep`, `cut`, and `awk`. ```bash List All theme={null} porter cloud-accounts list ``` ```bash Filter by Provider and Status theme={null} porter cloud-accounts list --provider aws --status connected ``` ```bash JSON Output theme={null} porter cloud-accounts list --json ``` *** ## `porter cloud-accounts connect` Connect a new cloud account to your Porter project. This is a parent command; run one of the provider-specific subcommands below. ```bash theme={null} porter cloud-accounts connect aws porter cloud-accounts connect gcp porter cloud-accounts connect azure ``` Each subcommand runs interactively by default, prompting for the identifiers it needs. Pass the corresponding flags to skip the prompts for use in scripts and CI. ### `porter cloud-accounts connect aws` Connect a new AWS cloud account by creating a CloudFormation stack that grants Porter a scoped IAM role. **Usage:** ```bash theme={null} porter cloud-accounts connect aws [flags] ``` **Options:** | Flag | Description | | - | - | | `--account-id` | AWS account ID to connect (skips the interactive prompt). Must be a 12-digit account ID | When you run the command, Porter generates a CloudFormation stack and prompts you to open the AWS console (or copy the URL) to deploy it. The CLI then waits for the connection to complete, polling for up to 10 minutes. ```bash Interactive theme={null} porter cloud-accounts connect aws ``` ```bash Non-Interactive theme={null} porter cloud-accounts connect aws --account-id 123456789012 ``` ### `porter cloud-accounts connect gcp` Connect a new GCP cloud account by running a setup command in Google Cloud Shell that grants Porter access via Workload Identity Federation. **Usage:** ```bash theme={null} porter cloud-accounts connect gcp [flags] ``` **Options:** | Flag | Description | | - | - | | `--project-id` | GCP project ID to connect to (skips the interactive prompt) | Porter prompts you to open Google Cloud Shell (or copy the URL) and displays a setup command to run there. The CLI waits for the connection to complete. The setup command expires after a period of time; if it does, re-run the command to generate a fresh one. ```bash Interactive theme={null} porter cloud-accounts connect gcp ``` ```bash Non-Interactive theme={null} porter cloud-accounts connect gcp --project-id my-gcp-project ``` ### `porter cloud-accounts connect azure` Connect a new Azure cloud account by granting Porter access to your subscription via Workload Identity Federation. **Usage:** ```bash theme={null} porter cloud-accounts connect azure [flags] ``` **Options:** | Flag | Description | | - | - | | `--subscription-id` | Azure subscription ID to connect (skips the interactive prompt) | | `--client-id` | Application (client) ID of the Azure App Registration (skips the interactive prompt) | | `--tenant-id` | Azure tenant ID (skips the interactive prompt) | ```bash Interactive theme={null} porter cloud-accounts connect azure ``` ```bash Non-Interactive theme={null} porter cloud-accounts connect azure \ --subscription-id 00000000-0000-0000-0000-000000000000 \ --client-id 11111111-1111-1111-1111-111111111111 \ --tenant-id 22222222-2222-2222-2222-222222222222 ``` > [!NOTE] > > The `connect` subcommands are idempotent: if the account you specify is already connected to the project, Porter detects it and won't create a duplicate connection. *** ## `porter cloud-accounts disconnect` Disconnect a cloud account from the current project. Porter tears down the resources it provisioned and revokes its stored credentials. **Usage:** ```bash theme={null} porter cloud-accounts disconnect [flags] ``` The `` argument identifies the account by its **ID**, **name**, or **provider ID** (AWS account ID, GCP project ID, or Azure subscription ID). Use [porter cloud-accounts list](#porter-cloud-accounts-list) to find these values. **Options:** | Flag | Short | Description | | - | - | - | | `--yes` | `-y` | Skip the confirmation prompt | > [!WARNING] > > All resources must be deleted from a cloud account before it can be disconnected. After disconnecting, Porter prints provider-specific instructions for the manual cleanup you may want to perform in your own cloud account: * **AWS** — Delete the CloudFormation stack (`PorterRole` in `us-east-2`) * **GCP** — Delete the Workload Identity pool and service account * **Azure** — Delete the app registration On an interactive terminal, the CLI also offers to open the relevant provider console for you. ```bash Disconnect by Name theme={null} porter cloud-accounts disconnect my-aws-account ``` ```bash Skip Confirmation theme={null} porter cloud-accounts disconnect 123456789012 --yes ``` *** ## Related Commands * [porter clusters](/standard/cli/command-reference/porter-clusters) - Create, list, and delete clusters in your cloud accounts * [porter config set-project](/standard/cli/command-reference/porter-config) - Set the active project * [porter auth login](/standard/cli/command-reference/porter-auth) - Authenticate the CLI # porter clusters Source: https://docs.porter.run/standard/cli/command-reference/porter-clusters Create, list, and delete Porter clusters in your own AWS, GCP, and Azure cloud accounts from the Porter CLI, including non-interactive provisioning for CI `porter clusters` contains commands for provisioning and managing the clusters in the current project. Creating a cluster provisions Kubernetes infrastructure in a cloud account you have already connected to Porter. Porter picks the cloud provider from the cloud account, runs preflight checks against it, and selects a Kubernetes version and node groups for you. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) * You've connected at least one cloud account with [porter cloud-accounts connect](/standard/cli/command-reference/porter-cloud-accounts) *** ## `porter clusters create` Create a new cluster in the current project from a name, region, and cloud account. **Usage:** ```bash theme={null} porter clusters create [flags] ``` **Options:** | Flag | Short | Description | | - | - | - | | `--name` | | Name for the new cluster | | `--region` | | Cloud region to provision the cluster in | | `--cloud-account-id` | | Cloud account to provision into, which determines the cloud provider | | `--enable-sandboxes` | | Enable [sandboxes](/sandboxes/overview) on the new cluster. Adds a sandbox node group and extra provisioning time. See the requirements below | | `--yes` | | Skip the cost confirmation prompt | | `--wait` | `-w` | Wait for the cluster to finish provisioning. Exits non-zero if provisioning fails, times out, or the wait loses its connection | | `--wait-timeout` | | How long to wait when `--wait` is set, as a Go duration such as `120s`, `30m`, or `90m`. Defaults to `1h` | ### Interactive prompts Any of `--name`, `--cloud-account-id`, and `--region` that you omit are prompted for: * **Name** — the prompt is prefilled with a suggested name based on your project and existing clusters * **Cloud account** — chosen from the accounts connected to the project. A project with a single connected account skips this prompt * **Region** — chosen from the regions the selected cloud account supports, filtered as you type. If the region list can't be fetched, or the account reports no regions, the prompt falls back to free-text entry The cloud account is asked before the region because the account determines which regions are offered. Unless you pass `--yes`, Porter then shows the resolved choices and asks you to confirm. Where the provider's base monthly cost can be determined, it is shown too. The prompt defaults to **no**. > [!WARNING] > > On a non-interactive terminal there is no name prompt: omitting `--name` silently provisions a cluster under the suggested name. Always pass `--name` in CI. Omitting `--cloud-account-id` or `--region` there is not equivalent, as those prompts have no non-interactive fallback. ### Provisioning Provisioning is asynchronous and can take up to an hour. The command prints a dashboard link as soon as the cluster is created, before any wait, so an interrupted wait still leaves you with a link. Pass `--wait` to block until the cluster is ready, which is what you want in CI. The wait polls the cluster's status and exits non-zero if provisioning fails, the timeout elapses, your Porter session expires, or it loses the connection after repeated polling failures. Interrupting the wait with `Ctrl-C` is the one case that exits zero: it stops watching only, and the cluster keeps provisioning in the background. Passing `--wait-timeout` without `--wait` is an error rather than a silently ignored flag, as is a `--wait-timeout` of zero or less. ### Preflight checks Porter runs preflight checks against the cloud account before provisioning, catching problems like insufficient quota. When they fail, the CLI prints each failing check and exits with code `3` without creating a cluster. Resolve the reported issues and re-run. ### Exit codes | Code | Meaning | | - | - | | `0` | Success | | `1` | Unexpected error | | `3` | Preflight checks failed | ### Sandbox requirements `--enable-sandboxes` is validated server-side and the create fails unless all of the following hold: * The cloud account is an **AWS** account. Sandboxes are not available on GCP or Azure * The AWS account is **not on the free tier**. Sandbox nodes need local NVMe storage, which no free tier instance type offers. The create also fails if Porter cannot determine free tier eligibility at all * **Sandboxes are enabled for your project**. Contact Porter support if they are not Sandboxes can also be enabled on an existing cluster from the dashboard. See [Sandboxes Getting Started](/sandboxes/getting-started). ```bash Interactive theme={null} porter clusters create ``` ```bash Non-Interactive theme={null} porter clusters create \ --name my-cluster \ --region us-east-1 \ --cloud-account-id ``` ```bash With Sandboxes theme={null} porter clusters create \ --name my-cluster \ --region us-east-1 \ --cloud-account-id \ --enable-sandboxes ``` ```bash In CI theme={null} porter clusters create \ --name my-cluster \ --region us-east-1 \ --cloud-account-id \ --yes --wait --wait-timeout 90m ``` > [!TIP] > > `--cloud-account-id` takes Porter's own cloud account UUID, which is printed by [porter cloud-accounts list](/standard/cli/command-reference/porter-cloud-accounts#porter-cloud-accounts-list) `--json` as `id`. It is not the **Provider ID** column of the default table, which holds the AWS account ID, GCP project ID, or Azure subscription ID. *** ## `porter clusters list` List the clusters in the current project. **Usage:** ```bash theme={null} porter clusters list [flags] ``` **Options:** | Flag | Description | | - | - | | `--json` | Print the clusters as JSON | The default output is a table with the cluster's **Name**, **Provider**, **Region**, **Status**, and **Created**, ordered newest first. On a terminal that supports it, each name links to the cluster's dashboard page. When the output is piped to another command, it is printed as tab-separated values so it stays friendly to tools like `grep`, `cut`, and `awk`. **Name** is the cluster's vanity name when one is set, falling back to the provisioned name otherwise. The table has no ID column, so use `--json` when you need cluster IDs or the underlying provisioned name. In the table, status is one of `Ready`, `Updating`, `Updating (Unavailable)`, `Deleting`, `Deleted`, `Failed`, `Unreachable`, or `-` when Porter does not yet know. `--json` reports the raw state instead, such as `ready` or `updating_unavailable`. ```bash List All theme={null} porter clusters list ``` ```bash JSON Output theme={null} porter clusters list --json ``` *** ## `porter clusters delete` Delete a cluster from the current project. Porter tears down the infrastructure it provisioned for the cluster. **Usage:** ```bash theme={null} porter clusters delete [flags] ``` The `` argument identifies the cluster by its **ID**, **name**, or **vanity name**. Run [porter clusters list](#porter-clusters-list) `--json` to see all three, since the default table shows neither the ID nor the provisioned name. If the identifier matches more than one cluster, the CLI prints the matches and asks you to re-run with the cluster ID. **Options:** | Flag | Short | Description | | - | - | - | | `--yes` | `-y` | Skip the confirmation prompt | | `--wait` | `-w` | Wait for the cluster to finish deleting. Exits non-zero if deletion fails, times out, or the wait loses its connection | | `--wait-timeout` | | How long to wait when `--wait` is set, as a Go duration such as `120s`, `30m`, or `90m`. Defaults to `1h` | > [!WARNING] > > A cluster can only be deleted once nothing is still running on it. If resources remain, the CLI lists them by type and name and exits non-zero without deleting anything. This check runs after the confirmation prompt, so you may confirm a delete and only then learn it is blocked. Deletion is asynchronous and can take up to an hour. The cluster moves to the deleting state while teardown proceeds. Deleting a cluster that is already being deleted is a no-op that exits zero, so a run with `--wait` can safely be repeated to resume watching an interrupted wait. On a non-interactive terminal there is nothing to prompt, so `--yes` is required. ```bash Delete by Name theme={null} porter clusters delete my-cluster ``` ```bash Delete by ID theme={null} porter clusters delete 42 --yes ``` ```bash Wait for Teardown theme={null} porter clusters delete my-cluster --yes --wait ``` *** ## Related Commands * [porter cloud-accounts](/standard/cli/command-reference/porter-cloud-accounts) - Connect and list the cloud accounts you provision clusters into * [porter config set-cluster](/standard/cli/command-reference/porter-config) - Set the active cluster * [porter auth login](/standard/cli/command-reference/porter-auth) - Authenticate the CLI # porter config Source: https://docs.porter.run/standard/cli/command-reference/porter-config View and set Porter CLI local configuration including project, cluster, host, and API token settings with the porter config command `porter config` contains commands for managing local CLI configuration settings. *** ## `porter config` Display the current CLI configuration, including project, cluster, and authentication details. **Usage:** ```bash theme={null} porter config ``` **Flags:** | Flag | Description | | - | - | | `--show-token` | Display the CLI authentication token in the output. By default, the token is hidden for security. Pass this flag to reveal the token value. | | `--json` | Print the configuration as JSON instead of YAML. Useful for piping into tools like `jq` in scripts. | **Example:** ```bash theme={null} porter config --json | jq '.project' ``` > [!NOTE] > > Configuration values can be overridden using flags or environment variables. See [Basic Usage](/cli/basic-usage) for details. *** ## `porter config set-project` Set the active project. This command shows a list of projects you have access to and lets you select one. **Usage:** ```bash Interactive Selection theme={null} porter config set-project ``` ```bash Direct ID theme={null} porter config set-project 12345 ``` *** ## `porter config set-cluster` Set the active cluster. This command shows a list of clusters in the current project and lets you select one. **Usage:** ```bash Interactive Selection theme={null} porter config set-cluster ``` ```bash Direct ID theme={null} porter config set-cluster 67890 ``` > [!TIP] > > Run [porter clusters list](/standard/cli/command-reference/porter-clusters#porter-clusters-list) first to see the available clusters. Pass `--json` to read their IDs. *** ## Configuration File Porter stores configuration in `~/.porter/config.json`. You can also override settings using environment variables: | Variable | Description | | - | - | | `PORTER_PROJECT` | Override project ID | | `PORTER_CLUSTER` | Override cluster ID | | `PORTER_TOKEN` | Override authentication token | | `PORTER_HOST` | Override API host | # porter datastore connect Source: https://docs.porter.run/standard/cli/command-reference/porter-datastore-connect Create a secure local tunnel to connect to Porter-provisioned Postgres and Redis datastores for local debugging and database administration `porter datastore connect` creates a secure tunnel to connect to Porter-provisioned datastores from your local machine. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) * You have a Porter-provisioned datastore (PostgreSQL, Redis, etc.) *** ## Usage ```bash theme={null} porter datastore connect [DATASTORE_NAME] [flags] ``` **Options:** | Flag | Description | | - | - | | `--port` | Local port for the tunnel (default: 8122) | *** ## Examples ```bash Connect to Datastore theme={null} porter datastore connect my-postgres ``` ```bash Custom Port theme={null} porter datastore connect my-postgres --port 5433 ``` *** ## Connecting to Your Datastore While the tunnel is running, connect using your preferred client in a separate terminal: ```bash PostgreSQL theme={null} psql -h 127.0.0.1 -p 8122 -U postgres -d postgres ``` ```bash Redis theme={null} redis-cli -p 8122 -a [YOUR_DATASTORE_PASSWORD] --tls ``` > [!NOTE] > > The tunnel stays open until you press `CTRL-C`. Keep the terminal open while you need access to the datastore. > [!WARNING] > > The connection credentials are displayed when the tunnel starts. Make sure to note the password if you need it for your client. # porter env Source: https://docs.porter.run/standard/cli/command-reference/porter-env Create, list, get, set, and unset environment group variables and secrets from the Porter CLI for project-wide configuration management `porter env` contains commands for managing environment groups — project-wide sets of environment variables and secrets that can be shared across applications. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) *** ## `porter env list` List environment groups for the current project. **Usage:** ```bash theme={null} porter env list [flags] ``` **Options:** | Flag | Description | | - | - | | `--json` | Print environment groups as JSON instead of a table | When a cluster is set (via `porter config set-cluster`, the `--cluster` flag, or the `PORTER_CLUSTER` environment variable), the list is scoped to that cluster: environment groups whose sync targets include the cluster, plus any cluster-scoped environment groups. This matches what the dashboard shows on the cluster's environment groups page. Without a cluster context, all environment groups in the project are listed. The default table output shows each group's name, current version, and last updated time, sorted alphabetically by name. ```bash Table (default) theme={null} porter env list ``` ```bash JSON output theme={null} porter env list --json ``` The `--json` output returns two arrays — `environment_groups` and `cluster_scoped_environment_groups` — sharing one object shape: ```json theme={null} { "environment_groups": [ { "id": "2a54d3ab-08e2-4922-9368-a7e45ad87d23", "name": "tessier-internal", "current_version": 9, "created_at": "2026-02-25T09:23:16.332254Z", "updated_at": "2026-06-30T04:23:25.23501Z", "sync_target_cluster_ids": [11] } ], "cluster_scoped_environment_groups": [ { "id": "", "name": "billing-db", "current_version": 202, "created_at": "2025-10-27T16:00:48Z", "updated_at": "2025-10-27T16:00:48Z", "sync_target_cluster_ids": [11] } ] } ``` *** ## `porter env create` Create a new environment group. **Usage:** ```bash theme={null} porter env create [flags] ``` **Options:** | Flag | Description | | - | - | | `--name` | Environment group name (prompted interactively if omitted) | | `--variables`, `-v` | Variables to set as key=value pairs | | `--secrets`, `-s` | Secrets to set as key=value pairs | | `--no-input` | Disable interactive prompts | ```bash Interactive theme={null} porter env create ``` ```bash Non-Interactive theme={null} porter env create --name production-secrets ``` ```bash With Variables and Secrets theme={null} porter env create --name production-secrets \ -v NODE_ENV=production,LOG_LEVEL=info \ -s DB_PASSWORD=secret,API_KEY=sk-123 ``` > [!NOTE] > > If an environment group with the same name already exists, the command will fail. Use `porter env set` to update an existing group. *** ## `porter env pull` Pull environment variables from an environment group to your local machine. **Usage:** ```bash theme={null} porter env pull [-g] [flags] ``` **Options:** | Flag | Short | Description | | - | - | - | | `--app` | `-a` | Target application | | `--group` | `-g` | Target cluster environment group | | `--variables` | `-v` | Output only variables (excludes secrets) | | `--secrets` | `-s` | Output only secrets (excludes variables) | | `--merged` | | Include variables from all linked env groups (only valid with `--app`) | ```bash Print to stdout theme={null} porter env pull production-secrets ``` ```bash Write to File theme={null} porter env pull production-secrets -f .env.local ``` ```bash Variables Only theme={null} porter env pull production-secrets -v ``` ```bash Secrets Only theme={null} porter env pull production-secrets -s ``` ```bash App with Linked Env Groups theme={null} porter env pull --app my-app --merged ``` > [!NOTE] > > By default, both variables and secrets are output together. Use `-v` or `-s` to filter. You cannot use both flags at the same time. *** ## `porter env set` Add or update variables and secrets in an environment group. **Usage:** ```bash theme={null} porter env set [flags] ``` **Options:** | Flag | Short | Description | | - | - | - | | `--variables` | `-v` | Variables to set as key=value pairs | | `--secrets` | `-s` | Secrets to set as key=value pairs | ```bash Set Variables theme={null} porter env set production-secrets -v LOG_LEVEL=debug,FEATURE_FLAG=true ``` ```bash Set Secrets theme={null} porter env set production-secrets -s API_KEY=sk-new-key ``` ```bash Set Both theme={null} porter env set production-secrets \ -v NODE_ENV=production,LOG_LEVEL=info \ -s DATABASE_URL=postgres://... ``` > [!WARNING] > > Use `-s/--secrets` for sensitive values like API keys and passwords. Secrets are stored in your cloud provider's secret manager and are not visible after creation. > [!NOTE] > > When you update an environment group, all applications synced to it are automatically redeployed with the new values. *** ## `porter env unset` Remove variables or secrets from an environment group. **Usage:** ```bash theme={null} porter env unset [flags] ``` **Options:** | Flag | Short | Description | | - | - | - | | `--variables` | `-v` | Comma-separated list of variable names to remove | | `--secrets` | `-s` | Comma-separated list of secret names to remove | ```bash Remove Variables theme={null} porter env unset production-secrets -v OLD_KEY,UNUSED_VAR ``` ```bash Remove Secrets theme={null} porter env unset production-secrets -s ROTATED_KEY,OLD_SECRET ``` ```bash Remove Both theme={null} porter env unset production-secrets -v VAR1 -s SECRET1 ``` > [!NOTE] > > When you update an environment group, all applications synced to it are automatically redeployed with the new values. *** ## Legacy Commands The following flag-based syntax is supported for managing app-level and cluster-level environment variables. These are legacy commands — for environment groups, use the positional argument syntax above. ### Legacy Flags | Flag | Short | Description | | - | - | - | | `--app` | `-a` | Target application | | `--group` | `-g` | Target cluster environment group | | `--target` | `-x` | Deployment target name | | `--skip-redeploys` | | Skip re-deploying apps linked to the cluster environment group | > [!NOTE] > > You must specify either `--app` or `--group`, but not both. ```bash Pull from App theme={null} porter env pull --app my-app ``` ```bash Pull from Cluster Env Group theme={null} porter env pull --group staging-config ``` ```bash Set App Variables theme={null} porter env set --app my-app -v KEY1=value1,KEY2=value2 ``` ```bash Set Cluster Group Variables theme={null} porter env set --group my-env-group -v KEY1=value1 ``` ```bash Unset App Variables theme={null} porter env unset --app my-app -v KEY1,KEY2 ``` ```bash Skip Redeploys theme={null} porter env set --group my-env-group -v KEY=value --skip-redeploys ``` *** ## Related * [Environment Groups](/applications/configure/environment-groups) — Overview of environment groups * [porter.yaml Reference](/applications/configuration-as-code/reference) — `envGroups` field in porter.yaml * [porter app](/standard/cli/command-reference/porter-app) — Application management commands * [porter apply](/standard/cli/command-reference/porter-apply) — Deploy with porter.yaml # porter job Source: https://docs.porter.run/standard/cli/command-reference/porter-job View and filter job runs for a Porter project from the CLI, with filters for job, application, revision, deployment target, and status `porter job` contains commands for inspecting job runs in your project. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) *** ## `porter job runs` View the job runs for a project, with optional filters applied. Job runs are sorted by creation time, with the most recent runs appearing first. **Usage:** ```bash theme={null} porter job runs [JOB_NAME] [flags] ``` **Options:** | Flag | Description | | - | - | | `--job` | Filter job runs by job name | | `--app` | Filter job runs by application name | | `--revision` | Filter job runs by revision number (requires `--app`) | | `--target` | Filter job runs by deployment target. Uses the default target if not provided; pass `ALL` to pull runs across all deployment targets | | `--status` | Filter job runs by status | | `--limit` | Limit the number of job runs returned (default: 100) | ```bash All Runs theme={null} porter job runs ``` ```bash By Job Name theme={null} porter job runs --job my-job ``` ```bash By Application theme={null} porter job runs --app my-app ``` ```bash By Revision theme={null} porter job runs --app my-app --revision 3 ``` ```bash Across All Targets theme={null} porter job runs --target ALL ``` ```bash By Status theme={null} porter job runs --status successful ``` ```bash Limit Results theme={null} porter job runs --limit 25 ``` > [!NOTE] > > The `--revision` flag requires `--app` to be set, since revisions are scoped to a specific application. *** ## Related Commands * [porter app run](/standard/cli/command-reference/porter-app) - Trigger a job with the `--job` flag * [porter target list](/standard/cli/command-reference/porter-target) - List deployment targets to filter runs by # porter project Source: https://docs.porter.run/standard/cli/command-reference/porter-project List available Porter projects and view project details including collaborators, connected cloud accounts, and settings using the CLI `porter project` contains commands for viewing and managing Porter projects. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) *** ## `porter project list` List all projects that you have access to. **Usage:** ```bash theme={null} porter project list ``` > [!TIP] > > Use `porter config set-project` to switch between projects after listing them. *** ## Related Commands * [porter config set-project](/standard/cli/command-reference/porter-config) - Set the active project * [porter clusters list](/standard/cli/command-reference/porter-clusters#porter-clusters-list) - List clusters in the current project # porter registry Source: https://docs.porter.run/standard/cli/command-reference/porter-registry List project-linked container registries and push local or freshly built images to them from the Porter CLI without running docker login `porter registry` contains commands for working with the container registries linked to your Porter project, including listing registries, browsing repositories and images, and pushing local images without running `docker login`. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) * At least one container registry is linked to the project * A running local Docker daemon (required for `porter registry push`) If your project has more than one linked registry, either select one with `porter config set-registry` or pass `--registry ` on each command. Run `porter registry list` to see the available IDs. *** ## `porter registry push` Push a local image to a registry linked to the project. Credentials are exchanged automatically — no `docker login` is required — and the remote repository is created on the first push. **Usage:** ```bash theme={null} porter registry push [flags] ``` **Options:** | Flag | Description | | - | - | | `--dest` | Remote destination in `repo[:tag]` form. Defaults to the source image's repository and tag | | `--build` | Build the image from this context directory before pushing, instead of pushing an existing local image | | `--dockerfile` | Path to the Dockerfile for `--build`. Defaults to `Dockerfile` inside the build context | | `--json` | Print the pushed image reference as JSON for scripting | | `--registry` | ID of the registry to push to. Required in non-interactive environments when the project has multiple registries | When the project has more than one linked registry URL, the CLI prompts interactively for which one to use. In non-interactive environments the command errors and asks you to pass `--registry` — it will not silently pick one. With `--build`, the image is built from the given context directory on your machine before it is pushed, using the same build path Porter apps use. Set `DOCKER_BUILDKIT=1` to build with buildx, and use `PORTER_BUILDKIT_ARGS` to pass extra flags such as `--platform`. Builds target `linux/amd64` by default. `--json` suppresses progress output on stdout so the JSON payload stays parseable. The payload has a single `image` field containing the fully qualified pushed reference. ```bash Push a local image theme={null} porter registry push myimage:dev ``` ```bash Override the remote repo and tag theme={null} porter registry push myimage:dev --dest sandboxes/base:v1 ``` ```bash Build before pushing theme={null} porter registry push myimage:dev \ --build ./app \ --dockerfile ./app/prod.Dockerfile ``` ```bash Multi-arch build with buildx theme={null} DOCKER_BUILDKIT=1 PORTER_BUILDKIT_ARGS="--platform linux/amd64,linux/arm64" \ porter registry push myimage:dev --build ./app ``` ```bash Capture the pushed reference theme={null} porter registry push myimage:dev --json | jq -r .image ``` > [!NOTE] > > Repository names are normalized to lowercase, with `_` and `.` replaced by `-`, to match how the server stores them. *** ## `porter registry list` List the container registries linked to the current project. The currently selected registry (from `porter config set-registry`) is highlighted. **Usage:** ```bash theme={null} porter registry list ``` Displays a table with each registry's ID, URL, and service (for example, `ecr`, `gcr`, `dockerhub`). *** ## `porter registry delete` Delete a linked registry from the project. Prompts for confirmation before removing. **Usage:** ```bash theme={null} porter registry delete ``` Use the ID from `porter registry list`. *** ## `porter registry repo list` List the repositories inside the selected registry. **Usage:** ```bash theme={null} porter registry repo list ``` Uses the registry from `porter config set-registry`, or from `--registry `. *** ## `porter registry image list` List the image tags in a repository along with their digests. **Usage:** ```bash theme={null} porter registry image list ``` Uses the registry from `porter config set-registry`, or from `--registry `. *** ## Related * [porter config](/standard/cli/command-reference/porter-config) — Select the active project and registry * [Deploy from a Docker registry](/applications/deploy/deploy-from-docker-registry) — Deploy a pushed image as an app # porter target Source: https://docs.porter.run/standard/cli/command-reference/porter-target List and manage deployment targets to control which cluster, namespace, and environment your Porter applications deploy to using the CLI `porter target` contains commands for managing deployment targets. ## Prerequisites * You've logged in to the Porter CLI after running [porter auth login](/standard/cli/command-reference/porter-auth) * You're connected to the correct project by running [porter config set-project](/standard/cli/command-reference/porter-config) * You're connected to the correct cluster by running [porter config set-cluster](/standard/cli/command-reference/porter-config) *** ## `porter target list` List all deployment targets in the current cluster. **Usage:** ```bash theme={null} porter target list [flags] ``` **Options:** | Flag | Description | | - | - | | `--preview` | Include preview environment targets | ```bash List Targets theme={null} porter target list ``` ```bash Include Previews theme={null} porter target list --preview ``` > [!NOTE] > > Deployment targets allow you to deploy the same application to different environments (production, staging, preview) within the same cluster. *** ## Using Targets with Deployments Specify a target when deploying with `porter apply`: ```bash theme={null} porter apply -f porter.yaml --target staging ``` > [!TIP] > > Preview environment targets are automatically created when you set up preview environments. See [Preview Environments](/preview-environments/overview) for more details. *** ## Related Commands * [porter apply](/standard/cli/command-reference/porter-apply) - Deploy to a specific target * [porter app list](/standard/cli/command-reference/porter-app) - List apps in the current target # Privacy policy Source: https://docs.porter.run/terms-and-privacy/privacy-policy Porter Technologies privacy policy covering data collection, usage, storage, and your rights when using the Porter platform and services Last updated: 01/05/2022 ### 1. Introduction[](#1-introduction "Direct link to heading") Welcome to Porter Technologies Inc. Porter Technologies Inc. (“us”, “we”, or “our”) operates [https://getporter.dev](https://getporter.dev) and [https://porter.run](https://porter.run) (hereinafter referred to as “**Service**”). Our Privacy Policy governs your visit to [https://getporter.dev](https://getporter.dev) and [https://porter.run](https://porter.run), and explains how we collect, safeguard and disclose information that results from your use of our Service. We use your data to provide and improve Service. By using Service, you agree to the collection and use of information in accordance with this policy. Unless otherwise defined in this Privacy Policy, the terms used in this Privacy Policy have the same meanings as in our Terms and Conditions. Our Terms and Conditions (“**Terms**”) govern all use of our Service and together with the Privacy Policy constitutes your agreement with us (“**agreement**”). ### 2. Definitions[](#2-definitions "Direct link to heading") **SERVICE** means the [https://getporter.dev](https://getporter.dev) and [https://porter.run](https://porter.run) website operated by Porter Technologies Inc. **PERSONAL DATA** means data about a living individual who can be identified from those data (or from those and other information either in our possession or likely to come into our possession). **USAGE DATA** is data collected automatically either generated by the use of Service or from Service infrastructure itself (for example, the duration of a page visit). **COOKIES** are small files stored on your device (computer or mobile device). **DATA CONTROLLER** means a natural or legal person who (either alone or jointly or in common with other persons) determines the purposes for which and the manner in which any personal data are, or are to be, processed. For the purpose of this Privacy Policy, we are a Data Controller of your data. **DATA PROCESSORS (OR SERVICE PROVIDERS)** means any natural or legal person who processes the data on behalf of the Data Controller. We may use the services of various Service Providers in order to process your data more effectively. **DATA SUBJECT** is any living individual who is the subject of Personal Data. **THE USER** is the individual using our Service. The User corresponds to the Data Subject, who is the subject of Personal Data. ### 3. Information Collection and Use[](#3-information-collection-and-use "Direct link to heading") We collect several different types of information for various purposes to provide and improve our Service to you. ### 4. Types of Data Collected[](#4-types-of-data-collected "Direct link to heading") **Personal Data** While using our Service, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you (“**Personal Data**”). Personally identifiable information may include, but is not limited to: (a) Email address (b) First name and last name (c) Cookies and Usage Data We may use your Personal Data to contact you with newsletters, marketing or promotional materials and other information that may be of interest to you. You may opt out of receiving any, or all, of these communications from us by emailing at [contact@porter.run](mailto:contact@porter.run). **Usage Data** We may also collect information that your browser sends whenever you visit our Service or when you access Service by or through a mobile device (“**Usage Data**”) . This Usage Data may include information such as your computer's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that you visit, the time and date of your visit, the time spent on those pages, unique device identifiers and other diagnostic data. When you access Service with a mobile device, this Usage Data may include information such as the type of mobile device you use, your mobile device unique ID, the IP address of your mobile device, your mobile operating system, the type of mobile Internet browser you use, unique device identifiers and other diagnostic data. **Tracking Cookies Data** We use cookies and similar tracking technologies to track the activity on our Service and we hold certain information. Cookies are files with a small amount of data which may include an anonymous unique identifier. Cookies are sent to your browser from a website and stored on your device. Other tracking technologies are also used such as beacons, tags and scripts to collect and track information and to improve and analyze our Service. You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may not be able to use some portions of our Service. Examples of Cookies we use: (a) **Session Cookies:** We use Session Cookies to operate our Service. (b) **Preference Cookies:** We use Preference Cookies to remember your preferences and various settings. (c) **Security Cookies:** We use Security Cookies for security purposes. (d) **Advertising Cookies:** Advertising Cookies are used to serve you with advertisements that may be relevant to you and your interests. ### 5. Use of Data[](#5-use-of-data "Direct link to heading") Porter Technologies Inc. uses the collected data for various purposes: (a) To provide and maintain our Service; (b) To notify you about changes to our Service; (c) To allow you to participate in interactive features of our Service when you choose to do so; (d) To provide customer support; (e) To gather analysis or valuable information so that we can improve our Service; (f) To monitor the usage of our Service; (g) To detect, prevent and address technical issues; (h) To fulfill any other purpose for which you provide it; (i) To carry out our obligations and enforce our rights arising from any contracts entered into between you and us, including for billing and collection; (j) To provide you with notices about your account and/or subscription, including expiration and renewal notices, email-instructions, etc.; (k) To provide you with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless you have opted not to receive such information; (l) In any other way we may describe when you provide the information; (m) For any other purpose with your consent. ### 6. Retention of Data[](#6-retention-of-data "Direct link to heading") We will retain your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies. We will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period, except when this data is used to strengthen the security or to improve the functionality of our Service, or we are legally obligated to retain this data for longer time periods. ### 7. Transfer of Data[](#7-transfer-of-data "Direct link to heading") Your information, including Personal Data, may be transferred to – and maintained on – computers located outside of your state, province, country or other governmental jurisdiction where the data protection laws may differ from those of your jurisdiction. If you are located outside United States and choose to provide information to us, please note that we transfer the data, including Personal Data, to United States and process it there. Your consent to this Privacy Policy followed by your submission of such information represents your agreement to that transfer. Porter Technologies Inc. will take all the steps reasonably necessary to ensure that your data is treated securely and in accordance with this Privacy Policy and no transfer of your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of your data and other personal information. ### 8. Disclosure of Data[](#8-disclosure-of-data "Direct link to heading") We may disclose personal information that we collect, or you provide: (a) **Disclosure for Law Enforcement:** Under certain circumstances, we may be required to disclose your Personal Data if required to do so by law or in response to valid requests by public authorities. (b) **Business Transaction:** If we or our subsidiaries are involved in a merger, acquisition or asset sale, your Personal Data may be transferred. (c) **Other cases. We may disclose your information also:** * To our subsidiaries and affiliates; * To contractors, service providers, and other third parties we use to support our business; * To fulfill the purpose for which you provide it; * For the purpose of including your company’s logo on our website; * For any other purpose disclosed by us when you provide the information; * With your consent in any other cases; * If we believe disclosure is necessary or appropriate to protect the rights, property, or safety of the Company, our customers, or others. ### 9. Security of Data[](#9-security-of-data "Direct link to heading") The security of your data is important to us but remember that no method of transmission over the Internet or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your Personal Data, we cannot guarantee its absolute security. ### 10. Your Data Protection Rights Under General Data Protection Regulation (GDPR)[](#10-your-data-protection-rights-under-general-data-protection-regulation-gdpr "Direct link to heading") If you are a resident of the European Union (EU) and European Economic Area (EEA), you have certain data protection rights, covered by GDPR. – See more at [https://eur-lex.europa.eu/eli/reg/2016/679/oj](https://eur-lex.europa.eu/eli/reg/2016/679/oj) We aim to take reasonable steps to allow you to correct, amend, delete, or limit the use of your Personal Data. If you wish to be informed what Personal Data we hold about you and if you want it to be removed from our systems, please email us at [contact@porter.run](mailto:contact@porter.run). In certain circumstances, you have the following data protection rights: (a) The right to access, update or to delete the information we have on you; (b) The right of rectification. You have the right to have your information rectified if that information is inaccurate or incomplete; (c) The right to object. You have the right to object to our processing of your Personal Data; (d) The right of restriction. You have the right to request that we restrict the processing of your personal information; (e) The right to data portability. You have the right to be provided with a copy of your Personal Data in a structured, machine-readable and commonly used format; (f) The right to withdraw consent. You also have the right to withdraw your consent at any time where we rely on your consent to process your personal information. Please note that we may ask you to verify your identity before responding to such requests. Please note, we may not able to provide Service without some necessary data. You have the right to complain to a Data Protection Authority about our collection and use of your Personal Data. For more information, please contact your local data protection authority in the European Economic Area (EEA). ### 11. Your Data Protection Rights under the California Privacy Protection Act (CalOPPA)[](#11-your-data-protection-rights-under-the-california-privacy-protection-act-caloppa "Direct link to heading") CalOPPA is the first state law in the nation to require commercial websites and online services to post a privacy policy. The law’s reach stretches well beyond California to require a person or company in the United States (and conceivable the world) that operates websites collecting personally identifiable information from California consumers to post a conspicuous privacy policy on its website stating exactly the information being collected and those individuals with whom it is being shared, and to comply with this policy. – See more at: [https://consumercal.org/about-cfc/cfc-education-foundation/california-online-privacy-protection-act-caloppa-3/](https://consumercal.org/about-cfc/cfc-education-foundation/california-online-privacy-protection-act-caloppa-3/) According to CalOPPA we agree to the following: (a) Users can visit our site anonymously; (b) Our Privacy Policy link includes the word “Privacy”, and can easily be found on the page specified above on the home page of our website; (c) Users will be notified of any privacy policy changes on our Privacy Policy Page; (d) Users are able to change their personal information by emailing us at [contact@porter.run](mailto:contact@porter.run). Our Policy on “Do Not Track” Signals: We honor Do Not Track signals and do not track, plant cookies, or use advertising when a Do Not Track browser mechanism is in place. Do Not Track is a preference you can set in your web browser to inform websites that you do not want to be tracked. You can enable or disable Do Not Track by visiting the Preferences or Settings page of your web browser. ### 12. Service Providers[](#12-service-providers "Direct link to heading") We may employ third party companies and individuals to facilitate our Service (“**Service Providers**”), provide Service on our behalf, perform Service-related services or assist us in analysing how our Service is used. These third parties have access to your Personal Data only to perform these tasks on our behalf and are obligated not to disclose or use it for any other purpose. ### 13. Analytics[](#13-analytics "Direct link to heading") We may use third-party Service Providers to monitor and analyze the use of our Service. **FullStory** FullStory is provided by FullStory Inc. You can prevent FullStory from using your information for analytics purposes by opting-out. To opt-out of FullStory service, please visit this page: [https://www.fullstory.com/optout/](https://www.fullstory.com/optout/) For more information on what type of information FullStory collects, please visit the Terms of Use page of FullStory: [https://www.fullstory.com/legal/terms-and-conditions/](https://www.fullstory.com/legal/terms-and-conditions/) ### 14. CI/CD tools[](#14-cicd-tools "Direct link to heading") We may use third-party Service Providers to automate the development process of our Service. **GitLab CI/CD**GitLab CI/CD is provided by GitLab, Inc. GitLab CI (Continuous Integration) service is a part of GitLab that build and test the software whenever developer pushes code to application. GitLab CD (Continuous Deployment) is a software service that places the changes of every code in the production which results in every day deployment of production. For more information on what data GitLab CI/CD collects for what purpose and how the protection of the data is ensured, please visit GitLab CI/CD Privacy Policy page: [https://about.gitlab.com/privacy/](https://about.gitlab.com/privacy/). ### 15. Payments[](#15-payments "Direct link to heading") We may provide paid products and/or services within Service. In that case, we use third-party services for payment processing (e.g. payment processors). We will not store or collect your payment card details. That information is provided directly to our third-party payment processors whose use of your personal information is governed by their Privacy Policy. These payment processors adhere to the standards set by PCI-DSS as managed by the PCI Security Standards Council, which is a joint effort of brands like Visa, Mastercard, American Express and Discover. PCI-DSS requirements help ensure the secure handling of payment information. The payment processors we work with are: **Stripe** Their Privacy Policy can be viewed at: [https://stripe.com/us/privacy](https://stripe.com/us/privacy) ### 16. Links to Other Sites[](#16-links-to-other-sites "Direct link to heading") Our Service may contain links to other sites that are not operated by us. If you click a third party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services. ### 17. Children's Privacy[](#17-childrens-privacy "Direct link to heading") Our Services are not intended for use by children under the age of 13 (“Children”). We do not knowingly collect personally identifiable information from Children under 13. If you become aware that a Child has provided us with Personal Data, please contact us. If we become aware that we have collected Personal Data from Children without verification of parental consent, we take steps to remove that information from our servers. ### 18. Changes to This Privacy Policy[](#18-changes-to-this-privacy-policy "Direct link to heading") We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. We will let you know via email and/or a prominent notice on our Service, prior to the change becoming effective and update “effective date” at the top of this Privacy Policy. You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. ### 19. Contact Us[](#19-contact-us "Direct link to heading") If you have any questions about this Privacy Policy, please contact us by email: [contact@porter.run](mailto:contact@porter.run). # Terms of service Source: https://docs.porter.run/terms-and-privacy/terms-of-service Porter Technologies terms of service governing your use of the Porter platform, including account responsibilities and usage policies Last updated: 01/05/2022 ### 1. Introduction[](#1-introduction "Direct link to heading") Welcome to Porter Technologies Inc. (“**Company**”, “**we**”, “**our**”, “**us**”)! As you have just clicked our Terms of Service, please pause, grab a cup of coffee and carefully read the following pages. It will take you approximately 20 minutes. These Terms of Service (“**Terms**”, “**Terms of Service**”) govern your use of our web pages located at [https://getporter.dev](https://getporter.dev) and [https://porter.run](https://porter.run) operated by Porter Technologies Inc. Our Privacy Policy also governs your use of our Service and explains how we collect, safeguard and disclose information that results from your use of our web pages. Please read it here [https://docs.porter.run/terms-and-privacy/privacy-policy](https://docs.porter.run/terms-and-privacy/privacy-policy). Your agreement with us includes these Terms and our Privacy Policy (“Agreements”). You acknowledge that you have read and understood Agreements, and agree to be bound of them. If you do not agree with (or cannot comply with) Agreements, then you may not use the Service, but please let us know by emailing at [contact@porter.run](mailto:contact@porter.run) so we can try to find a solution. These Terms apply to all visitors, users and others who wish to access or use Service. Thank you for being responsible. ### 2. Communications[](#2-communications "Direct link to heading") By creating an Account on our Service, you agree to subscribe to newsletters, marketing or promotional materials and other information we may send. However, you may opt out of receiving any, or all, of these communications from us by following the unsubscribe link or by emailing [contact@porter.run](mailto:contact@porter.run). ### 3. Purchases[](#3-purchases "Direct link to heading") If you wish to purchase any product or service made available through Service (“**Purchase**”), you may be asked to supply certain information relevant to your Purchase including, without limitation, your credit card number, the expiration date of your credit card, your billing address, and your shipping information. You represent and warrant that: (i) you have the legal right to use any credit card(s) or other payment method(s) in connection with any Purchase; and that (ii) the information you supply to us is true, correct and complete. We may employ the use of third party services for the purpose of facilitating payment and the completion of Purchases. By submitting your information, you grant us the right to provide the information to these third parties subject to our Privacy Policy. We reserve the right to refuse or cancel your order at any time for reasons including but not limited to: product or service availability, errors in the description or price of the product or service, error in your order or other reasons. We reserve the right to refuse or cancel your order if fraud or an unauthorized or illegal transaction is suspected. ### 4. Subscriptions[](#4-subscriptions "Direct link to heading") Some parts of Service are billed on a subscription basis (“**Subscription(s)**”). You will be billed in advance on a recurring and periodic basis (“**Billing Cycle**”). Billing cycles are set either on a monthly or annual basis, depending on the type of subscription plan you select when purchasing a Subscription. At the end of each Billing Cycle, your Subscription will automatically renew under the exact same conditions unless you cancel it or Porter Technologies Inc. cancels it. You may cancel your Subscription renewal either through your online account management page or by contacting Porter Technologies Inc. customer support team. A valid payment method, including credit card, is required to process the payment for your subscription. You shall provide Porter Technologies Inc. with accurate and complete billing information including full name, address, state, zip code, telephone number, and a valid payment method information. By submitting such payment information, you automatically authorize Porter Technologies Inc. to charge all Subscription fees incurred through your account to any such payment instruments. Should automatic billing fail to occur for any reason, Porter Technologies Inc. will issue an electronic invoice indicating that you must proceed manually, within a certain deadline date, with the full payment corresponding to the billing period as indicated on the invoice. ### 5. Fee Changes[](#5-fee-changes "Direct link to heading") Porter Technologies Inc., in its sole discretion and at any time, may modify Subscription fees for the Subscriptions. Any Subscription fee change will become effective at the end of the then-current Billing Cycle. Porter Technologies Inc. will provide you with a reasonable prior notice of any change in Subscription fees to give you an opportunity to terminate your Subscription before such change becomes effective. Your continued use of Service after Subscription fee change comes into effect constitutes your agreement to pay the modified Subscription fee amount. ### 6. Refunds[](#6-refunds "Direct link to heading") Except when required by law, paid Subscription fees are non-refundable. ### 7. Content[](#7-content "Direct link to heading") Our Service allows you to post, link, store, share and otherwise make available certain information, text, graphics, videos, or other material (“**Content**”). You are responsible for Content that you post on or through Service, including its legality, reliability, and appropriateness. By posting Content on or through Service, You represent and warrant that: (i) Content is yours (you own it) and/or you have the right to use it and the right to grant us the rights and license as provided in these Terms, and (ii) that the posting of your Content on or through Service does not violate the privacy rights, publicity rights, copyrights, contract rights or any other rights of any person or entity. We reserve the right to terminate the account of anyone found to be infringing on a copyright. You retain any and all of your rights to any Content you submit, post or display on or through Service and you are responsible for protecting those rights. We take no responsibility and assume no liability for Content you or any third party posts on or through Service. However, by posting Content using Service you grant us the right and license to use, modify, publicly perform, publicly display, reproduce, and distribute such Content on and through Service. You agree that this license includes the right for us to make your Content available to other users of Service, who may also use your Content subject to these Terms. Porter Technologies Inc. has the right but not the obligation to monitor and edit all Content provided by users. In addition, Content found on or through this Service are the property of Porter Technologies Inc. or used with permission. You may not distribute, modify, transmit, reuse, download, repost, copy, or use said Content, whether in whole or in part, for commercial purposes or for personal gain, without express advance written permission from us. ### 8. Prohibited Uses[](#8-prohibited-uses "Direct link to heading") You may use Service only for lawful purposes and in accordance with Terms. You agree not to use Service: 8.1. In any way that violates any applicable national or international law or regulation. 8.2. For the purpose of exploiting, harming, or attempting to exploit or harm minors in any way by exposing them to inappropriate content or otherwise. 8.3. To transmit, or procure the sending of, any advertising or promotional material, including any “junk mail”, “chain letter,” “spam,” or any other similar solicitation. 8.4. To impersonate or attempt to impersonate Company, a Company employee, another user, or any other person or entity. 8.5. In any way that infringes upon the rights of others, or in any way is illegal, threatening, fraudulent, or harmful, or in connection with any unlawful, illegal, fraudulent, or harmful purpose or activity. 8.6. To engage in any other conduct that restricts or inhibits anyone’s use or enjoyment of Service, or which, as determined by us, may harm or offend Company or users of Service or expose them to liability. Additionally, you agree not to: 8.7. Use Service in any manner that could disable, overburden, damage, or impair Service or interfere with any other party’s use of Service, including their ability to engage in real time activities through Service. 8.8. Use any robot, spider, or other automatic device, process, or means to access Service for any purpose, including monitoring or copying any of the material on Service. 8.9. Use any manual process to monitor or copy any of the material on Service or for any other unauthorized purpose without our prior written consent. 8.10. Use any device, software, or routine that interferes with the proper working of Service. 8.11. Introduce any viruses, trojan horses, worms, logic bombs, or other material which is malicious or technologically harmful. 8.12. Attempt to gain unauthorized access to, interfere with, damage, or disrupt any parts of Service, the server on which Service is stored, or any server, computer, or database connected to Service. 8.13. Attack Service via a denial-of-service attack or a distributed denial-of-service attack. 8.14. Take any action that may damage or falsify Company rating. 8.15. Otherwise attempt to interfere with the proper working of Service. ### 9. Analytics[](#9-analytics "Direct link to heading") We may use third-party Service Providers to monitor and analyze the use of our Service. **Mixpanel** **FullStory** FullStory is provided by FullStory Inc. You can prevent FullStory from using your information for analytics purposes by opting-out. To opt-out of FullStory service, please visit this page: [https://www.fullstory.com/optout/](https://www.fullstory.com/optout/) For more information on what type of information FullStory collects, please visit the Terms of Use page of FullStory: [https://www.fullstory.com/legal/terms-and-conditions/](https://www.fullstory.com/legal/terms-and-conditions/) ### 10. No Use By Minors[](#10-no-use-by-minors "Direct link to heading") Service is intended only for access and use by individuals at least eighteen (18) years old. By accessing or using any of Company, you warrant and represent that you are at least eighteen (18) years of age and with the full authority, right, and capacity to enter into this agreement and abide by all of the terms and conditions of Terms. If you are not at least eighteen (18) years old, you are prohibited from both the access and usage of Service. ### 11. Accounts[](#11-accounts "Direct link to heading") When you create an account with us, you guarantee that you are above the age of 18, and that the information you provide us is accurate, complete, and current at all times. Inaccurate, incomplete, or obsolete information may result in the immediate termination of your account on Service. You are responsible for maintaining the confidentiality of your account and password, including but not limited to the restriction of access to your computer and/or account. You agree to accept responsibility for any and all activities or actions that occur under your account and/or password, whether your password is with our Service or a third-party service. You must notify us immediately upon becoming aware of any breach of security or unauthorized use of your account. You may not use as a username the name of another person or entity or that is not lawfully available for use, a name or trademark that is subject to any rights of another person or entity other than you, without appropriate authorization. You may not use as a username any name that is offensive, vulgar or obscene. We reserve the right to refuse service, terminate accounts, remove or edit content, or cancel orders in our sole discretion. ### 12. Intellectual Property[](#12-intellectual-property "Direct link to heading") Service and its original content (excluding Content provided by users), features and functionality are and will remain the exclusive property of Porter Technologies Inc. and its licensors. Service is protected by copyright, trademark, and other laws of the United States. Our trademarks and trade dress may not be used in connection with any product or service without the prior written consent of Porter Technologies Inc.. ### 13. Copyright Policy[](#13-copyright-policy "Direct link to heading") We respect the intellectual property rights of others. It is our policy to respond to any claim that Content posted on Service infringes on the copyright or other intellectual property rights (“**Infringement**”) of any person or entity. If you are a copyright owner, or authorized on behalf of one, and you believe that the copyrighted work has been copied in a way that constitutes copyright infringement, please submit your claim via email to [contact@porter.run](mailto:contact@porter.run), with the subject line: “Copyright Infringement” and include in your claim a detailed description of the alleged Infringement as detailed below, under “DMCA Notice and Procedure for Copyright Infringement Claims” You may be held accountable for damages (including costs and attorneys' fees) for misrepresentation or bad-faith claims on the infringement of any Content found on and/or through Service on your copyright. ### 14. DMCA Notice and Procedure for Copyright Infringement Claims[](#14-dmca-notice-and-procedure-for-copyright-infringement-claims "Direct link to heading") You may submit a notification pursuant to the Digital Millennium Copyright Act (DMCA) by providing our Copyright Agent with the following information in writing (see 17 U.S.C 512(c)(3) for further detail): 14.1. An electronic or physical signature of the person authorized to act on behalf of the owner of the copyright's interest; 14.2 A description of the copyrighted work that you claim has been infringed, including the URL (i.e., web page address) of the location where the copyrighted work exists or a copy of the copyrighted work; 14.3 Identification of the URL or other specific location on Service where the material that you claim is infringing is located; 14.4 Your address, telephone number, and email address; 14.5 A statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law; 14.6 A statement by you, made under penalty of perjury, that the above information in your notice is accurate and that you are the copyright owner or authorized to act on the copyright owner's behalf. You can contact our Copyright Agent via email at [justin@porter.run](mailto:justin@porter.run) ### 15. Error Reporting and Feedback[](#15-error-reporting-and-feedback "Direct link to heading") You may provide us either directly at [contact@porter.run](mailto:contact@porter.run) or via third party sites and tools with information and feedback concerning errors, suggestions for improvements, ideas, problems, complaints, and other matters related to our Service (“**Feedback**”). You acknowledge and agree that: (i) you shall not retain, acquire or assert any intellectual property right or other right, title or interest in or to the Feedback; (ii) Company may have development ideas similar to the Feedback; (iii) Feedback does not contain confidential information or proprietary information from you or any third party; and (iv) Company is not under any obligation of confidentiality with respect to the Feedback. In the event the transfer of the ownership to the Feedback is not possible due to applicable mandatory laws, you grant Company and its affiliates an exclusive, transferable, irrevocable, free-of-charge, sub-licensable, unlimited and perpetual right to use (including copy, modify, create derivative works, publish, distribute and commercialize) Feedback in any manner and for any purpose. The third party sites and tools mentioned above include the following: **Sentry** Sentry is open-source error tracking solution provided by Functional Software Inc. More information is available here: [https://sentry.io/privacy/](https://sentry.io/privacy/) ### 16. Links To Other Web Sites[](#16-links-to-other-web-sites "Direct link to heading") Our Service may contain links to third party web sites or services that are not owned or controlled by Porter Technologies Inc. Porter Technologies Inc. has no control over, and assumes no responsibility for the content, privacy policies, or practices of any third party web sites or services. We do not warrant the offerings of any of these entities/individuals or their websites. YOU ACKNOWLEDGE AND AGREE THAT PORTER TECHNOLOGIES INC. SHALL NOT BE RESPONSIBLE OR LIABLE, DIRECTLY OR INDIRECTLY, FOR ANY DAMAGE OR LOSS CAUSED OR ALLEGED TO BE CAUSED BY OR IN CONNECTION WITH USE OF OR RELIANCE ON ANY SUCH CONTENT, GOODS OR SERVICES AVAILABLE ON OR THROUGH ANY SUCH THIRD PARTY WEB SITES OR SERVICES. WE STRONGLY ADVISE YOU TO READ THE TERMS OF SERVICE AND PRIVACY POLICIES OF ANY THIRD PARTY WEB SITES OR SERVICES THAT YOU VISIT. ### 17. Disclaimer Of Warranty[](#17-disclaimer-of-warranty "Direct link to heading") THESE SERVICES ARE PROVIDED BY COMPANY ON AN “AS IS” AND “AS AVAILABLE” BASIS. COMPANY MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, AS TO THE OPERATION OF THEIR SERVICES, OR THE INFORMATION, CONTENT OR MATERIALS INCLUDED THEREIN. YOU EXPRESSLY AGREE THAT YOUR USE OF THESE SERVICES, THEIR CONTENT, AND ANY SERVICES OR ITEMS OBTAINED FROM US IS AT YOUR SOLE RISK. NEITHER COMPANY NOR ANY PERSON ASSOCIATED WITH COMPANY MAKES ANY WARRANTY OR REPRESENTATION WITH RESPECT TO THE COMPLETENESS, SECURITY, RELIABILITY, QUALITY, ACCURACY, OR AVAILABILITY OF THE SERVICES. WITHOUT LIMITING THE FOREGOING, NEITHER COMPANY NOR ANYONE ASSOCIATED WITH COMPANY REPRESENTS OR WARRANTS THAT THE SERVICES, THEIR CONTENT, OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE SERVICES WILL BE ACCURATE, RELIABLE, ERROR-FREE, OR UNINTERRUPTED, THAT DEFECTS WILL BE CORRECTED, THAT THE SERVICES OR THE SERVER THAT MAKES IT AVAILABLE ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS OR THAT THE SERVICES OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE SERVICES WILL OTHERWISE MEET YOUR NEEDS OR EXPECTATIONS. COMPANY HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND FITNESS FOR PARTICULAR PURPOSE. THE FOREGOING DOES NOT AFFECT ANY WARRANTIES WHICH CANNOT BE EXCLUDED OR LIMITED UNDER APPLICABLE LAW. ### 18. Limitation Of Liability[](#18-limitation-of-liability "Direct link to heading") EXCEPT AS PROHIBITED BY LAW, YOU WILL HOLD US AND OUR OFFICERS, DIRECTORS, EMPLOYEES, AND AGENTS HARMLESS FOR ANY INDIRECT, PUNITIVE, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGE, HOWEVER IT ARISES (INCLUDING ATTORNEYS' FEES AND ALL RELATED COSTS AND EXPENSES OF LITIGATION AND ARBITRATION, OR AT TRIAL OR ON APPEAL, IF ANY, WHETHER OR NOT LITIGATION OR ARBITRATION IS INSTITUTED), WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, OR ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT, INCLUDING WITHOUT LIMITATION ANY CLAIM FOR PERSONAL INJURY OR PROPERTY DAMAGE, ARISING FROM THIS AGREEMENT AND ANY VIOLATION BY YOU OF ANY FEDERAL, STATE, OR LOCAL LAWS, STATUTES, RULES, OR REGULATIONS, EVEN IF COMPANY HAS BEEN PREVIOUSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. EXCEPT AS PROHIBITED BY LAW, IF THERE IS LIABILITY FOUND ON THE PART OF COMPANY, IT WILL BE LIMITED TO THE AMOUNT PAID FOR THE PRODUCTS AND/OR SERVICES, AND UNDER NO CIRCUMSTANCES WILL THERE BE CONSEQUENTIAL OR PUNITIVE DAMAGES. SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION OF PUNITIVE, INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE PRIOR LIMITATION OR EXCLUSION MAY NOT APPLY TO YOU. ### 19. Termination[](#19-termination "Direct link to heading") We may terminate or suspend your account and bar access to Service immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of Terms. If you wish to terminate your account, you may simply discontinue using Service. All provisions of Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability. ### 20. Governing Law[](#20-governing-law "Direct link to heading") These Terms shall be governed and construed in accordance with the laws of State of California without regard to its conflict of law provisions. Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service and supersede and replace any prior agreements we might have had between us regarding Service. ### 21. Changes To Service[](#21-changes-to-service "Direct link to heading") We reserve the right to withdraw or amend our Service, and any service or material we provide via Service, in our sole discretion without notice. We will not be liable if for any reason all or any part of Service is unavailable at any time or for any period. From time to time, we may restrict access to some parts of Service, or the entire Service, to users, including registered users. ### 22. Amendments To Terms[](#22-amendments-to-terms "Direct link to heading") We may amend Terms at any time by posting the amended terms on this site. It is your responsibility to review these Terms periodically. Your continued use of the Platform following the posting of revised Terms means that you accept and agree to the changes. You are expected to check this page frequently so you are aware of any changes, as they are binding on you. By continuing to access or use our Service after any revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, you are no longer authorized to use Service. ### 23. Waiver And Severability[](#23-waiver-and-severability "Direct link to heading") No waiver by Company of any term or condition set forth in Terms shall be deemed a further or continuing waiver of such term or condition or a waiver of any other term or condition, and any failure of Company to assert a right or provision under Terms shall not constitute a waiver of such right or provision. If any provision of Terms is held by a court or other tribunal of competent jurisdiction to be invalid, illegal or unenforceable for any reason, such provision shall be eliminated or limited to the minimum extent such that the remaining provisions of Terms will continue in full force and effect. ### 24. Acknowledgement[](#24-acknowledgement "Direct link to heading") BY USING SERVICE OR OTHER SERVICES PROVIDED BY US, YOU ACKNOWLEDGE THAT YOU HAVE READ THESE TERMS OF SERVICE AND AGREE TO BE BOUND BY THEM. ### 25. Contact Us[](#25-contact-us "Direct link to heading") Please send your feedback, comments, and requests for technical support by email to [contact@porter.run](mailto:contact@porter.run).