Tutoriais de VPS
Montar um blog Ghost em VPS (2026): Docker Compose, MySQL, HTTPS com Caddy, e-mail e backups
Implante um blog Ghost 6 com MySQL 8 em uma VPS usando Docker Compose, configure HTTPS com Caddy, e-mail transacional, persistência de conteúdo, atualizações e restauração de backups, ideal para blogs pessoais e pequenas equipes de conteúdo.

Se você quer um blog independente que carregue rápido, tenha um painel administrativo limpo e uma experiência de escrita fluida, o Ghost costuma dar menos dor de cabeça do que um WordPress cheio de plugins. Ele já vem com assinaturas de membros, newsletters, temas e uma API de conteúdo, e o front-end não carrega um monte de scripts administrativos inúteis.
Mas o Ghost não é algo que você sobe em um contêiner e coloca em produção com tranquilidade. Um ambiente de produção precisa de MySQL 8, uma URL pública correta, um proxy reverso com HTTPS, um diretório de conteúdo persistente e backups de banco de dados que possam ser restaurados. Se o e-mail não estiver configurado corretamente, os links de login de membros, a redefinição de senha e os convites também falharão.
Este tutorial usa a imagem oficial do Docker ghost:6-alpine e o MySQL 8, gerencia os serviços com Docker Compose e deixa o Caddy cuidar do HTTPS automático. A porta 2368 do Ghost escuta apenas em 127.0.0.1, e o MySQL não é mapeado de forma alguma para a rede pública do host.
O Ghost é mais uma "ferramenta de publicação de conteúdo" do que um framework universal de criação de sites.
| Necessidade | Opção mais adequada | Motivo |
|---|---|---|
| Foco em escrever artigos, assinaturas de membros, newsletters | Ghost | Editor e sistema de membros integrados de forma mais direta |
| Loja, fórum, ecossistema complexo de plugins | WordPress | Mais opções de plugins e temas |
| Documentação pura, custo muito baixo, sem painel administrativo | Hugo / Astro | Podem gerar arquivos estáticos, menor superfície de ataque |
| Necessidade de expor conteúdo para um app ou front-end | Ghost | API de conteúdo integrada, utilizável como CMS headless |
A vantagem do Ghost é o caminho curto entre escrever e publicar; a desvantagem é que a forma de estender não é tão livre quanto no WordPress. Quando você precisa de formulários complexos, e-commerce ou dezenas de plugins de negócio, forçar o Ghost a ser um CMS universal encarece a manutenção.
O stack de produção oficial do Ghost exige MySQL 8, com base mínima de 1GB de memória. Quando o Docker executa Ghost, MySQL e Caddy no host ao mesmo tempo, recomendo começar com 2 núcleos, 2GB de RAM e 30GB SSD; com muitas imagens, crescimento de membros ou envios frequentes de e-mail, 2 núcleos e 4GB será bem mais confortável.
| Cenário | Configuração recomendada |
|---|---|
| Blog novo, pouco tráfego | 2 núcleos 2GB, 30GB SSD |
| Vários autores, recursos de membros, muitas imagens | 2-4 núcleos 4GB, 50GB SSD |
| Grande acervo de conteúdo histórico ou alta concorrência | A partir de 4 núcleos 8GB, avaliando banco de dados e armazenamento de objetos separadamente |
O sistema do exemplo é o Ubuntu 24.04 LTS. Aponte com antecedência os registros A / AAAA de blog.example.com para a VPS, e abra apenas SSH, 80 e 443 no grupo de segurança e no UFW.
Instale o Docker Engine e o plugin Compose a partir do repositório oficial do Docker. Ao terminar, confirme que está usando o Compose v2:
docker --version
docker compose version
Se o comando só funciona com hífen, docker-compose, significa que você ainda está usando o antigo Compose v1; atualize antes de continuar.
Crie o diretório de implantação:
sudo mkdir -p /opt/ghost/backups
sudo chown -R "$USER":"$USER" /opt/ghost
cd /opt/ghost
O Ghost usa uma conta de banco de dados dedicada; não deixe a aplicação usar a senha root do MySQL. Gere dois valores aleatórios diferentes:
openssl rand -hex 32
openssl rand -hex 32
nano .env
chmod 600 .env
Grave os dois valores aleatórios em .env, respectivamente:
GHOST_URL=https://blog.example.com
MYSQL_ROOT_PASSWORD=替换成第一组随机值
MYSQL_PASSWORD=替换成第二组随机值
GHOST_URL deve ser o endereço HTTPS completo que os leitores vão acessar no final. Ele afeta os redirecionamentos do painel, as URLs canônicas, os links de e-mail e os endereços de recursos; não coloque um IP primeiro e depois esqueça de alterar ao publicar.
Crie /opt/ghost/compose.yaml:
services:
db:
image: mysql:8.0
container_name: ghost-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD in .env}"
MYSQL_DATABASE: ghost
MYSQL_USER: ghost
MYSQL_PASSWORD: "${MYSQL_PASSWORD:?set MYSQL_PASSWORD in .env}"
volumes:
- ghost_mysql:/var/lib/mysql
healthcheck:
test:
- CMD-SHELL
- >-
mysqladmin ping -h 127.0.0.1 -uroot
-p$$MYSQL_ROOT_PASSWORD --silent
interval: 10s
timeout: 5s
retries: 20
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
ghost:
image: ghost:6-alpine
container_name: ghost
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:2368:2368"
environment:
NODE_ENV: production
url: "${GHOST_URL:?set GHOST_URL in .env}"
database__client: mysql
database__connection__host: db
database__connection__port: 3306
database__connection__user: ghost
database__connection__password: "${MYSQL_PASSWORD:?set MYSQL_PASSWORD in .env}"
database__connection__database: ghost
volumes:
- ghost_content:/var/lib/ghost/content
healthcheck:
test:
- CMD
- node
- -e
- >-
require('http').get('http://127.0.0.1:2368/ghost/',
r=>process.exit(r.statusCode<500?0:1))
.on('error',()=>process.exit(1))
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
ghost_mysql:
name: ghost_mysql
ghost_content:
name: ghost_content
Aqui ghost:6-alpine está fixado, de modo que as atualizações acompanham os patches e versões menores do Ghost 6, mas não saltam de repente para a próxima versão maior ao reconstruir o contêiner. O MySQL também está fixado na versão maior 8.0, porque o ambiente de produção do Ghost 6 só suporta MySQL 8; não substitua por MariaDB ou PostgreSQL por conta própria.
Antes de iniciar, peça ao Compose para verificar as variáveis e o YAML:
docker compose config >/dev/null
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 ghost
A primeira inicialização do MySQL e a migração do banco de dados do Ghost levam um tempo. running não significa que já está pronto; espere até que os dois serviços apareçam como healthy em docker compose ps.
Após instalar o Caddy, edite /etc/caddy/Caddyfile:
blog.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:2368
request_body {
max_size 50MB
}
}
O reverse_proxy do Caddy define os X-Forwarded-For, X-Forwarded-Host e X-Forwarded-Proto que o Ghost precisa. Se você trocar por uma configuração manual de Nginx, omitir X-Forwarded-Proto https facilmente causa um redirecionamento infinito.
Verifique e carregue a configuração:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
Se você ainda não domina certificados e configuração de múltiplos domínios, consulte o guia completo de Caddy como proxy reverso em VPS.
Confirme que o backend do Ghost escuta apenas localmente:
sudo ss -lntp | grep ':2368'
curl -I http://127.0.0.1:2368
curl -I https://blog.example.com
A primeira linha deve mostrar 127.0.0.1:2368, não 0.0.0.0:2368. O MySQL não tem configuração ports, e uma varredura pública também não deve ver a 3306.
Abra no navegador:
https://blog.example.com/ghost/
Siga as instruções da página para criar o proprietário do site. Ao terminar, verifique três coisas:
- A URL do site em Settings corresponde a
GHOST_URL; - Envie uma imagem de teste e confirme que ela continua aparecendo após atualizar a página;
- Publique um rascunho e confirme que tanto o link do front-end quanto a URL canônica usam o domínio HTTPS.
O conteúdo principal do Ghost fica no MySQL, enquanto imagens, temas, routes.yaml e outros arquivos ficam em /var/lib/ghost/content. Se qualquer um dos dois lados não for persistente, o backup fica incompleto.
O ambiente de produção do Ghost precisa de e-mail transacional funcional para convites de equipe, redefinição de senha, cadastro de membros e links de login. O SMTP comum pode cuidar desses e-mails; newsletters em massa são outra coisa: o Ghost auto-hospedado atualmente só suporta envio em massa pela API do Mailgun.
Adicione a configuração SMTP em .env:
MAIL_FROM='My Blog <[email protected]>'
MAIL_HOST=smtp.example.com
MAIL_PORT=465
MAIL_SECURE=true
MAIL_USER=替换成SMTP用户名
MAIL_PASSWORD=替换成SMTP密码
Depois adicione os seguintes campos em ghost.environment:
mail__transport: SMTP
mail__from: "${MAIL_FROM}"
mail__options__host: "${MAIL_HOST}"
mail__options__port: "${MAIL_PORT}"
mail__options__secure: "${MAIL_SECURE}"
mail__options__auth__user: "${MAIL_USER}"
mail__options__auth__pass: "${MAIL_PASSWORD}"
Reconstrua o contêiner do Ghost e verifique os logs:
docker compose up -d ghost
docker compose logs --tail=100 ghost
Não teste apenas se "a porta SMTP está acessível". Convide de fato um membro de teste e percorra uma redefinição de senha ou um link de login de membro. O domínio de envio também precisa de SPF, DKIM e DMARC configurados; caso contrário, a aplicação pode indicar sucesso enquanto o e-mail ainda cai no spam.
Esta configuração do Compose oferece o blog Ghost principal, o MySQL e o HTTPS. Os recursos completos de Web Analytics auto-hospedado, ActivityPub e similares do Ghost 6 dependem da nova cadeia de ferramentas oficial do Docker Compose, ainda marcada como preview, que implanta adicionalmente Caddy, Tinybird e serviços relacionados.
Se você só escreve um blog, usa temas, assinaturas de membros e a API de conteúdo, não precisa enfiar todos os componentes à força por "ter todos os recursos". Quando realmente quiser analytics auto-hospedado ou Social Web, avalie separadamente recursos, migração e reversão conforme a documentação oficial de Docker preview do Ghost; não misture as duas orquestrações diretamente em um mesmo arquivo.
Primeiro crie o diretório de backups:
cd /opt/ghost
mkdir -p backups
Faça um dump consistente com a conta root do MySQL e comprima no host:
docker compose exec -T db sh -c \
'mysqldump --single-transaction --quick --routines --triggers \
--databases "$MYSQL_DATABASE" -uroot -p"$MYSQL_ROOT_PASSWORD"' \
| gzip > "backups/ghost-db-$(date +%F-%H%M%S).sql.gz"
Depois faça backup do volume de conteúdo onde ficam as imagens, os temas e a configuração de rotas:
docker run --rm \
-v ghost_content:/source:ro \
-v "$PWD/backups:/backup" \
alpine sh -c \
'tar -czf /backup/ghost-content-$(date +%F-%H%M%S).tar.gz -C /source .'
Guarde também cópias criptografadas de compose.yaml e .env. Se o diretório de backups continuar na mesma VPS, uma falha de disco perderá tudo junto; copie para outra máquina ou armazenamento de objetos e defina um período de retenção.
Você pode testar a restauração primeiro em uma VPS que não seja de produção. O dump do banco de dados inclui a instrução de criação do banco, então pare o Ghost antes de importar:
docker compose stop ghost
gzip -dc backups/ghost-db-YYYY-MM-DD-HHMMSS.sql.gz \
| docker compose exec -T db sh -c \
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD"'
docker compose start ghost
A restauração do volume de conteúdo sobrescreve os arquivos existentes, então você precisa confirmar que está usando o backup correto e um ambiente de teste. Após restaurar, abra de fato o painel, as imagens dos artigos e as páginas de tema; não verifique apenas se o comando de importação retornou 0. Para um simulado mais completo em outra máquina, consulte o guia de simulado de backup e restauração em VPS.
Faça backup do banco de dados e do volume de conteúdo mesmo antes de atualizações menores:
cd /opt/ghost
docker compose pull ghost
docker compose up -d ghost
docker compose ps
docker compose logs --tail=200 ghost
Como a imagem está fixada em ghost:6-alpine, este conjunto de comandos só atualiza dentro do Ghost 6. Ao se preparar para um salto de versão maior, primeiro atualize a versão maior atual para a última versão menor, verifique os breaking changes do Ghost e valide os temas personalizados com o GScan. Não simplesmente troque 6 por latest e reconstrua o site de produção.
A aceitação após a atualização deve incluir pelo menos: login no painel, edição de artigos, upload de imagens, e-mail de login de membros, página inicial do tema e integrações de API. O contêiner não entrar em loop de reinício só prova que o processo iniciou, não que o blog está totalmente funcional.
docker compose ps
docker compose logs --tail=200 db
docker compose exec db mysqladmin ping -h 127.0.0.1 -uroot -p
Primeiro verifique se o MySQL está healthy e depois confira .env com a conta de banco de dados do Ghost. Depois que o volume de dados do MySQL já foi inicializado, alterar MYSQL_PASSWORD não muda automaticamente a senha antiga no banco de dados; não pense que reconstruir o contêiner recria a conta.
docker compose logs --tail=200 ghost
curl -I http://127.0.0.1:2368
sudo journalctl -u caddy --since '15 minutes ago' --no-pager
Se a 2368 não abre nem localmente, o problema está no Ghost ou no MySQL; se localmente funciona mas o domínio retorna 502, verifique o Caddy e o firewall. Não reinstale todo o stack de imediato.
Primeiro confirme que GHOST_URL em .env usa https://, e verifique se o proxy reverso repassa X-Forwarded-Proto: https. O Caddy trata isso por padrão; com uma segunda camada de CDN ou outro proxy na frente, continue verificando os cabeçalhos de protocolo do upstream.
O exemplo do Caddy limita o corpo da requisição a 50MB. Se o arquivo for maior, primeiro confirme se é razoável e depois ajuste juntos os limites do proxy e do Ghost. Não deixe ilimitado por conveniência: um painel público ainda pode esgotar disco e memória com requisições grandes.
- Tanto
dbquantoghostmostram healthy; - A 2368 escuta apenas em
127.0.0.1, e a 3306 não tem porta pública; - O HTTP redireciona automaticamente para HTTPS, sem loop de redirecionamento no painel;
- Você consegue criar um administrador, publicar artigos, enviar imagens e trocar de tema;
- O e-mail transacional chega de fato e as verificações SPF, DKIM e DMARC passam;
- O Ghost e o MySQL se recuperam automaticamente após reiniciar a VPS;
- O banco de dados e o volume de conteúdo foram copiados para outra máquina e um teste de restauração foi concluído.
Para verificações de saúde em produção, limites de logs e princípios de reversão, continue com a lista de configuração de produção do Docker Compose em VPS.
Se o seu objetivo é focar em escrever e gerenciar assinaturas de membros, uma VPS de 2 núcleos e 2GB consegue rodar o Ghost; o que realmente determina se este blog é utilizável a longo prazo não é a velocidade da página inicial, mas se URLs, e-mail, MySQL, o volume de imagens, atualizações e restauração funcionam juntos. Valide essas cadeias antes de migrar o domínio oficial e os artigos históricos.