Containerize Your Next.js App with Podman

Build a production container image and deploy it locally with Kubernetes YAML.

Introduction

30 Second Summary

Every developer has heard "it works on my machine." Containers solve this by packaging your app with everything it needs to run identically anywhere.

In this project, you will containerize your Next.js web app using Podman and deploy it locally with Kubernetes YAML manifests. You will own your deployment artifact instead of depending on a hosting platform.

What You'll Build

A containerized version of your Next.js web app running inside a Podman-managed container on your Windows machine, orchestrated by Kubernetes-style YAML manifests.

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

  • A multi-stage Containerfile that produces a minimal production image of your Next.js app.
  • A working Kubernetes Deployment and Service YAML tested locally with podman kube play.
  • A portable containerized setup you can deploy to any server, VPS, or cloud Kubernetes cluster.
  • Secret Mission: Add a health check endpoint and resource limits to your Kubernetes manifest for production readiness.

Are there any prerequisites?

You need Node.js installed and a Next.js project with a package-lock.json (or equivalent lockfile). This project uses Windows with WSL2 and PowerShell.

Set Up WSL2 and Podman Desktop

Containers need a Linux environment to run. On Windows, that environment is provided by WSL2 (Windows Subsystem for Linux 2), a lightweight virtual machine built into Windows.

In this step, you will enable WSL2, install Podman Desktop (our container management tool), and verify everything is running. Once this is done, your machine is ready to build and run container images.

In this step, get ready to:
  • Enable WSL2 on your Windows machine.
  • Install Podman Desktop and complete the onboarding wizard.
  • Verify that the Podman engine and machine are running.
Enable WSL2 on Windows

WSL2 provides the Linux kernel that Podman uses to run containers. You need to enable it and update the WSL kernel before installing Podman Desktop.

  • Open PowerShell as an administrator. Press the Windows key to open your search bar.
  • Type PowerShell, right-click Windows PowerShell, and select Run as administrator.
  • Click Yes on the User Account Control prompt that appears.
  • Enable WSL2 by running these two commands one at a time:
wsl --update
wsl --install --no-distribution

What do these commands do?

  • wsl --update downloads and installs the latest WSL kernel from Microsoft.
  • wsl --install --no-distribution enables the WSL feature without installing a full Linux distribution like Ubuntu. Podman manages its own lightweight Linux environment, so you do not need a separate distro.
  • Restart your computer. The WSL feature requires a full reboot to finish enabling.
  • After your machine restarts, open PowerShell as administrator again using the same steps as before.
  • Verify WSL2 is enabled by running this command:
wsl --status

✔️ I see WSL version information

You should see output mentioning the default version (version 2) and kernel version details. WSL2 is ready. Continue to the next substep.

ⓧ I see an error or unrecognized command

This usually means one of two things:

  • Your Windows version is too old. You need Windows 10 Build 19043 or later, or Windows 11.
  • You did not restart after running the install commands. Close everything, restart your computer, and try again.
  • If you are on Windows 10 Enterprise LTSC, run wsl --install -d ubuntu-24.04 instead of the --no-distribution flag. This is a known workaround for LTSC editions.

help me troubleshoot WSL2 not enabling on my Windows machine

Install Podman Desktop

Podman Desktop is a graphical application that manages containers, images, and Podman machines. It also installs the Podman CLI engine for you during its onboarding wizard.

  • In your administrator PowerShell terminal, install Podman Desktop by running this command:
winget install -e --id RedHat.Podman-Desktop

What does this command do?

winget is Windows' built-in package manager. The -e flag means exact match, and --id RedHat.Podman-Desktop tells it to install the official Podman Desktop package from Red Hat.

  • Wait for the installation to complete. You will see a progress bar in your terminal.
  • Once the install finishes, open Podman Desktop. Press the Windows key, type Podman Desktop, and press Enter.

What to expect from the onboarding wizard

