Deploy Docker Containers to Vercel

Deploy a Node.js API and nginx frontend to Vercel from Dockerfiles.

Introduction

30 Second Summary

A small web service can work perfectly on your computer. Putting it online often adds an entire layer of infrastructure before anyone else can use it.

In this project, you will deploy a small Node.js server to Vercel from a plain Dockerfile. You will then split the app into separate frontend and API containers that receive traffic through rules in vercel.json.

What You'll Build

Open your finished Vercel URL to see an nginx page at the root while /api returns your caller IP as JSON from a separate Node.js container.

By the end of this project, you'll have:

  • A live container URL that returns your IP when you visit /api.
  • Visible build and runtime logs that let you follow the deployment process and trace requests through your container's output.
  • A two-container app that serves an nginx page at the root while forwarding /api requests to Node.js.
  • An optional Secret Mission to push your skills further.

Are there any prerequisites?

You need a free Vercel account plus Node.js and a terminal on your computer. Docker stays off your local machine because Vercel builds the image during deployment.

Before We Start

Your first deployment starts from the terminal. The Vercel CLI needs an authenticated account plus Node.js before it can send your project to the cloud.

Vercel builds the container image from your Dockerfile in the cloud. Docker is not required on your machine for this workflow.

In this step, get ready to:
  • Install Node.js plus its bundled npm package manager.
  • Prepare your Vercel account for CLI authentication.
  • Create an empty docker-vercel-app directory on your Desktop.
Install Node.js and npm

Node.js runs the JavaScript tools used by this project. Its installer also includes npm, which installs the Vercel CLI.

  • Press Cmd+Space on macOS or the Windows key on Windows to open system search.
  • Type Terminal on macOS or PowerShell on Windows.
  • Press Enter to open the terminal.
  • Check for Node.js 18 or newer plus npm by running these commands:
node --version
npm --version

What do these commands check?

  • The first command prints your installed Node.js version.
  • The second command prints your installed npm version.

✔️ I see a supported version

Node.js and npm are ready for the Vercel CLI.

  • Continue to the Vercel account setup below.

ⓧ I see an older version

The installer file is now available in your downloads folder.

  • Run the downloaded installer from your downloads folder.
  • Complete the installer prompts using the default options.

The installer updates Node.js plus npm on your machine.

  • Restart your terminal through your operating system's search.
  • Confirm the updated versions by running these commands:
node --version
npm --version

What should you see?

You should see a Node.js version beginning with v18 or a higher number. You should also see an npm version on the next line.

ⓧ Command not found

The installer file is now available in your downloads folder.

  • Run the downloaded installer from your downloads folder.
  • Complete the installer prompts using the default options.

The installer adds Node.js plus npm to your machine.

  • Restart your terminal through your operating system's search.
  • Confirm both tools are available by running these commands:
node --version
npm --version

What should you see?

You should see a Node.js version beginning with v18 or a higher number. You should also see an npm version on the next line.

Still missing Node.js or npm?

  • Restart your computer if a fresh terminal still cannot find the installed tools.
  • Run the Node.js installer again if the installation closed before reaching its completion screen.

Still stuck? Help me troubleshoot my Node.js and npm installation.

Create your Vercel account and authenticate the CLI

Your Vercel account owns the projects you deploy. The CLI stores an authenticated session so future deployment commands can use that account.

  • Visit the official Vercel sign-up page.
  • Choose your preferred sign-in method.
  • Complete the browser prompts until your Vercel dashboard loads.

The global CLI install may take a minute while npm downloads the package. Its commands become available from any project directory.

  • Install the latest Vercel CLI globally by running:
npm install --global vercel@latest

What does this command do?

The command downloads the latest Vercel CLI through npm. The global option makes the vercel command available throughout your terminal.

  • Verify the CLI installation by running:
vercel --version

What should you see?

You should see a Vercel CLI version number. That output confirms the command is installed.

The sign-in command starts a browser-based authentication flow. Check the request details before approving access to your account.

  • Start the Vercel CLI sign-in flow by running:
