Como publicar Nuxt na VPS com Docker e HTTPS (preset Node, Caddy e variáveis de produção)

Ilustração de um container Docker conectado a um servidor VPS com cadeado de HTTPS representando deploy do Nuxt

Fala aí, beleza? Subir um app Nuxt numa VPS parece tranquilo até tu perceber que o comando que funcionava na tua máquina não foi feito pra produção 😛

Este guia cobre um cenário bem específico: Nuxt 4.x, uma VPS com Docker Compose e um domínio próprio apontando pra ela

A ideia é ter dois containers, um com o servidor Node gerado pelo build e outro com o Caddy cuidando do HTTPS

E já fica o aviso logo de cara: nuxt dev é pra desenvolvimento e nuxt preview serve só pra pré-visualizar o build (o nuxt start é apenas um alias do preview), nenhum deles é servidor de produção!

Pré-requisitos para publicar Nuxt na VPS com Docker

Você precisa de quatro coisas: Node compatível com o Nuxt 4.x, uma VPS com Docker e acesso root, um domínio apontando pro servidor e as portas certas liberadas

Bora por partes 🙂

Qual versão do Node usar no Nuxt 4?

O requisito oficial do Nuxt 4.x é Node.js 22.x ou mais novo, com recomendação da versão LTS ativa (as de número par)

Se liga num detalhe: o Nuxt CLI v4, que chegou junto com o Nuxt 4.6 em outubro de 2026, exige Node v22.21+, v24.11+ ou v26+

Então, na hora de escolher a imagem Node do Dockerfile, pega uma tag que caia dentro dessas faixas

Qual VPS usar para o Nuxt com Docker?

A recomendação da Hora de Codar pra este projeto é a VPS da Hostinger (aviso: este artigo contém links de afiliado da Hostinger)

O motivo é técnico: o tutorial precisa de acesso root total pra rodar Docker e mexer em portas, e a VPS da Hostinger entrega isso com processadores AMD EPYC, armazenamento NVMe SSD e rede de 1 Gb/s em todos os planos, segundo a página oficial do serviço

Outro ponto que conversa direto com deploy: backups semanais gratuitos e recurso de snapshot, que te deixam reverter o servidor se um deploy der ruim

E tem um atalho massa: o template Ubuntu 24.04 com Docker, que já vem com docker-ce e docker-compose instalados

No hPanel o caminho é: VPS > Gerenciar > SO e Painel > Sistema Operacional, onde você escolhe o template Ubuntu 24.04 com Docker entre as opções de sistema disponíveis

Tome cuidado: a troca ou reinstalação do SO apaga permanentemente todos os dados atuais, inclusive o snapshot (mas não afeta os backups já existentes), então faz isso numa VPS nova ou logo no início, antes de ter algo importante lá

Domínio e portas

O domínio precisa ter os registros A (IPv4) e/ou AAAA (IPv6) apontando pro IP da VPS

E as portas 22 (SSH), 80 e 443 precisam estar liberadas, porque o Caddy usa 80 e 443 pra validar o domínio e servir o site

Passo a passo: Nuxt em Docker com Caddy e HTTPS

O fluxo é o mesmo de qualquer app em container atrás de proxy, se tu já viu como publicar uma API Go com Docker e HTTPS, vai reconhecer a lógica

  1. Configure o firewall da VPS Hostinger

Por que primeiro? Porque sem 80 e 443 abertas o Caddy não consegue emitir o certificado

No hPanel, dentro da seção do seu servidor VPS, acesse Firewall

O firewall gerenciado descarta todo o tráfego de entrada por padrão, então você precisa criar uma regra de aceitar para cada porta necessária: e a regra de SSH precisa vir primeiro, antes de qualquer outra, pra não perder o acesso ao servidor

Crie regras de aceitar para as portas nesta ordem:

  • 22 (SSH), ANTES de qualquer outra coisa
  • 80 (HTTP)
  • 443 (HTTPS)

Os demais campos da regra (protocolo e outras opções) variam conforme a versão da tela no hPanel, então confira as opções disponíveis na própria interface no momento do seu acesso

Resultado esperado: firewall com as três regras de entrada criadas

O erro comum deste passo: criar a regra de SSH depois das outras, ou esquecer dela

No firewall gerenciado, por padrão todo o tráfego é descartado, então sem a regra de SSH tu perde o acesso ao servidor na hora

  1. Confirme o preset e gere o artefato de produção

O preset node-server é o padrão do Nitro quando nenhum preset é especificado nem detectado automaticamente: numa VPS genérica como esta, sem plataforma que acione autodetecção, se tu não trocou o preset no nuxt.config, já está no lugar certo

Rode o build (aqui assumindo que o script build do teu package.json chama nuxt build):

npm run build

Resultado esperado: o arquivo .output/server/index.mjs existe no projeto

