Cómo desplegar app en VPS con Docker, Nginx y SSL
En pocas palabras: Se despliega empaquetando la app en una imagen Docker, subiéndola a GitHub Container Registry, corriéndola en un VPS Debian o Ubuntu con Docker Engine y exponiéndola mediante Nginx como proxy inverso, con certificado HTTPS gratuito emitido por Certbot y Let’s Encrypt en ocho pasos.
Desplegar una app en VPS con Docker, Nginx y SSL significa empaquetar tu aplicación en una imagen Docker, subirla a un registro como GitHub Container Registry, correrla en un servidor Debian o Ubuntu y exponerla al mundo a través de Nginx con un certificado HTTPS gratuito de Let’s Encrypt. Todo el proceso está documentado en una guía publicada en dev.to por Vladimir Vovk.
Desplegar app en VPS con Docker es el proceso de llevar una aplicación empaquetada en contenedores a un servidor privado virtual, usando Docker Engine para ejecutarla, Nginx como proxy inverso para enrutar el tráfico HTTP/HTTPS y Certbot para emitir certificados SSL sin costo de Let’s Encrypt. El flujo completo cubre desde el hardening inicial del servidor hasta el certificado activo.
En este artículo:
- En 30 segundos
- ¿Cómo preparar un VPS Debian/Ubuntu antes de instalar Docker?
- ¿Qué reglas de firewall (UFW) hay que habilitar en el VPS?
- ¿Cómo instalar Docker y sumar al usuario deploy al grupo docker?
- ¿Cómo crear la imagen Docker de la app con un Dockerfile multi-stage?
- ¿Cómo subir y descargar la imagen con GitHub Container Registry (ghcr.io)?
- ¿Cómo correr y reiniciar el contenedor de la app en el servidor?
- ¿Cómo configurar Nginx como proxy inverso para la app Docker?
- ¿Cómo apuntar el dominio y activar HTTPS gratis con Let’s Encrypt?
- ¿Dónde conviene alojar el VPS si tu equipo está en Argentina o LATAM?
- Errores comunes al desplegar app en VPS con Docker
- Preguntas Frecuentes
- Conclusión: cómo desplegar app en VPS con Docker sin romper nada
- Fuentes
En 30 segundos
- Ocho pasos completos: preparar el VPS, activar UFW, instalar Docker, crear la imagen, subirla a ghcr.io, correr el contenedor, configurar Nginx y activar HTTPS con Certbot.
- Firewall mínimo: UFW solo deja abiertos OpenSSH, 80/tcp y 443/tcp, con política default deny para todo lo demás.
- El contenedor no sale a internet directo: se publica en
127.0.0.1:3000, nunca en0.0.0.0, así que solo Nginx puede llegar a él. - ghcr.io reemplaza a Docker Hub: un token con permisos write/read:packages alcanza para subir y bajar la imagen desde el VPS.
- HTTPS en un solo comando:
certbot --nginx -d tudominio.com -d www.tudominio.comemite y configura el certificado junto.
¿Cómo preparar un VPS Debian/Ubuntu antes de instalar Docker?
Preparar el VPS implica conectarte como root, actualizar el sistema, crear un usuario sin privilegios para correr la app y reemplazar el login por contraseña con una clave SSH. Nada de esto es opcional si pensás dejar el servidor expuesto a internet.
El primer paso es ssh root@tu-ip y después apt update && apt -y upgrade. Ahí creás el usuario con adduser deploy y lo sumás al grupo sudo con usermod -aG sudo deploy. Usar root para correr la app es una mala idea, algo que cualquiera que haya administrado un servidor sabe por experiencia propia (a veces dolorosa).
Para la clave SSH, generás un par ed25519 en tu máquina local con ssh-keygen -t ed25519 -f vps-key y lo copiás con ssh-copy-id -i vps-key deploy@tu-ip. Una vez que confirmás que podés entrar con la clave, abrís /etc/ssh/sshd_config y poné PermitRootLogin no y PasswordAuthentication no. Reiniciás el servicio con systemctl restart ssh y listo: ya no hay login por password ni acceso directo como root. Ya lo cubrimos antes en automatizar el despliegue con GitHub Actions.
¿Qué reglas de firewall (UFW) hay que habilitar en el VPS?

