Quando você migra um site entre servidores, o maior risco não é o rsync. O maior risco é homologar o servidor novo sem trocar DNS público e sem depender que cada pessoa do time edite hosts manualmente.
Neste case, implementei uma camada de proxy em PHP para comparar ambiente legado (andamento) e ambiente novo (fortis) lado a lado, usando o mesmo host lógico da aplicação. O objetivo foi validar comportamento real de VirtualHost, sessão, formulário e recursos estáticos antes do cutover.
1) Problema real de migração que eu precisava resolver
URL de produção estava funcionando, mas o ambiente novo ainda não podia receber tráfego público. Eu precisava:
- testar exatamente o mesmo domínio no backend novo;
- não alterar DNS global antes da homologação;
- evitar ajuste de
hostspor máquina; - comparar resposta legado vs novo no mesmo instante.
Esse é o ponto onde o proxy inteligente entra: ele conecta no IP alvo, mas força Host: dominio.com no cabeçalho para o Apache responder o vhost correto.
2) Arquitetura aplicada (dev.php + proxy.php + config.php)
Estrutura implementada:
config.php: IPs/portas dos ambientes;dev.php: interface com dois iframes (andamento e fortis);proxy.php: motor de roteamento, cURL e tratamento de resposta.
Fluxo:
- operador informa domínio + caminho;
dev.phpmonta duas URLs para o proxy;proxy.phpresolve servidor alvo por alias;- cURL conecta por IP e envia
Hostdo domínio informado; - Apache do alvo escolhe o
VirtualHostcorreto; - conteúdo retorna no iframe correspondente.
3) Código completo de apoio
3.1 config.php
<?php
// Ambientes de comparação
const ANDAMENTO_IP = '10.10.10.11';
const FORTIS_IP = '10.10.10.12';
// Opcional: portas diferentes por ambiente
const DEFAULT_HTTP_PORT = 80;
const DEFAULT_HTTPS_PORT = 443;
3.2 dev.php (painel comparativo)
<?php
$uri = $_GET['uri'] ?? 'meudominio.com.br/';
$uri = trim($uri);
?>
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Comparador de Migração</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; }
.bar { padding: 12px; border-bottom: 1px solid #ddd; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; height: calc(100vh - 70px); }
iframe { width: 100%; height: 100%; border: 0; }
input { width: 70%; padding: 8px; }
button { padding: 8px 12px; }
</style>
</head>
<body>
<div class="bar">
<form method="get">
<label>URI (domínio + caminho):</label>
<input name="uri" value="<?= htmlspecialchars($uri, ENT_QUOTES, 'UTF-8') ?>">
<button type="submit">Comparar</button>
</form>
</div>
<div class="grid">
<iframe src="proxy.php?server=andamento&uri=<?= urlencode($uri) ?>"></iframe>
<iframe src="proxy.php?server=fortis&uri=<?= urlencode($uri) ?>"></iframe>
</div>
</body>
</html>
3.3 proxy.php (versão de campo)
<?php
require 'config.php';
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
$server = $_GET['server'] ?? '';
$uri = $_GET['uri'] ?? '';
$servers = [
'andamento' => ['ip' => ANDAMENTO_IP, 'http_port' => DEFAULT_HTTP_PORT, 'https_port' => DEFAULT_HTTPS_PORT],
'fortis' => ['ip' => FORTIS_IP, 'http_port' => DEFAULT_HTTP_PORT, 'https_port' => DEFAULT_HTTPS_PORT],
];
if (!isset($servers[$server]) || !preg_match('/^[\w\-\.\/\?\%\=\&\#\:]+$/', $uri)) {
http_response_code(400);
die('Parâmetros inválidos');
}
$srv = $servers[$server];
$uri = trim($uri);
if (strpos($uri, '://') === false) {
$uri = 'http://' . $uri;
}
$parsed = parse_url($uri);
$domain = $parsed['host'] ?? '';
$path = $parsed['path'] ?? '/';
if (!empty($parsed['query'])) {
$path .= '?' . $parsed['query'];
}
if ($domain === '') {
http_response_code(400);
die('Domínio inválido');
}
if ($path === '') {
$path = '/';
}
function executeCurl(string $url, string $domain): array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_USERAGENT => 'migration-proxy/1.0',
CURLOPT_HTTPHEADER => [
'Host: ' . $domain,
'Accept: */*',
'Accept-Encoding: gzip, deflate',
'Connection: keep-alive',
'Cache-Control: no-cache',
'Pragma: no-cache',
],
CURLOPT_ENCODING => 'gzip, deflate',
CURLOPT_HEADER => false,
]);
$response = curl_exec($ch);
$error = curl_error($ch);
$errno = curl_errno($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'text/html';
$info = curl_getinfo($ch);
curl_close($ch);
return [
'response' => $response,
'error' => $error,
'errno' => $errno,
'code' => $code,
'type' => $type,
'info' => $info,
];
}
// 1) tenta HTTP primeiro
$target = 'http://' . $srv['ip'] . ':' . $srv['http_port'] . $path;
$result = executeCurl($target, $domain);
// 2) fallback HTTPS se erro ou HTTP >= 400
if ($result['errno'] !== 0 || $result['code'] >= 400) {
$target = 'https://' . $srv['ip'] . ':' . $srv['https_port'] . $path;
$result = executeCurl($target, $domain);
}
if ($result['response'] === false || $result['response'] === null) {
http_response_code(502);
header('Content-Type: text/plain; charset=utf-8');
echo "Proxy falhou ao buscar conteúdo\n";
echo "target: {$target}\n";
echo "curl_errno: {$result['errno']}\n";
echo "curl_error: {$result['error']}\n";
exit;
}
$response = $result['response'];
$contentType = $result['type'];
// texto puro (ex.: teste.txt)
if (stripos($contentType, 'text/plain') !== false) {
header('Content-Type: text/plain; charset=utf-8');
echo $response;
exit;
}
// corrige assets relativos de CSS/JS/imagens
$base = 'http://' . $srv['ip'] . '/';
$response = str_replace('<head>', '<head><base href="' . $base . '">', $response);
header('Content-Type: text/html; charset=utf-8');
echo $response;
4) Comandos de validação que usei no troubleshooting
Antes de culpar código, validei virtual host e rota HTTP/HTTPS no backend.
4.1 Validar vhost Apache
apachectl -S
apachectl -t
4.2 Validar resposta do backend forçando Host no curl
# Testa servidor andamento
curl -sv "http://10.10.10.11/" -H "Host: meudominio.com.br" -o /dev/null
# Testa servidor fortis
curl -sv "http://10.10.10.12/" -H "Host: meudominio.com.br" -o /dev/null
4.3 Validar arquivo sentinela criado só no servidor novo
curl -sv "http://10.10.10.12/teste.txt" -H "Host: meudominio.com.br"
Se o teste.txt não aparece no painel do proxy para o novo host, o problema não é DNS público: é mapeamento de vhost, docroot ou regra local no backend.
5) Erros reais que peguei e como corrigi
5.1 "Webserver is functioning normally"
Sintoma: servidor novo devolvia página default do Apache.
Causa raiz: request chegou por IP, mas o Apache não encontrou ServerName/ ServerAlias compatível com domínio enviado no Host.
Correção:
- ajustar bloco vhost com
ServerNamee aliases corretos; - garantir que docroot está no caminho certo;
- recarregar Apache e validar com
curl -H "Host: ...".
5.2 Assets quebrados (CSS/JS/imagem)
Sintoma: HTML abriu, mas links de assets iam para domínio público e não para o backend de homologação.
Correção:
- injetar
<base href="http://IP_ALVO/">dentro de<head>; - revalidar páginas com caminhos relativos.
5.3 Texto puro com comportamento errado
Sintoma: endpoint /teste.txt retornava como HTML.
Correção:
- detectar
Content-Type: text/plain; - responder direto, sem mutação de conteúdo.
6) Configuração Apache obrigatória no host de destino
<VirtualHost *:80>
ServerName meudominio.com.br
ServerAlias www.meudominio.com.br
DocumentRoot /var/www/meudominio
<Directory /var/www/meudominio>
AllowOverride All
Require all granted
</Directory>
ErrorLog /var/log/apache2/meudominio-error.log
CustomLog /var/log/apache2/meudominio-access.log combined
</VirtualHost>
Comandos pós-ajuste:
apachectl -t && systemctl reload apache2
# em RHEL/CloudLinux
# apachectl -t && systemctl reload httpd
7) Hardening do painel de comparação (não deixar aberto)
Esse tipo de ferramenta é para janela de migração controlada.
Controles que apliquei:
- restringir por IP (firewall/reverse proxy);
- autenticação HTTP Basic no diretório do comparador;
- logs dedicados para auditoria de homologação;
- timeout curto no cURL (evita prender worker);
- validação regex do parâmetro
uri; - mapa fechado de servidores permitidos (
andamento,fortis).
CURLOPT_SSL_VERIFYPEER=false e CURLOPT_SSL_VERIFYHOST=false foram usados somente na homologação com TLS não finalizado. No ambiente final, o certo é ativar validação SSL completa.
8) Runbook de homologação antes do cutover DNS
- criar endpoint sentinela apenas no host novo (
/teste.txt); - validar carregamento legado vs novo no painel comparativo;
- validar login, sessão e logout;
- validar formulários críticos (cadastro, contato, checkout);
- validar uploads/downloads;
- validar redirecionamentos 301/302 e páginas 404;
- validar assets e dependências JS/CSS;
- registrar divergências e corrigir no host novo;
- repetir ciclo até paridade funcional;
- só então aprovar alteração DNS.
9) Resultado operacional
Com o proxy inteligente, a homologação deixou de depender de ajuste manual em estação de trabalho e passou a ser centralizada, reproduzível e auditável.
O cutover DNS foi executado com evidência técnica de paridade funcional, não por intuição. Esse é o ponto que reduz incidente pós-migração.
Este post está licenciado sob CC BY-NC.
Comentários
Participe da discussão abaixo.
Comentários ainda não configurados. Adicione as opções do Cusdis em /assets/json/config/blog-comments-config.json.