Ir al contenido
Guía completa para Dockerizar aplicaciones y servicios

Guía completa para Dockerizar aplicaciones y servicios

Tengo un servidor donde tengo desplegados muchos servicios y aplicaciones que uso en mi día a día: servidores web como el de Tutaim, bots de Telegram y servidores de juegos, entre otras cosas.

Pero, ¿qué pasa si un día se quema el datacenter donde está mi VPS, se borra todo o pasa cualquier cosa terrible que haga que no lo pueda usar más? Perdería absolutamente todos los servicios y tendría que configurar todo desde cero en un nuevo servidor, lo que perfectamente podría llevarme horas de trabajo.

Por eso he pensado en dockerizar todos los servicios y aplicaciones que pueda. Así, si ocurre alguna catástrofe, podré desplegarlos en minutos en cualquier otro servidor partiendo de cero. Para ello, en esta guía te voy a explicar todo el proceso con un ejemplo real: dockerizar un bot de Telegram, subir la imagen a GitHub y desplegarlo.

1. Instalar Docker: ¿Engine o Desktop?
#

Para instalar Docker hay varias formas. A nosotros nos interesa el Docker Engine, el motor nativo con el que crearemos y usaremos los contenedores. Sin embargo, también existe Docker Desktop, que es una aplicación con interfaz gráfica empaquetada que incluye el motor, pero le mete una tonelada de cosas extra (y máquinas virtuales) para que sea “fácil de usar”.

En la siguiente tabla puedes ver de forma rápida las diferencias clave entre ambas opciones:

CaracterísticaDocker EngineDocker Desktop
ArquitecturaProceso en segundo plano (dockerd) + CLI.App GUI + VM gestionada + Engine embebido.
RendimientoNativo (metal). Súper ligero y rápido.Pesado. Arrastra una Máquina Virtual y una interfaz gráfica.
SistemasLinux nativo (En Windows/Mac requiere VM manual).Windows, Mac y Linux (corre todo mediante VM).
InterfazTerminal pura (CLI).Interfaz gráfica amigable.
Coste100% Open Source y gratis.Gratis para uso personal. De pago para empresas grandes.
Uso idealEstándar absoluto en servidores de producción.Solo recomendado para desarrollo local cómodo.

Teniendo en cuenta estas características, elige una de las dos. Si buscas máximo rendimiento y estás en Linux, usa solo Docker Engine. Si buscas comodidad visual en Windows o Mac, usa Docker Desktop. No instales ambas opciones a la vez en Linux o tendrás conflictos severos a la hora de ejecutar comandos.

Yo voy a usar Docker Engine en Linux, que se instala de forma muy sencilla siguiendo estos pasos:

  1. Descargar e instalar el script oficial:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
  1. Arreglar los permisos (para poder usar Docker sin escribir sudo todo el rato):
sudo usermod -aG docker $USER
Important

Tras lanzar este comando, cierra sesión o reinicia el PC para que los permisos se apliquen

2. Preparar la aplicación
#

La aplicación que voy a usar para crear la imagen es un simple bot de Telegram que da la hora. Voy a crear una carpeta con los 5 archivos que necesito:

1. requirements.txt El fichero con las librerías de Python necesarias.

python-telegram-bot==20.8
python-dotenv==1.0.1
pytz==2024.1

2. .env El fichero de las variables de entorno. Este archivo es un secreto y debe ser siempre local.

TELEGRAM_TOKEN=123456:ABC-TuTokenFalsoAqui
TIMEZONE=Europe/Madrid

3. .dockerignore Aquí añadimos todos los ficheros temporales de caché, basura de los IDEs y, lo más importante, el archivo .env. Todo lo listado aquí NO se añadirá a la imagen pública.

__pycache__/
*.pyc
.env
.git/
venv/

4. main.py El código de mi bot.

import os
import logging
from datetime import datetime
import pytz
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler

# 1. Cargar variables de entorno (Oculta las claves)
load_dotenv()

# 2. Configurar Logging (Para ver errores en la consola)
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO
)

# 3. Lógica del Bot
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("¡Hola! Soy TimeBot. Usa /hora para saber qué hora es.")

async def get_time(update: Update, context: ContextTypes.DEFAULT_TYPE):
    # Pillamos la zona horaria del .env o usamos Madrid por defecto
    tz_name = os.getenv("TIMEZONE", "Europe/Madrid")
    try:
        zona = pytz.timezone(tz_name)
        hora_actual = datetime.now(zona).strftime("%H:%M:%S")
        await update.message.reply_text(f"En {tz_name} son las: {hora_actual}")
    except Exception as e:
        logging.error(f"Error con la zona horaria: {e}")
        await update.message.reply_text("He tenido un problema calculando la hora.")

