Autor: Roberto Miranda

  • Instalar SQL Server 2022 en Ubuntu 22.04

    Instalar SQL Server 2022 en Ubuntu 22.04

    Hay dos maneras de tener SQL Server en Ubuntu/linux, una es de manera nativa y otra mediante un contenedor en Docker.

    En mi experiencia, como buena práctica siempre es mejor tener la última versión de lo que este disponible en su momento, si hubiera problemas de compatibilidad o soporte, tuve un caso donde SQL Server no era compatible con mi versión de Ubuntu de ese momento ya que era muy reciente, la solución fue simple y sencilla, usar Docker y virtualizar una versión anterior de Ubuntu para correr SQL Server.

    En este caso es de forma directa y son pasos muy sencillos.

    Ejecutamos el siguiente comando para la clave pública:

    curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
    

    Posteriormente bajamos ubuntu del repositorio:

    curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list | sudo tee /etc/apt/sources.list.d/mssql-server-2022.list
    

    Ejecutamos un update

    sudo apt-get update

    Si nos muestra un error como el siguiente:

    W: GPG error: https://packages.microsoft.com/ubuntu/22.04/mssql-server-2022 jammy InRelease: The following signatures couldn’t be verified because the public key is not available: NO_PUBKEY EB3E94ADBE1229CF

    Ejecutamos el siguiente comando para corregir el error:

    curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc

    Ahora si ejecutamos nuestro apt-get update y después el siguiente comando:

    sudo apt-get install -y mssql-server

    Si nos muestra un error, podemos ejecutar el siguiente comando para continuar la instalación:

    sudo /opt/mssql/bin/mssql-conf setup

    Y posteriormente nos pedirá escoger nuestra versión, seleccionan su versión que requieran, nos pedirá aceptar la licencia, el idioma y con eso concluiremos nuestra instalación.

    Para validar la ejecución de SQL, haremos lo siguiente:

    systemctl status mssql-server --no-pager

  • Espacio en disco lleno en Ubuntu por /var/lib/docker/overlay2

    Espacio en disco lleno en Ubuntu por /var/lib/docker/overlay2

    Error: No space left, pero el disco no está lleno

    En esta ocasión vi que mi VPS con Ubuntu y un contenedor en docker de SQL Server y 2 aplicaciones en Net Core estaba al 99.99% de espacio en disco, el cuál ejecutando comandos como

    sudo df -h

    ó

    du -hs * | sort -nr | head -3

    Me ayudaron a identificar que era una carpeta de Docker:

    /var/lib/docker/overlay2

    Leyendo por internet vi algunos comandos como:

    docker system prune --all

    Que ayuda a depurar tu flujo en docker, es peligroso ya que elimina definitivamente contenedores que estén detenidos, yo sin conflicto alguno lo ejecuté y no me liberó nada de espacio.

    Navegando y navegando me encontré con el siguiente comando que me funcionó:

    sudo sh -c "truncate -s 0 /var/lib/docker/containers/*/*-json.log"
    
    

    lo cual hace referencia a el Controlador de registro de archivos JSON y en la documentación oficial podemos configurar como deben crecer en tamaño y cantidad.

    https://docs.docker.com/engine/logging/drivers/json-file

    Ejemplo máximo de 3 registros archivos de no más de 10 megabytes cada uno:

    docker run -it --log-opt max-size=10m --log-opt max-file=3 alpine ash
  • Copiando archivos entre servidores

    Copiando archivos entre servidores

    Primero haremos un respaldo de la base de datos

    Luego de los sitios que consta de un sitio estático en .js y otra web api

    Ruta del servidor donde se hará el back up

    tar cvzf /home/frontDiarioWeb.tar.gz /var/www/diarioweb.mirandamx.dev/html/
    tar cvzf /home/APIDiarioWeb.tar.gz /var/www/diarioapi.mirandamx.dev/html/

    Procedemos a descargar en nuestro equipo, en mi caso es windows usando PowerShell en modo administrador

    scp ubuntu@142.44.xxx.xxx:/home/frontDiarioWeb.tar.gz C:\bak\frontOVH\
    
    scp ubuntu@142.44.xxx.xxx:/home/APIDiarioWeb.tar.gz C:\bak\frontOVH\
    

    Ahora procedemos a transferirlos archivos al nuevo servidor

    scp -p ubuntu@142.44.xxx.xxx:/home/frontDiarioWeb.tar.gz root@194.163.xx.xx:/home/temporal
    
    scp -p ubuntu@142.44.xxx.xxx:/home/APIDiarioWeb.tar.gz root@194.163.xx.xx:/home/temporal
    

    Con esto ya transferimos nuestros sitios web

    Ahora si queremos descomprimir

  • Implementando un sitio .Net Core en un servidor Linux

    Implementando un sitio .Net Core en un servidor Linux

    Create Domain Directory
    sudo mkdir -p /var/www/diarioweb.mirandamx.dev/html

    Installing Nginx

    $ sudo apt-get update
    $ sudo apt-get install nginx
    sudo ufw app listOutput
    Available applications:
    Nginx Full
    Nginx HTTP
    Nginx HTTPS
    OpenSSH
    $ sudo ufw allow 'Nginx Full'
    $ sudo ufw allow ssh
    $ sudo ufw enable

    Validando el estatus de nuestro server nginx

    systemctl status nginx

    Algunos comandos útiles que necesitaremos en algún momento:

    $ sudo systemctl stop nginx
    $ sudo systemctl start nginx
    $ sudo systemctl restart nginx

    Creando los directorios y estructuras de nuestros sitios que requiramos

    sudo mkdir -p /var/www/diarioweb.mirandamx.dev/html
    sudo mkdir -p /var/www/diarioapi.mirandamx.dev/html

    Asignando permisos:

    sudo chown -R $USER:$USER /var/www/diarioweb.mirandamx.dev/html
    sudo chown -R $USER:$USER /var/www/diarioapi.mirandamx.dev/html

    sudo chmod -R 755 /var/www

    Server Block for Domain

    sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/diarioweb.mirandamx.dev
    
    sudo nano /etc/nginx/sites-available/diarioweb.mirandamx.dev

    Con CTRL + K podemos borrar contenido de manera rápida en nano

    server {
            listen 80;
            listen [::]:80;
    
            root /var/www/diarioweb.mirandamx.dev/html;
            index index.html index.htm index.nginx-debian.html;
    
            server_name diarioweb.mirandamx.dev www.diarioweb.mirandamx.dev;
    
            location / {
                    try_files $uri $uri/ =404;
            }
    }

    En este punto, dependiendo su versión de ubuntu, algunas versiones anteriores de net core podrían no ser compatible, en mi servidor Ubuntu 23.04 pude instalar net core 6.0 de la siguiente manera

    sudo apt-get update && \<br>sudo apt-get install -y aspnetcore-runtime-6.0
    

    Sin embargo en mi servidor con Ubuntu 24.04.1 LTS no pude realizarlo, así que decidí instalar la versión 8

     sudo apt-get update && \
     sudo apt-get install -y dotnet-sdk-8.0
    
    

    Si quisieran forzar la instalación de una versión no soportada, pueden poner lo siguiente:

    sudo add-apt-repository ppa:dotnet/backports
    sudo apt-get install aspnetcore-runtime-6.0

    Para validar que este instalado net core procedemos a hacer lo siguiente:

    dotnet --info

    Instalando SSL

    sudo snap install --classic certbot
    

    Antes de ejecutar el siguiente comando, nuestro dominio ya debe estar apuntando a nuestro servidor.

    sudo certbot –nginx -d diarioweb.mirandamx.dev -d diarioweb.mirandamx.dev

    Y a nuestro archivo de configuración de nginx

    sudo nano /etc/nginx/sites-available/diarioweb.mirandamx.dev

    Le añadimos lo siguiente:

    ssl_certificate /etc/letsencrypt/live/diarioweb.mirandamx.dev/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/diarioweb.mirandamx.dev/privkey.pem; # managed by Certbot

    Y se vería algo así:

    Ahora creamos nuestro archivo para levantar nuestra aplicación en el servidor kestrel mediante el siguiente comando:

    sudo nano /etc/systemd/system/kestrel-diarioWeb.service

    Le añadimos lo siguiente:

    Nota importante: Cuando configuremos múltiples sitios, tendremos que ir cambiando el puerto para que cada aplicación/sitio apunte de manera correspondiente.

    Ejemplos: <::5001>, <::5002>, <::5003>,

    [Unit]
    Description=Example .NET Web API App running on Ubuntu
    [Service]
    WorkingDirectory=/var/www/diarioweb.mirandamx.dev/html
    ExecStart=/usr/bin/dotnet /var/www/diarioweb.mirandamx.dev/html/DiarioFrontEndMVC.dll
    Restart=always
    # Restart service after 10 seconds if the dotnet service crashes:
    RestartSec=10
    KillSignal=SIGINT
    SyslogIdentifier=dotnet-example
    User=www-data
    Environment=ASPNETCORE_ENVIRONMENT=Release
    Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
    Environment=ASPNETCORE_URLS=http://127.0.0.1::5001
    [Install]
    WantedBy=multi-user.target
    
    

    Ahora en nuestro archivo de configuración cambiamos el location por un proxy_pass

    sudo nano /etc/nginx/sites-available/diarioapi.mirandamx.dev
           location / {
              proxy_pass  http://localhost:5001;    
           }
    
    
    server {
      listen 80;
      listen [::]:80;
    
      server_name api1.example.com *.example.com;
    
      root /var/www/api1.example.com;
    
      index index.html index.htm;
           location / {
              proxy_pass  http://localhost:5001;    
           }
    }
    

    Reiniciamos

    sudo systemctl enable kestrel-diarioWeb.service
    sudo systemctl start kestrel-diarioWeb.service
    sudo systemctl restart kestrel-diarioWeb.service
    sudo systemctl status kestrel-diarioWeb.service

    En este caso yo ya tenía mi compilado de mi otro sitio y solo copie mi compilado.

    Y si es exitoso, debería verse algo así:

  • Crear backup / respaldo de una base de datos de sql server en un contenedor de docker y descargar en windows mediante Powershell

    Crear backup / respaldo de una base de datos de sql server en un contenedor de docker y descargar en windows mediante Powershell

    Como el titulo lo menciona, tengo un vps con ubuntu, corre una aplicación de web api en net core y un contenedor de docker con sql server para la db, sin embargo, ando experimentando en temas de Dev Ops y cada día me hago más preguntas de «como se hace esto, aquello, etcétera. Posteriormente quiero automatizar estos procesos, desconozco del tema a profundidad, pero pudiera ser con bash scripting.

    Hay muchas maneras de crear un respaldo, anteriormente lo había hecho desde sql managmente studio, aquí está el post si gustan verlo:
    https://mirandamx.dev/copiar-archivos-de-un-contenedor-de-docker-en-ubuntu/

    Así que empezamos:

    Para eso debemos iniciar la consola de nuestro contenedor de sql server, con alguno de estos dos comandos:

    $ docker exec -it 466ae2e5db3d bash
    $ docker exec -it 466ae2e5db3d sh

    Si fuera su caso que no los deje ejecutar comandos, podemos iniciar con el usuario root

    $ sudo docker exec -it --user root 466ae2e5db3d bash

    Para crear nuestro .bak, ejecutamos el siguiente comando en la consola bash que acabamos de abrir (No olviden cambiar por sus datos de su db)

    $ /opt/mssql-tools/bin/sqlcmd -S localhost -U elapolonio -Q "BACKUP DATABASE [diario] TO DISK = N'/var/opt/mssql/data/20250219-Diario.bak' WITH NOFORMAT, NOINIT, NAME = '20250219 - diario', SKIP, NOREWIND, NOUNLOAD, STATS = 10"

    Y posteriormente nos pedirá nuestro password de sql server.

    Si lo quieren hacer de forma directa en la consola del servidor y no en la del contenedor:

    -- Alternativa más directa (cambiar datos por los suyos):
    $ docker exec -it 466ae2e5db3d /opt/mssql-tools/bin/sqlcmd -S localhost -U usernameSQL -P elPasswordSqlDelAmor -d Diario -Q "BACKUP DATABASE [Diario] TO DISK = N'/var/opt/mssql/data/20250219-Diario.bak' WITH NOFORMAT, NOINIT, NAME = N'DockerTest-Full Database Backup', COMPRESSION, STATS = 10"

    Dentro de nuestro contenedor, podemos validar la existencia del archivo:

    $ cd /var/opt/mssql/data/
    $ ls

    Copiamos el .bak de nuestro contenedor en docker a nuestro servidor

    $ sudo docker cp 466ae2e5db3d:/var/opt/mssql/data/20250219-Diario.bak /home

    Para descargar desde nuestro windows con ayuda de PowerShell ejecutamos:

    scp f ubuntu@142.44.243.239:/home/20250219-Diario.bak C:\bak\dbOvh

    Si muestra el mensaje » Permission denied», nos asignamos los siguientes permisos a nuestro archivo en nuestro servidor

    $ sudo chmod 755 /home/20250219-Diario.bak

    Borramos el .bak de nuestro servidor

    $ rm /home/20250219-Diario.bak

  • Error al ejecutar comando sudo ‘ * Is Not in the Sudoers File. This Incident Will Be Reported’

    Este error sucede cuando nuestro usuario no esta en la lista de sudoers, para necesitamos editar dicha lista que se encuentra en un fichero, para realizardo tendremos que ingresar como administrador de la siguiente manera:

    su root

    Ahora con el editor de texto nano

    nano /etc/sudoers

    Buscamos la linea de código: # User privilege specification y agregamos nuestro usuario, para que quede algo así:

    # User privilege specification
    root ALL=(ALL:ALL) ALL 
    resback ALL=(ALL:ALL) ALL

    Ahora apretamos Control + X y guardamos cambios con Y

    Y eso es todo, no olvidar los riesgos que conllleva un usuario con previlegios root.

  • Maneja DOCKER en 5 días. Mejora como SysAdmin Linux o DevOps

    Docker vs Hypervisor

    Docker nos permite virtualizar contenedores que estos manejan su propio sistema independiente y estos se ejecutan en un mismo sistema operativo compartiendo recursos.

    Aquí podemos ver la diferencia de docker vs máquinas virtuales.

    Para este curso necesitaremos linux, en mi caso use una máquina virtual con Oracle Virtual Box, en mi caso opté por el viejo confiable que es Ubuntu.

    La instalación la hacemos usando la terminal con las instrucciones desde la página oficial: https://docs.docker.com/engine/install/ubuntu

    Docker Hub

    Docker Hub es un sitio oficial de docker donde tenemos una biblioteca de imagenes listas para montar, desde sistemas operativos y aplicativos, como sql server for linux, que es el que he usado para un VPS que tengo en OVH.

    Para este curso bajaremos la siguiente versión de debian: debian:bullseye

    Recordemos que cada imagen puede tener múltiples tags, eso hace referencia que son versiones del mismo sistema y este se define con dos puntos <:>, ejemplo:

    Al ejecutar docker run, es el comando para inicializar un contenedor, pero al no encontrarlo comienza a descargarlo desde el repositorio.

    El comando para enlistar los contenedores en ejecución es el siguiente:

    docker ps

    Sin embargo posiblemente se muestre vacío, por eso añadir el parámetro -a para mostrar todos, aunque estos no esten en ejecución.

    docker ps -a

    También existe un comando similar que nos dirá que imágenes que tenemos descargadas/disponibles, una imagen es digamos que el archivo fuente para poner en marcha un contenedor. El contenedor es la imagen ya ejecutada/corriendo.

    docker images

    Comandos esenciales:

    Mostrar información de nuestro estado general de docker_

    docker info 

    Crear un contenedor a partir de una imagen

    docker run 

    Eliminar un contenedor:

    docker rm <IdContedor>

    Ejecutar un contenedor con un comando -it que es una abreviación de –interactive.

    Esta parte me trajo confusión ya que no encontré mucha documentación así que indagando esta respuesta de stackoverflow me pareción interesante:

    Docker’s / allows you to send commands to the container via standard input («STDIN»), which means you can «interactively» type commands to the pseudo-tty/terminal created by the switch.
    -i--interactive-t

    Aquí realizando la ejecución, descarga la última imagen de debian , inicia el contenedor y la consola bash:

    Como podemos ver, el @ ahora tiene un identificador y ya no es el nombre de nuestro equipo, esto quiere decir que estamos dentro del contenedor y su terminal.

    Tip de productividad al trabajar en la consola de linux.

    Para copiar y pegar algo en automático de la terminal, simplemente seleccionamos y apretamos el tercer botón del mouse.

    Ejecutar un contenedor en segundo plano

    Para este ejemplo lanzaremos un contenedor de un servidor http (apache), con el siguiente comando:

    docker run httpd

    Nos ejecutará en primer plano y para salir tendremos que presionar Ctrl + C como si estuviéramos en un editor de texto y se cerrara el contenedor.

    Para ejecutar en segundo plano usaremos el comando -d

    docker run -d httpd

    Saber la ip asignada a un contenedor

    Usaremos el comando docker inspect, sin embargo esto muestra bastante información, entonces añadiremos el parámetro | less

    docker inspect 2b113500a6c8| less

    Para salir del modo less presionamos la tecla «

    Para acceder directamente a la línea que nos interesa, podemos apoyarnos del comando grep

    docker inspect 2b113500a6c8| grep IPAddress
    

    Final mente ejecutamos docker stop para detener nuestra imagen y luego borrarla con docker rm

    Docker ps opciones

    Para poder visualizar la ayuda o opciones de un comando, podemos poner el parámetro –help al final de nuestro comando, se desplegará lo siguiente:

    Opciones con el comando docker ps

    -aMuestra todos los contenedores independientemente de su estado
    -lúltimos contenedores creados (lates)
    -qSolo los identificadores de los contenedores
    -sCuánto espacio ocupa cada contenedor
    -fFiltra la lista de contenedores según criterios (Profundizar con documentación)
    –formatPodemos elaborar nuestro propio formato del listado y que campos traer.
    (Profundizar con documentación)

    Eliminar contenedores

    El comando rápido para borrar nuestro contenedor (previamente detenidos)

    docker rm <ContainerId>

    Detener todos los contenedores activos

    docker stop $(docker ps -q)

    Ejecutar comandos dentro de contenedores

    Para ejecutar un comando en un contenedor, este tiene que estar en ejecución, para evitar su cerrado ponemos la opción -di o –detach

    docker run -di debian

    Ejemplo para ejecutar un comando dentro de un contenedor:

    docker exec 683cc25900a6 uname -a

    Para entrar dentro de batch y ejecutar múltiples comandos, podremos entrar de modo interactivo con el siguiente comando:

    docker exec -it  683cc25900a6 bash
  • Aprende a dominar Git de cero a experto!

    Aprende a dominar Git de cero a experto!

    Instalamos Git

    https://git-scm.com/downloads/win

    En el instalador seleccionamos que utilicemos VIM como el editor por default

    También seleccionamos la opción “Git  from the command line and also from 3rd party software” y todas las opciones de SSH.

    Inicilizar un proyecto

    Necesitamos dos programas opciones:

    • cmder
    • GitKraken
    • Visual Studio Code

    Para crear un nuevo repositorio nos situamos en nuestra carpeta con CMDER y ejecutamos el siguiente comando

    git init
    

    Esto creara una carpeta llamada [.git],e n visual studio creamos un elemento [index.html]

    Para saber el estatus de los archivos ejecutamos

    git status
    

     Los elementos no trackeado se mostraran en rojo, para añadirlo al trackeo usamos el comando:

    git add index.html
    

    Luego volvemos a ejectuar [git status] y se mostrará en verde,  veremos que no hay commits, para crear la instantánea usamos el siguiente comando con el parámetro -m que es el que define el mensaje:

    git commit -m "Se crea el archivo index.html"  
    

    Volvemos a editar nuestro archivo index.html y posteriormente ejecutamos nuevamente el comando :

    git status
    

    esto nos mostrara la leyenda de «modified» y en color rojo, lo cuál quiere decir que hubo cambios y no se han commiteado.

    Para hacer un commit de todos los archivos podemos ejecutar el siguiente comando:

    git add --all
    

    y posteriormente un git commit -m

    GitKraken

    Bajamos el git client para windows

    • Github es para tener repositorios en la nube y son publicos
    • Gitbucket sirve para tener repositorios privados

    Abrimos nuestro repositorio, seleccionando la carpeta, veremos nuestros commits:

    Para ver cambios, seleccionamos el comit en el panel central y posteriormente vamos al panel inferior del lado izquierdo, seleccionamos el archivo y veremos los cambios:

    Para probar el tracking de cambios, regresamos al visual studio y editamos el titulo añadiendo cualquier texto, regresamos a gitkraken y se mostrará de la siguiente manera:

    añadimos el commit message desde git kraken

    GitHub

    Creamos un repositorio públic en github, dejamos las opciones por default. Una vez hecho esto, tenemos que actualizar nuestro repositorio local al de github, para eso nos dará 3 opciones

    • Crear uno nuevo y enlazar
    • Enlazar uno existente
    • Importar desde otro repositorio

    En nuestro caso será la opción de enlazar, para eso copiamos el código y lo ejecutamos en Cmder:

    git remote add origin https://github.com/resback/GitCeroAExpertoProyect.git
    git branch -M main
    git push -u origin main
    

    Para validar ejecutamos el siguiente comando:

    git remote -v
    

    Nos mostrará el fetch y push:

    Ahora en GitKraken hacemos un Push y regreamos al panel web de github para refrescar la página y veremos el contenido de nuestro repositorio que hicimos en nuestro local.

    Crear una nueva rama

    Para crear una nueva rama nos vamos al panel izquierdo donde tenemos nuestra ramas (localk) y damos click izquierdo en la opción «Create branch here» y le asignamos un nombre, en este caso «develop». Posteriormente la pusheamos

    Unir ramas

    Para unir ramas , estando en la rama a unir, damos click derecho en la otra rama que agregaremos los cambios, le damos la opción «Merge develop into master» y generará un commit con los archivos modificados

    Nos posicionamos en la rama destino (checkout) y hacemos el push para confirmar los cambios

    Clonar repositorio

    Para emular un equipo, creamos una carpeta llamada «devs» y creamos dos subcarpetas con el nombre de los respectios devs, en mi caso Artemisa y Apolo.

    En GitKraken nos vamos [File > Clone Repository ] y seleccionamos la URL y el folder destino

    En consola con Cmder, ejecutamos el siguiente comando:

    git clone https://github.com/resback/GitCeroAExpertoProyect
    

    Para enviar cambios al servidor central

    git push
    

     Para obtener cambios del servidor central

    git pull
    

    Para saber en que rama estoy:

    git branch
    

    Resolución de conflictos

    Cuando dos usuarios editen el mismo archivo y mismas líneas, al realizar el pull/push se generará un conflicto y git no podrá resolver, con la ayuda de git kraken podremos hacer la resolución de manera más fácil con la ayuda visual simplemente damos click en el archivo del recuadro derecho (Conflicted files):

    Y de desplegará lo siguiente:

    Una vez resuelto los conflictos damos click en Save

    Ignorar archivos trackeados

    Simplemente damos click en el archivo y podemos ignorar ese archivo o todos los que contengan dicha extensión.

    Posteriormente se creará el archivo .gitignore el cual contiene el siguiente dato:

    *.config
    

    Git Stash – Guardar cambios para ser commiteados posteriormente

    Es un área para almacenar cambios temporalmente para poder ser enviados al repositorio después.

    Para guardar tus cambios en el stash, ejecuta el comando:

    git stash save "mensaje opcional para ti"
    

    o de forma visual en Git Kraken:

    Para visualizar mediante comando los cambios pendientes:

    git stash list
    

    Tags

    Sirve para asignar versiones a los commits de mayor importancia, por ejemplo, la conclusión de una versión

    Comandos básicos:

    Listar etiquetas:

    $ git tag
    

    Crear una etiqueta, con el parámetro -a para crear una y posterior -m paara especificar un mensaje:

    $ git tag -a v1.4 -m 'my version 1.4'
    

    En gitKraen podemos hacerlo simplemente dando click secundario en el commit y seleccionar «Create tag here»:

    Para subirlo al repositorio del panel izquierdo, en la sección de tags damos click secundario:

    Gitflow

    Para habilitar gitflow le damos en Preference > Gitflow

    Ahora en nuestro proyecto tendremos estas opciones disponibles:

    Referencias:

    Aprende a dominar Git de cero a experto!
    https://www.udemy.com/course/aprende-a-dominar-git-de-cero-a-experto/

    Diferencia Entre Git Rebase Y Git Merge , Workshop De Git .
    https://medium.com/@MiguelCasas/diferencia-entre-git-rebase-y-git-merge-workshop-de-git-8622dedde2d7

    Git Stash Explicado: Cómo Almacenar Temporalmente los Cambios Locales en Git
    https://www.freecodecamp.org/espanol/news/git-stash-explicado/

    2.6 Fundamentos de Git – Etiquetado
    https://git-scm.com/book/es/v2/Fundamentos-de-Git-Etiquetado