vercel login

What happens during sign-in?

The CLI guides you to a browser where you approve the login request. Your terminal receives an authenticated session after approval.

  • Follow the authentication flow shown in your browser.
  • Approve the login request after checking its details.
  • Return to your terminal after the browser confirms authentication.

Before you check, which Vercel username do you expect the CLI to report?

  • Confirm the signed-in account by running:
vercel whoami

What should you see?

You should see your Vercel username in the terminal. That username proves the CLI can access your account.

Your account and terminal are connected. The CLI is ready to create deployments under your Vercel username.

Having trouble with the Vercel CLI?

  • Restart your terminal if the installed vercel command is unavailable.
  • Repeat the login flow if the browser confirmation finished but the account check reports no authenticated user.
  • Check that the browser is using the same Vercel account you created earlier.

Still stuck? Help me troubleshoot my Vercel CLI installation or login.

Create the empty project directory

Your Dockerfile plus server code need one named directory that the CLI can deploy later. Starting with an empty directory keeps the first build focused.

  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The command changes your terminal location to the Desktop. Your new project directory will appear there.

  • Create the docker-vercel-app directory by running these commands:
mkdir docker-vercel-app
cd docker-vercel-app

What do these commands do?

  • The first command creates the docker-vercel-app directory on your Desktop.
  • The second command moves your terminal into that directory.

Before you check, what directory name do you expect at the end of the printed path?

  • Verify your location plus the empty directory by running:
pwd
ls

What should you see?

You should see a path ending in docker-vercel-app. You should see no file names below it because the directory is empty.

That's the groundwork done. Your Vercel account is authenticated from an empty project directory on your Desktop.

Next up, you'll write the Node.js server plus the Dockerfile that Vercel builds into your first container.

Create Your Node Container

A live container needs an application to handle requests. It also needs a build recipe that describes its runtime.

In this step, you'll create a Node.js server that returns the caller's IP address. You'll also write a Docker build file that Vercel can turn into a runnable image.

In this step, get ready to:
  • Create a Node.js server that returns the caller's IP address as JSON.
  • Write a root Dockerfile that packages the server.
  • Review every Dockerfile instruction and verify both files.

Why Build Without Docker?

A Dockerfile is a text recipe for building a container image. Vercel reads this recipe during deployment.

Vercel performs the image build in its own infrastructure. Your local machine only needs the two source files you create here.

Write the IP server

The HTTP server receives each request. It selects the forwarded client address when one is available.

Local requests fall back to the socket address. The response exposes the selected value as JSON.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type the name of your code editor and press Enter to open it.

You'll see your code editor window. The next actions connect it to the existing project directory.

  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the existing docker-vercel-app directory.
  • Click Open to load the directory.

Your editor's file sidebar should show the empty docker-vercel-app directory. This is where both container files belong.

  • Create server.ts inside docker-vercel-app by copying this code into a new file:
import { createServer } from "node:http";

// Keep the container's listening port in one place
const port = 3000;

// Handle each request and return the caller's IP address
const server = createServer((request, response) => {
  console.log(`${request.method} ${request.url}`);

  // Prefer the address forwarded by Vercel, then use the socket address
  const forwardedFor = request.headers["x-forwarded-for"];
  const ip =
    typeof forwardedFor === "string"
      ? forwardedFor.split(",")[0].trim()
      : request.socket.remoteAddress ?? "unknown";

  // Send the selected IP address as JSON
  response.writeHead(200, { "content-type": "application/json" });
  response.end(JSON.stringify({ ip }));
});

// Listen on every interface so container traffic can reach the server
server.listen(port, "0.0.0.0", () => {
  console.log(`Server listening on port ${port}`);
});

What Does This Code Do?

  • The createServer() function creates the HTTP server that handles incoming requests.
  • The console.log() call writes each request method and path to stdout.
  • The x-forwarded-for header carries the original client address when a request passes through Vercel.
  • The remoteAddress fallback supplies an address when you test the server locally.
  • The response contains an ip property that you can inspect in a browser.
  • Save server.ts.
  • Switch back to the terminal already open in docker-vercel-app.
  • Expect npm to ask for permission if it needs to download tsx for this test. Approve the download when prompted.
  • Start the server locally by running:
