Práctica de laboratorio · Independiente · ⏱ 90-120 min · 100% gratuito

De una cuenta nueva en Render a una app Django conectada a MySQL en la nube

Esta práctica es un laboratorio autocontenido: abres cuenta, configuras todo desde cero y terminas con 4 pruebas verificables que demuestran que tu app realmente lee y escribe en MySQL. Todos los nombres (repositorio, servicio, base de datos) se construyen con tus propios apellidos y nombre.

Cómo leer esta guía

💡 Tip — atajo o dato útil.
⚠️ Cuidado — error común, revisa dos veces.
🚫 Importante — paso obligatorio, no lo saltes.
Checkpoint — así debe verse tu pantalla.
>_ Terminal — cópialo tal cual.
{ } Código — cada línea con su comentario.
0

Antes de empezar: construye tu identificador único

Durante toda la práctica vas a nombrar repositorios, servicios y bases de datos. Para que el docente identifique tu trabajo de un vistazo, todo se nombra con tus apellidos y tu nombre, en ese orden, en minúsculas, sin espacios ni acentos.

Fórmula base

apellido-paterno-apellido-materno-nombre

Quita acentos y la letra ñ (cámbiala por "n"), usa solo tu primer nombre si tienes varios, y separa todo con guion medio.

Recurso Formato a usar Ejemplo con alumno ficticio "Ana Gómez Pérez"
Repositorio en GitHub apellidos-nombre-practica-mysql gomez-perez-ana-practica-mysql
Web Service en Render igual que el repositorio gomez-perez-ana-practica-mysql
Servicio MySQL en Aiven bd-apellidos-nombre bd-gomez-perez-ana
Carpeta / proyecto Django apellidos_nombre_practica (guion bajo) gomez_perez_ana_practica
Nombre lógico de la base de datos apellidos_nombre_bd (guion bajo) gomez_perez_ana_bd
🚫 Importante: los nombres de carpetas de Django se convierten en módulos de Python, y Python no permite guiones medios en nombres de módulos. Por eso el proyecto y la base de datos usan guion bajo ( _ ), pero GitHub y Render sí aceptan guion medio ( - ) y es la convención que usan casi todos los proyectos reales.

Escribe aquí tu propio identificador antes de continuar:

[tu-apellido-paterno]-[tu-apellido-materno]-[tu-nombre]

De aquí en adelante, esta guía usa como ejemplo:

gomez-perez-ana

Cada vez que veas ese texto en un comando, reemplázalo por tu propio identificador.

1

Abrir y configurar tu cuenta de Render

Render es el hosting gratuito donde va a vivir tu aplicación. No pide tarjeta de crédito para el plan gratuito.

Paso 1.1 — Crear la cuenta

  1. Ve a render.com y haz clic en "Get Started".
  2. Elige "Sign up with GitHub" (recomendado: conecta directo tu cuenta de GitHub y evita otro usuario/contraseña).
  3. Autoriza el acceso cuando GitHub te lo pida — Render solo lee tus repositorios, no los modifica sin tu permiso.
💡 Tip: si usas la misma cuenta de GitHub del resto del curso, Render y GitHub quedan sincronizados automáticamente para todos tus proyectos futuros.

Paso 1.2 — Configurar tu perfil

  1. Esquina superior derecha → ícono de tu cuenta → "Account Settings".
  2. En "Profile", confirma que tu nombre y correo sean correctos (el docente puede revisarlos para identificar entregas).
  3. En "Notifications" puedes activar el correo de aviso cada vez que un deploy termine o falle — muy útil para no estar refrescando la página.

Paso 1.3 — Entender el Dashboard

El panel principal de Render tiene 3 zonas que usarás constantemente en esta práctica:

"New +"

Botón para crear un Web Service, Static Site o Base de Datos nueva.

Lista de servicios

Cada proyecto que despliegues aparece aquí con un círculo de estado (gris = building, verde = live, rojo = failed).

Dentro de un servicio