# 4. Ejecución Principal
if __name__ == '__main__':
    token = os.getenv("TELEGRAM_TOKEN")
    if not token:
        raise ValueError("¡NO HAY TOKEN! Revisa tu archivo .env")

    application = ApplicationBuilder().token(token).build()
    
    application.add_handler(CommandHandler('start', start))
    application.add_handler(CommandHandler('hora', get_time))
    
    application.run_polling()

5. Dockerfile

Dockerfile es el fichero de instrucciones para crear la imagen. Si nunca has hecho un Dockerfile, puede parecer que estás escribiendo conjuros en un idioma antiguo, pero en realidad es el manual de instrucciones más tonto del mundo. Es como darle a un becario una hoja de papel diciéndole exactamente cómo tiene que configurar un ordenador desde cero.

FROM python:3.12-slim

# Argumento para detectar arquitectura (amd64/arm64)
ARG TARGETARCH

WORKDIR /app

# Instalamos dependencias del sistema si hicieran falta (ej. ffmpeg, curl...)
# RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .

# Instalamos librerías de Python
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Comando de arranque
CMD ["python", "main.py"]

¿Qué hace exactamente este Dockerfile?
#

Docker lee este archivo de arriba hacia abajo, ejecutando cada instrucción y guardando el resultado como una “capa” de la imagen. Vamos a destripar las instrucciones más importantes que hemos usado:

  • FROM python:3.12-slim (La base) Siempre es la primera instrucción. Le dice a Docker: “Bájate un Linux lite que ya traiga Python 3.12 instalado de fábrica”. Así nos ahorramos tener que instalar el lenguaje nosotros a mano.

  • ARG TARGETARCH (La arquitectura) Esta instrucción es preparatoria. Le dice a Docker que se prepare para recibir una variable externa mágica mientras está construyendo la imagen. No hace que tu imagen sea multiarquitectura por sí sola (de eso se encargará el comando buildx que veremos después), pero es vital ponerla si tu aplicación crece. ¿Por qué? Porque si mañana necesitas instalar un programa de Linux que en procesadores normales (Intel/AMD) se llama programa-x64, y en Raspberry (ARM) se llama programa-arm, puedes usar esta variable dentro del Dockerfile para decirle: “Si TARGETARCH es amd64 bájate el primero, si es arm64 bájate el segundo”. De momento no lo usamos activamente en los RUN, pero es una buena práctica dejar el Dockerfile preparado.

  • WORKDIR /app (La carpeta de trabajo) Le dice al contenedor: “A partir de ahora, todos los comandos que ejecute y los archivos que copie, mételos dentro de la carpeta /app. Obviamente en tu Docker puedes guardar los archivos donde quieras.

  • RUN apt-get update... (Ejecuta comandos de Linux) Usar RUN seguido de algo, es el equivalente a abrir la terminal de ese Linux virtual y teclear ese algo. Aquí puedes instalar programas del sistema operativo que tu aplicación necesite por debajo (como ffmpeg si vas a procesar audio o vídeo, o curl).

Note

Si vas a ejecutar varios comandos de sistema, únelos siempre con && en un solo RUN. Si pones 10 comandos RUN separados, Docker creará 10 “capas” inútiles y tu imagen pesará muchísimo más. Para entender mejor las capas de Docker de forma opcional puedes revisar su explicación oficial.

  • COPY requirements.txt . (Mete tus archivos en la olla) Copia archivos desde tu ordenador hacia dentro de la imagen. Fíjate que primero copiamos solo el archivo de requisitos y luego ejecutamos el pip install en el siguiente RUN. Esto se hace así para aprovechar el sistema de caché de Docker: si modificas tu código en main.py pero no tocas las librerías, Docker se saltará la instalación de pip y construirá la imagen más rápidamente.
  • COPY . . (El volcado final) Copia TODO lo que hay en la carpeta de tu proyecto (tu código fuente, carpetas, etc.) dentro de la imagen. Todo aquello que no quieras que entre (como el .env con tus claves reales o los temporales de __pycache__), debe estar listado en el archivo .dockerignore.
  • CMD ["python", "main.py"] (El botón de encendido) Es la orden final. Le dice al contenedor qué tiene que hacer cuando alguien lo encienda en el servidor. Ojo con esto: si el script main.py crashea o termina de ejecutarse, el contenedor entero se apagará al instante. Docker solo mantiene vivo el contenedor mientras el proceso del CMD siga corriendo en primer plano.