npx tsx server.ts

What Does This Command Do?

The npx tsx server.ts command uses the TypeScript runtime to execute server.ts directly. The running server keeps this terminal occupied until you stop it.

You'll see Server listening on port 3000 in the terminal. This confirms that the server has started.

Server Fails to Start?

  • Check that the terminal path ends with docker-vercel-app.
  • Confirm that the file is named server.ts with the same capitalization.
  • Check the line number shown by the terminal against server.ts for a missing quote or bracket.

Still stuck? Help me debug why npx tsx server.ts does not start my server on port 3000.

Before you send a request, which source do you expect the server to use for your local IP address? Hold that prediction in mind.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type the name of your browser and press Enter to open it.
  • Enter http://localhost:3000 in the address bar.

You'll see a JSON object containing an ip property. The local value comes from the request socket because no forwarded header was supplied.

  • Return to the terminal running the server.

You'll see a request line containing GET /. That log entry proves incoming requests reach stdout.

Your first working slice is complete. The server accepts a request, records it in the terminal, and returns the caller's address.

Create the Dockerfile

The root Dockerfile defines the environment around server.ts. Its instructions add the TypeScript runtime and declare how the container starts.

  • Return to your code editor.
  • Create a new file named Dockerfile inside docker-vercel-app.
  • Build the container recipe by copying this code into Dockerfile:
# Use a compact Node.js runtime as the image foundation
FROM node:26-alpine

# Keep the application files together inside the image
WORKDIR /app

# Install the runtime that executes the TypeScript server
RUN npm install tsx

# Add the server source to the image
COPY server.ts .

# Document the port used by the HTTP server
EXPOSE 3000

# Start the server when the container launches
CMD ["npx", "tsx", "server.ts"]

Read the Dockerfile Line by Line

  • The FROM node:26-alpine instruction starts from a compact Node.js image.
  • The WORKDIR /app instruction makes /app the location for the remaining build steps.
  • The RUN npm install tsx instruction adds the runtime that executes TypeScript inside the image.
  • The COPY server.ts . instruction places your server in /app.
  • The EXPOSE 3000 instruction documents the port used by the server.
  • The CMD instruction defines the command that runs when the container launches.
  • Save Dockerfile.
  • Confirm that your editor's file sidebar lists Dockerfile beside server.ts.

You now have the application source and its image recipe in the same directory. Vercel can use that pairing during the deployment step.

Dockerfile Missing From the Sidebar?

  • Confirm that you created Dockerfile inside docker-vercel-app.
  • Remove any file extension added by your editor. The filename must end at Dockerfile.
  • Check that the capital D appears at the beginning of the filename.

Need another pair of eyes? Help me check why my Dockerfile is missing or named incorrectly.

Verify the container source

Before the final check, which two visible signals prove that the application source is ready? Keep your prediction in mind.

  • Refresh http://localhost:3000 in your browser.
  • Return to the terminal running server.ts.

You'll see an ip property in the browser. You'll also see another GET / request in the terminal.

  • Return to your code editor.
  • Confirm that server.ts and Dockerfile appear directly inside docker-vercel-app.

The runtime check proves the server works. The two saved files give Vercel everything it needs to build the first container.

✔️ Awesome, I've got everything!

Great work. Keep the local server running until you capture the checkpoint below.

ⓧ I'd like to double check the full code

Compare both files with these complete versions. The comments are part of the files.

import { createServer } from "node:http";

// Keep the container's listening port in one place
const port = 3000;

// Handle each request and return the caller's IP address
const server = createServer((request, response) => {
  console.log(`${request.method} ${request.url}`);

  // Prefer the address forwarded by Vercel, then use the socket address
  const forwardedFor = request.headers["x-forwarded-for"];
  const ip =
    typeof forwardedFor === "string"
      ? forwardedFor.split(",")[0].trim()
      : request.socket.remoteAddress ?? "unknown";

  // Send the selected IP address as JSON
  response.writeHead(200, { "content-type": "application/json" });
  response.end(JSON.stringify({ ip }));
});