Pestañas "Logs", "Environment", "Shell" y "Settings" — las usarás en los pasos de despliegue.

✅ Checkpoint de la sección Render

Tienes sesión iniciada en render.com, ligada a tu cuenta de GitHub, y ya identificas el botón "New +".

2

Crear tu base de datos MySQL gratuita (Aiven)

Render no ofrece MySQL gestionado gratis, por eso la base de datos vive en Aiven — un proveedor separado, también sin tarjeta de crédito, y tu app en Render se conecta a ella por internet.

Paso 2.1 — Crear la cuenta en Aiven

  1. Ve a aiven.io → "Get started for free".
  2. Regístrate con GitHub o Google (más rápido) o con correo y contraseña.
  3. Confirma tu correo si te lo pide.

Paso 2.2 — Crear el servicio MySQL

  1. Botón "Create service".
  2. Selecciona el motor MySQL.
  3. Plan: Free Plan (aparece marcado como gratuito, sin costo ni vencimiento de prueba).
  4. Región: elige la más cercana a tu ubicación para menor latencia.
  5. Nombre del servicio: usa tu identificador → bd-gomez-perez-ana
  6. Clic en "Create service".
⚠️ Cuidado: el servicio tarda entre 1 y 3 minutos en pasar de estado "Rebuilding" a "Running". No lo canceles ni cierres la pestaña mientras carga.

Paso 2.3 — Copiar tus credenciales de conexión

Cuando el estado sea "Running", entra a la pestaña "Overview" y copia estos 5 datos a un lugar seguro (los necesitarás en el Paso 4):

Hostalgo así como mysql-xxxx.aivencloud.com
Portun número de 5 dígitos, distinto para cada servicio
Usernormalmente "avnadmin"
Passwordcadena larga generada por Aiven
Database namenormalmente "defaultdb"
🚫 Importante: esta contraseña nunca se vuelve a mostrar completa después de salir de la página. Si la pierdes, hay un botón "Reset password" dentro de la misma pestaña.

Paso 2.4 — Verificar la conexión SSL

Aiven exige que toda conexión venga cifrada (SSL). En la pestaña "Overview" descarga o copia el certificado CA Certificate si tu conector lo requiere; con pymysql y la opción ssl-mode: REQUIRED (que verás en el Paso 9) es suficiente para esta práctica.

✅ Checkpoint de la sección MySQL

Tu servicio muestra estado "Running" y tienes los 5 datos de conexión copiados en un lugar seguro.

3

Construir la mini-app que prueba la conexión

Un proyecto Django mínimo: una vista que consulta la hora del servidor MySQL y un modelo simple para probar que se pueden guardar datos reales.

Paso 3.1 — Crear carpeta, entorno virtual e instalar dependencias

mkdir gomez-perez-ana-practica-mysql
# Crea la carpeta raíz del proyecto, nombrada con tu identificador
cd gomez-perez-ana-practica-mysql
# Entra a esa carpeta

python -m venv venv
# Crea el entorno virtual aislado

# Windows (PowerShell):
venv\Scripts\Activate.ps1
# Ejecuta el script "activate" que vive dentro de venv; a partir de este comando, "python" y "pip" usan la copia aislada del proyecto

# Mac / Linux:
source venv/bin/activate
# "source" ejecuta ese mismo script activate dentro de la sesión actual de la terminal, activando el entorno aislado

pip install django pymysql python-decouple gunicorn whitenoise
# django = framework · pymysql = conector MySQL · python-decouple = lee el .env
# gunicorn = servidor de producción · whitenoise = sirve archivos estáticos en Render

Paso 3.2 — Crear el proyecto y la app (nombres con guion bajo)

django-admin startproject gomez_perez_ana_practica .
# Crea el proyecto Django; el "." final = "en esta misma carpeta"

python manage.py startapp conexion
# Crea la app "conexion", donde vivirá la vista y el modelo de prueba