UFW se configura con política default deny incoming y solo tres excepciones: OpenSSH, 80/tcp y 443/tcp. Todo lo demás queda bloqueado de entrada, mientras el tráfico saliente queda permitido sin restricciones.
Los comandos son cortos: apt -y install ufw, después ufw default deny incoming, ufw default allow outgoing, ufw allow OpenSSH, ufw allow 80/tcp y ufw allow 443/tcp. Activás todo con ufw enable y chequeás el estado con ufw status. Ojo: si te olvidás de permitir OpenSSH antes de activar el firewall, te quedás afuera del servidor.
¿Cómo instalar Docker y sumar al usuario deploy al grupo docker?
Docker Engine se instala siguiendo la documentación oficial de Docker, que soporta Ubuntu, Debian, CentOS, Fedora y RHEL en arquitecturas x86_64 y arm64. Después hay que sumar al usuario deploy al grupo docker para no tener que escribir sudo antes de cada comando.
Según la documentación oficial, “Docker Engine es un proyecto open source, soportado por los mantenedores de Moby y la comunidad”, y Docker no da soporte directo sobre Engine salvo que lo uses dentro de Docker Desktop. Para el grupo, corrés usermod -aG docker $USER, aplicás el cambio con newgrp docker y confirmás que todo quedó bien con docker version.
¿Cómo crear la imagen Docker de la app con un Dockerfile multi-stage?
Un Dockerfile multi-stage separa la compilación de la ejecución en etapas distintas (builder, deps y runner), así la imagen final no arrastra herramientas de build que no necesita en producción. Para una app Node.js, eso significa instalar dependencias, compilar TypeScript y recién en la última etapa copiar solo lo que corre en runtime. Te puede servir nuestra cobertura de runtimes más livianos que Docker en VPS.
El ejemplo de la guía usa dhi.io/node:24-alpine3.23-dev como base en las etapas builder y deps: en builder instala dependencias y compila, y en deps repite la instalación pero en modo producción (con npm install --omit=dev). La etapa runner, en cambio, usa dhi.io/node:24-alpine3.23 (sin el sufijo -dev) y ahí copia node_modules y el build compilado con COPY --from=deps y COPY --from=builder. El build final se hace con docker buildx build --platform linux/amd64 -t my-app:latest ., apuntando siempre a linux/amd64 porque el servidor destino corre esa arquitectura, aunque estés compilando desde una Mac con chip ARM.
¿Cómo subir y descargar la imagen con GitHub Container Registry (ghcr.io)?
GitHub Container Registry permite subir la imagen desde tu máquina local y descargarla en el VPS sin pasar por Docker Hub. El requisito es un Personal Access Token con los scopes write:packages y read:packages.
Generás el token desde GitHub en Settings, Developer settings, Personal access tokens (classic), y lo usás para loguearte con echo "tu-token" | docker login ghcr.io -u tu-usuario --password-stdin. Después reconstruís la imagen con el nombre completo del registry (ghcr.io/tu-usuario/tu-app:latest) y la subís con docker push --all-tags ghcr.io/tu-usuario/tu-app. En el VPS repetís el login y bajás la imagen con docker pull. Todo esto reemplaza lo que antes se hacía copiando archivos a mano por SCP, con los dolores de cabeza que eso traía.
¿Cómo correr y reiniciar el contenedor de la app en el servidor?
El contenedor se corre con docker run -d --name tu-app --restart unless-stopped -p 127.0.0.1:3000:3000 ghcr.io/tu-usuario/tu-app. La clave está en publicar el puerto solo en localhost, no en todas las interfaces, para que únicamente Nginx pueda hablar con la app. Tema relacionado: elegir entre PM2 o Docker en tu servidor.
Verificás que todo esté arriba con docker ps y seguís los logs con docker logs -f --tail tu-app. Para actualizar la app a una versión nueva, el ciclo es siempre el mismo: parás el contenedor con docker stop, lo borrás con docker rm y lo volvés a levantar con la imagen actualizada. Subís el código, lo probás en local, funciona bárbaro, armás la imagen, la subís a ghcr.io, la bajás en el servidor y corrés el mismo docker run de siempre, sin improvisar nada nuevo cada vez.
¿Cómo configurar Nginx como proxy inverso para la app Docker?
Nginx recibe el tráfico en los puertos 80 y 443 y lo redirige hacia el contenedor que escucha en 127.0.0.1. Esto permite correr varias apps en el mismo servidor usando distintos dominios, cada una apuntando a su propio puerto interno.
Instalás Nginx con apt -y install nginx, borrás el sitio default con rm -f /etc/nginx/sites-enabled/default y creás un archivo nuevo en /etc/nginx/sites-available/tu-app con un server block que incluya proxy_pass http://127.0.0.1:8000 y los headers X-Real-IP, X-Forwarded-For y X-Forwarded-Proto. Habilitás el sitio con un symlink hacia sites-enabled, validás la sintaxis con nginx -t y recargás con systemctl reload nginx.
¿Cómo apuntar el dominio y activar HTTPS gratis con Let’s Encrypt?
Activar HTTPS gratis requiere dos cosas: un registro DNS tipo A que apunte al VPS y Certbot instalado con el plugin de Nginx. Sin el registro A bien configurado, Let’s Encrypt no puede validar que sos dueño del dominio. Para más detalles técnicos, mirá publicar tus imágenes en Docker Hub automáticamente.
Según Certbot (EFF), para usar el plugin de Nginx necesitás un sitio HTTP ya online en el puerto 80 y acceso sudo al servidor. Instalás con apt -y install certbot python3-certbot-nginx y corrés certbot --nginx -d tudominio.com -d www.tudominio.com. Certbot reconfigura el server block automáticamente y deja el certificado listo para renovarse solo. Como cierra la guía de Vovk: “That’s it. The app is live now!” (traducido: “Listo. La app ya está en vivo”).
¿Dónde conviene alojar el VPS si tu equipo está en Argentina o LATAM?
La latencia hacia los usuarios finales depende de dónde esté físicamente el datacenter, no solo de la configuración de Docker y Nginx. Para equipos que atienden tráfico mayormente argentino o latinoamericano, un VPS con datacenter en la región reduce los tiempos de respuesta frente a instancias alojadas en Europa o la costa este de Estados Unidos.
Si estás evaluando dónde contratar el servidor para este flujo, donweb.com ofrece VPS con soporte local en español, algo que no es poco cuando necesitás resolver un problema de SSH o de firewall a las dos de la mañana.
Errores comunes al desplegar app en VPS con Docker
- Exponer el puerto del contenedor en todas las interfaces: usar
-p 3000:3000en vez de-p 127.0.0.1:3000:3000deja la app accesible directo por IP, saltándose a Nginx y al certificado SSL. - Activar UFW sin permitir OpenSSH primero: si corrés
ufw enableantes deufw allow OpenSSH, te quedás bloqueado afuera del servidor sin acceso a consola. - Correr Certbot antes de que el DNS propague: si el registro A todavía no resolvió hacia la IP del VPS, Let’s Encrypt no puede validar el dominio y falla la emisión del certificado.
- Olvidar el flag –platform al compilar en Mac con chip ARM: sin
--platform linux/amd64, la imagen queda compilada para arm64 y no corre en un VPS Linux x86_64 estándar.
Preguntas Frecuentes
¿Necesito Docker Hub para desplegar app en VPS con Docker?
No. GitHub Container Registry (ghcr.io) funciona como alternativa completa a Docker Hub, usando un Personal Access Token con scopes write:packages y read:packages para subir y bajar imágenes desde cualquier VPS.
Antes de meter manos en el servidor, conviene entender las diferencias para elegir bien cómo desplegar app en VPS.
¿Por qué publicar el contenedor solo en 127.0.0.1 y no en todas las interfaces?
Porque así solo Nginx, que corre en el mismo servidor, puede acceder al contenedor. Si publicás en 0.0.0.0, cualquiera en internet puede llegar directo a la app por IP y puerto, sin pasar por el certificado SSL ni por las reglas de Nginx.
¿Cuánto cuesta el certificado SSL de Let’s Encrypt vía Certbot?
Nada. Certbot, la herramienta de la Electronic Frontier Foundation, emite certificados de Let’s Encrypt sin costo y los deja listos para renovación automática mientras el dominio siga apuntando al servidor.
¿Qué pasa si no deshabilito el login por contraseña en el VPS?
El servidor queda expuesto a ataques de fuerza bruta contra SSH, algo común en VPS con IP pública. Deshabilitar PasswordAuthentication en sshd_config y dejar solo login por clave SSH reduce ese vector de forma directa.
¿Puedo correr varias apps Docker en el mismo VPS con Nginx?
Sí. Cada app corre en su propio contenedor con un puerto interno distinto (por ejemplo 127.0.0.1:3000 y 127.0.0.1:3001), y Nginx usa un server block separado por dominio para rutear el tráfico hacia el puerto correspondiente.
Conclusión: cómo desplegar app en VPS con Docker sin romper nada
El flujo completo (preparar el servidor, instalar Docker, empaquetar la app, subirla a ghcr.io, correrla detrás de Nginx y activar HTTPS con Certbot) no tiene atajos mágicos, pero tampoco es complicado si seguís el orden. Cada paso depende del anterior: sin firewall bien configurado no hay seguridad, sin imagen bien construida no hay contenedor estable, y sin DNS propagado no hay certificado.
Lo que vos podés hacer hoy es replicar este flujo en un VPS de prueba antes de tocar producción. Si algo falla (el certificado no se emite, Nginx tira 502, el contenedor no levanta) revisá en ese orden: DNS, firewall, puerto publicado y logs del contenedor. Eso resuelve la mayoría de los problemas sin necesidad de reinstalar nada desde cero.






