Docker y CI/CD
Lineamientos para dockerizar aplicaciones Flask y configurar pipeline GitLab con ambientes staging y producción.
Contenedores y automatización — De código fuente a producción con GitLab CI
Objetivo
Dockerizar aplicaciones Flask generadas con IA y configurar pipeline CI/CD que maneje: build, tests, deployment a staging, y deployment a producción usando el container registry de GitLab.
Requisitos
- Docker instalado localmente
- Acceso a GitLab institucional (gitlab.unach.cl)
- Repositorio configurado con estructura de ramas (main, staging, develop)
- Container Registry habilitado en el proyecto GitLab
Dockerfile multi-stage
Crear Dockerfile en la raíz del proyecto:
# Stage 1: Builder
FROM python:3.12-slim AS builder
WORKDIR /app
# Instalar dependencias
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: Production
FROM python:3.12-slim AS production
WORKDIR /app
# Copiar dependencias del builder
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
# Copiar código de la aplicación
COPY . .
# Usuario no root para seguridad
RUN useradd --create-home appuser && \
chown -R appuser:appuser /app
USER appuser
# Variables de entorno
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV FLASK_APP=run.py
# Exponer puerto
EXPOSE 5000
# Usar gunicorn como servidor de producción
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--timeout", "120", "run:app"]
Archivo de dependencias
Crear requirements.txt:
flask>=3.0.0
gunicorn>=21.0.0
python-dotenv>=1.0.0
.dockerignore
Crear .dockerignore para optimizar build:
__pycache__
*.pyc
.git
.env
.env.*
venv/
venv
.venv/
tests/
docs/
*.md
README.md
.DS_Store
GitLab CI/CD
Crear .gitlab-ci.yml en la raíz del proyecto:
stages:
- build
- test
- deploy-staging
- deploy-production
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE:pivot
DOCKER_DRIVER: overlay2
.build:
&build
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE_NAME:$CI_COMMIT_SHA .
- docker build -t $IMAGE_NAME:latest .
- docker push $IMAGE_NAME:$CI_COMMIT_SHA
- docker push $IMAGE_NAME:latest
after_script:
- docker logout $CI_REGISTRY || true
build:
<<: *build
stage: build
only:
- develop
- staging
- main
test:
stage: test
image: python:3.12-slim
before_script:
- pip install -r requirements.txt
- pip install pytest pytest-flask
script:
- pytest tests/ -v --junitxml=report.xml
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
when: always
reports:
junit: report.xml
only:
- develop
- staging
- main
deploy-staging:
<<: *build
stage: deploy-staging
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker pull $IMAGE_NAME:$CI_COMMIT_SHA
- |
docker-compose -f docker-compose.staging.yml down || true
docker-compose -f docker-compose.staging.yml up -d
- docker logout $CI_REGISTRY || true
environment:
name: staging
url: https://pivot-staging.unach.cl
only:
- staging
deploy-production:
<<: *build
stage: deploy-production
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker pull $IMAGE_NAME:$CI_COMMIT_SHA
- |
docker-compose -f docker-compose.production.yml down || true
docker-compose -f docker-compose.production.yml up -d
- docker logout $CI_REGISTRY || true
environment:
name: production
url: https://pivot.unach.cl
when: manual
only:
- main
Docker Compose para producción
Crear docker-compose.production.yml:
version: "3.9"
services:
app:
image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
container_name: pivot-app
restart: unless-stopped
ports:
- "5000:5000"
environment:
- APP_ENV=production
- FLASK_ENV=production
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=${SECRET_KEY}
- JWT_SECRET=${JWT_SECRET}
- ALLOWED_DTI_EMAILS=${ALLOWED_DTI_EMAILS}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
env_file:
- .env.production
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
nginx:
image: nginx:alpine
container_name: pivot-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/production.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- app
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 5s
retries: 3
networks:
default:
name: pivot-network
Crear configuración NGINX nginx/production.conf:
upstream pivot_app {
server app:5000;
}
server {
listen 80;
server_name pivot.unach.cl;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name pivot.unach.cl;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 10M;
location / {
proxy_pass http://pivot_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
}
location /health {
proxy_pass http://pivot_app/health;
access_log off;
}
}
Docker Compose para staging
Crear docker-compose.staging.yml:
version: "3.9"
services:
app:
image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
container_name: pivot-staging-app
restart: unless-stopped
ports:
- "5001:5000"
environment:
- APP_ENV=staging
- FLASK_ENV=staging
- DATABASE_URL=${STAGING_DATABASE_URL}
- SECRET_KEY=${STAGING_SECRET_KEY}
- JWT_SECRET=${STAGING_JWT_SECRET}
- ALLOWED_DTI_EMAILS=${ALLOWED_DTI_EMAILS}
- GOOGLE_CLIENT_ID=${STAGING_GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${STAGING_GOOGLE_CLIENT_SECRET}
env_file:
- .env.staging
networks:
default:
name: pivot-staging-network
Variables de entorno en GitLab
Configurar en GitLab: Settings → CI/CD → Variables
Staging
| Variable | Descripción |
|---|---|
STAGING_DATABASE_URL | URL de base de datos staging |
STAGING_SECRET_KEY | Secret key para staging |
STAGING_JWT_SECRET | JWT secret para staging |
STAGING_GOOGLE_CLIENT_ID | Google OAuth Client ID staging |
STAGING_GOOGLE_CLIENT_SECRET | Google OAuth Client Secret staging |
Producción
| Variable | Descripción |
|---|---|
DATABASE_URL | URL de base de datos producción |
SECRET_KEY | Secret key para producción |
JWT_SECRET | JWT secret para producción |
GOOGLE_CLIENT_ID | Google OAuth Client ID producción |
GOOGLE_CLIENT_SECRET | Google OAuth Client Secret producción |
Nunca hardcodear credenciales en el código.
Build local
# Construir imagen
docker build -t pivot:latest .
# Ejecutar localmente
docker run -d --name pivot -p 5000:5000 --env-file .env pivot:latest
# Ver logs
docker logs -f pivot
# Verificar
curl http://localhost:5000/health
# Con docker-compose
docker-compose -f docker-compose.staging.yml up -d
Referencia de imagen en el registry
Una vez que el pipeline ejecuta, la imagen queda disponible en:
gitlab.unach.cl/dti/pivot:<commit-sha>
gitlab.unach.cl/dti/pivot:latest
Para hacer pull manualmente:
# Login al registry
docker login gitlab.unach.cl
# Pull imagen
docker pull gitlab.unach.cl/dti/pivot:latest
# Tag para uso local
docker tag gitlab.unach.cl/dti/pivot:latest pivot:latest
Siguiente paso
Con Docker configurado, revisar Contributing para establecer flujo de branching y PRs.
Referencias
- Onboarding PIVOT — Pasos previos de versionamiento
- Contributing — Flujo de ramas y revisión técnica