Abre gomez_perez_ana_practica/settings.py y agrega tu app dentro de INSTALLED_APPS:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'conexion',   # Registramos nuestra app para que Django la reconozca
]

Paso 3.3 — Guardar credenciales en .env (nunca en el código)

DB_NAME=defaultdb          # Database name copiado de Aiven
DB_USER=avnadmin            # User copiado de Aiven
DB_PASSWORD=tu-password      # Password copiado de Aiven
DB_HOST=tu-host.aivencloud.com  # Host copiado de Aiven
DB_PORT=12345                 # Port copiado de Aiven
SECRET_KEY=escribe-una-cadena-larga-y-aleatoria
DEBUG=True                    # True solo mientras trabajas en tu computadora
IDENTIFICADOR=gomez-perez-ana # Tu identificador, usado dentro de la app para las pruebas

Crea también .gitignore:

.env
# Nunca subas tus credenciales reales a GitHub
venv/
# Tampoco subas la carpeta del entorno virtual

Paso 3.4 — Conectar Django a MySQL en settings.py

import pymysql
# Traemos el conector que traduce entre Django y MySQL
pymysql.install_as_MySQLdb()
# Hace que Django use pymysql como si fuera el driver oficial
from decouple import config
# Función para leer variables desde el archivo .env

SECRET_KEY = config('SECRET_KEY')
# La clave secreta se lee del .env, jamás escrita directo aquí
DEBUG = config('DEBUG', default=False, cast=bool)
# Convierte el texto "True"/"False" del .env en un booleano real de Python

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',   # Motor: MySQL
        'NAME': config('DB_NAME'),                # Nombre de la base de datos
        'USER': config('DB_USER'),                # Usuario de conexión
        'PASSWORD': config('DB_PASSWORD'),        # Contraseña de conexión
        'HOST': config('DB_HOST'),                # Servidor donde vive la base de datos
        'PORT': config('DB_PORT'),                # Puerto de conexión
        'OPTIONS': {'ssl': {'ssl-mode': 'REQUIRED'}},  # Aiven exige conexión cifrada
    }
}

Paso 3.5 — Migrar por primera vez

python manage.py migrate
# Crea en MySQL las tablas internas de Django (usuarios, sesiones, permisos, etc.)
✅ Si termina sin errores rojos, tu conexión a MySQL en la nube ya funciona desde tu computadora.

Paso 3.6 — Crear el modelo de prueba (models.py)

En conexion/models.py:

from django.db import models
# Herramientas de Django para describir tablas como clases de Python

class RegistroPrueba(models.Model):
    # Cada clase que hereda de models.Model se convierte en una tabla en MySQL

    identificador = models.CharField(max_length=100)
    # Aquí guardamos tu identificador (apellidos-nombre) como dato de prueba

    mensaje = models.CharField(max_length=200)
    # Texto corto que describe la prueba realizada

    creado_en = models.DateTimeField(auto_now_add=True)
    # Django guarda automáticamente la fecha y hora del registro

    def __str__(self):
        # Define qué se muestra al "imprimir" este objeto (ej. en el panel admin)
        return f"{self.identificador} - {self.mensaje}"
        # f"..." arma un texto combinando los dos campos

En conexion/admin.py:

from django.contrib import admin
from .models import RegistroPrueba

admin.site.register(RegistroPrueba)
# Hace visible el modelo dentro del panel /admin
python manage.py makemigrations
# Prepara los cambios pendientes según el nuevo modelo
python manage.py migrate
# Crea la tabla "conexion_registroprueba" en tu base de datos MySQL real
python manage.py createsuperuser
# Crea tu usuario administrador para entrar a /admin

Paso 3.7 — Vista que prueba la conexión (views.py)

from django.db import connection
# connection = la conexión activa de Django hacia MySQL
from django.shortcuts import render
from decouple import config
from .models import RegistroPrueba