Podman Desktop opens with a "Get started with Podman Desktop" screen. This wizard will install the Podman engine and create a Podman machine (the Linux VM that runs your containers). Here is what to click through:

  • Click Start Onboarding.
  • Click Next, then click Yes on the confirmation prompt.
  • On the Podman Setup screen, leave the default WSLv2 provider selected and click Install.
  • When you see "Installation Successfully Completed", click Close.
  • Click Next to create a Podman machine, then click Next again, then click Create.
  • When the machine is ready, click Next through the remaining screens (kubectl and compose CLI install prompts) until you reach the Dashboard.
  • Complete the onboarding wizard by following the steps in the callout above.
  • Once you reach the Dashboard, your Podman engine and machine are set up.

Onboarding wizard not appearing?

If Podman Desktop opens straight to the Dashboard without showing the wizard, go to Settings > Resources in the left sidebar. Look for the Podman tile and click Setup Podman to trigger the wizard manually.

help me get the Podman Desktop onboarding wizard to appear

Verify your Podman setup

With the wizard complete, let's confirm the Podman CLI is accessible from your terminal and the machine is running.

  • Open a new PowerShell terminal (it does not need to be elevated this time). Press the Windows key, type PowerShell, and press Enter.
  • Check the Podman CLI version by running this command:
podman version

✔️ I see version 5.x or higher

You should see version 5.8.2 or similar in the output. Podman is installed and accessible. Continue below.

ⓧ I see an older version

If you see a version lower than 5.0, your Podman engine was not updated by the onboarding wizard.

  • Open Podman Desktop and go to Settings > Resources.
  • Look for an update option on the Podman tile.
  • If no update is available, delete the current Podman machine from the Resources page, then re-run the onboarding wizard to install the latest engine.

help me upgrade my Podman engine to version 5.x on Windows

ⓧ Command not found

If PowerShell does not recognize podman, the CLI is not in your system PATH yet.

  • Close all PowerShell windows and open a fresh one. Newly installed programs often need a new terminal session to be recognized.
  • If it still does not work, open Podman Desktop and go to Settings > Resources. Click Setup Podman to re-run the Podman engine installation.
  • As a last resort, restart your computer. Some PATH changes require a full reboot.

help me fix podman command not found in PowerShell on Windows

  • Now check that your Podman machine is running by running this command:
podman machine list
  • You should see a machine (typically named podman-machine-default) with a status of Running.

Machine not showing as Running?

If the machine shows a different state (like "Stopped"), start it manually by running:

podman machine start

If it fails entirely, try resetting and recreating the machine:

podman machine reset
podman machine init
podman machine start

help me get my Podman machine running on Windows

Your Windows machine now has WSL2 enabled and a Podman machine running. Next up, you will configure your Next.js project to produce a container-ready build.

Configure Next.js for Containerization

In the previous step, you installed Podman Desktop and verified your Podman machine is running. Now it's time to prepare your Next.js project to live inside a container.

Containers need a recipe that describes how to build and run your app. In this step, you will create three files that tell Podman exactly what to include, what to ignore, and how to assemble a production-ready image of your Next.js app.

In this step, get ready to:
  • Configure Next.js to produce a self-contained build.
  • Create a .containerignore file to keep builds fast and lean.
  • Write a multi-stage Containerfile that produces a minimal production image.
Add standalone output to your Next.js config

By default, a Next.js build produces output that depends on the full node_modules folder. That folder can be hundreds of megabytes. Standalone mode tells Next.js to bundle only the files the server actually needs at runtime.

  • Open your next.config.js (or next.config.ts) file in your code editor.
  • Add output: 'standalone' inside the config object so it looks similar to this:
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  // ...your other config options stay here
}

module.exports = nextConfig

What does this change do?

The output: 'standalone' setting tells Next.js to produce a .next/standalone directory during build. This directory contains a self-sufficient server.js file and only the Node.js files your app needs to run.

Without standalone mode, your container image would need the entire node_modules folder. With it, the final image drops from roughly 1GB to around 110MB.

  • Save next.config.js.

Not sure where to put it?

The output property goes at the top level of the config object, alongside any other settings you already have (like reactStrictMode or images). If your file uses export default syntax instead of module.exports, add output: 'standalone' inside the exported object the same way.

help me add standalone output to my next.config file

