Tema
Checklist de producción
Llegó el día: el sitio del cliente está terminado y hay que lanzarlo. Esta lección es el proyecto final del curso: un recorrido completo, paso a paso, para publicar el sitio de un cliente sin olvidar nada. Imprímela, cópiala a tu gestor de tareas y úsala en cada proyecto.
El escenario
"Clínica Dental Sonrisa" te contrató un sitio con:
- Páginas informativas + blog (frontend estático con VitePress o Astro).
- Formulario de citas que guarda en base de datos y envía correo (API en Node o PHP).
- Dominio
clinicasonrisa.comy correocitas@clinicasonrisa.com.
Arquitectura elegida:
txt
clinicasonrisa.com → VPS Ubuntu 24.04: Nginx (estáticos) + API Node con PM2 + PostgreSQL
www → redirige al dominio raíz
correo → Google Workspace (o Zoho Mail)
DNS + CDN → Cloudflare
Respaldos → script diario + rclone a almacenamiento externo
Monitoreo → UptimeRobot + Sentry
Despliegue → GitHub Actions al hacer push a mainFase 1: semanas antes del lanzamiento
Cuentas y accesos
- [ ] Dominio registrado a nombre del cliente (tú como colaborador o con acceso delegado).
- [ ] Renovación automática del dominio activada y método de pago del cliente.
- [ ] Cuenta del VPS / hosting a nombre del cliente o facturada claramente en tu plan de mantenimiento.
- [ ] Todas las contraseñas en un gestor (Bitwarden, 1Password) compartido con el cliente.
- [ ] Autenticación de dos factores en registrador, hosting, Cloudflare, GitHub y correo.
Servidor
- [ ] VPS configurado según Configurar un VPS desde cero: usuario sudo, SSH con llaves, ufw, fail2ban, actualizaciones automáticas.
- [ ] Nginx con server blocks para el sitio y la API (Nginx).
- [ ] App gestionada con PM2 y arranque automático (Node con PM2).
- [ ] Base de datos con usuario propio, sin puerto público.
- [ ] Entorno de staging (
staging.clinicasonrisa.com) donde el cliente aprobó todo.
Fase 2: configuración de producción
Variables de entorno y secretos
- [ ] Archivo
.envde producción en el servidor, con permisos600/640, fuera de Git. - [ ] Modo depuración desactivado:
NODE_ENV=production,APP_DEBUG=false,display_errors=Off. - [ ] Claves y tokens de producción distintos a los de desarrollo.
- [ ] Llaves de API restringidas por dominio o IP cuando el servicio lo permita (Google Maps, reCAPTCHA).
env
# .env de producción (ejemplo)
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://clinica_app:CLAVE_LARGA@localhost:5432/clinica
SMTP_HOST=smtp.proveedor.com
SMTP_USER=citas@clinicasonrisa.com
SMTP_PASS=CLAVE_DE_APLICACION
CORS_ORIGIN=https://clinicasonrisa.com
SENTRY_DSN=https://xxxx@sentry.io/123Seguridad
- [ ] HTTPS con certificado válido y redirección HTTP → HTTPS (SSL).
- [ ] Cabeceras de seguridad:
X-Content-Type-Options,X-Frame-Options,Referrer-Policy, HSTS. - [ ] Archivos ocultos bloqueados (
.git,.env): pruebahttps://clinicasonrisa.com/.env→ debe dar 403/404. - [ ] Formularios con validación en servidor, protección anti-spam (honeypot, reCAPTCHA o Turnstile) y límite de peticiones.
- [ ] Consultas a la base con parámetros preparados (sin concatenar SQL).
- [ ] CORS restringido al dominio del sitio.
- [ ] Dependencias sin vulnerabilidades críticas:
npm audit/composer audit.
Server block final de referencia:
nginx
# /etc/nginx/sites-available/clinicasonrisa.com
server {
listen 80;
listen [::]:80;
server_name clinicasonrisa.com www.clinicasonrisa.com;
return 301 https://clinicasonrisa.com$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.clinicasonrisa.com;
ssl_certificate /etc/letsencrypt/live/clinicasonrisa.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/clinicasonrisa.com/privkey.pem;
return 301 https://clinicasonrisa.com$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name clinicasonrisa.com;
ssl_certificate /etc/letsencrypt/live/clinicasonrisa.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/clinicasonrisa.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
root /var/www/clinicasonrisa.com/dist;
index index.html;
# Cabeceras de seguridad
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
access_log /var/log/nginx/clinica.access.log;
error_log /var/log/nginx/clinica.error.log;
# API
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Sitio estático con URLs limpias
location / {
try_files $uri $uri.html $uri/ =404;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
}
location ~ /\.(?!well-known) {
deny all;
}
error_page 404 /404.html;
}Y el límite de peticiones, en http { } de /etc/nginx/nginx.conf:
nginx
# 10 peticiones por segundo por IP para la zona "api"
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;Correo
- [ ] Registros MX del proveedor de correo.
- [ ] SPF, DKIM y DMARC configurados (Dominios y DNS).
- [ ] Los correos del formulario se envían por SMTP autenticado o un servicio transaccional (no con
mail()sin autenticar). - [ ] Prueba: envía un correo a mail-tester.com y apunta a 9/10 o más.
Fase 3: contenido, SEO y experiencia
- [ ] Cada página con
titleúnico ymeta description. - [ ] Etiquetas Open Graph (imagen, título y descripción al compartir en WhatsApp y redes).
- [ ]
favicone íconos para móvil. - [ ]
sitemap.xmlgenerado yrobots.txtcorrecto. - [ ] Página 404 personalizada y útil.
- [ ] Textos revisados (ortografía, teléfonos, direcciones, horarios).
- [ ] Aviso de privacidad / política de cookies según la legislación del país del cliente.
- [ ] Imágenes optimizadas y con
altdescriptivo. - [ ] Accesibilidad básica: contraste, navegación con teclado, etiquetas en formularios.
- [ ] Probado en Chrome, Firefox, Safari y en celulares reales (Android e iPhone).
- [ ] Lighthouse ≥ 90 en rendimiento, accesibilidad, buenas prácticas y SEO (Rendimiento).
txt
# robots.txt de producción
User-agent: *
Allow: /
Disallow: /api/
Sitemap: https://clinicasonrisa.com/sitemap.xmlEl robots.txt de staging
Es muy común lanzar con el robots.txt de staging que contiene Disallow: /, y el sitio no aparece en Google. Revísalo el día del lanzamiento. En staging, mejor protege con contraseña (auth básica).
Fase 4: operación
Despliegue
- [ ] Workflow de GitHub Actions que despliega al hacer push a
main(CI/CD). - [ ] Rama
mainprotegida; cambios mediante pull request. - [ ] Procedimiento de rollback documentado (volver al commit anterior y redesplegar).
Respaldos y monitoreo
- [ ] Respaldo diario de base de datos y archivos subidos, copiado fuera del servidor (Monitoreo y respaldos).
- [ ] Restauración probada al menos una vez.
- [ ] Snapshots automáticos del proveedor activados.
- [ ] Monitor de disponibilidad (UptimeRobot / Better Stack) con alertas a tu correo o Telegram.
- [ ] Sentry (o similar) capturando errores de la app.
- [ ] Alerta de expiración de certificado y de dominio.
Analítica
- [ ] Google Search Console verificado (registro TXT) y sitemap enviado.
- [ ] Analítica instalada (Google Analytics 4, Plausible, Umami o Cloudflare Web Analytics), respetando el aviso de privacidad.
- [ ] Ficha de Google Business Profile apuntando al sitio (clave para negocios locales).
Fase 5: el día del lanzamiento
Orden recomendado:
- 24 h antes: baja el TTL de los registros DNS a 300 segundos.
- Haz un respaldo completo del sitio anterior (si existía).
- Despliega la versión final en el servidor de producción.
- Prueba con el archivo
hostso concurl -H "Host: clinicasonrisa.com" https://IPantes de cambiar el DNS. - Cambia los registros DNS (A / CNAME) al servidor nuevo.
- Obtén o verifica el certificado SSL.
- Recorre el sitio completo en producción: cada página, cada enlace, cada formulario.
- Envía una cita de prueba y confirma que llega el correo y se guarda en la base.
- Revisa
robots.txt,sitemap.xmly que no haya errores en la consola del navegador. - Envía el sitemap a Search Console.
- Avisa al cliente con un mensaje claro y celebra.
Script rápido de verificación post-lanzamiento:
bash
#!/usr/bin/env bash
# verificar.sh — comprobaciones básicas tras el lanzamiento
DOMINIO="clinicasonrisa.com"
echo "== DNS =="
dig +short "$DOMINIO" A
dig +short "www.$DOMINIO"
echo "== Redirecciones =="
curl -s -o /dev/null -w "http:// → %{http_code} %{redirect_url}\n" "http://$DOMINIO"
curl -s -o /dev/null -w "https://www → %{http_code} %{redirect_url}\n" "https://www.$DOMINIO"
curl -s -o /dev/null -w "https:// → %{http_code}\n" "https://$DOMINIO"
echo "== Archivos que NO deben ser públicos =="
for ruta in .env .git/config; do
curl -s -o /dev/null -w "/$ruta → %{http_code}\n" "https://$DOMINIO/$ruta"
done
echo "== SEO =="
curl -s -o /dev/null -w "robots.txt → %{http_code}\n" "https://$DOMINIO/robots.txt"
curl -s -o /dev/null -w "sitemap.xml → %{http_code}\n" "https://$DOMINIO/sitemap.xml"
echo "== API =="
curl -s "https://$DOMINIO/api/salud"; echo
echo "== Certificado =="
echo | openssl s_client -connect "$DOMINIO:443" -servername "$DOMINIO" 2>/dev/null \
| openssl x509 -noout -datesFase 6: la semana siguiente
- [ ] Revisa los logs de error diariamente los primeros días.
- [ ] Revisa Search Console: errores de indexación o de cobertura.
- [ ] Sube de nuevo el TTL de DNS a 3600.
- [ ] Verifica que el primer respaldo automático se generó y llegó al almacenamiento externo.
- [ ] Entrega al cliente la documentación: accesos, cómo editar contenido, a quién llamar si algo falla y qué incluye el mantenimiento (ver Entrega y capacitación).
- [ ] Pide testimonio y permiso para incluir el proyecto en tu portafolio.
Plantilla reutilizable
Copia esta checklist a Notion, Trello o un issue de GitHub con casillas. Cada proyecto que lances la hará más completa y cada vez lanzarás más rápido y con menos estrés.
Error común
Lanzar un viernes por la tarde. Si algo falla, el fin de semana se arruina (el tuyo y el del cliente). Lanza de martes a jueves por la mañana.
Ejercicios
- Adapta la arquitectura del escenario para un cliente con presupuesto mínimo y un sitio solo informativo con formulario de contacto.
- Escribe los registros DNS completos para el escenario (VPS
203.0.113.10, correo en Google Workspace) incluyendo SPF y DMARC. - Después del lanzamiento el sitio no aparece en Google tras dos semanas. Enumera qué revisarías.
Ver solución
- Sitio estático en Netlify/Cloudflare Pages (gratis o casi), formulario con Netlify Forms o una función serverless, dominio con DNS en Cloudflare, correo con el proveedor que el cliente prefiera, monitoreo con UptimeRobot. Sin VPS que mantener.
txt
A @ 203.0.113.10
CNAME www clinicasonrisa.com.
MX @ 1 smtp.google.com.
TXT @ "v=spf1 include:_spf.google.com ~all"
TXT google._domainkey "v=DKIM1; k=rsa; p=...(lo genera Google Workspace)"
TXT _dmarc "v=DMARC1; p=quarantine; rua=mailto:dmarc@clinicasonrisa.com"robots.txt(que no tengaDisallow: /), etiquetasnoindexolvidadas, Search Console verificado y sitemap enviado, errores de cobertura en Search Console, que las páginas respondan 200 (no redirecciones en bucle) y que el contenido sea útil y con títulos únicos.
Resumen
- Lanzar es un proceso: cuentas, servidor, secretos, seguridad, correo, SEO, despliegue, respaldos, monitoreo.
- Cuentas y dominio a nombre del cliente, con 2FA y gestor de contraseñas.
- Revisa
robots.txt,.envinaccesible, HTTPS, SPF/DKIM/DMARC y formularios funcionando. - Baja el TTL antes, verifica con un script después y vigila la primera semana.
- Documenta y entrega: ese cierre profesional es lo que te consigue el siguiente cliente.
- Continúa con Trabajo con clientes para dominar la parte de negocio.