// Listen on every interface so container traffic can reach the server
server.listen(port, "0.0.0.0", () => {
  console.log(`Server listening on port ${port}`);
});
# Use a compact Node.js runtime as the image foundation
FROM node:26-alpine

# Keep the application files together inside the image
WORKDIR /app

# Install the runtime that executes the TypeScript server
RUN npm install tsx

# Add the server source to the image
COPY server.ts .

# Document the port used by the HTTP server
EXPOSE 3000

# Start the server when the container launches
CMD ["npx", "tsx", "server.ts"]

Your Node.js server works locally, and its Dockerfile is ready for a remote build. Next up, you'll deploy the container with the Vercel CLI.

Deploy Your Container

Your root Dockerfile already describes the container that runs your IP server. The remaining gap is deployment.

The Vercel Container Runtime builds that file on Vercel's infrastructure. The Vercel CLI links your local folder to the remote project.

Docker stays off your computer because the remote build handles the image.

In this step, get ready to:
  • Link your local folder to a new Vercel project.
  • Start a production deployment from the Vercel CLI.
  • Confirm the Container build reached Vercel Container Registry.
Connect the folder to a project

Project linking stores the connection between your local docker-vercel-app folder and its Vercel project. Future deployment commands can use that connection automatically.

  • Return to the terminal you used for the docker-vercel-app folder.
  • Select the account or team scope where you want the project to live.
  • Choose to create a new project when the CLI asks about an existing project.
  • Enter docker-vercel-app as the project name.
  • Keep the detected project settings when the CLI asks about customization.
  • Launch the setup prompts by running this command:
vercel link

What does this command do?

  • The link command associates the current folder with a Vercel project.
  • Your scope choice decides which account or team owns the project.
  • The project name keeps the remote resource aligned with your local docker-vercel-app folder.
  • Check the terminal output for confirmation that the folder is linked to docker-vercel-app.

You should see a confirmation that names your selected scope and the docker-vercel-app project. Future commands from this folder now target that project.

Can't link the folder?

  • Confirm the authenticated Vercel account has access to the scope you selected.
  • Check that you entered docker-vercel-app with both hyphens if the project name is rejected.

Still stuck? Ask for help with your project-linking output. You can also post the output in the Vercel Community Help forum.

Deploy the container image

A production deployment uploads your project files to Vercel. The Container preset reads the root Dockerfile and turns it into a runnable image.

The remote image build can take a few minutes. Continuous progress output means the job is still moving.

Before you deploy, which build stages do you expect to appear between the source upload and the production URL?

  • Start the production deployment with visible build logs by running this command:
vercel deploy --prod --logs

What does this command do?

  • The deployment command sends the linked project to Vercel.
  • The --prod option targets the production environment.
  • The --logs option prints the remote build process in your terminal.
  • The Container preset builds the root Dockerfile image and pushes it to Vercel Container Registry.

Your terminal first identifies the Container preset. Later output reports the image build and its push to Vercel Container Registry.

At the end of the deployment, your terminal prints the production deployment URL.

  • Record the production URL from the final terminal output here: your Vercel deployment URL.

That is the heavy lifting done. Vercel has turned your root Dockerfile into a production container image.

Deployment not completing?

  • Confirm that Dockerfile sits at the top level of the docker-vercel-app folder if the Container preset is missing.
  • Compare Dockerfile with the version you reviewed earlier if the image build stops.
  • Check that server.ts remains beside Dockerfile if the build cannot copy the server file.

Need another pair of eyes? Ask for help with your Vercel Container build logs. You can also share the non-sensitive output in the Vercel Community Help forum.

✔️ Awesome, my container is deployed

Everything is in place. Your local folder is linked to docker-vercel-app with a successful production Container deployment.