Create a .containerignore file

When Podman builds an image, it sends your entire project folder (the "build context") to the build engine. A .containerignore file excludes folders and files that the build does not need, like node_modules and .git.

  • Create a new file called .containerignore in your project root (the same folder that contains package.json).
  • Add the following contents to the file:
node_modules
.next
.git
.gitignore
*.md
.env
.env.*
.vscode
.idea

What does this file do?

Each line is a pattern that Podman will skip when copying files into the build. node_modules is excluded because the Containerfile installs dependencies from scratch using your lockfile. .next is excluded because the build creates a fresh output inside the container.

Excluding .env files keeps secrets out of the image. Excluding .git and editor folders (.vscode, .idea) reduces noise and speeds up the build.

  • Save .containerignore.
  • Confirm the file exists by running:
ls .containerignore

You should see .containerignore printed in the output.

File not found?

Make sure you created the file in your project root folder (next to package.json), not inside a subfolder. The filename starts with a dot, which makes it a hidden file. Some editors hide dotfiles by default.

help me find or create my .containerignore file in the right location

Write your multi-stage Containerfile

A Containerfile is the recipe that Podman follows to build your container image. You will use a multi-stage build pattern that separates the work into three stages: installing dependencies, building the app, and assembling the final runtime image.

Why use multiple stages?

Each stage starts from a clean base image and only carries forward the files the next stage needs. The final image never contains source code, build tools, or the full node_modules folder. This keeps the production image small and secure.

  • Create a new file called Containerfile in your project root (no file extension).
  • Add the base image and dependency stage by pasting this code into the file:
FROM node:20-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* .npmrc* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
  else echo "Lockfile not found." && exit 1; \
  fi

What does this stage do?

The base line picks node:20-alpine as the starting image. Alpine Linux is a minimal distribution that keeps the image small.

The deps stage installs libc6-compat (needed by some native Node.js packages on Alpine), then copies your lockfile and runs the appropriate install command. The wildcard patterns (package-lock.json*) let this Containerfile work with npm, yarn, or pnpm.

You won't see a change yet. The next section adds the builder stage.

  • Below the deps stage, add the builder stage:
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NODE_ENV=production

RUN \
  if [ -f yarn.lock ]; then yarn build; \
  elif [ -f package-lock.json ]; then npm run build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm build; \
  else echo "Lockfile not found." && exit 1; \
  fi

What does the builder stage do?

This stage copies node_modules from the deps stage and then copies your full source code. Setting NODE_ENV=production ensures Next.js produces an optimized build.

The build command runs next build (via your package manager). Because you set output: 'standalone' earlier, this produces a .next/standalone directory containing everything the app needs to run.

Almost there. The final section adds the runner stage.

  • Below the builder stage, add the runner stage:
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

COPY --from=builder /app/public ./public

RUN mkdir .next
RUN chown nextjs:nodejs .next

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]

What does the runner stage do?

This is the stage that becomes your final production image. It creates a non-root user called nextjs for security. Running containers as root is a security risk.

It copies only the standalone output and static assets from the builder stage. No source code, no node_modules, and no build tools end up in the final image. The CMD instruction starts the Next.js server on port 3000.

  • Save Containerfile.
  • Confirm it exists by running:
ls Containerfile

You should see Containerfile printed in the output.

Don't see the Containerfile?

Make sure the file is named exactly Containerfile with no file extension (not Containerfile.txt or Containerfile.dockerfile). It should be in your project root alongside package.json.

help me verify my Containerfile is in the right place with the right name

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: next.config.js, .containerignore, and Containerfile.

ⓧ I'd like to double check the full code

Here is the complete Containerfile for comparison:

FROM node:20-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* .npmrc* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
  else echo "Lockfile not found." && exit 1; \
  fi

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NODE_ENV=production

RUN \
  if [ -f yarn.lock ]; then yarn build; \
  elif [ -f package-lock.json ]; then npm run build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm build; \
  else echo "Lockfile not found." && exit 1; \
  fi

FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

COPY --from=builder /app/public ./public

RUN mkdir .next
RUN chown nextjs:nodejs .next

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]

