Skip to content

Deploying Versioned Applications

This guide explains how to deploy versioned applications that ICC can manage with skew protection. Each version is a separate immutable workload: a Deployment and Service on Kubernetes, or an ECS service and target group on ECS.

For a customer-managed Kubernetes deployment, include these labels on the pod template:

LabelRequiredPurpose
app.kubernetes.io/nameYesApplication identity — must be the same across all versions
plt.dev/versionRequired for query pinningExplicit version identifier, unique per version
app.kubernetes.io/instanceYes (Kubernetes only)This workload’s own identity — unique per version, set to the Deployment name. Also required on the Service

ICC detects these labels automatically when the pod registers. ICC-managed deployments add them automatically.

The last two are not duplicates. plt.dev/version is the routing key carried in ?dpl=. app.kubernetes.io/instance is the workload name, so it must be a valid Kubernetes resource name: lowercase, with no underscores. They match only when the version is also legal as a resource name.

app.kubernetes.io/instance is Kubernetes-only: on ECS a version is one ECS service, which already identifies itself. On Kubernetes it is how ICC tells one live version’s resources from another’s, and it is used in three places:

  • Finding the version’s Service. When a pod registers, ICC looks up the Service belonging to that version. Without the instance label it falls back to plt.dev/version; without both it matches every version’s Service and picks one arbitrarily. For a workflow application that means queue handlers can be registered at another version’s address, and runs then execute on code they were not started against.
  • Per-version metrics. The Memory, CPU, ELU and latency charts select a version’s instances by joining on this label. Without it those charts are empty.
  • Confirming teardown. Before expiring a version, ICC reads its pods by this label.

A version deployed through ICC without an explicit version can still be tracked: ICC derives a stable version from the image reference. It cannot be query-pinned because the derived value was not available when the image assets were built.

Query pinning, the default, matches a ?dpl=<version> parameter that the application carries in its own assets. Nothing at deploy time can add it, because it has to be present in the assets the browser has already downloaded, so it must be supplied at image build time.

Pass the version to the build as the PLT_DEPLOYMENT_ID build argument, using the same value as the plt.dev/version label:

ARG PLT_DEPLOYMENT_ID=
ENV PLT_DEPLOYMENT_ID=$PLT_DEPLOYMENT_ID
RUN npm run build
Terminal window
docker build --build-arg PLT_DEPLOYMENT_ID=v2 -t myapp:v2 .
kubectl set image ... # deployed with plt.dev/version: v2

The build argument is visible to RUN on its own; the ENV line persists the value into the image so the running pod can report it back to ICC. That report is what tells ICC the version is pinnable, and it is checked rather than trusted: ICC compares the reported id against the version label and only emits a pinning rule when they match.

Cookie pinning requires nothing from the build. If your pipeline cannot pass a version, use PLT_SKEW_DEFAULT_ROUTING_MODE=cookie, subject to the controller requirements in Prerequisites.

Cookie pinning is available only on Kubernetes. ECS supports query pinning only.

LabelPurposeDefault
plt.dev/pathCustom Kubernetes route prefix/{appName} (or / when plt.dev/hostname is set)
plt.dev/hostnameRoute hostname (e.g., myapp.example.com); required on ECSNot set on Kubernetes
plt.dev/workflowSet to "true" for apps using Vercel Workflow DevKit with @platformatic/world. Enables workflow-aware draining.Not set

POST /applications/:id/deploy creates the workload through Machinist. A CI deploy token can use the equivalent POST /deploy endpoint without putting the application id in the URL. The minimum body is:

{
"image": "your-registry.com/myapp:v2"
}

Pass version when using query pinning, and build the image with the same value as PLT_DEPLOYMENT_ID. When version is omitted, ICC derives a stable version from the image reference for tracking, but that version cannot be query-pinned. The deploy API accepts lowercase, resource-safe versions containing letters, digits, dots, and hyphens. It rejects uppercase, underscores, wildcards, and other values that would be unsafe as provider resource names or ALB matches.

The deploy endpoint applies the workload in observe, manage, and advise modes. To inspect the desired workload and route without changing either provider, use POST /applications/:id/deploy/plan or POST /deploy/plan.

On Kubernetes, ICC applies a Deployment, Service, and optional image-pull Secret. On ECS, ICC sends a neutral workload specification to Machinist, which creates the task definition, ECS service, target group, Cloud Map service, and registry secret when required.

