Cómo automatizar la instalación base de una VPS

Hay una cosa que intento aplicar siempre que trabajo como desarrollador o con infraestructura:

Si hago algo un par de veces y sé que voy a volver a hacerlo, intento automatizar la parte repetitiva.

Esto mismo es lo que hice en su día y quiero compartirlo por si también te ayuda a reducir el tiempo de preparación inicial de una VPS, automatizando estos pasos:

  • actualizar el sistema;
  • crear o preparar el usuario;
  • configurar acceso por SSH;
  • instalar herramientas básicas;
  • instalar Docker;
  • configurar fail2ban;
  • preparar las claves para Git;
  • instalar algunas herramientas opcionales;
  • aplicar unas configuraciones básicas de seguridad.

Hacerlo una vez no supone demasiado trabajo. El problema es cuando empiezas a preparar un segundo servidor, luego otro de desarrollo, uno de producción, uno para hacer pruebas…

Y además quieres que todos partan de una base parecida.

De ahí nació vps-base en GitHub: https://github.com/Pistatxos/vps-base


La idea era sencilla: pasar de Ubuntu recién instalado a servidor preparado para empezar a trabajar, sin repetir manualmente los mismos pasos cada vez y, sobre todo, intentando hacerlo siempre de la misma forma.

Actualmente tengo tres variantes:

install_base.sh
install_base_dev.sh
install_base_prod.sh

La primera es completamente interactiva y las otras dos están pensadas para repetir configuraciones de servidores de desarrollo y producción, con algunas diferencias entre ambos.


Ejecutar la instalación

Para facilitar todavía más el proceso preparé un pequeño menú de entrada que podemos lanzar directamente con:

curl -fsSL https://raw.githubusercontent.com/Pistatxos/vps-base/main/install.sh | sudo bash

El script descarga el catálogo disponible y muestra el menú para elegir qué instalación quieres ejecutar.

Actualmente las opciones son:

1. Instalación base interactiva
2. VPS de desarrollo
3. VPS de producción

Internamente el menú utiliza un fichero scripts.conf que me permite añadir nuevos scripts al catálogo sin tener que modificar toda la lógica del menú.


Sí, aquí aparece el famoso curl | bash. Es cómodo, pero estamos descargando código de Internet y ejecutándolo como root.

Por eso también está la opción que personalmente recomiendo si quieres comprobar exactamente qué vas a ejecutar:

curl -fsSL https://raw.githubusercontent.com/Pistatxos/vps-base/main/install.sh -o install.sh
chmod +x install.sh
sudo ./install.sh

Así puedes abrir antes install.sh, revisar su contenido y ejecutarlo después.

Además, el acceso debe realizarse siempre mediante HTTPS.

Si estoy ejecutando un script remoto como root, quiero tener claro desde dónde lo estoy descargando.


¿Qué prepara el servidor?

Bien, vamos a lo interesante. Los scripts comparten una base común.

Entre otras cosas realizan:

  • actualización del sistema;
  • instalación de herramientas básicas;
  • creación de swap si el servidor no tiene;
  • instalación de Docker y Docker Compose;
  • instalación de fail2ban;
  • configuración de actualizaciones de seguridad automáticas;
  • preparación del acceso SSH;
  • creación de una Deploy Key para Git;
  • instalación de Cockpit;
  • creación de un pequeño README con comandos útiles.

La idea es que cuando termina el script el servidor ya tenga la base que normalmente necesito para empezar a trabajar.


SSH

Esta era una de las partes que más veces terminaba configurando manualmente.

Los scripts permiten trabajar con un usuario nuevo o con uno que ya exista.

Después aplican algunas medidas básicas de seguridad sobre SSH, como:

PasswordAuthentication no
PermitRootLogin no
AllowUsers <usuario>

También se configura fail2ban para proteger el acceso frente a intentos repetidos.

Aquí hay una precaución importante:

Si utilizas un usuario existente, asegúrate primero de que puedes entrar correctamente utilizando tu clave SSH.

Si desactivas el acceso por contraseña sin tener funcionando la clave, puedes dejarte fuera del servidor.


Una Deploy Key diferente para cada servidor

Otra cosa que repetía constantemente era crear la clave SSH necesaria para que el servidor pudiera acceder a los repositorios.

