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.
Required Labels
Section titled “Required Labels”For a customer-managed Kubernetes deployment, include these labels on the pod template:
| Label | Required | Purpose |
|---|---|---|
app.kubernetes.io/name | Yes | Application identity — must be the same across all versions |
plt.dev/version | Required for query pinning | Explicit version identifier, unique per version |
app.kubernetes.io/instance | Yes (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.
Building for Query Pinning
Section titled “Building for Query Pinning”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_IDRUN npm run builddocker build --build-arg PLT_DEPLOYMENT_ID=v2 -t myapp:v2 .kubectl set image ... # deployed with plt.dev/version: v2The 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.
Optional Labels
Section titled “Optional Labels”| Label | Purpose | Default |
|---|---|---|
plt.dev/path | Custom Kubernetes route prefix | /{appName} (or / when plt.dev/hostname is set) |
plt.dev/hostname | Route hostname (e.g., myapp.example.com); required on ECS | Not set on Kubernetes |
plt.dev/workflow | Set to "true" for apps using Vercel Workflow DevKit with @platformatic/world. Enables workflow-aware draining. | Not set |
Deploying Through ICC
Section titled “Deploying Through ICC”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.
Customer-Managed Kubernetes Deployment
Section titled “Customer-Managed Kubernetes Deployment”Create a Deployment and Service for each version. The key requirements:
- Same
app.kubernetes.io/namelabel across all versions - Unique
plt.dev/versionlabel per version - Unique
app.kubernetes.io/instancelabel 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_URLconfigured to connect to ICC
apiVersion: apps/v1kind: Deploymentmetadata: 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: v1kind: Servicemetadata: 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: 9090Deploying a New Version
Section titled “Deploying a New Version”To deploy a new version of your application:
- Build a new container image with the updated application code (e.g.,
myapp:1.2.5) - Create a new Deployment and Service manifest with the same
app.kubernetes.io/namebut a newplt.dev/version - Apply the manifest:
kubectl apply -f myapp-v1.2.5.yaml- 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.
Customer-Managed ECS Deployment
Section titled “Customer-Managed ECS Deployment”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
9090and path/ready, while user traffic uses port3042 - 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.
Using a Custom Path Prefix on Kubernetes
Section titled “Using a Custom Path Prefix on Kubernetes”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.
Using Hostname-Based Routing
Section titled “Using Hostname-Based Routing”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.
API Clients
Section titled “API Clients”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:
curl -H "x-deployment-id: v1.2.3-abc123" https://myapp.example.com/api/dataThe 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.
Rollback
Section titled “Rollback”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.
# Re-deploy version 1.2.3 (which was previously expired)kubectl apply -f myapp-v1.2.3.yamlICC treats this as a reactivation. After the route is confirmed, it becomes the active version and the current version transitions to Draining.
Verifying Deployment
Section titled “Verifying Deployment”After deploying, verify that ICC detected the new version:
- ICC Dashboard — navigate to the Deployments page and check for the new version in the active versions panel
- Logs — check ICC logs for version detection messages
- Provider route — on Kubernetes, verify that the HTTPRoute was updated:
kubectl get httproute -n platformatickubectl describe httproute myapp -n platformaticOn ECS, inspect the application’s host-scoped ALB listener rules and confirm that every live version forwards to its own target group.