def probar_conexion(request):
    identificador = config('IDENTIFICADOR')
    # Leemos tu identificador desde el .env para usarlo en la prueba
    try:
        # try = "intenta hacer esto; si falla, no rompas la página, maneja el error"
        with connection.cursor() as cursor:
            # Abre un cursor: el objeto que ejecuta comandos SQL directos
            cursor.execute("SELECT NOW();")
            # Le pide a MySQL su fecha y hora actuales, la prueba más simple posible
            hora_servidor = cursor.fetchone()[0]
            # fetchone() trae el resultado; [0] toma el primer valor de esa fila
        RegistroPrueba.objects.create(
            identificador=identificador,
            mensaje="Conexion exitosa desde probar_conexion()"
        )
        # Inserta un nuevo registro real en la tabla, probando también la ESCRITURA
        contexto = {
            'exito': True,
            'hora_servidor': hora_servidor,
            'identificador': identificador,
            'registros': RegistroPrueba.objects.all().order_by('-creado_en')[:5],
            # Trae los últimos 5 registros, del más nuevo al más viejo
        }
    except Exception as e:
        # Si algo falla (credenciales mal escritas, base apagada, etc.) cae aquí
        contexto = {'exito': False, 'error': str(e), 'identificador': identificador}
    return render(request, 'conexion/resultado.html', contexto)

Paso 3.8 — Ruta y plantilla de resultado

Crea conexion/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.probar_conexion, name='probar_conexion'),
    # La URL raíz "" ejecuta la vista que prueba la conexión
]

En gomez_perez_ana_practica/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('conexion.urls')),
    # Cualquier URL en la raíz del sitio se busca dentro de conexion/urls.py
]

Crea la carpeta conexion/templates/conexion/ y adentro resultado.html:

<!DOCTYPE html>
<!-- Le dice al navegador "esto es HTML5 moderno", así lo interpreta con las reglas actuales -->

<html lang="es">
<!-- Abre el documento completo; lang="es" ayuda a lectores de pantalla y traductores automáticos a saber el idioma -->

<head>
<!-- Aquí va configuración que NO se ve directamente dibujada en la página -->

  <meta charset="UTF-8">
  <!-- Fija la codificación de caracteres; sin esto, tildes y la ñ pueden verse rotas -->

  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Descarga Tailwind CSS desde internet cada vez que se abre la página; con esto ya puedes usar sus clases de diseño -->

  <title>Prueba de conexión MySQL</title>
  <!-- Texto que aparece en la pestaña del navegador -->

