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.
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.
Copy link to headingBefore you start: will your workload move
Most containers move cleanly. Three things may prevent you from moving as-is, so check them first.
Copy link to headingSize
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.
Copy link to headingReachability
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.
Copy link to headingStatefulness
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 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.
Copy link to headingDeploy 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.
Copy link to headingAdd 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.
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 or a push to a connected Git branch.
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.
Copy link to headingPort 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.
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.
Copy link to headingActive CPU bills CPU time, not wall-clock time
Container images bill on 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 |
Copy link to headingDeploy 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.
Copy link to headingDeclare 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.
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 has the full set. experimentalServices is the earlier model, replaced for new projects.
Copy link to headingServices 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.
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.
Copy link to headingConfigure 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.
Copy link to headingAWS 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 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 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 | 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 | Caching, compression, and TLS come with the deployment rather than with a separate distribution. |
| AWS WAF | Vercel Firewall | Rules are configured once at the project level and apply across every service. |
| Route 53 hosted zone | Vercel domains and 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 | 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 and @vercel/otel | Metrics are available on all plans with no agent or sidecar to run. Exporting traces to your existing backend uses Trace 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. |
Copy link to headingKeep 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.
Copy link to headingRegister 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 for Global mode, alternate audiences, and the CLI equivalent.
Copy link to headingScope 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.
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.
Copy link to headingRead 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.
For S3, pass the provider to the client constructor and use the SDK exactly as you do today.
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:
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.
Copy link to headingPin 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.
Copy link to headingDelete 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.
Copy link to headingCompliance, 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.
Copy link to headingCompliance 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 has the current reports and scope for each.
Copy link to headingControl 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.
Copy link to headingEgress 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.
Copy link to headingThree routes when a service isn’t reachable
Pick by which side of the connection you can change.
Copy link to heading1. 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.
Copy link to heading2. 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.
Copy link to heading3. 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.
Copy link to headingMove DNS to Vercel without downtime
Deploy and verify against the deployment URL before you change any DNS record.
Copy link to headingLower 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.
- Open the hosted zone and read the current TTL on the records serving your apex and www. The www subdomain is a
CNAMEwith 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. - Lower the www
CNAMEto 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. - Wait at least as long as the old TTL before you change anything else.
- Leave the lowered records at 60 through cutover, and raise them back once the new records have held for a day.
Copy link to headingDeploy, verify, then move the DNS record
Run the cutover in this order:
- 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.
- Check the domain for a
CAArecord. If one exists and doesn't listletsencrypt.org, Let's Encrypt issuance hard-fails, and the failure surfaces only after the production record has moved. Addletsencrypt.orgbefore you cut over. - 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
Arecord for an apex domain, aCNAMEfor a subdomain. See adding a custom domain for the values. - Point a temporary hostname such as
vercel.example.comat theCNAMEVercel 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. - Change the production record in Route 53. Vercel issues a Let’s Encrypt certificate over the
HTTP-01challenge, 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. - Watch
RequestCountin theAWS/ApplicationELBnamespace 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 inINSUFFICIENT_DATAand never inOK. Treat missing datapoints as the signal, and keep the ECS service running.
Copy link to headingWhat 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.
Copy link to headingRoll 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.
- Change the Route 53 record back to the load balancer.
- Leave the Vercel deployment live while resolvers drain, because anything holding the Vercel answer keeps arriving there for up to the TTL.
- 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.
Copy link to headingTroubleshooting 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 SIGTERMand 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 stderrare 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 |
Copy link to headingWhere 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.vercellike 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 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 in the documentation.
Copy link to headingFAQ
Can Vercel run a container that is not a Next.js app?
Yes. Vercel runs any HTTP server in any language that listens on $PORT, including Go, Rails, Spring Boot, Express, Laravel, ASP.NET, FastAPI, and nginx.
What happens to my AWS bill?
Three things change shape.
Your serving cost moves from Fargate task hours to Active CPU plus provisioned memory plus invocations, and from CloudFront delivery to CDN Requests plus Fast Data Transfer. Which side wins depends on utilization. A service that spends most of its time waiting on RDS or an upstream API pays for a fraction of the hours a same-sized Fargate task bills around the clock, while a service pinned near full CPU is a different calculation.
Your AWS services keep their rates. Compute leaving the VPC turns reads that were free intra-AZ, or free through an S3 gateway endpoint, into data transfer out at your published rate. Include that line in your estimate.
The third shift is in your favor. Vercel does not charge for traffic that DDoS mitigation blocks or that the Vercel WAF denies, challenges, or rate-limits, waiving both CDN Requests and Fast Data Transfer. You still pay for requests served before mitigation engages, and for bot or crawler traffic that isn't classified as a DDoS event.