Además, este archivo está preparado con la variable TARGETARCH por si en el futuro queremos instalar dependencias distintas dependiendo de si el servidor usa procesadores Intel/AMD (amd64) o procesadores ARM como las Raspberry Pi (arm64).

3. Subir la imagen a GitHub (GHCR)
#

Para poder descargar nuestra aplicación desde cualquier servidor del mundo, vamos a alojar la imagen Docker en el GitHub Container Registry.

Para ello necesitas un Token. Genera uno nuevo y asegúrate de marcar la casilla write:packages. Copia ese token.

Captura del scope del token de GitHub

Ahora, abre tu terminal y inicia sesión en Github:

docker login ghcr.io -u TuUsuarioGitHub

Pega el token generado cuando te lo pida.

4. Compilación nativa y multiarquitectura
#

Multiarquitectura
#

Normalmente, si construyes la imagen en tu PC (amd64), no funcionará en máquinas ARM (arm64) como las Raspberry. Para solucionar esto y construir ambas versiones de golpe, usaremos Docker Buildx.

Ejecuta estos tres comandos en tu terminal local, dentro de la carpeta del proyecto:

# 1. Instala los emuladores para que tu PC pueda compilar ARM (solo la primera vez)
docker run --privileged --rm tonistiigi/binfmt --install all

# 2. Crea un constructor avanzado (solo la primera vez)
docker buildx create --use

# 3. Compila las dos versiones a la vez y súbelas automáticamente a GitHub
docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/TuUsuarioGitHub/timebot:latest --push .

Una vez que termine, la imagen estará disponible en la pestaña “Packages” de tu perfil de GitHub.

Package de timebot en GitHub

Nativa
#

Si solo te interesa compilar la imagen para la arquitectura de tu sistema actual, simplemente ejecuta estos comandos y ya:

# 1. Compilas la imagen
docker build -t ghcr.io/TuUsuarioGitHub/timebot:latest .

# 2. La subes al registro de GitHub
docker push ghcr.io/TuUsuarioGitHub/timebot:latest
Important

ghcr.io/TuUsuarioGitHub/timebot:latest no solo es el nombre de la imagen, es toda la dirección de envío completa, como si fuera la etiqueta de un paquete de correos. Le indicamos que suba la imagen a GitHub Container Registry en la ruta /TuUsuarioGitHub/timebot.

:latest Es la versión específica de la imagen. En un entorno profesional se suele usar una etiqeuta numérica, pero a nosotros nos sirve así.

5. El despliegue en el servidor (el orquestador)
#

Por fin llegamos al servidor de producción. Accede a una terminal de tu servidor o VPS.

En este punto, podrías encender tu contenedor usando un comando larguísimo de docker run en la terminal, pero eso es una mala práctica. Si el servidor se reinicia, el contenedor no se levantará solo y perderás la configuración.

Para solucionarlo usamos Docker Compose, que es un “orquestador local”. Nos permite escribir un archivo de configuración (docker-compose.yml) donde definimos cómo queremos que se comporte nuestro contenedor, si necesita reiniciarse automáticamente, qué puertos usa y dónde lee las contraseñas.

Como el código de nuestro bot ya está dentro de la imagen alojada en GitHub, no necesitas subir ni copiar tus archivos de Python al VPS. Solo necesitas decirle a Docker Compose que descargue la imagen y la encienda.

Crea una carpeta nueva en tu servidor, entra en ella y crea tu archivo de credenciales, que en nuestro caso sí que lo necesita, ya que es un archivo que no viene con la imagen, se debe crear localmente por seguridad.

mkdir timebot && cd timebot
nano .env

A continuación, crea el orquestador, el nombre debe ser docker-compose.yml o compose.yml, en las últimas versiones la documentación de Docker recomienda usar compose.yml, aunque docker-compose.yml sigue siendo completamente válido.

nano docker-compose.yml

Y pega la siguiente configuración:

services:
  timebot:
    image: ghcr.io/TuUsuarioGitHub/timebot:latest
    container_name: timebot_prod
    restart: always
    env_file:
      - .env