And here is the complete .containerignore:

node_modules
.next
.git
.gitignore
*.md
.env
.env.*
.vscode
.idea

Your project is now fully configured for containerization. Next up, you will build the container image with Podman and watch the multi-stage build in action.

Build Your Container Image

Your Containerfile and .containerignore are ready. Now it's time to turn them into a real container image.

Podman reads your Containerfile, executes each stage in order, and produces a single lightweight image at the end. The multi-stage pattern means only the final runner stage ends up in the finished image. Everything else (the full node_modules, the build toolchain) gets discarded automatically.

In this step, get ready to:
  • Build a production container image from your Containerfile.
  • Verify the image exists and check its size.
Build the container image with Podman

The podman build command reads your Containerfile and executes each stage sequentially. The first run will take a few minutes because Podman needs to pull the node:20-alpine base image from Docker Hub, install your dependencies, and run the Next.js build.

  • Build your container image by running this command in your PowerShell terminal:
podman build -t gabayer-app:latest .

What does this command do?

  • podman build tells Podman to read the Containerfile in the current directory and build an image.
  • -t gabayer-app:latest tags the resulting image with the name gabayer-app and the version label latest.
  • . sets the build context to your current directory. Podman sends all non-ignored files to the build engine.

Your .containerignore file keeps node_modules, .next, and .git out of the build context, which makes the upload fast and the build clean.

The build will take 2-5 minutes on a first run. You will see output for each stage: STEP 1/N lines track progress through the Containerfile instructions.

What should I see during the build?

You will see Podman pull the node:20-alpine base image, install dependencies in the deps stage, run npm run build in the builder stage, then copy the standalone output into the runner stage.

The last line will show a commit hash followed by your image tag. This confirms the build completed successfully.

Build failing with an error?

  • If you see a message about the Containerfile not being found, make sure your PowerShell terminal is open in the same directory where your Containerfile lives. Run ls to check.
  • If the build fails during dependency installation with network errors, confirm your Podman machine is running. Run podman machine list and check the status.
  • If the npm run build step fails, make sure output: 'standalone' is set in your next.config.js. The build needs this to produce the standalone server files.

Help me troubleshoot my Podman build failure

Verify the image was created

Once the build completes, you need to confirm the image exists and check its size. The multi-stage build should produce a lean image because only the standalone output and static files make it into the final stage.

  • Check that your image was created by running:
podman images
  • Confirm that gabayer-app with the tag latest appears in the list.
  • Check the SIZE column. Your image should be approximately 100-200MB.

Why is the image so small?

A full Next.js project with node_modules can be over 1GB. The standalone output mode strips everything down to just the files needed to run your server. Combined with the Alpine base image (about 5MB), you get a production image under 200MB.

Don't see your image in the list?

  • If podman images returns an empty table, the build did not complete successfully. Scroll up in your terminal to find the error message, then re-run the build command.
  • If you see <none> in the REPOSITORY column instead of gabayer-app, the tagging failed. Re-run the build with the -t gabayer-app:latest flag included.

Help me figure out why my Podman image isn't showing up

Your container image is built and ready to go. Next up, you will run this image as a container and see your app served from inside it.

Run and Test Your Container

In the previous step, you built a production-ready container image of your Next.js app. The image is sitting on your machine, but it is not doing anything yet.

Now you will start the container and feed it your Supabase environment variables so it can talk to your database. Once the container is running, you will open your app in the browser to confirm everything works inside its new containerized home.

In this step, get ready to:
  • Run your container image with Supabase environment variables.
  • Verify your app loads correctly at localhost:3000.
  • Stop and remove the container to clean up.
Run the container with environment variables

Your Next.js app needs two Supabase values to connect to your database: the project URL and the anonymous key. You will pass these into the container as environment variables at runtime.

Why pass environment variables at runtime?

Variables prefixed with NEXT_PUBLIC_ are baked into the JavaScript bundle at build time. For local testing, passing them with the -e flag works because your Containerfile runs the build inside the container.