Your production deployment is available at your Vercel deployment URL.

ⓧ I'd like to double check the deployment

Use this checklist to compare the final project state.

  • Confirm the linked Vercel project is named docker-vercel-app.
  • Confirm the build output identifies the Container preset.
  • Confirm the build output records an image push to Vercel Container Registry.
  • Confirm the final output matches the URL you recorded as your Vercel deployment URL.

The root Dockerfile and server.ts remain unchanged from the previous step.

Your production container now has a live URL. Next, you'll request it in your browser and see your caller IP return as JSON.

Test the API and Inspect Logs

The container image is deployed. Build success proves Vercel created it.

A real request now needs to prove that the server can answer from the internet. Runtime logs then reveal the server output behind that request.

In this step, get ready to:
  • Request the live production URL.
  • Confirm that the response contains your caller IP as JSON.
  • Find the matching request output in Vercel runtime logs.
Request the live API

A browser request tests the full path from the public URL to your running server. The response gives you visible proof that the deployed container is serving traffic.

Before you send the request, what information do you think the container will return about you?

  • Switch back to the terminal output from the previous step.
  • Locate the production deployment URL in that output.
  • Copy the production deployment URL.
  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type the name of your browser and press Enter to open it.
  • Paste the copied URL into the browser address bar.
  • Press Enter to request the live API.

You'll see a JSON object containing your caller IP address. You have just proved that your production container can answer a real request from the internet.

What Does the Browser Response Prove?

Your browser reached the production deployment. The running Node.js server handled the request.

The JSON body contains the caller IP calculated by server.ts. That response confirms the container is serving live traffic.

Don't See the IP Response?

  • Confirm that you copied the production deployment URL from the successful deployment output.
  • Reload the page after confirming that the deployment is ready.
  • Check that the address bar contains the root deployment URL without an added path.

Still stuck? Help me troubleshoot why my live Vercel container is not returning the caller IP as JSON.

Trace the request in runtime logs

The browser response shows what the caller received. The runtime log shows what your server recorded while handling that request.

Matching one request can feel fiddly when several rows arrive together. Live mode puts your next request at the top of the stream.

Signing in here only opens observability for the project you already deployed. Your production deployment stays unchanged.

  • Open a new browser tab.
  • Visit the Vercel dashboard in the new tab.
  • Sign in with the Vercel account linked to your CLI if prompted.
  • Select docker-vercel-app from your project list.
  • Select Logs in the project sidebar.
  • Set Timeline to Live mode.

Before you refresh the API, what should appear in the log stream if the request reaches your server?

  • Switch back to the live deployment tab from the previous substep.
  • Refresh the page once.
  • Return to the Logs tab.
  • Select the newest log row that matches the time of your refresh.
  • Scroll to Log Messages in the details panel.

You'll see a fresh log row from the time of your refresh. Its log messages include the server.ts output for the incoming request.

You've now traced one browser request all the way into the live container. The matching stdout entry gives you server-side proof.

Why Does stdout Matter?

The stdout stream carries output written by your running server. Vercel collects that stream in the project logs.

The matching entry proves that the code inside your container handled the request. You now have evidence from both sides of the connection.

Can't Find the Request Log?

  • Set Timeline back to Live mode.
  • Refresh the production deployment URL once more.
  • Use the Deployment filter to narrow the logs to your live production deployment.

Still unable to match the request? Help me find my container's stdout entry in Vercel runtime logs.

Your API answers from its live container. Its runtime logs expose the server-side trace for each test request. Next up, you'll split the project into separate frontend and backend containers.

Split the Frontend and API

Your single container proved that the IP API works on a live URL. Its runtime logs also proved that each request reaches your server.

The next version gives the static page and API separate build boundaries. Vercel Services builds each boundary as its own service. Top-level rewrites decide which service handles each request.

Your current production deployment stays live during this reorganization. The new layout only takes over after the next deployment.

In this step, get ready to:
  • Move the existing IP API into a dedicated backend folder.
  • Create a static frontend served by nginx.
  • Define the two services and route requests between them.