Esse é o ponto de entrada que já sobe um servidor Node pronto, e o comando oficial pra rodar é:

NODE_ENV=production node .output/server/index.mjs

Por padrão ele escuta na porta 3000

O erro comum deste passo: publicar com nuxt preview ou nuxt start

A documentação do comando preview deixa claro: ele existe pra pré-visualizar, carrega o .env no process.env e, em produção, quem define as variáveis é você

  1. Monte o Dockerfile multi-stage

A ideia aqui é separar: um estágio instala dependências e faz o build, o outro só roda o resultado

# Troque <TAG> por uma tag oficial do Node em 22.21+, 24.11+ ou 26+
FROM node:<TAG> AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:<TAG>
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

E um .dockerignore pra não mandar lixo (nem segredo) pra imagem:

node_modules
.output
.env

Se teu projeto usa pnpm ou yarn, adapta as linhas de instalação

Antes de seguir, testa: sobe o container e confere se o app responde

Resultado esperado: a imagem builda e o container inicia o servidor na porta 3000

O erro comum deste passo: esquecer o NODE_ENV=production

Algumas dependências, como o Vue Router, só removem os avisos de desenvolvimento com essa variável, sem ela os logs podem ser inundados

  1. Defina as variáveis de produção no Compose

O runtimeConfig do Nuxt pode ser sobrescrito por variáveis de ambiente com prefixo

Primeiro declara as chaves no nuxt.config.ts:

export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: '',
    public: {
      apiBase: ''
    }
  }
})

Aí, no docker-compose.yml, o prefixo decide quem enxerga o quê:

  • NUXT_ sobrescreve valores do runtimeConfig, que só existem no servidor
  • NUXT_PUBLIC_ sobrescreve valores do runtimeConfig.public, que vão pro navegador
    environment:
      NODE_ENV: production
      NUXT_API_SECRET: troque-por-um-segredo-real
      NUXT_PUBLIC_API_BASE: https://api.seudominio.com

Porta e host do servidor também são variáveis: NITRO_PORT ou PORT (padrão 3000) e NITRO_HOST ou HOST (padrão 0.0.0.0)

Neste cenário os padrões já servem, só mexe se precisar

O erro comum deste passo: confiar no .env

Em produção o Nuxt não lê o arquivo .env, ele vale em desenvolvimento e no build, então as variáveis precisam vir do ambiente do container

E nunca coloque segredo em variável NUXT_PUBLIC_, porque tudo que está em public chega no cliente

  1. Deixe a porta 3000 só na rede interna

Que diferença faz ports ou expose? Toda!

  • ports publica a porta no host, ficando acessível por fora do container
  • expose não publica a porta no host, deixando-a disponível apenas pros outros serviços dentro da rede Docker

Como quem fala com o Nuxt é o Caddy, o serviço app fica só com expose:

  app:
    build: .
    environment:
      NODE_ENV: production
      NUXT_API_SECRET: troque-por-um-segredo-real
      NUXT_PUBLIC_API_BASE: https://api.seudominio.com
    expose:
      - "3000"

O erro comum deste passo: colocar "3000:3000" em ports e deixar o app acessível direto pela internet, por fora do HTTPS

Usando expose, a porta 3000 fica restrita à rede interna criada pelo Compose, e só o Caddy (que está na mesma rede) consegue alcançá-la

  1. Configure o Caddy como proxy com HTTPS automático

O Caddy faz HTTPS automático quando sabe o domínio: emite o certificado, redireciona HTTP pra HTTPS e renova antes de expirar

Crie uma pasta caddy/ na raiz do projeto e, dentro dela, o arquivo Caddyfile:

seudominio.com {
  reverse_proxy app:3000
}

O app ali é o nome do serviço no Compose, e o Docker resolve esse nome dentro da rede interna

Agora o docker-compose.yml completo:

services:
  app:
    build: .
    environment:
      NODE_ENV: production
      NUXT_API_SECRET: troque-por-um-segredo-real
      NUXT_PUBLIC_API_BASE: https://api.seudominio.com
    expose:
      - "3000"

  caddy:
    image: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy:/etc/caddy
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - app

volumes:
  caddy_data:
  caddy_config:

Na imagem oficial do Caddy, os certificados e chaves ficam em /data (fundamental persistir) e as configs ficam em /config; o Caddyfile padrão mora em /etc/caddy, então a forma confirmada de usar teu próprio arquivo é montar a pasta caddy/ inteira nesse caminho, como no Compose acima

E o Nitro servindo HTTPS sozinho? Ele até consegue, se receber NITRO_SSL_CERT e NITRO_SSL_KEY, mas a própria documentação do Nitro recomenda isso só pra teste, com o Nitro atrás de um proxy reverso que termina o SSL em produção

Ou seja, a emissão e a renovação do certificado viram problema teu, enquanto o Caddy já faz isso automático

