Terraform describe infraestructura como código y la mantiene sincronizada con un archivo de estado. Con el proveedor Docker (kreuzwerker/docker) puedes gestionar imágenes, redes, volúmenes y contenedores de un host Docker igual que gestionarías recursos en la nube: con plan para ver los cambios antes de aplicarlos y destroy para eliminarlo todo. En este tutorial instalarás Terraform en Ubuntu 24.04 y desplegarás una base de datos PostgreSQL con volumen persistente y una interfaz web Adminer, conectadas por una red Docker privada.
Requisitos previos
Necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM.
- Un usuario no root con privilegios
sudo. - Docker Engine instalado desde el repositorio oficial y tu usuario en el grupo
docker, para que Terraform pueda usar el socket/var/run/docker.socksinsudo. Compruébalo condocker run --rm hello-world.
Paso 1: Instalar Terraform
HashiCorp publica Terraform en su propio repositorio APT. Instala las herramientas necesarias y descarga la clave de firma:
sudo apt update
sudo apt install gnupg curl lsb-release
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg
Añade el repositorio. El nombre en clave de la versión (noble) se obtiene con lsb_release:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
Instala Terraform:
sudo apt update
sudo apt install terraform
Comprueba la instalación:
terraform version
Terraform v1.x.x
on linux_amd64
Paso 2: Configurar el proveedor Docker
Crea un directorio para el proyecto. Terraform lee todos los archivos .tf de un directorio, así que puedes repartir la configuración como prefieras:
mkdir -p ~/terraform-docker
cd ~/terraform-docker
Declara el proveedor y su versión. Fijar la versión principal evita que una actualización con cambios incompatibles rompa tu configuración:
nano versions.tf
terraform {
required_version = ">= 1.5"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {
host = "unix:///var/run/docker.sock"
}
Inicializa el proyecto. terraform init descarga el proveedor y crea el archivo de bloqueo .terraform.lock.hcl, que fija la versión exacta usada:
terraform init
Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 3.0"...
- Installing kreuzwerker/docker v3.x.x...
...
Terraform has been successfully initialized!
Paso 3: Definir variables
La contraseña de la base de datos no debe estar escrita en el código. Declárala como variable sensible, para que Terraform no la muestre en plan ni en apply:
nano variables.tf
variable "db_password" {
description = "Contraseña del usuario postgres"
type = string
sensitive = true
}
variable "adminer_port" {
description = "Puerto del host donde se publica Adminer"
type = number
default = 8080
}
Terraform lee automáticamente las variables de entorno con el prefijo TF_VAR_. Exporta la contraseña en la sesión actual (el espacio inicial evita que se guarde en el historial de bash con la configuración por defecto de Ubuntu):
export TF_VAR_db_password='your_strong_password'
Paso 4: Definir imágenes, red y volumen
Crea el archivo principal con los recursos de base. docker_image descarga la imagen; con keep_locally = true Terraform no la borra al destruir el entorno. La red de tipo bridge permite que los contenedores se encuentren por nombre, y el volumen guarda los datos de PostgreSQL fuera del contenedor:
nano main.tf
resource "docker_image" "postgres" {
name = "postgres:16"
keep_locally = true
}
resource "docker_image" "adminer" {
name = "adminer:latest"
keep_locally = true
}
resource "docker_network" "backend" {
name = "tf-backend"
driver = "bridge"
}
resource "docker_volume" "pgdata" {
name = "tf-pgdata"
}
Paso 5: Definir los contenedores
Añade al final de main.tf los dos contenedores. El contenedor de PostgreSQL no publica ningún puerto en el host: solo es accesible desde la red tf-backend. Adminer sí se publica, y como está en la misma red puede conectar con la base de datos usando el nombre db:
nano main.tf
resource "docker_container" "db" {
name = "db"
image = docker_image.postgres.image_id
restart = "unless-stopped"
env = [
"POSTGRES_PASSWORD=${var.db_password}",
]
networks_advanced {
name = docker_network.backend.id
}
volumes {
volume_name = docker_volume.pgdata.name
container_path = "/var/lib/postgresql/data"
}
healthcheck {
test = ["CMD-SHELL", "pg_isready -U postgres"]
interval = "10s"
timeout = "5s"
retries = 5
}
}
resource "docker_container" "adminer" {
name = "adminer"
image = docker_image.adminer.image_id
restart = "unless-stopped"
env = [
"ADMINER_DEFAULT_SERVER=db",
]
networks_advanced {
name = docker_network.backend.id
}
ports {
internal = 8080
external = var.adminer_port
}
depends_on = [docker_container.db]
}
Usar image_id en lugar del nombre de la imagen crea una dependencia: si la imagen cambia (por ejemplo, tras un nuevo pull), Terraform sabe que debe recrear el contenedor.
Añade unas salidas para tener a mano la información útil tras el despliegue:
nano outputs.tf
output "adminer_url" {
value = "http://localhost:${var.adminer_port}"
}
output "db_container_id" {
value = docker_container.db.id
}
Formatea y valida la configuración antes de aplicarla:
terraform fmt
terraform validate
Success! The configuration is valid.
Paso 6: Revisar el plan y aplicar
terraform plan compara la configuración con el estado actual y muestra lo que va a crear, cambiar o destruir, sin tocar nada:
terraform plan
Plan: 6 to add, 0 to change, 0 to destroy.
Fíjate en que la variable POSTGRES_PASSWORD aparece como (sensitive value). Aplica los cambios y confirma con yes:
terraform apply
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
Outputs:
adminer_url = "http://localhost:8080"
db_container_id = "3f2a9c..."
Paso 7: Verificar el despliegue
Comprueba con Docker que los contenedores están en marcha y que PostgreSQL pasa su healthcheck:
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES STATUS PORTS
adminer Up 30 seconds 0.0.0.0:8080->8080/tcp
db Up 31 seconds (healthy) 5432/tcp
Verifica que la base de datos responde y que Adminer sirve su página:
docker exec db pg_isready -U postgres
curl -sI http://localhost:8080 | head -n 1
/var/run/postgresql:5432 - accepting connections
HTTP/1.1 200 OK
Para abrir Adminer desde tu navegador sin exponerlo a Internet, crea un túnel SSH desde tu equipo con ssh -L 8080:localhost:8080 your_user@your_server_ip y visita http://localhost:8080. Entra con el servidor db, el usuario postgres y la contraseña de TF_VAR_db_password.
AdvertenciaDocker publica los puertos añadiendo sus propias reglas de iptables, que no pasan por UFW. Si no quieres que Adminer sea accesible desde fuera, publícalo solo en localhost añadiendo
ip = "127.0.0.1"dentro del bloqueports.
Lista los recursos que Terraform tiene en su estado:
terraform state list
docker_container.adminer
docker_container.db
docker_image.adminer
docker_image.postgres
docker_network.backend
docker_volume.pgdata
Paso 8: Modificar y detectar cambios
La ventaja de Terraform frente a lanzar contenedores a mano es que corrige las desviaciones. Borra el contenedor de Adminer directamente con Docker:
docker rm -f adminer
Ejecuta de nuevo el plan. Terraform detecta que falta y propone recrearlo:
terraform plan
# docker_container.adminer will be created
...
Plan: 1 to add, 0 to change, 0 to destroy.
Aplica para volver al estado declarado:
terraform apply
Los cambios de configuración funcionan igual. Si cambias adminer_port a otro valor con terraform apply -var 'adminer_port=8081', el plan mostrará que el contenedor debe reemplazarse, porque los puertos publicados no se pueden cambiar en un contenedor existente.
Paso 9: Proteger el estado y destruir el entorno
El archivo terraform.tfstate guarda todos los atributos de los recursos, incluida la contraseña de la base de datos en texto claro, aunque la variable sea sensible. Protégelo y no lo subas a Git:
chmod 600 terraform.tfstate
nano .gitignore
.terraform/
*.tfstate
*.tfstate.*
Sí debes versionar .terraform.lock.hcl para que todo el equipo use la misma versión del proveedor. En equipos de varias personas, guarda el estado en un backend remoto con bloqueo en lugar de en el disco local.
Cuando ya no necesites el entorno, elimínalo:
terraform destroy
Terraform borra los contenedores, la red y el volumen tf-pgdata con todos los datos. Las imágenes se conservan por keep_locally = true. Si quieres impedir que un destroy borre el volumen por error, añade al recurso docker_volume un bloque lifecycle { prevent_destroy = true }.
Solución de problemas
Cannot connect to the Docker daemon at unix:///var/run/docker.sock: tu usuario no está en el grupodockero no has vuelto a iniciar sesión tras añadirlo. Comprueba condocker ps.No value for required variableal hacerplan: falta exportarTF_VAR_db_passworden la sesión actual.port is already allocated: otro proceso usa el puerto 8080 del host. Cambiaadminer_port.- El contenedor
dbse reinicia en bucle: revisadocker logs db. Si reutilizas un volumen creado con otra versión mayor de PostgreSQL, el servidor no arranca con esos datos.
Conclusión
Has instalado Terraform, configurado el proveedor Docker y desplegado una base de datos con volumen persistente y una interfaz web en una red privada, con detección de desviaciones y destrucción controlada. Como siguientes pasos, puedes apuntar el proveedor a un host remoto con host = "ssh://your_user@your_server_ip", mover el estado a un backend remoto y agrupar los recursos en módulos reutilizables por entorno.