</head>
<body class="bg-slate-50 p-6 font-sans">
<!-- bg-slate-50 (CSS) = pinta de gris muy claro el fondo de TODA la página -->
<!-- p-6 (CSS) = padding de 1.5rem: deja un margen interno de aire en los 4 lados del body -->
<!-- font-sans (CSS) = usa la familia de letra "sans-serif" configurada al inicio del archivo (Inter) -->

  <div class="max-w-xl mx-auto">
  <!-- max-w-xl (CSS) = el contenido de adentro nunca crece más de ~36rem de ancho, para que sea legible en pantallas grandes -->
  <!-- mx-auto (CSS) = centra este bloque horizontalmente (margen izquierdo y derecho calculados automáticamente) -->

    <h1 class="text-xl font-bold mb-4">Práctica: {{ identificador }}</h1>
    <!-- text-xl (CSS) = tamaño de letra grande · font-bold (CSS) = negritas · mb-4 (CSS) = margen inferior de aire -->
    <!-- {{ identificador }} = imprime el valor que la vista probar_conexion() envió al contexto con ese mismo nombre -->

    {% if exito %}
      <!-- {% if %} es una instrucción de Django Template Language: SOLO dibuja lo de adentro cuando "exito" llegó como True -->

      <div class="bg-emerald-50 border border-emerald-300 rounded-xl p-5">
      <!-- bg-emerald-50 (CSS) = fondo verde muy claro · border border-emerald-300 (CSS) = borde delgado verde -->
      <!-- rounded-xl (CSS) = esquinas bien redondeadas · p-5 (CSS) = relleno interno generoso -->

        <p class="text-emerald-800 font-bold">✅ Conexión exitosa a MySQL</p>
        <!-- text-emerald-800 (CSS) = texto verde oscuro, con buen contraste sobre el fondo claro de arriba -->

        <p class="text-emerald-700 text-sm mt-1">Hora del servidor: {{ hora_servidor }}</p>
        <!-- text-sm (CSS) = letra más pequeña que el párrafo anterior · mt-1 (CSS) = separación arriba -->
        <!-- {{ hora_servidor }} = imprime el valor que trajo cursor.fetchone() en views.py -->
      </div>

      <h2 class="font-bold mt-6 mb-2">Últimos registros guardados</h2>
      <!-- mt-6 (CSS) = separa este título del bloque verde de arriba · mb-2 (CSS) = un poco de aire antes de la lista -->

      <ul class="space-y-2">
      <!-- space-y-2 (CSS) = agrega separación vertical automática entre cada <li> hijo, sin escribir margen a mano en cada uno -->

        {% for r in registros %}
          <!-- {% for %} repite todo lo de adentro una vez por cada elemento de la lista "registros" que envió la vista -->
          <!-- en cada vuelta del ciclo, la variable "r" representa UN registro distinto de la base de datos -->

          <li class="bg-white border rounded-lg p-3 text-sm">
          <!-- bg-white (CSS) = fondo blanco, para que cada tarjeta resalte sobre el fondo gris del body -->

            <strong>{{ r.identificador }}</strong> — {{ r.mensaje }}
            <!-- <strong> pone en negritas el identificador; luego imprime el mensaje de ESTE registro -->

            <span class="text-slate-400">({{ r.creado_en }})</span>
            <!-- text-slate-400 (CSS) = gris tenue, para que la fecha se lea como un dato secundario -->
          </li>
        {% endfor %}
        <!-- {% endfor %} cierra de forma obligatoria el bucle {% for %} que abriste arriba -->
      </ul>

    {% else %}
      <!-- {% else %} se ejecuta SOLO cuando "exito" llegó como False desde la vista (hubo un error de conexión) -->

      <div class="bg-rose-50 border border-rose-300 rounded-xl p-5">
      <!-- Misma estructura de tarjeta que la verde de arriba, pero en tonos rosa/rojo (bg-rose-50, border-rose-300) para comunicar error -->

        <p class="text-rose-800 font-bold">❌ No se pudo conectar</p>

        <p class="text-rose-700 text-sm mt-1 font-mono">{{ error }}</p>
        <!-- font-mono (CSS) = muestra el texto del error con fuente de código, más fácil de leer para un mensaje técnico -->
        <!-- {{ error }} = imprime el texto que guardamos con str(e) en el bloque except de views.py -->
      </div>
    {% endif %}
    <!-- {% endif %} cierra de forma obligatoria el bloque {% if %} / {% else %} que abriste arriba -->

  </div>
</body>
</html>

Paso 3.9 — Probar en local

python manage.py runserver
# Levanta el servidor local

Abre http://127.0.0.1:8000/ en tu navegador.

✅ Debes ver la tarjeta verde "Conexión exitosa a MySQL" con la hora real del servidor de Aiven y al menos un registro en la lista.
4

Publicar en Render

Subes tu código a GitHub y le indicas a Render cómo instalarlo y ejecutarlo — con el mismo identificador que ya definiste.

Paso 4.1 — Generar requirements.txt y subir a GitHub

pip freeze > requirements.txt
# pip freeze = lista, con su versión exacta, todo lo que instalaste dentro del entorno virtual
# > requirements.txt = en vez de mostrar esa lista en pantalla, la redirige y la guarda dentro de ese archivo

git init
# Convierte esta carpeta en un repositorio Git: a partir de aquí, Git empieza a "vigilar" los cambios de tus archivos

