Deploy a v0 app without Vercel
How to deploy a v0 app on a server you control instead of Vercel: export it to GitHub, recover the environment variables, build a standalone Next.js image and serve it over HTTPS.
TL;DR
- Every v0 (opens in a new tab) app is a Next.js (opens in a new tab) app with React, TypeScript, Tailwind CSS and shadcn/ui, so deploying it without Vercel (opens in a new tab) means self-hosting Next.js.
- Connect the chat to GitHub (opens in a new tab) under Project menu, Settings, GitHub, and v0 creates a private repository and pushes the code.
- The environment variables live in the Vercel project, where Production values are Secret by default and cannot be read back, so get them from each service again.
- Set
outputtostandaloneinnext.config, build with Node.js (opens in a new tab) 22 and theNEXT_PUBLIC_values (inlined at build time), and runnode server.jsbehind a reverse proxy such as Caddy (opens in a new tab).
On this page
To deploy a v0 app without Vercel, connect the v0 chat to GitHub, copy its environment variables out of the Vercel project, set output: "standalone" in next.config, and build a Docker (opens in a new tab) image that runs node server.js. Run that image on your own server behind a reverse proxy such as Caddy, which serves your domain over HTTPS. This is one of the builder guides in deploying an AI-built app to your own server.
The v0 and Vercel facts here come from their documentation, checked on 11 November 2026. The Dockerfile and the Caddy and Compose files were built and run that day in Docker, on Node.js 22, against a fresh app from create-next-app (Next.js 16.4.6), the same framework v0 generates.
What v0 builds, and what is tied to Vercel
v0 writes apps with Next.js, React, TypeScript, Tailwind CSS and shadcn/ui. Its FAQ says plainly that you can export the code and deploy elsewhere, and recommends a Vercel-connected repository only for the full experience: environment variables, deployments and previews.
| On Vercel | On your server |
|---|---|
| Hosting and production deployments on Publish | A Docker container behind a reverse proxy |
| Environment variables in the Vercel project | An env file on the server, and build arguments for NEXT_PUBLIC_ values |
| A preview deployment for every working branch | Your own previews, or none |
| Databases added through the Vercel Marketplace | The same Neon (opens in a new tab), Supabase (opens in a new tab) or Upstash (opens in a new tab) account, used directly |
When you add Neon, Supabase or Upstash in v0, the Vercel Marketplace creates an account at that provider and adds its connection variables to the Vercel project. The database is the provider's, not Vercel's, so the app can keep using it after the move with the same variables. Run the app on your own computer with those values first: if it connects from there, it should connect from your server too, unless the provider only accepts listed IP addresses. Vercel Blob is Vercel's own storage; if the app uses it, check Vercel's docs for using it from outside Vercel, or move the files to other object storage.
Step 1: Connect the chat to GitHub
In v0, open the Project menu (the ...), then Settings, then GitHub. Choose Connect, pick the account or organisation, name the repository and choose Create Repository. v0 creates a private repository and pushes the project's current code to it.
From then on, a chat starts from a base branch (usually main). The first change in a chat creates a working branch, v0 pushes its changes there, and Publish merges a pull request into the base branch. If you keep building in v0 after the move, the base branch is the one your server should deploy from.
Clone the repository and run npm install and npm run build on your own computer first: a clean build there means the Docker build will work too.
Step 2: Collect the environment variables
v0 manages environment variables through Vercel: Project menu, then Settings, then Environment Variables. Make a list of every name, and sort it in two:
NEXT_PUBLIC_variables. Next.js inlines these into the JavaScript bundle duringnext build. They are public by design and must be present at build time.- Everything else (
DATABASE_URL, API keys, auth secrets). Next.js keeps these on the server and reads them when the app runs.
Vercel's CLI docs give Secret as the default type of a new Production variable, and a Secret's value cannot be read back from Vercel: vercel env pull writes a placeholder for it instead. Variables with a public prefix such as NEXT_PUBLIC_ are stored as Config, so those can be read and copied as they are. For each secret, go back to the service that issued it: the connection string from the Neon or Supabase dashboard, the key from Stripe, a newly generated value for your auth secret.
Step 3: Switch Next.js to standalone output
Next.js can trace exactly which files the server needs and copy them, with a minimal server.js, into .next/standalone. That makes the image much smaller than one with the whole node_modules. Add one line to next.config.ts (or next.config.js):
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;Keep whatever else your config already has; only output is new.
Step 4: Add the Dockerfile
This is a shortened, npm-only version of the official example in the Next.js repository. The example uses a Node.js 24 Debian-based image; this one uses Node.js 22 on Alpine, the version this guide was tested with, and either works for Next.js 16:
# Dependencies: installed from the lockfile
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
# Build: next build writes .next/standalone (output: "standalone")
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# NEXT_PUBLIC_ variables are inlined into the bundle at build time
ARG NEXT_PUBLIC_SITE_URL
ENV NODE_ENV=production
RUN npm run build
# Run: only the standalone server, the static files and public/
FROM node:22-alpine AS run
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
COPY --from=build --chown=node:node /app/public ./public
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]Add one ARG line per NEXT_PUBLIC_ variable from step 2; NEXT_PUBLIC_SITE_URL is an example. If the repository uses pnpm or another package manager, the official example detects it from the lock file. Keep HOSTNAME=0.0.0.0 from the official example: the standalone server listens on the address in HOSTNAME, and 0.0.0.0 means every interface of the container, so the proxy can reach it.
A .dockerignore keeps local builds and env files out of the image:
node_modules
.next
.git
.env*Step 5: Run it behind Caddy
Next.js's self-hosting guide recommends a reverse proxy in front of the Next.js server rather than exposing it directly. Caddy is a good fit because it also gets and renews the HTTPS certificate. The Caddyfile:
app.example.com {
reverse_proxy app:3000
}And compose.yaml, with the public values as build arguments and the runtime values from an env file:
services:
app:
build:
context: .
args:
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL}
env_file: .env.production
restart: unless-stopped
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:On the server, replace app.example.com with your domain, put the NEXT_PUBLIC_ values in .env (Docker Compose reads it for ${...}) and the server-side values in .env.production. Keep both out of Git. Point the domain's A record at the server and start it:
docker compose up -d --buildIn our test, with localhost as the site address (Caddy then uses its own local certificate authority instead of a public one), the page showed the NEXT_PUBLIC_SITE_URL value passed at build time, the container received DATABASE_URL from .env.production at run time, the server ran as the node user, and /_next/image returned an optimised PNG. A public certificate for a real domain needs a real server, which the test did not use.
For more on the server side (provisioning, the firewall, deploying from GitHub Actions (opens in a new tab)), deploy Next.js to a VPS goes deeper on the same stack, and how to deploy a web app to a VPS covers the server from scratch.
Step 6: What changes when Next.js is not on Vercel
Most of Next.js works the same on one self-hosted server. Its self-hosting guide lists what to know:
- Images.
next/imageoptimisation works with no configuration. On glibc-based Linux, the image library may need a memory allocator setting; the Alpine image above uses musl, not glibc. - Proxy (formerly middleware) and
after()work with no configuration on a Node.js server. - Environment variables. Server code reads
process.envat run time during dynamic rendering, so one image can run in several environments with different values. OnlyNEXT_PUBLIC_values are fixed at build. - Cache and ISR. The cache lives on the server's own disk, which is fine for a single server. If you later run several copies, they need a shared cache handler, the same
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, and a consistent build ID or deployment ID. - Streaming. If you put nginx (opens in a new tab) in front instead of Caddy, turn off its response buffering for streaming to work.
Step 7: Check the Vercel-specific parts, then move the domain
- Search the code for imports from
@vercel/. Each is a Vercel product (analytics, storage and others); check its own docs for use outside Vercel, and remove or replace the ones that do not apply. - Search for
vercel.appaddresses in the code and in your variables: OAuth callback URLs, webhook endpoints (Stripe, GitHub), email links. - Add the new domain to every OAuth provider and auth service the app uses (Supabase's URL Configuration page, NextAuth's provider apps, and so on).
- If
vercel.jsonhas redirects, rewrites or headers, move them tonext.config(redirects,rewrites,headers), which works on any host.
Then point the domain at the server and remove it from the Vercel project. Own server vs Vercel compares the two hosting models side by side. The Claude Code guide covers the same Next.js apps when they come from a coding agent instead of v0, and the Bolt.new guide covers the static Vite case.
Doing this with OpsCrew
OpsCrew recognises a repository exported from v0 and builds its image with its own Next.js Dockerfile in GitHub Actions: a non-root image with every base image pinned, and NEXT_PUBLIC_ values handled as public build-time values. It deploys to a new server on Hetzner Cloud (opens in a new tab) or DigitalOcean (opens in a new tab) or to a server you already have, with HTTPS on your domain, and it translates the rules in vercel.json into the reverse proxy's configuration. A database the app already uses on Neon or Supabase can stay where it is, or OpsCrew runs PostgreSQL next to the app. A server-rendered Next.js app can also go on Cloudflare's hosting through OpenNext. Every change waits for your approval of its exact plan.
For what applies to every builder, see the guide to deploying an AI-built app to your own server.
Key facts
| Fact | Value | Source |
|---|---|---|
| What v0 generates | Next.js, React, TypeScript, Tailwind CSS and shadcn/ui | v0 FAQs (opens in a new tab)Verified on |
| Default type of a new Production variable on Vercel | Secret (its value cannot be read back from Vercel) | vercel env (opens in a new tab)Verified on |
| Next.js version used in this guide's test | Next.js 16.4.6 on Node.js 22Created with create-next-app on 11 November 2026; 16.4.6 was the latest release on npm (opens in a new tab) that day. | next on npm (opens in a new tab)Verified on |
Frequently asked questions
How do I publish a v0 app?
Publish in v0 confirms the domain and visibility, opens or reuses a pull request, merges it into the base branch and waits for the Vercel production deployment. Branch protections in GitHub, such as required reviews, can block the merge until you complete them.
Why are my environment variables empty after I move off Vercel?
Two common reasons. NEXT_PUBLIC_ variables are inlined when next build runs, so they must be set during the build, not only when the container starts. And production variables on Vercel are Secret by default, so vercel env pull writes placeholders for them; get those values from the service that issued them.
Sources
- v0 FAQs (opens in a new tab)
- GitHub (opens in a new tab)
- Databases (opens in a new tab)
- vercel env (opens in a new tab)
- How to self-host your Next.js application (opens in a new tab)
- next (npm package, version 16.4.6) (opens in a new tab)
- Next.js with-docker example (opens in a new tab)
- Automatic HTTPS (opens in a new tab)
Related articles
- Deploying
How to deploy an app you built with AI to your own server
From a Lovable, Bolt.new, Replit, v0 or Claude Code project to a live URL on a server you control: code, build, backend, secrets, HTTPS and what breaks on the way.
- Deploying
Deploy a Bolt.new app to your own server
How to deploy a Bolt.new app outside Bolt Cloud: get the code, find out which stack Bolt chose, build it in Docker, serve it over HTTPS, and claim the database into Supabase.
- Deploying
Deploy a Lovable app to your own server (Vite or TanStack Start)
How to deploy a Lovable app to a server you control: tell the Vite and TanStack Start stacks apart, sync it to GitHub, build it in Docker, serve it over HTTPS, and keep or move the Supabase backend.