← Blog

Desplegar Flask en DigitalOcean

Railyard

22 de julio de 2026 · 7 min de lectura

Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta

flask run es un servidor de desarrollo por diseño, los documentos oficiales de Flask lo dicen directamente, y no está pensado para servir tráfico real. Llevar una app Flask a producción significa elegir un servidor WSGI, apuntarlo al módulo correcto, aprovisionar una base de datos, y decidir cómo corren las migraciones, decisiones que un framework minimalista como Flask deja deliberadamente a la app en vez de asumir una sola convención como podría hacer un framework más grande. Esta guía hace todo eso en Docker en un Droplet de DigitalOcean, con Caddy manejando el HTTPS, y termina con una app Flask en tu propio dominio que sabrás cómo redesplegar.

Lo que necesitas

  • Un Droplet de DigitalOcean. El plan Basic de 2 GB de memoria, 1 vCPU, 50 GB de disco ($12/mes) corre cómodamente una sola app Flask con Postgres al lado; el nivel de 1 GB ($6/mes) funciona para una app de bajo tráfico sin mucho margen de sobra.
  • Un dominio, con el DNS ya apuntando al Droplet antes de pedir HTTPS.
  • Una cuenta de DigitalOcean.
  • Una app Flask en un repositorio Git. Esta guía usa Flask 3.1.3, que requiere Python 3.9 o más reciente; el Dockerfile de abajo fija Python 3.13.
  • Docker y el plugin de Compose, ya sea localmente para probar el build primero o directamente en el Droplet.

Paso 1: crea el Droplet

En el panel de control de DigitalOcean, Create → Droplets: elige Ubuntu 24.04, la región más cercana a tus usuarios, el plan Basic de 2 GB/1 vCPU, agrega tu clave SSH, y créalo.

ssh root@your-droplet-ip

Paso 2: instala Docker

curl -fsSL https://get.docker.com | sh
docker --version
docker compose version

Paso 3: escribe el Dockerfile

FROM python:3.13-slim

WORKDIR /app

RUN apt-get update \
    && apt-get install -y --no-install-recommends build-essential libpq-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "app:app"]

Los documentos oficiales de despliegue de Flask apuntan directamente a gunicorn: “se integra fácilmente con plataformas de hosting” y no necesita dependencias extra que instalar. --workers 4 es un punto de partida razonable (una regla común de los documentos de gunicorn es más o menos 2 * núcleos de CPU + 1); ajústalo al número real de vCPU del Droplet. Si tu app usa una app factory en vez de una variable app a nivel de módulo, el comando se vuelve gunicorn --bind 0.0.0.0:8000 'app:create_app()' en su lugar.

Un requirements.txt mínimo:

flask>=3.1
gunicorn>=23.0
psycopg[binary]>=3.2
flask-sqlalchemy>=3.1
flask-migrate>=4.0

Paso 4: escribe el archivo Compose

services:
  app:
    build: .
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    expose:
      - "8000"

  db:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - app

volumes:
  db-data:
  caddy-data:
  caddy-config:

Tu archivo .env:

DB_NAME=app
DB_USER=app
DB_PASSWORD=change-this
SECRET_KEY=change-this-too
FLASK_ENV=production
DATABASE_URL=postgresql+psycopg://app:change-this@db:5432/app

Valida antes de arrancar:

docker compose config

Paso 5: escribe el Caddyfile

app.example.com {
    reverse_proxy app:8000
}

Paso 6: primer arranque y migraciones

docker compose up -d
docker compose exec app flask db upgrade
docker compose ps

Deberías ver app, db y caddy todos como running, y https://app.example.com debería responder con un certificado válido.

Paso 7: despliega nuevas versiones

git pull
docker compose build app
docker compose up -d --no-deps app
docker compose exec app flask db upgrade

Reconstruir solo app deja db y caddy intactos, así que el certificado y la base de datos siguen corriendo durante el despliegue.

Respaldos

docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz

Copia el archivo fuera del Droplet con una tarea cron, y prueba una restauración en una base de datos de prueba de vez en cuando para saber que de verdad funciona.

Sirviendo archivos estáticos

Flask puede servir su propia carpeta static/ directamente, pero Caddy ya está al frente de cada solicitud y está mejor posicionado para eso que pedirle a los workers de gunicorn que gasten tiempo sirviendo archivos en vez de ejecutar la lógica de la aplicación. Agrega un selector antes del proxy general:

app.example.com {
    handle_path /static/* {
        root * /srv/static
        file_server
    }
    reverse_proxy app:8000
}

Monta el directorio estático de la app en un volumen que Caddy también pueda leer, y corre el comando que tu proyecto use para recolectar o copiar los recursos estáticos a esa ruta durante el build de Docker, para que los archivos existan antes de que el contenedor de Caddy empiece a buscarlos.

El atajo: hazlo con Railyard

Desplegar Flask en DigitalOcean corre la misma receta desde una URL de repositorio. Conecta una cuenta de DigitalOcean con un token que tenga acceso de escritura a Droplets, o trae un servidor que ya hayas aprovisionado. Railyard detecta Flask desde requirements.txt o pyproject.toml, sirve app:app en $PORT con gunicorn (apuntado a tu propio módulo si está en otro lugar), y reconoce psycopg como dependencia para aprovisionar Postgres con DATABASE_URL inyectada automáticamente. Define flask db upgrade o alembic upgrade head como comando de release para migrar en cada release, igual que el paso 6 de arriba pero corrido automáticamente antes de que cada versión nueva reciba tráfico. RQ se ofrece como worker cuando es una dependencia, y cualquier otro proceso del Procfile se vuelve uno también. Mira los docs de Flask para las reglas exactas de detección.

Solución de problemas

  • Connection refused en el primer arranque: app arrancó antes de que Postgres terminara de inicializarse; la condición de healthcheck del paso 4 debería prevenirlo, pero un bucle corto de reintento alrededor del primer comando de migración es una protección extra razonable.
  • gunicorn arranca pero las solicitudes se cuelgan: usualmente un número de workers demasiado alto para el CPU real del Droplet, o una llamada bloqueante dentro de un manejador de solicitud que hambrea a los demás workers; revisa docker stats bajo carga y baja --workers antes de subir el tamaño del Droplet en su lugar.
  • sqlalchemy.exc.OperationalError aunque el contenedor de base de datos esté corriendo: confirma que DATABASE_URL en .env de verdad coincide con las credenciales del contenedor db; un .env desactualizado que quedó del desarrollo local es la causa más común.
  • El HTTPS nunca se emite, Caddy sigue reintentando: el DNS todavía no apunta al Droplet. dig +short app.example.com debería coincidir con la IP del Droplet.
  • flask db upgrade falla con “Can’t locate revision”: la carpeta migrations/ no se subió al repositorio, o la imagen se construyó desde un checkout desactualizado que le falta un archivo de migración reciente; confirma que git status esté limpio en el servidor antes de reconstruir.
  • Los recursos estáticos devuelven 404 vía Caddy pero funcionan al pegarle a gunicorn directamente: el selector handle_path de la sección de archivos estáticos de arriba no coincidió con la ruta de la solicitud, usualmente porque el prefijo de URL estático real de la app difiere de /static/; revisa qué prefijo genera la app de verdad en el HTML renderizado antes de asumir que Caddy está mal configurado.

Preguntas frecuentes

¿Por qué gunicorn en vez del servidor integrado de Flask? Los documentos de Flask son explícitos: el servidor de desarrollo no está diseñado para ser particularmente seguro, estable o eficiente; gunicorn es un servidor WSGI de producción hecho exactamente para esto, sin dependencias extra que compilar.

¿Y las rutas async de Flask? Flask soporta funciones de vista async def desde Flask 2.0, pero el worker sync por defecto de gunicorn no corre un loop de eventos; si la app de verdad necesita manejo async a escala, la clase de worker gevent de gunicorn o cambiar a un framework basado en ASGI son las dos opciones reales, no algo que el worker por defecto de esta guía maneje bien.

¿Postgres o MySQL? Flask-SQLAlchemy es agnóstico de base de datos; esta guía usa Postgres, pero cambiar la imagen db y el esquema de la URI de conexión de SQLAlchemy funciona igual para MySQL.

¿De verdad necesito un Droplet de 2 GB? Un Droplet de 1 GB corre Flask y Postgres juntos, pero con muy poco margen de memoria para picos de tráfico o un segundo proceso; 2 GB es una base más realista en cuanto aparecen usuarios reales.

¿Y si mi app necesita un worker de fondo, no solo el proceso web? Agrega un proceso estilo Procfile (worker: rq worker --url $REDIS_URL por ejemplo) como su propio servicio en el archivo Compose, construido desde la misma imagen que app, con su propio command sobrescrito, en vez de correrlo dentro del mismo contenedor que el proceso web.

¿Cómo veo logs y métricas básicas sin conectarme por SSH cada vez? El panel del Droplet de DigitalOcean muestra gráficos de CPU, memoria y disco desde el inicio sin configuración extra; para logs de la aplicación, docker compose logs -f app por SSH sigue siendo la opción más directa a menos que los envíes a un agregador de logs externo, lo cual vale la pena en cuanto más de una persona necesita revisar logs regularmente.

Una app Flask en un Droplet de DigitalOcean que aprovisionaste tú mismo cuesta $12 al mes, con gunicorn, Postgres y las migraciones explícitas en archivos que controlas en vez de asumidos. Desplegar una versión nueva una vez que existen es reconstruir un contenedor y correr un comando de migración, nada más que recordar.

Por qué eligen Railyard para esto

  • Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
  • Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
  • Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
  • Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
  • Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.

Sáltate la configuración del servidor.

Sube un repositorio Flask a Railyard y aprovisiona Postgres, lo sirve con gunicorn, y despliega en cada push.

Plan gratis, sin tarjeta, y 14 días de Pro para cada equipo nuevo. Un precio fijo por equipo.