For a production deployment where you pre-build the image in CI, you would pass these as build args instead. For this project, the runtime flag approach keeps things simple.

  • Start your container by running this command in PowerShell (replace the placeholder values with your actual Supabase credentials):
podman run -d -p 3000:3000 --name gabayer -e NEXT_PUBLIC_SUPABASE_URL=your-url-here -e NEXT_PUBLIC_SUPABASE_ANON_KEY=your-key-here gabayer-app:latest

What does this command do?

  • -d runs the container in detached mode (in the background), so your terminal stays free.
  • -p 3000:3000 maps port 3000 inside the container to port 3000 on your machine.
  • --name gabayer gives the container a human-readable name so you can reference it easily.
  • -e passes environment variables into the container for your Supabase connection.
  • You should see a long container ID printed in the terminal. This confirms the container started successfully.
  • Confirm the container is running by running:
podman ps
  • You should see gabayer in the list with a status of Up and port 0.0.0.0:3000->3000/tcp in the PORTS column.

Container exited immediately?

If podman ps shows nothing, the container crashed on startup. Check the logs by running podman logs gabayer to see what went wrong.

  • If you see a "port already in use" error, another process is using port 3000. Close it or use a different port like -p 3001:3000.
  • If you see a "name already in use" error, a previous container with the name gabayer still exists. Remove it with podman rm gabayer and try again.

Still stuck? Help me debug why my Podman container exits immediately after starting.

Verify your app in the browser
  • Open your browser and navigate to http://localhost:3000.

You should see your app load with the same content as the Vercel-hosted version. If your app uses Supabase data, verify that the data displays correctly.

What if the page looks different from Vercel?

Any features that depend on server-side environment variables (not prefixed with NEXT_PUBLIC_) will not work because you only passed the public Supabase variables. The core pages and client-side features should still load fine.

  • If the page does not load or you see an error, check the container logs by running:
podman logs gabayer
  • The logs show all output from the Next.js server, including any startup errors or request failures.

Seeing a blank page or connection refused?

  • Make sure the container is still running with podman ps. If it is not listed, it crashed after starting.
  • Confirm your Containerfile has ENV HOSTNAME="0.0.0.0". Without this, Next.js only listens on localhost inside the container, which is unreachable from your host machine.
  • Check that you copied the static assets in your Containerfile. Missing .next/static causes pages to load without CSS or JavaScript.

Need more help? Help me figure out why my containerized Next.js app is not loading in the browser.

Stop and remove the container

Now that you have confirmed your app works inside a container, clean up the running instance. In the next step, you will deploy the same image using Kubernetes YAML instead of a manual run command.

  • Stop the running container by running:
podman stop gabayer
  • Remove the stopped container by running:
podman rm gabayer
  • Confirm the container is gone by running:
podman ps -a
  • You should NOT see gabayer in the list. The image still exists (check with podman images), but the container instance is gone.

What is the difference between an image and a container?

An image is a blueprint. A container is a running instance of that blueprint. You can create many containers from the same image, and removing a container does not delete the image.

Think of it like a recipe (image) and a cooked meal (container). You can cook the meal again anytime from the same recipe.

Your container works. You have proven that your Next.js app can run anywhere Podman is installed, completely independent of Vercel. Next up, you will define a Kubernetes YAML manifest to deploy your container in a more production-like way.

Deploy with Kubernetes YAML

In the previous step, you confirmed your container image works by running it directly with podman run. That approach works, but it means remembering long commands with environment variables every time you want to start your app.

Now you will write a Kubernetes YAML manifest that describes your desired deployment as a file. With a single command, Podman reads that file and creates everything your app needs to run. The same YAML works on any Kubernetes cluster, making your setup portable.

In this step, get ready to:
  • Create a Kubernetes YAML manifest with a Deployment and Service.
  • Deploy your app locally with podman kube play.
  • Verify the app is accessible and tear down the deployment.
Create your Kubernetes YAML manifest

A Kubernetes YAML manifest is a declarative configuration file. Instead of typing commands to start containers, you describe the desired state and let the orchestrator handle the rest.