Move the API into its own service

Each service needs its own folder because that folder becomes its build context. The existing API files already work together, so you can move them without changing their contents.

  • Return to the code editor where you wrote server.ts.
  • Select the existing docker-vercel-app folder in the editor's file sidebar.
  • Create a folder named frontend inside docker-vercel-app.
  • Create a folder named backend inside docker-vercel-app.

You should now see empty frontend and backend folders beside the two existing root files.

  • Drag server.ts from the project root into backend.
  • Drag Dockerfile from the project root into backend.

The backend folder should now contain server.ts plus Dockerfile. Both files should be gone from the project root.

Why does the Dockerfile still work?

The backend service treats backend as its build context. The existing COPY server.ts . instruction now reads backend/server.ts from that context.

The API keeps its existing port and startup command. Only its location changes.

Files in the wrong folder?

  • Check that backend sits directly inside docker-vercel-app.
  • Move server.ts back if your editor created a duplicate instead of relocating it.
  • Move Dockerfile back if a copy remains at the project root.

Still stuck? Help me correct my frontend and backend folder layout.

Build the static frontend

The frontend needs one page plus a web server that can serve it. nginx provides that server in a small container image.

  • Create index.html inside the frontend folder.
  • Fill frontend/index.html by copying this code:
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vercel Container Frontend</title>
</head>
<body>
  <!-- Identify which service handled the browser request. -->
  <h1>Hello from the frontend container</h1>
  <!-- Point to the API route handled by the backend service. -->
  <p>Visit <a href="/api">/api</a> to see your IP address as JSON.</p>
</body>
</html>

What does this page do?

  • The heading identifies the frontend container that handled the root request.
  • The link points to /api. That path belongs to the backend service.
  • The page keeps the two outcomes visible. The root path shows HTML. The API path returns JSON.
  • Save frontend/index.html.
  • Confirm the editor shows the heading Hello from the frontend container.

Your frontend now has a page that clearly identifies its service. The /api link also gives you a direct way to test routing after deployment.

Page content missing?

  • Check that the file is named index.html in lowercase.
  • Check that index.html sits inside frontend.
  • Check that the closing </html> tag remains at the bottom of the file.

Need another pair of eyes? Help me check my static frontend HTML.

  • Create Dockerfile inside the frontend folder.
  • Fill frontend/Dockerfile by copying this code:
# Serve the static page with the lightweight nginx image.
FROM nginx:alpine

# Replace nginx's default page with this project's frontend.
COPY index.html /usr/share/nginx/html/

What does this Dockerfile do?

  • The FROM instruction selects the nginx image that serves static files.
  • The COPY instruction places index.html in nginx's static content directory.
  • nginx serves that copied page when a request reaches the frontend container.
  • Save frontend/Dockerfile.
  • Expand the frontend folder in your editor's file sidebar.

You should see Dockerfile plus index.html inside frontend.

Frontend files not together?

  • Move Dockerfile into frontend if it appears at the project root.
  • Check that the filename has no extension after Dockerfile.
  • Check that the copy source is exactly index.html.

Still not matching? Help me fix my nginx frontend Dockerfile layout.

Route requests to both services

The two folders define what Vercel builds. A root vercel.json file defines which service receives each public path.

Rewrite order matters because Vercel uses the first matching rule. The specific API rule must appear before the frontend catch-all rule.

  • Create vercel.json directly inside docker-vercel-app.
  • Fill vercel.json by copying this configuration:
{
  "services": {
    "frontend": {
      "runtime": "container",
      "root": "frontend/"
    },
    "backend": {
      "runtime": "container",
      "root": "backend/"
    }
  },
  "rewrites": [
    {
      "source": "/api",
      "destination": { "service": "backend" }
    },
    {
      "source": "/(.*)",
      "destination": { "service": "frontend" }
    }
  ]
}

How does the routing work?

  • The services object names the two independently built container services.
  • Each root value tells Vercel which folder belongs to that service.
  • The /api rewrite sends the IP request to backend.
  • The /(.*) rewrite catches every remaining path for frontend.
  • Save vercel.json.

