~/blog/de-localhost-a-produccion-vps-dns-docker.md
De localhost a producción sin llorar: VPS, DNS, hosting y Docker para desarrolladores
Guía práctica para sacar un proyecto a producción: cómo asegurar un VPS desde cero, configurar DNS y Cloudflare, elegir entre hosting compartido, VPS o cloud, y levantar tu base de datos local con Docker Compose en segundos.
- autor
- Alejandro Peñaloza
- Publicado
- Lectura
- 6 min
- English
- Read in English →

En este artículo
Programar la aplicación es la mitad del trabajo. La otra mitad empieza cuando hay que sacarla de localhost y dejarla corriendo en internet, con dominio, HTTPS y sin que el primer bot que pase por ahí entre a tu servidor.
En este post reúno lo que yo mismo hago cada vez que monto un proyecto: cómo dejar un VPS seguro desde cero, cómo entender el DNS de una vez, cuándo conviene cada tipo de hosting y cómo uso Docker para no ensuciar mi máquina con bases de datos.
Configurar un VPS desde cero
Los ejemplos son para Ubuntu Server 24.04, pero aplican casi igual a Debian.
1. Primer acceso y usuario sin privilegios
Lo primero es dejar de usar root para el día a día.
# Conectado como root
apt update && apt upgrade -y
adduser deploy
usermod -aG sudo deploy2. Llaves SSH en lugar de contraseñas
Desde tu máquina local:
ssh-keygen -t ed25519 -C "tu-correo@ejemplo.com"
ssh-copy-id deploy@IP_DEL_SERVIDORPrueba que entras con la llave antes de tocar la configuración de SSH.
3. Endurecer SSH
Edita /etc/ssh/sshd_config (o crea un archivo en /etc/ssh/sshd_config.d/):
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yesImportante: antes de reiniciar el servicio, deja otra sesión SSH abierta y verifica que tienes acceso alternativo (por ejemplo, la consola web de tu proveedor). Si algo queda mal configurado, esa sesión abierta es la que te salva.
sudo sshd -t # valida la sintaxis
sudo systemctl restart sshAbre una nueva terminal y confirma que entras. Recién ahí cierra la sesión antigua.
4. Firewall con UFW
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseOjo: si usas Docker, los puertos publicados con -p pueden saltarse UFW porque Docker escribe sus propias reglas de iptables. La solución más simple es publicar los contenedores solo en 127.0.0.1 y dejar que Nginx sea la única puerta de entrada:
ports:
- "127.0.0.1:3000:3000"5. Fail2ban y actualizaciones automáticas
sudo apt install fail2ban unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgradesFail2ban bloquea IPs que fallan el login repetidamente; unattended-upgrades aplica los parches de seguridad solo.
6. Nginx como proxy inverso
sudo apt install nginx -yArchivo /etc/nginx/sites-available/miapp:
server {
listen 80;
server_name miapp.cl www.miapp.cl;
location / {
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}sudo ln -s /etc/nginx/sites-available/miapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx7. HTTPS gratis con Let's Encrypt
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d miapp.cl -d www.miapp.cl
sudo certbot renew --dry-runCertbot modifica el bloque de Nginx, agrega la redirección de HTTP a HTTPS y deja un temporizador que renueva el certificado antes de que venza.
Checklist rápido
- Usuario sin privilegios con
sudo - Login solo con llave SSH,
rootdeshabilitado - UFW activo con 22, 80 y 443
- Contenedores expuestos solo en
127.0.0.1 - Fail2ban y actualizaciones automáticas
- Nginx + certificado SSL con renovación probada
Dominios y DNS explicados para devs
Comprar el dominio es fácil. Lo que confunde es qué registro poner y dónde.
Los registros que realmente vas a usar
| Registro | Para qué sirve | Ejemplo |
|---|---|---|
| A | Apunta un nombre a una IPv4 | miapp.cl → 203.0.113.10 |
| AAAA | Lo mismo, para IPv6 | miapp.cl → 2001:db8::10 |
| CNAME | Un nombre es alias de otro nombre | www → miapp.cl |
| MX | Servidores que reciben el correo | miapp.cl → mx1.proveedor.com |
| TXT | Texto libre: verificación, SPF, DKIM, DMARC | v=spf1 include:... ~all |
| CAA | Qué autoridades pueden emitir certificados | 0 issue "letsencrypt.org" |
Dos reglas que ahorran horas:
- Un CNAME no puede convivir con otros registros del mismo nombre. Por eso el dominio raíz (
miapp.cl) normalmente usa un registro A, no un CNAME. Cloudflare resuelve esto con CNAME flattening. - El TTL manda. Si vas a migrar de servidor, baja el TTL a 300 segundos un día antes; así el cambio se propaga en minutos y no en horas.
Dominios .cl y delegación a Cloudflare
En NIC Chile compras el dominio y, en la sección de servidores DNS, reemplazas los que vienen por defecto por los dos nameservers que te asigna Cloudflare al agregar el sitio. Desde ese momento todos los registros se administran en Cloudflare.
Para revisar que todo propagó:
dig miapp.cl NS +short
dig miapp.cl A +short
dig www.miapp.cl CNAME +shortSacarle el jugo a Cloudflare
- Proxy (nube naranja): oculta la IP real del servidor y activa caché y protección. Para registros que no son web (correo, SSH) déjalo en gris.
- Modo SSL "Full (strict)": cifra el tramo navegador–Cloudflare y también Cloudflare–servidor, validando tu certificado de Let's Encrypt. Evita el modo "Flexible", que deja el tramo al servidor sin cifrar y suele provocar bucles de redirección.
- Reglas de caché: cachea de forma agresiva
/assets/*,/_next/static/*o/build/*, y salta la caché para/api/*y/admin/*. - Protección básica: el plan gratuito ya incluye mitigación DDoS. Suma reglas de WAF para limitar intentos en
/loginy un desafío para países o ASN de donde nunca recibes tráfico legítimo. - Cloudflare Tunnel: permite publicar un servicio que corre en tu casa o en una red sin IP pública, sin abrir puertos en el router.
cloudflared tunnel login
cloudflared tunnel create homelab
cloudflared tunnel route dns homelab app.miapp.cl
cloudflared tunnel run homelabHosting compartido vs. VPS vs. cloud
No existe "el mejor hosting"; existe el adecuado para el presupuesto, el tráfico y el tiempo que tienes para administrarlo.
| Hosting compartido | VPS | Cloud administrado (PaaS / serverless) | |
|---|---|---|---|
| Costo típico | Muy bajo y fijo | Bajo a medio, fijo | Desde gratis, crece con el uso |
| Control | Mínimo (cPanel, PHP) | Total (root) | Medio: decides código, no servidor |
| Mantenimiento | Lo hace el proveedor | Lo haces tú | Casi nulo |
| Escalado | Limitado | Manual (subes de plan) | Automático |
| Ideal para | WordPress, sitios simples | APIs, varios proyectos, Docker | Frontends, picos de tráfico, equipos sin DevOps |
Mi forma de decidir:
- Sitio informativo o WordPress de un cliente pequeño: hosting compartido. Pagar un VPS para eso es pagar por administrar algo que no necesitas.
- Varios proyectos propios, APIs, bases de datos y workers: un VPS con Docker. Por un costo mensual fijo corres muchos servicios y el gasto es predecible.
- Frontend en Next.js o un producto con tráfico muy variable: una plataforma administrada. Pagas más por unidad, pero no pierdes tiempo en servidores.
- Tráfico serio y requisitos de disponibilidad: cloud (AWS, GCP, Azure, HuaweiCloud) con balanceadores, réplicas y almacenamiento de objetos.
Un detalle que casi nadie compara: el costo de salida de datos (egress). En un VPS suele venir una cuota mensual generosa incluida; en la nube tradicional se cobra por GB y puede ser la línea más cara de la factura. Revisa siempre los precios vigentes del proveedor antes de decidir, porque cambian a menudo.
Docker para el día a día: tu base de datos local en 30 segundos
Instalar PostgreSQL, MySQL y Redis directamente en el sistema funciona hasta que necesitas dos versiones distintas para dos proyectos. Con Docker Compose cada proyecto trae su propio entorno.
docker-compose.yml:
services:
postgres:
image: postgres:16-alpine
container_name: dev-postgres
restart: unless-stopped
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: app
ports:
- "127.0.0.1:5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./docker/init:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev -d app"]
interval: 5s
retries: 10
redis:
image: redis:7-alpine
ports:
- "127.0.0.1:6379:6379"
adminer:
image: adminer
ports:
- "127.0.0.1:8080:8080"
depends_on:
postgres:
condition: service_healthy
volumes:
pgdata:Comandos que uso todos los días:
docker compose up -d # levantar todo
docker compose ps # ver estado
docker compose logs -f postgres # seguir logs
docker compose exec postgres psql -U dev -d app
docker compose down # apagar (los datos quedan en el volumen)
docker compose down -v # apagar y borrar datos: empezar de ceroBuenas prácticas:
- Pon cualquier
.sqlen./docker/init/y se ejecutará solo la primera vez que se cree el volumen. Ideal para esquemas y datos de prueba. - Expón los puertos en
127.0.0.1para que la base de datos no quede accesible desde tu red. - Mueve credenciales a un archivo
.envy agrégalo a.gitignore. - Fija versiones (
postgres:16-alpine, nolatest) para que todo el equipo tenga exactamente el mismo motor.
Conclusión
Sacar un proyecto a producción no tiene que ser una tarde de sufrimiento. Con un checklist de seguridad para el VPS, un entendimiento claro de los registros DNS, el tipo de hosting correcto y Docker para el entorno local, pasas de localhost a un dominio con HTTPS en una mañana, y con la tranquilidad de saber qué hiciste y por qué.
Comentarios
Aún no hay comentarios. ¿Quieres dejar el primero?
Deja un comentario
$ ls ~/blog | grep relacionados
Sigue leyendo

Bitácora de guerra: depurar CORS, refactorizar código propio e integrar Webpay sin perder la tarde
Problemas reales y cómo los resolví: un error de CORS que solo fallaba en producción, una fuga de memoria en el servidor, código mío de hace un año refactorizado sin vergüenza y una guía práctica para integrar Webpay Plus, Mercado Pago y Flow.

Productividad para desarrolladores: VS Code, Git avanzado, gestión ágil y scripts de automatización
Mi flujo de trabajo diario como desarrollador: las extensiones y ajustes de VS Code que sí aportan, Git más allá de commit y push (rebase, cherry-pick y bisect), cómo gestionar proyectos pequeños sin burocracia y los scripts que automatizan lo repetitivo.

PHP2Shell: qué son CVE-2026-109618 y CVE-2026-109619 y cómo proteger tus servidores PHP
Dos fallas críticas en PHP permiten ejecutar código remoto sin autenticación, antes de que corra tu aplicación. Qué versiones afectan, quién está expuesto, qué cubre el WAF de Cloudflare, cómo actualizar y qué mitigar mientras tanto.