Your manifest will contain two resources: a Deployment that manages your container, and a Service that exposes it on a port.

  • Create a new file called k8s.yaml in your project root and add the Deployment resource:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gabayer-app
  labels:
    app: gabayer
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gabayer
  template:
    metadata:
      labels:
        app: gabayer
    spec:
      containers:
        - name: gabayer
          image: localhost/gabayer-app:latest
          ports:
            - containerPort: 3000
          env:
            - name: NEXT_PUBLIC_SUPABASE_URL
              value: "your-supabase-url-here"
            - name: NEXT_PUBLIC_SUPABASE_ANON_KEY
              value: "your-supabase-anon-key-here"

What does this Deployment do?

The Deployment tells Podman to run 1 replica of your gabayer-app:latest image.

  • image: localhost/gabayer-app:latest points to the image you built locally. The localhost/ prefix tells Podman to use the local image store instead of pulling from a remote registry.
  • containerPort: 3000 declares that the container listens on port 3000.
  • The env section passes your Supabase connection details into the container at runtime.
  • Replace your-supabase-url-here and your-supabase-anon-key-here with your actual Supabase project URL and anon key.
  • Below the Deployment, add a YAML document separator and the Service resource:
---
apiVersion: v1
kind: Service
metadata:
  name: gabayer-service
spec:
  selector:
    app: gabayer
  ports:
    - port: 3000
      targetPort: 3000
      nodePort: 30000
  type: NodePort

What does this Service do?

A Service defines how traffic reaches your container.

  • type: NodePort exposes the service on a port accessible from your host machine.
  • targetPort: 3000 routes incoming traffic to the container's port 3000.
  • The selector matches pods with the label app: gabayer, connecting this Service to your Deployment.
  • Save k8s.yaml.
  • Confirm the file exists by running:
ls k8s.yaml

You should see k8s.yaml printed in the output.

Don't see k8s.yaml?

  • Make sure you created the file in your project root directory, not inside a subfolder.
  • Check the filename is exactly k8s.yaml with no extra extension like .txt.

Help me troubleshoot my k8s.yaml file creation

Why podman kube play instead of a full cluster?

Podman's podman kube play command reads real Kubernetes YAML and creates equivalent pods and containers locally. You get hands-on experience with the same manifest format used in production clusters, without needing to install Minikube or Kind.

When you are ready to deploy to a real cluster, the same k8s.yaml file works with kubectl apply.

✔️ Awesome, I've got everything!

Great. Double check you have saved k8s.yaml with both the Deployment and Service sections.

ⓧ I'd like to double check the full code

Here is the complete k8s.yaml file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gabayer-app
  labels:
    app: gabayer
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gabayer
  template:
    metadata:
      labels:
        app: gabayer
    spec:
      containers:
        - name: gabayer
          image: localhost/gabayer-app:latest
          ports:
            - containerPort: 3000
          env:
            - name: NEXT_PUBLIC_SUPABASE_URL
              value: "your-supabase-url-here"
            - name: NEXT_PUBLIC_SUPABASE_ANON_KEY
              value: "your-supabase-anon-key-here"
---
apiVersion: v1
kind: Service
metadata:
  name: gabayer-service
spec:
  selector:
    app: gabayer
  ports:
    - port: 3000
      targetPort: 3000
      nodePort: 30000
  type: NodePort
Deploy the manifest with podman kube play

With your manifest written, a single command spins up everything it describes.

  • Deploy your app by running:
podman kube play k8s.yaml

What does this command do?

podman kube play reads your YAML file and creates a Pod with the containers described in the Deployment. It also sets up the port mapping from the Service, making your app reachable from the host.

  • Verify the pod is running by checking pod status:
podman pod ps
  • Check that the container inside the pod is active:
podman ps --pod

You should see a pod with a status of Running in the podman pod ps output. The podman ps --pod output shows individual containers and which pod they belong to.

Pod not showing as Running?

  • Check the pod logs for errors by running podman logs followed by the container name from podman ps --pod.
  • Make sure your gabayer-app:latest image exists locally by running podman images.
  • Confirm the YAML has no indentation errors. YAML is whitespace-sensitive and incorrect spacing breaks the manifest.