Neste cenário, deixa o Nuxt em HTTP na rede interna e o HTTPS fica com o Caddy 🙂

O erro comum deste passo: não persistir o /data

Sem volume, cada recriação do container joga fora os certificados e o Caddy precisa emitir tudo de novo

  1. Suba e valide

Na VPS, dentro da pasta do projeto:

docker-compose up -d --build

Agora confere três coisas:

  • https://seudominio.com abre com cadeado válido no navegador
  • http://seudominio.com redireciona pra HTTPS (dá pra ver com curl -I http://seudominio.com)
  • os logs dos dois containers estão limpos
docker-compose logs app
docker-compose logs caddy

Resultado esperado: site no ar em HTTPS e nenhum acesso direto pela porta 3000

Manutenção a cada deploy

Mudou o código? Rebuilda a imagem com o mesmo docker-compose up -d --build, porque o .output nasce dentro do build

E antes de mudanças grandes (troca de versão do Node, do Nuxt ou do proxy), cria um snapshot da VPS no hPanel, assim tu tem pra onde voltar

Erros comuns ao publicar Nuxt com Docker e Caddy

Quase tudo que dá errado nesse setup é configuração, não servidor fraco

Então nada de trocar de plano por causa de erro de config haha

Certificado HTTPS não é emitido

Causa: o domínio não aponta pra VPS (registros A/AAAA errados) ou as portas 80 e 443 estão fechadas no firewall

Correção: confere os registros DNS e as regras do firewall, e veja o log com docker-compose logs caddy

Prevenção: configura DNS e firewall antes do primeiro up

Sem 80 e 443 acessíveis, o Caddy não consegue completar a validação padrão do certificado; resolver isso por outro caminho é uma configuração que fica fora do escopo deste guia

Variável não aparece no app

Causa: a variável está só no .env, está sem o prefixo NUXT_ ou NUXT_PUBLIC_, a chave não foi declarada no runtimeConfig ou é um valor privado sendo lido no cliente

Correção: declara a chave no nuxt.config, passa a variável no environment do Compose com o prefixo certo e recria o container

Prevenção: lembra da regra: privado só no servidor, public vai pro navegador

Caddy retorna erro de upstream

Causa: o reverse_proxy aponta pra um nome de serviço ou porta diferente do que o Compose usa, ou alguém mudou o NITRO_PORT e esqueceu do Caddyfile

Correção: alinha o nome do serviço (app) e a porta (3000, ou a que estiver no NITRO_PORT) nos dois arquivos

Prevenção: se não precisa mudar a porta, não muda 😛

Logs inundados de avisos

Causa: o container roda sem NODE_ENV=production

Correção: define a variável no Dockerfile ou no environment e recria o container

Prevenção: deixa no Dockerfile, assim não depende de alguém lembrar no Compose

App acessível direto pela porta 3000

Causa: o serviço app usa ports em vez de expose

Correção: troca por expose: ["3000"]

Prevenção: no serviço do app, a regra é não publicar nada no host

Vídeo complementar do canal Hora de Codar, sobre um assunto relacionado.

Próximo passo depois do deploy

Com o site no ar, a prioridade é endurecer o servidor: o firewall já está lá, então vale somar outras camadas de proteção, como o Fail2ban

Depois cria um snapshot da VPS logo após o primeiro deploy funcionando, porque ele vira teu ponto de retorno conhecido

E documenta o rebuild num README curto: tag do Node usada, variáveis esperadas (sem os valores dos segredos) e o comando de deploy, assim o próximo deploy não depende da tua memória

Se tu prefere gerenciar deploys por painel em vez de Compose na mão, dá uma olhada em como instalar o Coolify numa VPS

E se ainda vai contratar o servidor, dá pra conferir a VPS para este projeto e já começar pelo template com Docker

Até o próximo post! 🙂

Perguntas frequentes

O Nuxt 5 já está disponível pra esse tipo de deploy?

Não, o Nuxt 5 ainda não é estável e o roadmap prevê o lançamento pro 4º trimestre de 2026 (estimativa). A versão atual é a linha 4.6 do Nuxt, iniciada em outubro de 2026 junto com o Nuxt CLI v4, que é a base deste guia.

O Caddy emite certificado HTTPS se eu usar só o IP da VPS, sem domínio?

Não, o HTTPS automático do Caddy depende do domínio estar presente na configuração, porque é assim que ele sabe pra quem emitir o certificado. Sem domínio apontando pro servidor, não dá pra seguir o fluxo deste guia; resolver isso exigiria outra configuração, fora do escopo deste tutorial.


Quer aprender mais sobre inteligência artificial e programação na prática? Conheça o canal Hora de Codar no YouTube e acompanhe os próximos vídeos.

Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Formações

Formação Claude Code

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 48min

Blog | Mais populares