¿Qué significa cada línea del docker-compose.yml?
#

  • services:: Aquí empieza la lista de aplicaciones que vamos a levantar. En nuestro caso, solo una (nuestro bot).
  • timebot:: Es el nombre interno que le damos al servicio. Si en el futuro añadiéramos una base de datos de PostgreSQL, la pondríamos debajo con el nombre db:.
  • image:: Le dice a Docker de dónde tiene que descargar el paquete exacto. Al poner :latest, nos aseguramos de que siempre apunte a la última versión que hayamos subido a GitHub.
  • container_name:: Es el nombre “bonito” que verás cuando listes los procesos de tu servidor con docker ps.
  • restart: always: La línea salvavidas. Le dice a Docker que, si el script de Python crashea por un error, o si el servidor VPS entero se reinicia por un corte de luz, Docker debe volver a encender el bot automáticamente.
  • env_file:: Le inyecta de forma segura las contraseñas y configuraciones que guardamos en nuestro archivo .env del servidor, para que el código en Python pueda leer el TELEGRAM_TOKEN sin que esté público en GitHub.

Para ver todas las opciones aparte de las explicadas aquí, que ofrece el fichero Docker Compose, te recomiendo mirar la documentación oficial de docker.

Una vez guardado el archivo, enciende el bot en segundo plano ejecutando:

docker compose up -d

(El parámetro -d o “detached” sirve para que el bot se quede corriendo de fondo y te devuelva el control de la terminal).

Ejecutamos y esperamos a que baje la imagen y la encienda:

ubuntu@myvps:~/dockers/timebot$ docker compose up -d
[+] up 11/11
 ✔ Image ghcr.io/mr-umar/timebot:latest Pulled                                                                                                                                                                  2.4s
 ✔ Network timebot_default              Created                                                                                                                                                                 0.5s
 ✔ Container timebot_prod               Started                                                                                                                                                                 0.3s
ubuntu@myvps:~/dockers/timebot$ 

Para comprobar que nuestro contenedor está funcionando, podemos comprobarlo con docker ps:

ubuntu@myvps:~/dockers/timebot$ docker ps
CONTAINER ID   IMAGE                                     COMMAND                  CREATED              STATUS                 PORTS                                     NAMES
4ba9a9dd53df   ghcr.io/mr-umar/timebot:latest            "python main.py"         About a minute ago   Up About a minute                                                timebot_prod

Como vemos, el bot ya está funcionando en la nube de forma segura, aislada y preparado para sobrevivir a reinicios.

Captura del bot de Telegram

6. Actualizar en el futuro (El ciclo DevOps)
#

¿Has modificado el código en tu ordenador y quieres actualizar la aplicación en el VPS? Este es el proceso que deberías seguir:

  1. En tu PC: Vuelves a lanzar el comando de subida.
docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/TuUsuarioGitHub/timebot:latest --push .

o

# 1. Compilas la imagen
docker build -t ghcr.io/TuUsuarioGitHub/timebot:latest .

# 2. La subes al registro de GitHub
docker push ghcr.io/TuUsuarioGitHub/timebot:latest
  1. En tu VPS: Te descargas la actualización y reinicias el contenedor.
docker compose pull
docker compose up -d

Con estos dos simples pasos, Docker detecta la nueva versión, apaga el contenedor viejo y levanta el nuevo en cuestión de pocos segundos. Ya tienes un sistema DevOps completo que te salvará la vida la próxima vez que tu servidor decida salir ardiendo.

Conclusión
#

Este bot de Telegram es solo la excusa: el flujo es el mismo para casi cualquier app o servicio que quieras dejar “a prueba de incendios” (Dockerfile → build → subir a un registry → desplegar con Compose en el servidor → actualizar con pull + up). La gracia no es el ejemplo, es que separas código (dentro de la imagen) de config/secrets (fuera, en .env/variables), y así puedes recrear todo en minutos en cualquier máquina limpia sin copiar proyectos a mano.

A partir de aquí, tu trabajo es repetir el patrón: si la app usa puertos, los declaras en Compose; si necesita persistencia, le metes volúmenes; si depende de otros servicios (DB, Redis), los añades como más services y listo. Para no liarla, cuando quieras hacer algo “raro” (healthchecks, redes, volúmenes, límites de recursos, depends_on, etc.), tira primero de la documentación oficial de Docker Compose, porque ahí están todas las opciones con el comportamiento real y actualizado. Y sí: puedes usar un LLM para que te genere el esqueleto del Dockerfile/compose.yml o para adaptar el despliegue a tu caso, pero luego no seas un NPC: compruébalo en local, y versiona/taggea tus imágenes si no quieres sorpresas.

Umar Mohammad
Autor
Umar Mohammad
Analista de ciberseguridad y estudiante de telecos