---
title: Migrate self-hosted Next.js and containers from AWS to Vercel
description: "Migrate containers from AWS to Vercel: deploy with Dockerfile.vercel, keep RDS, S3, and SQS in AWS over OIDC, and cut over DNS with no downtime."
url: /kb/guide/migrate-containers-from-aws-to-vercel
canonical_url: "https://vercel.com/kb/guide/migrate-containers-from-aws-to-vercel"
published: 2026-08-04
last_updated: 2026-08-04
authors: Vercel
related:
  - /docs/functions
  - /docs/workflows
  - /docs/cli
  - /docs/git/vercel-for-github
  - /docs/environment-variables
  - /docs/functions/usage-and-pricing
  - /docs/services/config-reference
  - /docs/cron-jobs
  - /docs/regions
  - /docs/vercel-firewall
  - /docs/domains
  - /docs/domains/working-with-dns
  - /docs/logs/runtime
  - /docs/observability
  - /docs/tracing/instrumentation
  - /docs/drains
  - /docs/oidc
  - /docs/domains/working-with-domains/add-a-domain
  - /docs/functions/container-images
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

This guide walks through moving a self-hosted Next.js app and the containers beside it from AWS to Vercel, whether you run them today on a managed container service or on EC2 behind a load balancer.

Vercel runs the Next.js app the way it runs any Next.js app, from your repo, with no Dockerfile. For the container-shaped pieces, the workers and any non-Node services, you add a `Dockerfile.vercel` to your project root and Vercel builds the image and runs it on [Vercel Functions](https://vercel.com/docs/functions).

Your data stays in AWS. RDS, S3, SQS, DynamoDB, and Cognito remain in your own account, reached by assuming an IAM role over OpenID Connect (OIDC) instead of a stored access key. What moves is the compute: the container scheduling, load balancer rules, health checks, and CDN config in front of your code. You run on Vercel, keep your AWS backend, and store no long-lived credentials.

## Before you start: will your workload move

Most containers move cleanly. Three things may prevent you from moving as-is, so check them first.

### Size

Most tasks will fit. Vercel Functions cap at 4 GB and 2 vCPU on Pro and Enterprise, so a task sized at 4 or 8 vCPU is the exception, and splitting it across concurrent invocations is a rewrite. The image has limits too: 15 GB total and 500 MB per compressed layer, so a heavy ML or Chromium layer can fail at push.

### Reachability

A Vercel function reaches AWS across the public internet, rather than from inside your VPC. S3, SQS, DynamoDB, and Cognito expose public API endpoints a Vercel function can reach, unless a bucket policy, VPC endpoint policy, or SCP restricts access by source VPC or IP.

RDS works only when it is genuinely publicly reachable, which a production instance in private subnets is not without real network changes: public subnets with an internet gateway route, the VPC's DNS hostnames and DNS resolution attributes, and a security group rule, on top of `PubliclyAccessible`. If exposing it isn't an option, use route 2 or 3 below. ElastiCache has no public endpoint, so a container function can’t reach it; keeping that service on non-container Functions with Secure Compute is one of three routes in the security section. Anything behind an IP allowlist is unreachable from a container, which has no static egress IP as of the publish date, and the same three routes apply.

### Statefulness

Containers on Vercel are stateless, and there is no durable disk right now. A workload that holds state in memory for hours, runs always-on (like a queue consumer polling continuously), or writes to a local volume should stay on ECS. For long-running orchestration that waits on approvals or external systems, [Vercel Workflows](https://vercel.com/docs/workflows) pauses and resumes between steps with no duration cap.

One route-level limit an ALB doesn't have: Vercel Functions cap the request and response body at 4.5 MB, so any upload route above that needs a presigned, direct-to-S3 upload.

## Deploy your existing container with `Dockerfile.vercel`

Your Next.js app deploys the way any Next.js app deploys to Vercel: connect the repo and framework detection builds and runs it, with no Dockerfile. Everything in this section is for the container-shaped services beside it, the workers and non-Node backends that already ship as images. A Next.js app wrapped in a custom server is the one exception, and it moves as a container like the rest.

### Add `Dockerfile.vercel` and deploy

Add a file named Dockerfile.vercel to your project root and deploy. Vercel detects it, builds the image, pushes it to VCR, and adds a rewrite routing all traffic to the container.

`FROM node:lts-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . # The server must listen on $PORT CMD ["node", "server.js"]` The image definition does not change. This is the same Dockerfile your ECS task builds from, with the same base images, layers, and entrypoint. What changes is the builder and the registry, because Vercel runs the build and pushes to VCR instead of your CI pushing to Amazon ECR. Deploy the way you already do, with the [Vercel CLI](https://vercel.com/docs/cli) or a push to a connected [Git branch](https://vercel.com/docs/git/vercel-for-github).

`vercel deploy`

Images live at `vcr.vercel.com/<team-slug>/<project-slug>/<repository>:<tag>`, and the build pushes there for you. To push by hand, run `vercel vcr login docker` in a linked project, which mints a project-scoped OIDC token valid for 12 hours. Local runs go through `vercel dev`, which needs the Docker CLI and daemon on the machine.

### Port binding, scale-to-zero, and shutdown behavior for Vercel containers

Your server has to listen on `$PORT`, which defaults to `80` and is overridden by setting `PORT` in project [environment variables](https://vercel.com/docs/environment-variables).

Instances scale down after 5 minutes without traffic in production and 30 seconds in preview. On the way down the container receives a `SIGTERM` with a 30 second grace period before it is forcefully terminated, so finish in-flight work inside that handler.

### Active CPU bills CPU time, not wall-clock time

Container images bill on [Active CPU](https://vercel.com/docs/functions/usage-and-pricing#active-cpu), the same model as every other Vercel Function. You are charged per CPU-hour only while your code runs, so billing pauses while a request waits on an RDS query, an S3 read, an upstream API, or model inference, and nothing is charged between requests. Provisioned memory bills separately per GB-hour, but only while the instance is handling requests, from the first request until the last in-flight one finishes. Once all requests complete the instance is paused and neither CPU nor memory is billed, even during the few minutes it stays warm for the next request.

| Item               | Rate                           |
| ------------------ | ------------------------------ |
| Active CPU         | $0.128 to $0.221 per CPU-hour  |
| Provisioned memory | $0.0106 to $0.0183 per GB-hour |
| Invocations on Pro | $0.60 per million              |
| VCR storage        | $0.10 per GB                   |

## Deploy multiple containers in one project with services and bindings

A single Vercel project runs several containers side by side, each declared as a service in `vercel.json`. What runs today as separate ECS services behind separate ALB target groups becomes one deployment, promoted as one unit.

One project deploys and rolls back as a unit, so a change to any service ships all of them together. That is the tradeoff for a shared route table, private service-to-service bindings, and one firewall and domain config for everything. If a service needs its own deploy and rollback cadence, keep it in its own project.

### Declare each container as a service in `vercel.json`

Each service gets a `root` directory and an `entrypoint` pointing at its Dockerfile, relative to that root. The services do not have to share a language or a framework.

`{ "services": { "web": { "root": "apps/web/", "entrypoint": "Dockerfile.vercel" }, "worker": { "root": "services/worker/", "entrypoint": "Dockerfile.vercel" }, "search": { "root": "services/search/", "entrypoint": "Dockerfile.vercel" } }, "rewrites": [{ "source": "/(.*)", "destination": { "service": "web" } } ] }` Once `services` is present, the build and runtime keys move into each service, because their owner would otherwise be ambiguous. That covers `functions`, `buildCommand`, `installCommand`, `devCommand`, `ignoreCommand`, `outputDirectory`, and `framework`. The [service configuration reference](https://vercel.com/docs/services/config-reference) has the full set. `experimentalServices` is the earlier model, replaced for new projects.

### Services are internal by default until a top-level rewrite exposes them

A service with no rewrite pointing at it receives no public traffic. In the config above, `worker` and `search` are unreachable from the internet, and exposing either takes an explicit top-level rewrite.

For service-to-service calls, the caller declares a binding. Vercel generates the target’s URL and injects it as an environment variable, and the target stays internet-unreachable.

`{ "services": { "web": { "root": "apps/web/", "entrypoint": "Dockerfile.vercel", "bindings": [{ "type": "service", "service": "search", "format": "url", "env": "SEARCH_URL" } ] } } }` `const res = await fetch(new URL('search?q=shoes', process.env.SEARCH_URL))` The injected value is deployment-aware, so a preview deployment’s `web` reaches that same preview’s `search` and never a fixed hostname. Bindings resolve at runtime only, not during builds, and code in middleware cannot call a service over one. ### Configure Vercel Firewall, Deployment Protection, and redirects once for the whole project Every public request enters through the top-level route table, so Vercel Firewall rules, Deployment Protection, and redirects are configured once for all services rather than per listener rule and per target group. Internal calls over a binding skip that pipeline, so a binding grants reachability but does not authenticate the caller. Authorization between your own services stays your code’s job. ## AWS to Vercel concept mapping for compute, networking, data, and observability This table maps every AWS concept in a container migration to its Vercel equivalent, or notes where none exists. | AWS concept                                            | Vercel equivalent                                                                                                                 | Migration note                                                                                                                                                                                        | | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ECS or Fargate task                                    | [Vercel Functions](https://vercel.com/docs/functions) running your container image                                                | Same Open Container Initiative (OCI) image definition, rebuilt by Vercel from `Dockerfile.vercel`. Scaling is automatic in both directions with no capacity target to set.                            |
| Task definition CPU, memory, and environment variables | Project settings                                                                                                                  | Defaults to 2 GB and 1 vCPU. Pro and Enterprise configure it in the dashboard up to 4 GB and 2 vCPU.                                                                                                  |
| ECS service behind an ALB target group                 | A [service](https://vercel.com/docs/services/config-reference) in `vercel.json`                                                   | Several containers deploy together in one project on one domain. Each service is internal until a top-level rewrite exposes it.                                                                       |
| ALB listener rules and path-based routing              | Top-level `rewrites` in `vercel.json`                                                                                             | Path routing consolidates into one route table for the whole project.                                                                                                                                 |
| ECS scheduled task                                     | [Cron Jobs](https://vercel.com/docs/cron-jobs)                                                                                    | Declared with the `crons` key. Vercel sends an HTTP GET to a path on your production deployment. Schedules are UTC only.                                                                              |
| CloudFront distribution and cache behaviors            | [Vercel’s global network](https://vercel.com/docs/regions)                                                                        | Caching, compression, and TLS come with the deployment rather than with a separate distribution.                                                                                                      |
| AWS WAF                                                | [Vercel Firewall](https://vercel.com/docs/vercel-firewall)                                                                        | Rules are configured once at the project level and apply across every service.                                                                                                                        |
| Route 53 hosted zone                                   | [Vercel domains](https://vercel.com/docs/domains) and [DNS](https://vercel.com/docs/domains/working-with-dns)                     | Point an existing record at Vercel or delegate the zone. Lower the TTL before you change anything.                                                                                                    |
| NAT gateway with an Elastic IP for static egress       | Not supported on the container path                                                                                               | Neither works with container images. On a non-container Function, Static IPs (Pro) gives a fixed egress IP and Secure Compute (Enterprise) gives private VPC connectivity.                            |
| Amazon RDS                                             | Stays in AWS                                                                                                                      | Reached with an IAM auth token over OIDC federation, and only when the instance is genuinely publicly reachable (route 1 has the four conditions).                                                    |
| Amazon S3, SQS, DynamoDB, and Cognito                  | Stays in AWS                                                                                                                      | Public API endpoints, same SDK, same buckets, queues, tables, and user pools, unless a resource policy or SCP scopes them to a source VPC or IP. Credentials come from an assumed role.               |
| Amazon ElastiCache                                     | Stays in AWS, unreachable from a container function                                                                               | No public endpoint exists, so this service does not clear the reachability gate.                                                                                                                      |
| EFS volume mounted into the task                       | Not supported on the container path                                                                                               | Containers on Vercel are stateless, and persistent state lives in an attached backing service.                                                                                                        |
| IAM task role, and access keys in the task definition  | An IAM role assumed over OIDC                                                                                                     | The role model is unchanged. The trust policy moves from the ECS task principal to the Vercel OIDC provider, and the stored keys have nothing left to authenticate.                                   |
| CloudWatch Logs                                        | Vercel [runtime logs](https://vercel.com/docs/logs/runtime)                                                                       | Container `stdout` and `stderr` are broadcast to all inflight requests on the instance rather than tied to one request, so per-request correlation needs request-scoped logging.                      |
| CloudWatch Metrics, Container Insights, and X-Ray      | [Vercel Observability](https://vercel.com/docs/observability) and [@vercel/otel](https://vercel.com/docs/tracing/instrumentation) | Metrics are available on all plans with no agent or sidecar to run. Exporting traces to your existing backend uses [Trace Drains](https://vercel.com/docs/drains), on Pro and Enterprise and metered. |
| CodePipeline, CodeBuild, and ECS blue/green deployment | Git-based deployments with preview URLs and instant rollback                                                                      | Every push builds and deploys. Verify on the preview URL, promote to production, and roll back by re-pointing production at a previous deployment.                                                    |

## Keep RDS, S3, and SQS in AWS with OIDC federation

Your container reaches AWS by assuming an IAM role over OIDC, with no stored access key. The trust policy you write is the authorization boundary for this whole section: the sub claim in a Vercel OIDC token encodes team, project, and environment as one string, so a policy written too loosely lets every preview deployment on your team assume a role that reads production data.

### Register Vercel as an OpenID Connect identity provider in IAM

In the AWS console, open IAM, then Identity Providers, then Add Provider, and select OpenID Connect. The provider URL is `https://oidc.vercel.com/[TEAM_SLUG]` in Team mode and `https://oidc.vercel.com` in Global mode. Enter `https://vercel.com/[TEAM_SLUG]` in the Audience field. See the [OIDC federation docs](https://vercel.com/docs/oidc) for Global mode, alternate audiences, and the CLI equivalent.

### Scope the trust policy to one project and one environment

The trust policy names who may assume the role. The action is `sts:AssumeRoleWithWebIdentity`, and the condition keys narrow it to one project and one environment.

`{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::[AWS_ACCOUNT_ID]:oidc-provider/oidc.vercel.com/[TEAM_SLUG]" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.vercel.com/[TEAM_SLUG]:sub": "owner:[TEAM_SLUG]:project:[PROJECT_NAME]:environment:production", "oidc.vercel.com/[TEAM_SLUG]:aud": "https://vercel.com/[TEAM_SLUG]" } } } ] }` This admits production deployments of one project and nothing else. Previews need a second value, and this is where the danger is. A `StringLike` matching `project:*:environment:preview` covers previews across every project on your team, so anyone who can push a branch gets credential-bearing execution against this role. Scope it to the project instead, with `owner:[TEAM_SLUG]:project:[PROJECT_NAME]:environment:preview` under `StringEquals`, and give previews a separate read-only role if they need production data at all. Development is excluded because neither value matches it. ### Read from S3 and connect to RDS with `awsCredentialsProvider` The `awsCredentialsProvider` function from `@vercel/oidc-aws-credentials-provider` hands the AWS SDK a credential source backed by the OIDC token instead of a key pair. Install it with the SDK clients you need. `npm i @aws-sdk/client-s3 @aws-sdk/rds-signer pg @vercel/oidc-aws-credentials-provider` For S3, pass the provider to the client constructor and use the SDK exactly as you do today. `import * as S3 from '@aws-sdk/client-s3'; import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider'; const s3 = new S3.S3Client({ region: process.env.AWS_REGION!, credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN!, }), }); export async function GET() { const result = await s3.send( new S3.ListObjectsV2Command({ Bucket: process.env.S3_BUCKET_NAME! }), ); return Response.json(result.Contents?.map((object) => object.Key) ?? []); }` RDS takes three prerequisites, shown here for PostgreSQL: turn on IAM database authentication for the instance (`--enable-iam-database-authentication`), grant the account IAM authentication so it accepts a token in place of a password (`GRANT rds_iam TO [RDS_USERNAME]` on PostgreSQL; MySQL and MariaDB instead create the user with the AWS authentication plugin), and pass an `ssl` option in your client, because IAM database authentication requires a TLS connection. Pass the same `awsCredentialsProvider` to `@aws-sdk/rds-signer`, and give `pg` a `password` function that calls the signer's `getAuthToken`, so a fresh, short-lived token is minted per connection: `const pool = new Pool({ // host, port, user, database as usual password: () => signer.getAuthToken(), // fresh IAM token per connection, not a static value ssl: { ca: process.env.RDS_CA_BUNDLE, rejectUnauthorized: true }, // IAM auth requires TLS });` One RDS trap to be aware of is the size of the connection pool against the database, not the instance. Fluid compute runs many concurrent requests on each instance and adds instances as traffic grows, and every live instance opens its own `pg` pool, which defaults to 10 connections. The connections your database sees are that pool size times the number of live instances, so a scale-out can push the total past `max_connections`. Read `max_connections` for your instance class and set the pool `max` low enough that a burst of instances cannot exhaust it. The real per-instance ceiling is the 1,024 file descriptors shared across all concurrent executions, not a request count. RDS Proxy does not solve this here, its endpoint is VPC-only. ### Pin `AWS_REGION` before your first production deploy Vercel sets `AWS_REGION` to the region the function executes in, so with multi-region routing or failover an unpinned SDK client will address a bucket, database, or queue in a region where none exist, surfacing as a resource-not-found error mid-failover. Set `AWS_REGION` explicitly to the region your AWS resources live in, and set the `regions` key in vercel.json to co-locate your functions. New projects default to `iad1` (Washington, D.C.), co-located with `us-east-1` and a transatlantic round trip from `eu-west-1`. ### Delete your access keys Two ways to handle secrets from Secrets Manager or SSM: copy values into Vercel environment variables (simplest, but the secret now lives in two places and every rotation updates both), or fetch them at runtime through the same assumed role (AWS stays the source of truth, at the cost of a call you cache per instance). Either way, no long-lived AWS access key remains in this path. The image push uses a short-lived OIDC token, and the running container reaches RDS, S3, and SQS with credentials STS issues on demand against your trust policy. The access keys in your task definition have nothing left to authenticate, so deleting them is part of the migration, not a follow-up. ## Compliance, data residency, and egress An enterprise security review comes down to three things: what Vercel is attested for, where your functions run, and whether egress IP can be pinned. ### Compliance and audit posture Vercel holds SOC 2 Type 2, ISO 27001:2022, PCI DSS v4.0, HIPAA (BAAs for eligible Pro and Enterprise customers), GDPR, the EU-U.S. Data Privacy Framework, and TISAX AL2. The [Vercel Trust Center](https://security.vercel.com/) has the current reports and scope for each.

### Control which region your functions run in

Set the `regions key` in vercel.json (or Function Regions in project settings) to pin where functions execute, and the functions key to override per route. European regions include Paris (`cdg1`), Dublin (`dub1`), Frankfurt (`fra1`), and London (`lhr1`). Hobby is single-region, Pro allows 5, and Enterprise allows all.

Vercel deploys Routing Middleware to every region regardless of your region settings, so logic that touches regulated data belongs in a function, not in middleware.

### Egress IP cannot be pinned on the container path

A container on Vercel has no static egress IP. Secure Compute and Static IPs, the products that provide one, don’t support custom container images. If a backing service requires a fixed egress IP or an allowlist, use one of the three routes below.

## Three routes when a service isn’t reachable

Pick by which side of the connection you can change.

### 1\. Give the service a public endpoint and authenticate with IAM.

For RDS, public access is four conditions, not one: set `PubliclyAccessible` to true, place the instance in a DB subnet group whose subnets are public with a route to an internet gateway, enable the VPC's DNS hostnames and DNS resolution attributes, and open the security group. And because a container function has no static egress IP, and Vercel egress can come from any IP, the only security-group rule that reliably admits it is the database port open to `0.0.0.0/0`, the entire internet. You then replace the IP allowlist with IAM database authentication over OIDC, which scopes access per project and environment with credentials that expire on their own, so IAM and TLS, not network position, are the only controls between an attacker and your database.

That is the line a security review turns on: a production RDS in private subnets meets none of the network conditions as-is, and getting there means opening the port to the world. If that exposure isn't acceptable, it is exactly what Static IPs (route 2) and keeping the service on ECS (route 3) avoid. Doesn't apply to ElastiCache, which has no public endpoint to expose.

### 2\. Keep that service on non-container Functions.

Code on a standard runtime (Node.js, Ruby, or Python, not Edge) can move without the Dockerfile, which unlocks the two egress options container images can't use, and the right one depends on your plan. For an IP allowlist, Static IPs gives the Function a fixed egress IP to add to the list, available on Pro at $100 per month per project. For private connectivity into your VPC, Secure Compute peers with your VPC and is an Enterprise feature. Either way, a system library or unsupported language baked into the image makes dropping the Dockerfile a rewrite, not a config change. If you're on Pro and the service needs private-VPC access rather than an allowlist, Secure Compute isn't available to you, so route 3 is the move.

### 3\. Keep that service on ECS.

The rest of the app moves and this one stays. You keep operating one task definition, pipeline, and bill instead of all of them, and this is the right answer when the allowlist isn’t yours to change.

## Move DNS to Vercel without downtime

Deploy and verify against the deployment URL before you change any DNS record.

### Lower your Route 53 TTL 24 to 48 hours before cutover

A record’s TTL is how many seconds resolvers cache it before asking again, which also governs how long a stale answer survives a change. Lower it before cutover so a rollback propagates fast.

1. Open the hosted zone and read the current TTL on the records serving your apex and www. The www subdomain is a `CNAME` with an editable TTL; the apex, if it's an ALIAS to your ALB, has no TTL field, because Route 53 answers it with the ALB's own 60-second TTL.
   
2. Lower the www `CNAME` to 60 seconds and save. The apex alias already resolves at 60 seconds, so it needs no change and is not the slow record; the window you care about comes from any non-alias records you just lowered.
   
3. Wait at least as long as the old TTL before you change anything else.
   
4. Leave the lowered records at 60 through cutover, and raise them back once the new records have held for a day.
   

### Deploy, verify, then move the DNS record

Run the cutover in this order:

1. Deploy and exercise the deployment URL directly. Request a real route, sign in, and read from RDS through the assumed role, so the credential path is proven before it carries traffic.
   
2. Check the domain for a `CAA` record. If one exists and doesn't list `letsencrypt.org`, Let's Encrypt issuance hard-fails, and the failure surfaces only after the production record has moved. Add `letsencrypt.org` before you cut over.
   
3. Add your domain in the project’s Domains settings, and add both the apex and www if you serve both, since Vercel does not add the second for you. Vercel shows the record it expects: an `A` record for an apex domain, a `CNAME` for a subdomain. See [adding a custom domain](https://vercel.com/docs/domains/working-with-domains/add-a-domain) for the values.
   
4. Point a temporary hostname such as `vercel.example.com` at the `CNAME` Vercel gives you. It resolves immediately, gets its own certificate, and lets you test the production build over HTTPS while the live record still points at the ALB.
   
5. Change the production record in Route 53. Vercel issues a Let’s Encrypt certificate over the `HTTP-01` challenge, which can only succeed once that record resolves to Vercel, so there is a gap between the record changing and the certificate issuing. A 60 second TTL does not shorten that gap. It moves resolvers onto the new answer faster, which puts more traffic inside the gap, not less. What the low TTL buys is the same speed in reverse if you have to back out. Enterprise teams can upload a custom certificate ahead of the switch and remove the gap entirely.
   
6. Watch `RequestCount` in the `AWS/ApplicationELB` namespace while your Vercel runtime logs fill. The metric stops producing datapoints rather than reporting zero once traffic stops arriving, so an alarm built on it sits in `INSUFFICIENT_DATA` and never in `OK`. Treat missing datapoints as the signal, and keep the ECS service running.
   

### What changes about your logs

Vercel broadcasts container stdout and stderr to all inflight requests on the instance rather than tying them to the request that produced them, so per-request correlation has to come from your own code, by threading a request ID through a request-scoped logger.

Check retention before cutover. Runtime logs are held for 1 day on Pro and 3 days on Enterprise. If your CloudWatch retention is longer, configure a log drain to the sink you already use, available on Pro and Enterprise and billed by volume.

### Roll back by reverting DNS, and keep AWS alive for a full TTL

Rolling back is one record change back to the ALB, and it only works if the AWS side is still there to receive it.

1. Change the Route 53 record back to the load balancer.
   
2. Leave the Vercel deployment live while resolvers drain, because anything holding the Vercel answer keeps arriving there for up to the TTL.
   
3. Do not scale the ECS service to zero, deregister the target group, or delete the ALB until a full TTL has elapsed past cutover and RequestCount has produced no datapoints for a day.
   

Keep the ECS service healthy after a successful cutover, not only a failed one. Resolvers that cached the ALB answer before the switch keep arriving at AWS for the length of the old TTL either way.

## Troubleshooting common container migration failures

Start in the Logs tab, match the symptom, then apply the fix.

| Symptom                                         | Likely cause                                                                                                                                                                                                              | Fix                                                                                                                                                                                                     |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Build succeeds, requests time out               | The server is not listening on `$PORT`                                                                                                                                                                                    | Bind to `$PORT`, which defaults to 80                                                                                                                                                                   |
| Container killed mid-request                    | The request passed the function duration limit, or cleanup exceeded the `SIGTERM` grace period                                                                                                                            | Handle `SIGTERM`and finish inside 30 seconds, and check the request against your duration limit                                                                                                         |
| AWS SDK returns `AccessDenied`                  | The trust policy `sub` condition or the audience does not match the token Vercel issues                                                                                                                                   | Re-check the `sub` scoping and audience against the identity provider you registered in IAM                                                                                                             |
| RDS connection is refused or hangs              | The instance is not actually publicly reachable (private DB subnet group, no internet gateway route, or the VPC's DNS attributes are off), or the connection wasn't over TLS, which IAM database authentication requires. | Confirm all four public-access conditions (`PubliclyAccessible`, public subnets with an IGW route, VPC DNS hostnames and resolution, security group), then pass the `ssl` option with the RDS CA bundle |
| AWS calls reach the wrong region                | `AWS_REGION` defaults to the function’s execution region                                                                                                                                                                  | Pin `AWS_REGION` as a project environment variable                                                                                                                                                      |
| A service returns 404 from the internet         | Services are internal by default                                                                                                                                                                                          | Add a top-level rewrite whose destination is `{ "service": "name" }`                                                                                                                                    |
| Log lines interleave across concurrent requests | Container `stdout` and `stderr`are broadcast to all inflight requests on the instance                                                                                                                                     | Thread a request ID through a request-scoped logger                                                                                                                                                     |
| First request after an idle period is slow      | The function scaled to zero after 5 minutes in production or 30 seconds in preview                                                                                                                                        | Expect a boot. Vercel streams the compressed boot image on demand and Fluid compute keeps instances warm                                                                                                |
| Image push to VCR is rejected                   | The project-scoped OIDC token expired after 12 hours                                                                                                                                                                      | Run `vercel vcr login docker` again                                                                                                                                                                     |

## Where Vercel is not the right home for an AWS workload

Four workload types typically have a better destination than the container path:

- OpenNext or SST on Lambda and CloudFront. The artifact is a Lambda bundle, not a container, and the mapping needs its own guide.
  
- AWS Amplify Hosting. A different build and routing model, with its own guide.
  
- Kubernetes you use as Kubernetes. If your EKS workload is just a container being served, it’s covered, take the same image through `Dockerfile.vercel` like any other. What has no equivalent is Kubernetes itself: operators, custom resources, service meshes, and cluster-level scheduling. Moving those is a separate migration, not this one.
  
- Always-on stateful processes. A process that must stay running and hold state in memory when no request is arriving, like a queue consumer that polls continuously or a daemon holding a long-lived connection, belongs on ECS. But if the work is long because it waits, on approvals, external systems, or human review, that’s durable orchestration, and [Vercel Workflows](https://vercel.com/docs/workflows) handles it with no duration cap.
  

The container path is currently in beta. Check the container images documentation for current numbers before you plan a cutover. Otherwise, running a container on Vercel Functions is self-serve: no plugin to install, no allowlist to join. Add `Dockerfile.vercel` to your project root, make your server listen on $PORT, and deploy.

Learn more about [containers](https://vercel.com/docs/functions/container-images) in the documentation.