Help me debug why my pod is not running after podman kube play

Access your app and tear down the deployment
  • Open your browser and navigate to http://localhost:3000.

You should see your Next.js app loading, displaying the same content as your Vercel-hosted version.

App not loading at localhost:3000?

  • Confirm the pod is still running with podman pod ps.
  • Check container logs for startup errors. Run podman logs followed by the container name you see in podman ps --pod.
  • Your Containerfile sets HOSTNAME="0.0.0.0" which binds to all interfaces. If you changed this value, the app will not be reachable from the host.

Help me troubleshoot why my containerized Next.js app is not accessible at localhost:3000

Once you have confirmed the app works, tear down the deployment so the resources are freed.

  • Tear down the deployment by running:
podman kube play --down k8s.yaml

What does --down do?

The --down flag removes all pods and containers that were created from the specified YAML file. This is how you cleanly tear down a deployment without manually stopping and removing each container.

  • Confirm the pod is gone by running:
podman pod ps

You should see no pods listed, or at least no pod related to your gabayer-app deployment.

Your app is now fully deployable from a single YAML file. The same k8s.yaml manifest you just tested works on any Kubernetes cluster with kubectl apply. Next up, you will tackle the Secret Mission and make your deployment production-ready with health checks and resource limits.

Secret mission

Add Health Checks and Resource Limits

Your containerized app is running, but a production Kubernetes cluster needs to know if your app is healthy. In this secret mission, you'll add a health check endpoint and configure your Deployment with probes and resource limits that real-world deployments require.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Running Podman pod (deployed via podman kube play k8s.yaml).
  • Container image gabayer-app:latest stored in local Podman storage.
  • Podman machine (WSL2-backed virtual machine).
  • Podman Desktop application installed via winget.
  • Project files added to your Next.js app (Containerfile, .containerignore, k8s.yaml, app/api/health/route.ts).

Keep everything running

No action needed. Choose this if you are still actively building or plan to iterate on your container setup soon.

  • Your Podman machine stays running in the background and uses some memory.
  • The pod from podman kube play continues serving your app at http://localhost:3000.
  • WSL2 remains enabled as a Windows feature. You may use it for other Linux workflows too.

Pause - I'll come back to this later

Shut down running processes to free up memory while keeping your files and image intact.

  • Tear down the running pod and stop the Podman machine by running these commands:
podman kube play --down k8s.yaml
podman machine stop

When you are ready to resume, start the machine and redeploy:

podman machine start
podman kube play k8s.yaml
  • Your container image and project files remain on disk.
  • WSL2 stays enabled.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow these steps in order.

  • Tear down the running pod:
podman kube play --down k8s.yaml
  • Remove the container image:
podman rmi gabayer-app:latest
  • Stop and remove the Podman machine:
podman machine stop
podman machine rm

Podman will ask you to confirm the machine removal. Enter y to proceed.

  • Uninstall Podman Desktop:
winget uninstall --id RedHat.Podman-Desktop
  • Delete the container-related files from your Next.js project:
Remove-Item Containerfile, .containerignore, k8s.yaml
  • If you completed the Secret Mission, also delete the health endpoint directory:
Remove-Item -Recurse -Force app\api\health
  • Revert the output: 'standalone' line in your next.config.js if you no longer need standalone builds.
  • WSL2 remains enabled as a Windows feature. Disabling it may affect other tools that depend on it. Only disable WSL2 if you are certain nothing else needs it.

Nice Work!

Nice Work!

Nice work! You just containerized your Next.js web app with Podman, built a production-grade image, and deployed it locally using real Kubernetes YAML.

You've learned how to:

  • Written a multi-stage Containerfile that produces a lean production image from your Next.js app using Podman.
  • Configured Next.js standalone output to create a self-contained deployment artifact that runs without node_modules.
  • Deployed your containerized app using Kubernetes YAML with podman kube play, proving your app is portable to any container-compatible infrastructure.
  • Secret Mission: Added health check probes and resource limits to your Kubernetes manifest for production readiness.

Ready to quiz yourself?