Create a Deployment and Service for each version. The key requirements:

  • Same app.kubernetes.io/name label across all versions
  • Unique plt.dev/version label per version
  • Unique app.kubernetes.io/instance label per version, on both the pod template and the Service, set to the Deployment name
  • Unique Deployment and Service names (we recommend {appName}-{version})
  • PLT_ICC_URL configured to connect to ICC
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-v1.2.4
namespace: platformatic
labels:
app.kubernetes.io/name: myapp
plt.dev/version: "1.2.4"
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: myapp
plt.dev/version: "1.2.4"
template:
metadata:
labels:
app.kubernetes.io/name: myapp
app.kubernetes.io/instance: myapp-v1.2.4
plt.dev/version: "1.2.4"
platformatic.dev/monitor: prometheus
spec:
containers:
- name: watt
image: your-registry.com/myapp:1.2.4
ports:
- name: app
containerPort: 3042
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
env:
- name: PLT_ICC_URL
value: "http://icc.platformatic.svc.cluster.local"
- name: PLT_SERVER_HOSTNAME
value: "0.0.0.0"
---
apiVersion: v1
kind: Service
metadata:
name: myapp-v1.2.4
namespace: platformatic
labels:
app.kubernetes.io/name: myapp
app.kubernetes.io/instance: myapp-v1.2.4
plt.dev/version: "1.2.4"
spec:
selector:
app.kubernetes.io/name: myapp
plt.dev/version: "1.2.4"
ports:
- name: app
port: 3042
targetPort: 3042
- name: metrics
port: 9090
targetPort: 9090

To deploy a new version of your application:

  1. Build a new container image with the updated application code (e.g., myapp:1.2.5)
  2. Create a new Deployment and Service manifest with the same app.kubernetes.io/name but a new plt.dev/version
  3. Apply the manifest:
Terminal window
kubectl apply -f myapp-v1.2.5.yaml
  1. ICC handles the rest — when the pods start and register with ICC, the new version is detected and routing transitions automatically

ICC first records the new version as pending-apply, or staged when approval is required. After the desired route is applied or observed, the new version becomes active and the previous version becomes draining.

ECS skew protection requires a distinct ECS service and target group for every version. The ECS service must expose equivalent metadata through service tags, including app.kubernetes.io/name, plt.dev/version, and plt.dev/hostname. Propagating those tags to tasks is supported but not required; Machinist falls back to the parent service tags. The service also needs a target group from creation time so the version can become an ALB backend.

ECS has these additional constraints:

  • Only query pinning is supported
  • A hostname is required; ALB does not perform ICC’s Kubernetes path rewrite
  • The target-group health check must use the dedicated Watt readiness endpoint, normally port 9090 and path /ready, while user traffic uses port 3042
  • Version matching is case-insensitive in ALB query conditions
  • * and ? are ALB wildcards, so do not use them in version labels
  • Two live labels must not differ only by case

Using ICC’s deploy API avoids having to construct these resources and tags manually. Machinist exposes both ports and configures the target-group health check on 9090/ready. For a customer-managed service, configure the same split explicitly. Probing an application route on port 3042 is counted as ingress traffic and prevents both HTTP and workflow policies from observing zero RPS.

By default, ICC creates HTTPRoute rules with a path prefix of /{appName} (derived from app.kubernetes.io/name). If your application uses a different path, set the plt.dev/path label:

metadata:
labels:
app.kubernetes.io/name: myapp
plt.dev/version: "1.2.4"
plt.dev/path: "/api/leads"

The path prefix scopes the routing rules to your application, which is important when multiple applications share the same Gateway. Path-based routing is not supported on ECS because ALB cannot remove the external prefix before forwarding the request.

For production setups with per-application domains, set the plt.dev/hostname label:

metadata:
labels:
app.kubernetes.io/name: myapp
plt.dev/version: "1.2.4"
plt.dev/hostname: "myapp.example.com"

On Kubernetes, ICC adds the hostname to the HTTPRoute. On ECS, Machinist scopes every managed listener rule to the hostname. A hostname is mandatory on ECS.

Browser-based clients pin themselves automatically: in query mode the page’s own assets carry ?dpl=<version>, and in cookie mode the browser returns the __plt_dpl cookie. For API clients (server-to-server, CLI tools, and anything with no page to load) use the x-deployment-id header, which works in either mode:

Terminal window
curl -H "x-deployment-id: v1.2.3-abc123" https://myapp.example.com/api/data

The deployment ID is the version label itself — the same value that appears as ?dpl= on the application’s asset URLs, as plt.dev/version on the workload, and (in cookie mode) in the __plt_dpl cookie. Read it from any asset URL the application serves, or from the version list in ICC.

To roll back to a version that is still draining, promote it from the ICC version manager. To restore an expired version, redeploy its workload with the same version label. ICC moves it through pending-apply and makes it active only after its route is confirmed.

Terminal window
# Re-deploy version 1.2.3 (which was previously expired)
kubectl apply -f myapp-v1.2.3.yaml

ICC treats this as a reactivation. After the route is confirmed, it becomes the active version and the current version transitions to Draining.

After deploying, verify that ICC detected the new version:

  1. ICC Dashboard — navigate to the Deployments page and check for the new version in the active versions panel
  2. Logs — check ICC logs for version detection messages
  3. Provider route — on Kubernetes, verify that the HTTPRoute was updated:
Terminal window
kubectl get httproute -n platformatic
kubectl describe httproute myapp -n platformatic

On ECS, inspect the application’s host-scoped ALB listener rules and confirm that every live version forwards to its own target group.