git add .
# Marca TODOS los archivos de la carpeta como "listos para guardar" (staging); el "." significa "aquí y en las subcarpetas"
# Respeta lo que hayas puesto en .gitignore, así que venv/ y .env NO se agregan

git commit -m "Practica: conexion Django a MySQL en la nube"
# Guarda una "fotografía" fija de todos los archivos marcados con git add, formando un punto en tu historial
# -m = "message": el texto entre comillas es obligatorio y debe describir qué cambió en este avance

git branch -M main
# Renombra tu rama de trabajo actual a "main" (-M fuerza el renombrado aunque ya tenga otro nombre)
# "main" es el nombre estándar que usa GitHub para la rama principal de un repositorio

git remote add origin https://github.com/TU-USUARIO/gomez-perez-ana-practica-mysql.git
# git remote add = registra una dirección en internet donde vivirá una copia de este repositorio
# origin = el apodo con el que Git identificará esa dirección de aquí en adelante (nombre estándar por convención)
# Reemplaza TU-USUARIO por tu usuario real de GitHub, y el nombre del repositorio por tu propio identificador

git push -u origin main
# git push = envía (sube) los commits que ya guardaste en tu computadora hacia el remoto llamado "origin"
# origin main = súbelo específicamente a la rama "main" de ese remoto
# -u (--set-upstream) = deja conectada tu rama local "main" con la remota; los próximos "git push" ya no necesitarán repetir "origin main"
🚫 Importante: antes de crear el repositorio en GitHub.com, nómbralo exactamente gomez-perez-ana-practica-mysql (con tu propio identificador), en "Public".

Paso 4.2 — Crear el Web Service en Render

  1. Dashboard de Render → "New +" → "Web Service".
  2. Conecta el repositorio gomez-perez-ana-practica-mysql.
  3. Name: gomez-perez-ana-practica-mysql (mismo identificador — así tu URL final queda clara).
  4. Build Command: pip install -r requirements.txt && python manage.py collectstatic --noinput
  5. Start Command: gunicorn gomez_perez_ana_practica.wsgi
  6. Instance Type: Free.

Paso 4.3 — Variables de entorno en Render

Pestaña "Environment" → "Add Environment Variable", una por una:

DB_NAME=defaultdb
DB_USER=avnadmin
DB_PASSWORD=tu-password
DB_HOST=tu-host.aivencloud.com
DB_PORT=12345
SECRET_KEY=la-misma-clave-secreta
DEBUG=False
# En producción siempre False
ALLOWED_HOSTS=gomez-perez-ana-practica-mysql.onrender.com
# Debe coincidir exactamente con el dominio que Render te asignó
IDENTIFICADOR=gomez-perez-ana

Agrega en settings.py, si no existe ya:

ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='').split(',')
# Lee la variable y la separa por comas si hubiera más de un dominio

Paso 4.4 — Desplegar y migrar en la nube

Guarda las variables → Render construye automáticamente. Cuando el círculo de estado esté verde ("Live"), abre la pestaña "Shell":

python manage.py migrate
# Crea las tablas en MySQL desde el servidor de Render (misma base de Aiven)
python manage.py createsuperuser
# Crea tu usuario administrador ya en producción
✅ Abre https://gomez-perez-ana-practica-mysql.onrender.com/ — debes ver la misma tarjeta verde que en local, ahora accesible desde cualquier dispositivo del mundo.

Cierre de la práctica

4 pruebas para comprobar que todo funciona de verdad

Marca cada prueba solo cuando obtengas el resultado esperado. Esto es lo que se revisa en la defensa oral de esta práctica.

Si alguna prueba falla

  • Error de conexión (tarjeta roja): revisa que las 5 variables DB_* en Render sean idénticas a las de tu .env local.
  • Error 500 en Render: abre la pestaña "Logs" del servicio y busca la última línea en rojo — casi siempre indica el nombre exacto de la variable faltante.
  • La app funciona pero no aparecen registros: confirma que corriste python manage.py migrate también en la Shell de Render, no solo en tu computadora.