Before you inspect the project, which files do you expect to remain at the root?

  • Inspect the docker-vercel-app tree in your editor's file sidebar.
  • Read the rewrite rules from top to bottom.

What should I see?

  • The project root contains frontend, backend, and vercel.json.
  • The project root no longer contains server.ts or Dockerfile.
  • The frontend folder contains index.html plus Dockerfile.
  • The backend folder contains the unchanged server.ts plus Dockerfile.
  • The API rewrite appears before the frontend catch-all rewrite.

That is the split complete. Your local source now describes two container services while the previous single-container production deployment remains live.

Layout does not match?

  • Move vercel.json to the project root if it appears inside either service folder.
  • Check that each root value matches the corresponding folder name.
  • Move the /api rewrite above the catch-all rule if their order is reversed.

Need help comparing the structure? Help me debug my two-service Vercel configuration.

✔️ Awesome, I've got everything!

Great work. Your API now has its own backend service, your static page has its own frontend service, and the root configuration connects both.

ⓧ I'd like to double check the full code

Confirm that backend/server.ts matches the former root server.ts exactly. Confirm that backend/Dockerfile matches the former root Dockerfile exactly.

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vercel Container Frontend</title>
</head>
<body>
  <!-- Identify which service handled the browser request. -->
  <h1>Hello from the frontend container</h1>
  <!-- Point to the API route handled by the backend service. -->
  <p>Visit <a href="/api">/api</a> to see your IP address as JSON.</p>
</body>
</html>
# Serve the static page with the lightweight nginx image.
FROM nginx:alpine

# Replace nginx's default page with this project's frontend.
COPY index.html /usr/share/nginx/html/
{
  "services": {
    "frontend": {
      "runtime": "container",
      "root": "frontend/"
    },
    "backend": {
      "runtime": "container",
      "root": "backend/"
    }
  },
  "rewrites": [
    {
      "source": "/api",
      "destination": { "service": "backend" }
    },
    {
      "source": "/(.*)",
      "destination": { "service": "frontend" }
    }
  ]
}

Your two service boundaries are ready. Next up, you will deploy this configuration and prove that the root path reaches nginx while /api still returns the IP JSON.

Redeploy and Route Traffic

The split layout is ready, but production still serves the earlier version from one container. Your new routes cannot take over until Vercel receives the updated project.

This final deployment sends both services live under one production URL. You will test the rewrites to prove that each path reaches the correct service.

In this step, get ready to:
  • Deploy the two-service configuration to production.
  • Confirm the root URL serves the nginx frontend page.
  • Confirm /api returns the caller IP as JSON.
Deploy both services to production

A production deployment updates the live project domain after the build succeeds. The --prod option makes that target explicit.

Before you deploy, which services do you expect Vercel to build from the new layout?

  • Switch back to the terminal session for the linked docker-vercel-app folder.
  • Start the production deployment by running this command:
vercel deploy --prod

What does this command do?

  • The vercel deploy --prod command creates a production deployment for the linked project.
  • The vercel.json file tells Vercel to build the frontend service and the backend service.
  • Vercel runs the frontend/Dockerfile build on its infrastructure.
  • Vercel also runs the backend/Dockerfile build on its infrastructure.
  • Docker remains unnecessary on your machine.

A few minutes of waiting is normal here. Vercel is completing two separate service builds.

You should see a successful deployment followed by a production URL.

  • Copy the production URL into your production URL.

That is the heavy lift done. Both services now have fresh production builds.

Deployment did not finish?

  • Check that your terminal is inside the linked docker-vercel-app folder.
  • Find the first failed service in the build output.
  • Compare that service's Dockerfile with the version you completed in the previous step.

Still blocked? Help me troubleshoot the two-service Vercel deployment. You can also share the non-secret build output with the NextWork community.

Check the frontend route

The catch-all rewrite directs every path except /api to the frontend service. The root URL gives you the quickest proof that this rule is active.

