Move an app from Vercel to an EU host
How to migrate a Next.js app from Vercel to blitz.cloud in Germany: standalone Docker build, environment variables, database, and what doesn't carry over yet.
To move a Next.js app from Vercel to blitz.cloud, you build it with output: "standalone" into a Docker image that runs on port 8080 without root, push the image to Docker Hub, deploy it on blitz.cloud, and copy your environment variables across. The app then runs on servers in Falkenstein, Germany, operated by a German company. Whether that is a good trade depends on how much of Vercel's platform your app uses, so read the next section before you start.
What moves cleanly, and what doesn't yet
What carries over today:
- Next.js apps that run as a Node server: server components, server actions, route handlers in
app/api, and middleware (called proxy since Next.js 16) running in the Node server. - Fully static sites, including
output: "export"builds. - Environment variables and secrets.
- A PostgreSQL, MySQL/MariaDB or Redis database, as long as your data can be loaded by the app itself (see below).
What you give up, at least for now:
- Deploys on
git pushand preview deployments per pull request. blitz.cloud can't build from GitHub yet; you push an image. - Your own domain. Apps live under
yourname.blitz.clouduntil custom domains ship. - A global edge network. blitz.cloud serves from one data centre in Germany. Visitors in Europe won't notice; visitors in Australia will.
- Edge functions and Vercel-specific services such as Vercel Blob, Edge Config, KV, Cron Jobs and Web Analytics.
- Automatic image optimisation as a managed service.
next/imagestill works in standalone mode, but the resizing runs inside your app and uses its memory. - Scale to zero and per-request billing. An app on blitz.cloud is a long-running process with 128 MB of memory reserved and a ceiling of 512 MB.
If the second list is where your app lives, stay on Vercel or look at a host that has those features today. If your app is a normal Next.js server with a database and the reason to move is EU hosting, keep going.
Step 1: build a standalone image
Turn on standalone output in next.config.mjs:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
export default nextConfig;
Add a Dockerfile next to package.json:
FROM node:22.23.2-alpine3.24 AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22.23.2-alpine3.24 AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:22.23.2-alpine3.24
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=8080 HOSTNAME=0.0.0.0
# standalone/ holds server.js and the node_modules it needs; static files and
# public/ are not copied into it by Next, so they are added here.
COPY --from=build --chown=1000:1000 /app/.next/standalone ./
COPY --from=build --chown=1000:1000 /app/.next/static ./.next/static
COPY --from=build --chown=1000:1000 /app/public ./public
USER 1000
EXPOSE 8080
CMD ["node", "server.js"]
And a .dockerignore, so your local build output doesn't end up in the image:
node_modules
.next
.git
HOSTNAME=0.0.0.0 makes the server listen on every interface instead of only inside the container. The --chown flags let the app write its cache as user 1000.
We tested this Dockerfile with Next.js 16.3.5: a page, a route handler and a file from public/ all answered 200 when run as user 1000 with every capability dropped. If you use pnpm or yarn, swap the install command and lockfile name.
Step 2: test it locally the way blitz.cloud runs it
docker build --platform linux/amd64 -t yourname/my-next-app:1.0.0 -t yourname/my-next-app:latest .
docker run --rm --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges \
-p 8080:8080 --env-file .env.production.local yourname/my-next-app:1.0.0
Click through the app at http://localhost:8080 before you go further. Anything that breaks here, like a missing variable or a write to a folder user 1000 may not touch, would break the same way on blitz.cloud.
Step 3: move the environment variables
Pull the production variables from Vercel with the CLI:
vercel env pull .env.production.local --environment=production
There is one trap. Variables starting with NEXT_PUBLIC_ are baked into the JavaScript at build time. Setting them on blitz.cloud afterwards changes nothing in the browser. Pass them to docker build instead, for example with ARG NEXT_PUBLIC_API_URL and ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL in the build stage. Server-only variables are read when the app runs, so those go on blitz.cloud's Environment tab.
Remove anything Vercel-specific, like VERCEL_URL, and replace code that relies on it with your new address.
Step 4: push and deploy
docker push --all-tags yourname/my-next-app
The Docker Hub repository must be public. Your image contains your built app, so anyone can pull it; keep secrets out of the image and in environment variables.
- In the blitz.cloud dashboard, click Host something new and choose "An app that is already packaged up".
- Search for
yourname/my-next-appand pick1.0.0. - Check What this app needs says port 8080. If the app uses a database, open Advanced settings and switch on Needs a database.
- Pick an address and click Put it online.
- Open the app's Environment tab, add your server-side variables and restart the app.
Step 5: the database
If your database is Vercel Postgres, Neon or Supabase, the least disruptive option is to leave it where it is for now and point DATABASE_URL at it. Apps on blitz.cloud can make outgoing connections. Note that this keeps your data outside the EU if that database is outside the EU, which may be the very thing you are trying to fix.
To bring the data into blitz.cloud, create the app with a PostgreSQL database, then load a dump from inside your app. There is no way to connect psql to a blitz.cloud database from your laptop today, so a one-off import has to run as part of the app, for example a migration script that restores seed data on first start. For a large database that is awkward, and it is worth waiting until outside access exists.
Checklist
-
output: "standalone"set and the image builds forlinux/amd64 - Image runs locally with
--user 1000:1000 --cap-drop ALL -
NEXT_PUBLIC_variables passed at build time - Server-side variables added on the Environment tab, app restarted
- No Vercel-only APIs left (Blob, KV, Edge Config,
VERCEL_URL) - Database either reachable from blitz.cloud or migrated
- Links and OAuth callback URLs updated to the new
*.blitz.cloudaddress - Old Vercel project kept running until the new one has served real traffic
Why move at all?
Vercel is a US company, so data it handles falls under US law, including the CLOUD Act, whatever region it sits in. blitz.cloud is run by BlitzWorks UG (haftungsbeschränkt) in Regensburg on Hetzner servers in Germany, and neither company has a US parent. For some teams that settles a GDPR question. More on Data location and GDPR. A side-by-side feature comparison is on blitz.cloud vs Vercel.
Put your first app online today.
Free plan, no credit card, no waiting list.
Create a free account