Ahora cada servidor genera su propia Deploy Key y después simplemente añado la clave pública al repositorio correspondiente en GitLab, GitHub o la plataforma Git que esté utilizando.

Es un detalle sencillo, pero precisamente este tipo de pequeñas tareas son las que terminan acumulando tiempo cada vez que montas una máquina.


DEV y PROD no tienen por qué ser iguales

Al principio podría haber creado un único script enorme con veinte preguntas.

Pero con el tiempo terminé separando una instalación para desarrollo y otra para producción.

No porque sean servidores completamente diferentes, sino porque no necesito instalar exactamente lo mismo en ambos.

Por ejemplo, en desarrollo suelo querer herramientas de compilación y Python.

En producción intento dejar solamente lo necesario para ejecutar el servicio.

Un ejemplo de las diferencias actuales:

ConfiguraciónDEVPROD
Herramientas de compilaciónSíNo
Python mediante uvSíNo
DockerSíSí
fail2banSíSí
CockpitSíSí
Hardening SSHBásicoMás restrictivo
Historial BashLimitadoDesactivado

No busco que producción tenga muchas herramientas “por si acaso”. Si no las necesito, prefiero no instalarlas.


Automatizado no significa sin control

Una cosa que he intentado evitar es que el script tome decisiones que podrían dejarme sin acceso al servidor.

Un ejemplo es ufw que el script lo instala, pero no lo activa automáticamente.

¿Por qué? – Porque cada servidor puede necesitar reglas diferentes.

Antes de ejecutar: ufw enable prefiero comprobar qué puertos necesita ese servidor y asegurarme de mantener acceso por SSH o mediante VPN, porque automatizar algo no significa necesariamente automatizar absolutamente todas las decisiones.

Hay pasos donde prefiero que el script prepare el terreno y que la decisión final siga siendo manual.


Opciones que dependen del servidor

También hay herramientas que no necesito siempre.

Por ejemplo:

AWS CLI
Tailscale
ZeroTier

Los scripts permiten decidir si instalarlas o no.

En las versiones DEV y PROD además puedo dejar algunas variables configuradas previamente.

Por ejemplo:

TARGET_USER=""
CREATE_USER=""
ADD_SSH_KEY=""
INSTALL_AWSCLI=""
INSTALL_TAILSCALE=""
INSTALL_ZEROTIER=""

Si una variable está vacía, el script pregunta.

Si ya tiene un valor definido, utiliza ese valor directamente.

Esto me permite utilizar el mismo script de dos formas:

De forma interactiva: ejecuto → respondo preguntas → preparo servidor

De forma automatizada: configuro variables → ejecuto → servidor preparado

Sin mantener más scripts distintos para pequeñas variaciones de la misma instalación.


Un pequeño detalle que termina siendo muy útil

Al finalizar se genera un archivo:

README_started.md

en el home del usuario.

Ahí dejo algunos comandos de referencia para ese servidor:

Git
Docker
ufw
fail2ban
Python / uv
ZeroTier

dependiendo de lo que se haya instalado.

Puede parecer una tontería, pero meses después, cuando vuelves a una máquina, tener ahí un pequeño recordatorio de cómo está preparada resulta bastante cómodo. Y si ese servidor lo va a utilizar otra persona, también tendrá unas notas básicas desde el principio.


¿Por qué Bash y no Ansible, Terraform o Cloudformation?

No siempre hace falta utilizar una herramienta más grande simplemente porque exista. Sí, utilizo Terraform, CloudFormation o Ansible cuando tiene sentido, pero para determinadas tareas sencillas sobre una VPS un script Bash puede ser mucho más rápido y directo.

Si el día de mañana el proyecto necesita gestionar decenas de servidores, estados complejos o configuraciones mucho más grandes, probablemente tenga sentido usarlas.

No porque preparar manualmente un VPS sea especialmente difícil, sino porque si voy a hacer lo mismo varias veces prefiero dedicar ese tiempo una vez a automatizarlo y hacerlo siempre de la misma manera.

El proyecto está disponible en GitHub: https://github.com/Pistatxos/vps-base

Seguramente seguirá cambiando según vaya encontrando cosas que vuelva a repetir más de una vez como suele pasar con este tipo de scripts. 😉

Y hasta aquí el post de hoy.

Espero que os sirva de ayuda

¡Salu2!