Before you open the root URL, do you expect the earlier IP response or the new static page?

  • Paste your production URL into your browser's address bar.
  • Press Enter.

You should see a static page that identifies the frontend container. You should also see a prompt to visit /api.

The root path is now reaching the nginx service through the catch-all rewrite.

Check the API route

The specific /api rewrite takes priority over the catch-all rule. This final check reveals whether the request reaches the backend service.

Before you open the API route, what response format do you expect the backend to return?

  • Append /api to your production URL in the browser's address bar.
  • Press Enter.

You should see a JSON object containing your caller IP address. This confirms that the specific rewrite reached the backend container.

That routing split is live. Your production URL now reaches the correct service for each path.

Does one route show the wrong service?

  • Confirm that the backend URL ends with exactly /api.
  • Return to the production URL from the latest deployment if your browser still shows the earlier single-container response.
  • Check the deployment output for successful frontend and backend service builds.

Need another pair of eyes? Help me debug why my Vercel routes reach the wrong container.

Secret mission

Run a Two-Service Routing Smoke Test

Check /index.html as the frontend probe. Check /api as the backend probe. Use backend stdout to connect the API response to its container.

Clean Up Your Resources

Clean Up Your Resources

Your deployment costs $0 for this project on the Vercel Hobby plan. Decide whether to keep the live project available, step away while preserving it, or delete every project resource.

Resources you used:

  • The Vercel project docker-vercel-app with its production deployment.
  • Two Vercel-built container services serving the root page and /api.
  • The local docker-vercel-app folder containing your two-container configuration.

Keep everything running

No action is needed while you continue building. The production deployment stays available for more tests.

  • Leave the docker-vercel-app project deployed on Vercel.
  • Keep the local docker-vercel-app folder in its current location.
  • Use the live project URL whenever you want to demonstrate the frontend page or the /api response.

Pause - I'll come back to this later

There is no local Docker process to stop. Pausing here means closing your workspace while preserving the live deployment.

  • Leave the docker-vercel-app project deployed on Vercel.
  • Close the Vercel dashboard.
  • Close your terminal.
  • Keep the local docker-vercel-app folder for your next session.

Delete - I don't want to use this again

Deleting the Vercel project looks final because it is. Your local folder stays separate until you remove it too.

  • Switch back to the Vercel dashboard from earlier.
  • Select docker-vercel-app from your project list.
  • Open Settings for the project.
  • Scroll to the project deletion section.
  • Delete the Vercel project with the deletion control in that section.
  • Complete the dashboard confirmation prompt.
  • Return to your project list to confirm that docker-vercel-app is absent.

You should no longer see docker-vercel-app in the project list. Its production URL stops serving the root page or /api.

The local folder is the final independent copy of your project files.

macOS

  • Open the macOS file browser from its Dock icon or system search.
  • Search for docker-vercel-app.
  • Select the folder named docker-vercel-app.
  • Move the selected folder to the Trash.
  • Empty the Trash to complete the deletion.
  • Search for docker-vercel-app again.

You should see no matching project folder in the search results.

Windows

  • Open the Windows file browser from the taskbar or system search.
  • Search for docker-vercel-app.
  • Select the folder named docker-vercel-app.
  • Press the Delete key to move the folder to the recycle bin.
  • Empty the recycle bin to complete the deletion.
  • Search for docker-vercel-app again.

You should see no matching project folder in the search results.

Cleanup is complete. Your live deployment and local project copy are gone.

Nice Work!

Nice Work!

You made it! Your two-container app is live on Vercel from plain Dockerfiles.

You've learned how to:

  • Deployed a plain Dockerfile through Vercel's Container runtime without installing Docker locally.
  • Verified the live Node.js API through its JSON response at /api. The matching request appeared in Vercel's runtime logs as container stdout.
  • Split the app into two container services: an nginx frontend plus a Node.js API. Configured rewrites in vercel.json to send each URL path to its matching service.
  • Completed an optional Secret Mission to push your container deployment skills further.

Ready to quiz yourself?