Mova uma aplicação Next.js da Vercel pro seu próprio servidor
Siga isso no seu próprio servidor. Comece grátis, sem cartão
A sua aplicação Next.js roda na Vercel e você quer ela num servidor que você controla, com uma conta mensal fixa. O código quase não muda. O que muda é tudo que a Vercel fazia em volta dele: fazer build em todo push, guardar as suas variáveis de ambiente, definir algumas variáveis próprias, rodar cron jobs, e servir HTTPS. Este guia move cada uma dessas peças pra um único servidor, depois troca o DNS sem downtime, e mantém o deploy na Vercel como o seu plano B até o novo ter se provado.
O que você vai precisar
- Um servidor. Este guia usa um droplet Basic da DigitalOcean,
s-1vcpu-2gb(1 vCPU, 2 GB de memória, 50 GB SSD), a US$12 por mês. Se o seu build é pesado,s-2vcpu-4gbé US$24 por mês. Qualquer servidor Ubuntu 24.04 com um IP público funciona do mesmo jeito. - Acesso ao DNS do domínio, onde quer que ele esteja hospedado hoje (os nameservers da Vercel ou o seu registrador).
- O CLI da Vercel na sua própria máquina (
npm i -g vercel), logado na equipe que é dona do projeto. - O repositório do qual a Vercel faz deploy, com um lockfile commitado.
Os comandos assumem Next.js 16 (16.4.3 no momento em que isso foi escrito) e Node.js 24, a linha LTS atual.
Passo 1: liste o que a Vercel faz por você
Antes de tocar no servidor, cheque o projeto pelas partes que dependem da própria Vercel:
grep -rn "VERCEL_" --include=*.ts --include=*.tsx --include=*.js --include=*.mjs . | grep -v node_modules
cat vercel.json 2>/dev/null
Você tipicamente vai encontrar algumas destas:
- Variáveis de ambiente de sistema. A Vercel define
VERCEL_ENV,VERCEL_URL,VERCEL_PROJECT_PRODUCTION_URL,VERCEL_GIT_COMMIT_SHAe outras em todo deploy. Nenhuma delas existe no seu servidor. O Passo 3 substitui as que o seu código lê. - Cron jobs definidos em
cronsnovercel.json. A Vercel dispara eles mandando uma requisição GET pro caminho que você listou, num horário em UTC. O Passo 8 move eles pro cron do servidor. - Deploys de preview pra toda branch e pull request. Um servidor só roda um ambiente; as perguntas frequentes cobrem as suas opções.
- Pacotes exclusivos da Vercel como
@vercel/analyticsou@vercel/functions. Cheque a documentação deles pro que fazem fora da Vercel antes de depender deles.
As próprias funcionalidades do framework vêm com você. O guia de self-hosting do Next.js afirma que a Image Optimization através do next/image e o Proxy (a convenção de arquivo antes chamada de Middleware) funcionam os dois sem configuração alguma quando você roda o servidor do Next.js.
Passo 2: exporte as suas variáveis de ambiente
A partir do diretório do projeto, conecte ele e baixe as variáveis de produção pra um arquivo:
vercel link
vercel env pull .env.vercel --environment=production
Abra o .env.vercel e cheque todo valor. Variáveis armazenadas com o tipo Secret da Vercel não podem ser lidas de volta, então o CLI escreve um placeholder pra cada uma delas. Pegue os valores reais de onde quer que eles vieram (o seu provedor de banco de dados, o Stripe, o seu provedor de autenticação) e preencha eles. Mantenha esse arquivo fora do Git; o .gitignore padrão do Next.js já ignora arquivos .env*.
Você deve acabar com uma lista completa de linhas NAME=value e nenhum placeholder.
Passo 3: substitua as variáveis que a Vercel definia por você
Pra cada variável VERCEL_ que o seu código lê, adicione uma variável própria e aponte o código pra ela. O caso comum é construir URLs absolutas:
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
Depois adicione NEXT_PUBLIC_SITE_URL=https://app.example.com ao seu arquivo de ambiente. Faça o mesmo pras bibliotecas de autenticação: o Auth.js lê AUTH_URL (o NextAuth v4 lê NEXTAUTH_URL), e na Vercel ele conseguia inferir a URL. No seu servidor, defina ela pro seu domínio real.
Faça commit das mudanças de código. Você deve conseguir rodar grep -rn "VERCEL_" de novo e não ver nada que a sua aplicação ainda precisa.
Passo 4: ative a saída standalone
Adicione output: "standalone" ao next.config.ts, mantendo as suas outras opções:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
Com isso, next build escreve um servidor autocontido em .next/standalone, então a imagem de produção não precisa do seu node_modules completo.
Passo 5: adicione um Dockerfile
Este Dockerfile segue o exemplo oficial with-docker do repositório do Next.js: instala dependências com o gerenciador de pacotes a que o seu lockfile pertence, faz o build, e roda só a saída standalone como o usuário sem privilégios node.
FROM node:24-slim AS dependencies
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; \
elif [ -f yarn.lock ]; then corepack enable yarn && yarn install --frozen-lockfile --production=false; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \
else echo "No lockfile found." && exit 1; \
fi
FROM node:24-slim AS builder
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV NODE_ENV=production
RUN if [ -f package-lock.json ]; then npm run build; \
elif [ -f yarn.lock ]; then corepack enable yarn && yarn build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm build; \
fi
FROM node:24-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
COPY --from=builder --chown=node:node /app/public ./public
RUN mkdir .next && chown node:node .next
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Na Vercel, toda variável estava presente durante o build. No Docker, só o que você passa está. O Next.js embute variáveis NEXT_PUBLIC_ no bundle do navegador durante o next build, então cada uma precisa de um par ARG e ENV no estágio de build, como NEXT_PUBLIC_SITE_URL acima. Variáveis só do servidor não precisam; a aplicação lê elas em tempo de execução.
Adicione um .dockerignore pra manter arquivos locais e secrets fora do build:
.git
.next
node_modules
.env*
Faça o build e rode ele uma vez na sua própria máquina com as variáveis exportadas, pra pegar problemas antes do servidor estar envolvido:
docker build --build-arg NEXT_PUBLIC_SITE_URL=http://localhost:3000 -t my-app .
docker run --rm -p 3000:3000 --env-file .env.vercel my-app
Você deve ver a aplicação em http://localhost:3000, não uma quebra sobre uma variável faltando.
Passo 6: prepare o servidor
Crie o droplet (Ubuntu 24.04, Basic, US$12), conecte via SSH e instale o Docker com o script oficial dele:
curl -fsSL https://get.docker.com | sh
docker compose version
Você deve ver uma versão do Compose impressa. Depois clone o repositório e copie o seu arquivo de ambiente pra ele, renomeado pra .env:
git clone https://github.com/you/your-app.git /opt/your-app
scp .env.vercel root@your-server-ip:/opt/your-app/.env
Rode o primeiro comando no servidor e o segundo na sua própria máquina. No servidor, confirme que o .env contém NEXT_PUBLIC_SITE_URL com o seu domínio real, e trave o arquivo com chmod 600 /opt/your-app/.env.
Passo 7: adicione o Compose e o Caddy, depois inicie
O guia de self-hosting do Next.js recomenda um proxy reverso na frente do servidor Node. O Caddy preenche esse papel e trata os certificados. Adicione compose.yaml ao repositório:
services:
app:
build:
context: .
args:
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL}
restart: unless-stopped
env_file: .env
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- app
volumes:
caddy-data:
caddy-config:
E um Caddyfile com o seu domínio:
app.example.com {
reverse_proxy app:3000
}
Faça commit dos dois e git pull no servidor. Inicie só a aplicação por enquanto. O Caddy espera até o Passo 9, porque ele não consegue um certificado enquanto o domínio ainda aponta pra Vercel:
cd /opt/your-app
docker compose up -d --build app
O primeiro build leva alguns minutos. Depois cheque se a aplicação responde dentro do próprio container:
docker compose exec app node -e "fetch('http://localhost:3000').then((r) => console.log(r.status))"
Você deve ver 200. Se você ver um erro em vez disso, docker compose logs app mostra por que a aplicação não iniciou, que geralmente é uma variável faltando.
Passo 8: mova os cron jobs da Vercel pro servidor
Pra cada entrada em crons no vercel.json, adicione uma linha de crontab que requisita o mesmo caminho. Os agendamentos da Vercel são sempre UTC, e um servidor Ubuntu novo também é, então as expressões copiam sem mudar nada. Se a sua rota checa CRON_SECRET (a Vercel manda ele como Authorization: Bearer <value>), continue checando ele e mande o header você mesmo.
Crie um script pequeno no servidor:
cat > /opt/your-app/cron-request.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
set -a
source /opt/your-app/.env
set +a
curl -fsS -H "Authorization: Bearer ${CRON_SECRET}" "https://app.example.com$1"
EOF
chmod 700 /opt/your-app/cron-request.sh
Depois crontab -e e adicione uma linha por job, usando o caminho e o agendamento do vercel.json:
0 5 * * * /opt/your-app/cron-request.sh /api/cron >> /var/log/your-app-cron.log 2>&1
Coloque um # na frente de cada linha por enquanto, e remova quando o DNS tiver se movido no Passo 9, pra nenhum job rodar nas duas plataformas ao mesmo tempo. Uma vez no ar, tail /var/log/your-app-cron.log depois do horário agendado deve mostrar a resposta da sua rota.
Passo 9: troque o DNS
Reduza o TTL no registro de DNS do domínio um dia antes se você puder. Depois mude o registro pra apontar pro seu servidor: um registro A com o endereço IPv4 do servidor, substituindo o registro da Vercel. Se o domínio usa os nameservers da Vercel, faça a mudança nas configurações de DNS da Vercel, ou mova o domínio pra outro provedor de DNS primeiro.
Espere até o endereço novo ser o que o mundo vê:
dig +short app.example.com
Quando isso imprimir o IP do seu servidor, inicie o Caddy e observe ele pegar o certificado:
docker compose up -d caddy
docker compose logs -f caddy
Você deve ver uma linha dizendo que o certificado foi obtido com sucesso. Depois https://app.example.com carrega do seu servidor com um certificado válido. Descomente as linhas de crontab e desative os cron jobs nas configurações do projeto na Vercel.
Mantenha o projeto na Vercel por alguns dias. Visitantes cujo DNS ainda não atualizou continuam alcançando ele, e se algo der errado você pode apontar o registro de volta.
Passo 10: implante atualizações e mantenha backups
Um release agora é:
cd /opt/your-app
git pull
docker compose up -d --build app
O Caddy continua rodando e mantém os próprios certificados; a aplicação fica indisponível pelos poucos segundos que o container novo leva pra iniciar. A Vercel também para de fazer build dos seus pushes quando você desconectar a integração com o Git, então faça isso quando você estiver confiante na configuração nova.
O próprio servidor não guarda nada que você não consiga reconstruir exceto o .env, então mantenha uma cópia dele num gerenciador de senhas. O seu banco de dados fica onde está hoje e mantém os próprios backups; você pode migrar ele separadamente depois.
O caminho mais rápido: faça isso com a Railyard
Os Passos 4 a 10 são o que a Railyard faz por você. Conecte o mesmo repositório, e ela faz o build e o deploy da aplicação num servidor na sua própria conta de nuvem, com HTTPS e um redeploy em todo push. Não existe Dockerfile, Compose file ou Caddyfile pra escrever.
- Adicione um servidor: no painel de controle da DigitalOcean, abra API, gere um personal access token com acesso de leitura e escrita, e crie o droplet em Servers → Add a server na Railyard. Ou conecte um servidor que você já tem (Ubuntu 22.04 ou 24.04,
sudosem senha, portas 22, 80 e 443 abertas). - Faça deploy do repositório, ou abra
https://app.railyard.run/deploy?repo=https://github.com/you/your-app.gitcom a sua própria URL. - Cole as variáveis do Passo 2 em Variables → Bulk import.
A Railyard escolhe o gerenciador de pacotes a partir do seu lockfile, roda o seu script de build e inicia a aplicação com npm start, escutando em $PORT. Postgres ou Redis adicionados pela página da aplicação têm a connection string injetada como uma variável. Recrie cada cron job do vercel.json como uma entrada na aba Cron jobs da aplicação, que roda um comando na imagem da aplicação num horário agendado (em UTC) e mantém um histórico de cada execução. Pra requisitar uma rota, use o fetch nativo do Node com a variável RAILYARD_URL da aplicação:
node -e "fetch(process.env.RAILYARD_URL + '/api/cron', { headers: { authorization: 'Bearer ' + process.env.CRON_SECRET } }).then((r) => process.exit(r.ok ? 0 : 1))"
``` Previews de pull request podem ser ativados pra aplicação depois que o GitHub for conectado; veja [Environments and previews](/docs/environments-and-previews).
O plano Free cobre um servidor mais um servidor de banco de dados e três aplicações, sem precisar de cartão; o Pro é US$19 por mês por equipe e o Team é US$49 por mês por equipe. Comece com [Deploy from a repository](/docs/deploy-from-a-repository), ou veja [Deploy Next.js to DigitalOcean](/deploy/nextjs-to-digitalocean) pro lado do provedor. Também movendo aplicações do Heroku, Render, Fly ou Railway? [Import from another platform](/docs/import-from-another-platform) lê elas com um token de API.
## Resolução de problemas
**O build falha com uma variável faltando, ou um valor `NEXT_PUBLIC_` fica vazio no navegador.** Ele não estava presente durante o `next build`. Adicione um par `ARG` e `ENV` pra ele no Dockerfile, passe ele em `build.args` no `compose.yaml`, e refaça o build. Reiniciar o container não muda valores que foram embutidos em tempo de build.
**Logins redirecionam pra um endereço `vercel.app`.** A sua URL de autenticação ainda tem um valor da Vercel a partir da exportação. Defina `AUTH_URL` ou `NEXTAUTH_URL` pro seu domínio no `.env` e rode `docker compose up -d app`.
**O Caddy registra erros de certificado por horas.** O DNS ainda não está apontando pro servidor, ou um firewall bloqueia as portas 80 e 443. `dig +short app.example.com` deve retornar o IP do seu servidor. O Caddy tenta de novo por conta própria depois que isso acontecer.
**Código que lê geolocalização ou headers de requisição da Vercel não recebe nada.** Headers como `x-vercel-ip-country` são adicionados pela rede da Vercel e não existem atrás do Caddy. Remova esse caminho de código ou use um serviço de geolocalização próprio.
**O build é derrubado no servidor de 2 GB.** Ele ficou sem memória. Adicione um arquivo de swap de 2 GB (`fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile`), mude pro tamanho de US$24, ou faça o build da imagem em outro lugar e baixe de um registry.
## Perguntas frequentes
**O código da minha aplicação vai precisar mudar?** Geralmente só onde ele lê variáveis `VERCEL_` ou usa pacotes específicos da Vercel. Routing, Server Components, Server Actions, Image Optimization e Proxy todos rodam no servidor standalone.
**E os deploys de preview?** Um servidor serve um ambiente. Um segundo servidor, menor, pro staging, construído a partir de outra branch, cobre a maioria das necessidades. Na Railyard, previews de pull request são uma configuração na aplicação.
**O ISR continua funcionando?** Sim. O Next.js armazena o cache dele no disco local de cada instância de servidor por padrão, que funciona pra uma instância só. O cache começa vazio depois de cada deploy, porque o container é substituído.
**Eu posso continuar usando serviços que comprei através da Vercel?** Serviços que te dão uma connection string ou token de API, como um banco de dados do Marketplace, continuam funcionando a partir de qualquer servidor desde que você copie as credenciais. Cheque a documentação própria de cada um pelos limites fora da Vercel.
A mudança é principalmente burocracia: variáveis, cron jobs e DNS. Depois que esses três estão feitos, um release no seu próprio servidor é um `git pull` e um comando. Por que escolhem o Railyard para isso
- Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
- Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
- Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
- Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
- Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.