Skip to content

🛠️ Guia Prático: Implementar Stack Vendor-Agnostic

Objetivo Final

Uma stack 100% portável que roda em: - Seu laptop agora - VPS em 2 minutos
- Qualquer cloud em 5 minutos - Kubernetes em 30 minutos


🎯 Passo 1: Preparação (1 hora)

1.1 Instalar Docker (se não tiver)

# macOS
brew install docker

# Ou baixar Docker Desktop
# https://www.docker.com/products/docker-desktop

# Verificar
docker --version
docker-compose --version

1.2 Estrutura do Projeto

cd /Users/glauberportella/Projects/ifriend/ifriend-agents

# Criar diretorios
mkdir -p infra/docker infra/k8s data/postgres data/ollama

# Estrutura resultante:
proyecto/
├─ docker-compose.yml          # Stack orquestrado
├─ .env                         # Configurações secretas
├─ Dockerfile                   # Imagem personalizada
├─ requirements.txt             # Dependências Python
│
├─ infra/
│  ├─ docker/
│    ├─ init_db.sql           # Schema PostgreSQL    ├─ compose.prod.yml      # Para produção    └─ compose.local.yml     # Local com volumes  │
│  └─ k8s/
│     ├─ namespace.yaml        # Namespace k8s     ├─ postgres.yaml         # StatefulSet     ├─ ollama.yaml           # Deployment     └─ api.yaml              # Deployment
│
├─ data/
│  ├─ postgres/                # Dados de DB (gitignored)  └─ ollama/                  # Modelos Ollama (gitignored)
│
├─ busca_productos/            # Seu código (já existe)  ├─ agent.py
│  ├─ slack_bot.py
│  ├─ memory_service_agnostic.py
│  └─ requirements.txt
│
└─ docs/
   ├─ VENDOR_AGNOSTIC_STACK.md
   └─ SETUP_GUIDE.md (este arquivo)

🐳 Passo 2: Docker Compose Local (2 horas)

2.1 Criar docker-compose.yml

version: '3.8'

services:
  # PostgreSQL com pgvector
  postgres:
    image: pgvector/pgvector:pg16-latest
    container_name: ifriend-postgres
    environment:
      POSTGRES_DB: ${DB_NAME:-ifriend}
      POSTGRES_USER: ${DB_USER:-ifriend}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
      POSTGRES_INITDB_ARGS: "-c shared_preload_libraries=vector"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./infra/docker/init_db.sql:/docker-entrypoint-initdb.d/01-init.sql
      - ./infra/docker/seed_data.sql:/docker-entrypoint-initdb.d/02-seed.sql
    ports:
      - "${DB_PORT:-5432}:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-ifriend}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - ifriend_net
    restart: unless-stopped
    environment:
      TZ: America/Sao_Paulo

  # Ollama para embeddings
  ollama:
    image: ollama/ollama:latest
    container_name: ifriend-ollama
    volumes:
      - ollama_data:/root/.ollama
    ports:
      - "${OLLAMA_PORT:-11434}:11434"
    environment:
      - OLLAMA_HOST=0.0.0.0:11434
      - OLLAMA_NUM_GPU=0                 # Mude para 1 se tiver GPU
      - OLLAMA_NUM_THREAD=2              # Ajuste para seu CPU
      - TZ=America/Sao_Paulo
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - ifriend_net
    restart: unless-stopped

  # API Backend (seu código)
  api:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        PYTHON_VERSION: 3.11
    container_name: ifriend-api
    environment:
      # Database
      DATABASE_URL: postgresql://${DB_USER:-ifriend}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-ifriend}

      # Ollama
      OLLAMA_URL: http://ollama:11434
      OLLAMA_MODEL: nomic-embed-text

      # Slack
      SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}
      SLACK_APP_TOKEN: ${SLACK_APP_TOKEN}

      # Logging
      LOG_LEVEL: ${LOG_LEVEL:-INFO}

      # Environment
      ENVIRONMENT: ${ENVIRONMENT:-development}
      TZ: America/Sao_Paulo

    depends_on:
      postgres:
        condition: service_healthy
      ollama:
        condition: service_healthy

    ports:
      - "${API_PORT:-8000}:8000"

    volumes:
      - ./busca_productos:/app/busca_productos
      - ./ifriend_agent:/app/ifriend_agent

    networks:
      - ifriend_net

    restart: unless-stopped

    # Liiveness probe para k8s depois
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  # (Optional) pgAdmin para gerenciar DB
  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: ifriend-pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
    ports:
      - "${PGADMIN_PORT:-5050}:80"
    networks:
      - ifriend_net
    restart: unless-stopped
    profiles:
      - dev  # Só roda se especificar: docker-compose --profile dev up

volumes:
  postgres_data:
    driver: local
  ollama_data:
    driver: local

networks:
  ifriend_net:
    driver: bridge

2.2 Criar .env

# Database
DB_NAME=ifriend
DB_USER=ifriend
DB_PASSWORD=your_secure_password_here_change_this
DB_PORT=5432

# Ollama
OLLAMA_PORT=11434
OLLAMA_MODEL=nomic-embed-text

# API
API_PORT=8000
ENVIRONMENT=development
LOG_LEVEL=INFO

# Slack (copie seus tokens)
SLACK_BOT_TOKEN=xoxb-your-token-here
SLACK_APP_TOKEN=xapp-your-token-here

# PgAdmin (apenas dev)
PGADMIN_PASSWORD=admin
PGADMIN_PORT=5050

2.3 Criar Dockerfile

FROM python:3.11-slim

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    curl \
    postgresql-client \
    git \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements
COPY requirements.txt .

# Install Python packages
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Create health check endpoint in Slack bot
RUN mkdir -p /app/health && echo "OK" > /app/health/status.txt

# Start application
CMD ["python", "-m", "busca_productos.slack_bot"]

2.4 Criar infra/docker/init_db.sql

-- Extensions
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS uuid-ossp;

-- Schema
CREATE SCHEMA IF NOT EXISTS memories;

-- Memories table
CREATE TABLE memories.memories (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  app_name TEXT NOT NULL,
  user_id TEXT NOT NULL,
  session_id TEXT,
  sector TEXT NOT NULL DEFAULT 'general',
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  embedding vector(384),
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now(),
  metadata JSONB DEFAULT '{}',

  CONSTRAINT memories_nonempty_content CHECK (length(trim(content)) > 0)
);

-- Índices para performance
CREATE INDEX idx_memories_app_user ON memories.memories(app_name, user_id);
CREATE INDEX idx_memories_embedding ON memories.memories USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX idx_memories_sector ON memories.memories(sector);
CREATE INDEX idx_memories_created ON memories.memories(created_at DESC);
CREATE INDEX idx_memories_metadata ON memories.memories USING GIN(metadata);

-- Sessions table
CREATE TABLE memories.sessions (
  id TEXT PRIMARY KEY,
  app_name TEXT NOT NULL,
  user_id TEXT NOT NULL,
  data JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_sessions_app_user ON memories.sessions(app_name, user_id);
CREATE INDEX idx_sessions_created ON memories.sessions(created_at DESC);

-- Vector search function
CREATE OR REPLACE FUNCTION memories.match_memories(
  query_embedding vector,
  p_user_id TEXT,
  p_app_name TEXT,
  p_limit INT DEFAULT 5,
  p_min_similarity FLOAT DEFAULT 0.5
)
RETURNS TABLE (
  id UUID,
  title TEXT,
  content TEXT,
  sector TEXT,
  similarity FLOAT8
) AS $$
BEGIN
  RETURN QUERY
  SELECT 
    m.id,
    m.title,
    m.content,
    m.sector,
    1 - (m.embedding <=> query_embedding)::FLOAT8 as similarity
  FROM memories.memories m
  WHERE 
    m.user_id = p_user_id 
    AND m.app_name = p_app_name
    AND (m.embedding <=> query_embedding) <= (1 - p_min_similarity)
  ORDER BY m.embedding <=> query_embedding
  LIMIT p_limit;
END;
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;

-- Grants
GRANT USAGE ON SCHEMA memories TO ifriend;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA memories TO ifriend;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA memories TO ifriend;
GRANT USAGE ON TYPE vector TO ifriend;

-- Seed data (optional)
COMMENT ON SCHEMA memories IS 'Vector memory and session storage for ifriend agents';

2.5 Testar Localmente

# 1. Build imagem
docker-compose build

# 2. Iniciar todos os serviços
docker-compose up -d

# 3. Verificar status
docker-compose ps

# OUTPUT esperado:
# NAME                COMMAND                  SERVICE      STATUS
# ifriend-postgres    "docker-entrypoint..."   postgres     Up (healthy)
# ifriend-ollama      "ollama serve"           ollama       Up (healthy)
# ifriend-api         "python -m busca..."     api          Up

# 4. Puxar modelo Ollama
docker exec ifriend-ollama ollama pull nomic-embed-text

# 5. Testar Database
docker exec ifriend-postgres psql -U ifriend -d ifriend -c "SELECT version();"

# 6. Testar Ollama
curl http://localhost:11434/api/tags

# 7. Ver logs
docker-compose logs -f api

# 8. Parar tudo
docker-compose down

🌐 Passo 3: Deploy em VPS (3 horas)

3.1 Criar VM no Digital Ocean

# Via console em https://cloud.digitalocean.com
# Ou via CLI:
doctl compute droplet create ifriend-api \
  --region sfo3 \
  --image ubuntu-22-04-x64 \
  --size s-1vcpu-2gb \
  --ssh-keys your-ssh-key-id \
  --enable-backups

# Output:
# ID          Name         Memory  vCPUs  Disk  Status  Price
# 456789      ifriend-api  2048    1      50    new     $0.0149/hr ($10/mth)

# Pegar IP
VPS_IP=$(doctl compute droplet get ifriend-api -o json | jq -r '.droplets[0].networks.v4[0].ip_address')
echo $VPS_IP  # 167.99.xxx.xxx

3.2 SSH e Setup Inicial

# SSH na VM
ssh -i ~/.ssh/id_rsa root@$VPS_IP

# 1. Update sistema
apt update && apt upgrade -y

# 2. Instalar Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

# 3. Instalar Docker Compose
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

# 4. Criar user não-root (melhor prática)
useradd -m -s /bin/bash deploy
usermod -aG docker deploy

# 5. Setup SSH para deploy
su - deploy
mkdir -p ~/.ssh
# Adicionar sua chave pública

3.3 Clonar e Configurar

# Como user deploy
cd /home/deploy

# Clone repo
git clone seu-repo.git ifriend
cd ifriend

# Setup .env (alterar valores!)
cp .env.example .env
nano .env

# Valores importantes para VPS:
# - DB_PASSWORD: Use algo seguro
# - SLACK_BOT_TOKEN: Seu token real
# - ENVIRONMENT: production
# - OLLAMA_NUM_THREAD: Ajuste para 1 (VPS pequeno)

3.4 Iniciar Stack em Produção

# Docker Compose para produção (com restart automático)
docker-compose -f docker-compose.prod.yml up -d

# Verificar
docker-compose ps

# Puxar modelo (demora um pouco primeira vez)
docker exec ifriend-ollama ollama pull nomic-embed-text

3.5 Setup Reverse Proxy (Nginx)

# Instalar Nginx
sudo apt install -y nginx

# Criar config
sudo nano /etc/nginx/sites-available/ifriend

# Conteúdo:
upstream api {
  server localhost:8000;
}

server {
  listen 80;
  server_name your-domain.com;

  # Redirecionar HTTP → HTTPS
  return 301 https://$server_name$request_uri;
}

server {
  listen 443 ssl http2;
  server_name your-domain.com;

  ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

  location / {
    proxy_pass http://api;
    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;
  }
}

# Enable site
sudo ln -s /etc/nginx/sites-available/ifriend /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

# SSL com Let's Encrypt
sudo apt install -y certbot python3-certbot-nginx
sudo certbot certonly -d your-domain.com

☸️ Passo 4: Kubernetes (4 horas)

4.1 Setup k3s (lightweight Kubernetes)

# Na VM ou seu laptop
curl -sfL https://get.k3s.io | sh -

# Verificar
sudo kubectl get nodes

# Get kubeconfig para usar localmente
cat /etc/rancher/k3s/k3s.yaml
# Copie para ~/.kube/config

4.2 Criar Kubernetes Manifests

infra/k8s/namespace.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: ifriend

infra/k8s/postgres.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: ifriend
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: ifriend
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: pgvector/pgvector:pg16-latest
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_DB
          valueFrom:
            configMapKeyRef:
              name: db-config
              key: db-name
        - name: POSTGRES_USER
          valueFrom:
            configMapKeyRef:
              name: db-config
              key: db-user
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: ifriend
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
  - port: 5432

infra/k8s/ollama.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ollama
  namespace: ifriend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ollama
  template:
    metadata:
      labels:
        app: ollama
    spec:
      containers:
      - name: ollama
        image: ollama/ollama:latest
        ports:
        - containerPort: 11434
        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "4Gi"
            cpu: "2000m"
        volumeMounts:
        - name: data
          mountPath: /root/.ollama
      volumes:
      - name: data
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: ollama
  namespace: ifriend
spec:
  selector:
    app: ollama
  ports:
  - port: 11434
    targetPort: 11434

infra/k8s/api.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: ifriend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: api
        image: ifriend-api:latest
        imagePullPolicy: Never  # Use local image
        ports:
        - containerPort: 8000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: api-secret
              key: database-url
        - name: OLLAMA_URL
          value: "http://ollama:11434"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: ifriend
spec:
  type: LoadBalancer
  selector:
    app: api
  ports:
  - port: 8000
    targetPort: 8000

4.3 Deploy em Kubernetes

# Create namespace
kubectl apply -f infra/k8s/namespace.yaml

# Create secrets e configs
kubectl create secret generic db-secret \
  -n ifriend \
  --from-literal=password='your-secure-password'

kubectl create secret generic api-secret \
  -n ifriend \
  --from-literal=database-url='postgresql://ifriend:password@postgres:5432/ifriend'

# Create config
kubectl create configmap db-config \
  -n ifriend \
  --from-literal=db-name=ifriend \
  --from-literal=db-user=ifriend

# Deploy
kubectl apply -f infra/k8s/

# Verificar
kubectl get all -n ifriend
kubectl logs -f deployment/api -n ifriend

✅ Checklist de Validação

Local (docker-compose)

# ✅ Database
docker exec ifriend-postgres psql -U ifriend -d ifriend -c "SELECT 1;"

# ✅ Ollama
curl http://localhost:11434/api/tags

# ✅ API health
curl http://localhost:8000/health

# ✅ Memory service
docker logs ifriend-api | grep -i memory

VPS (docker-compose)

# SSH na VM
ssh deploy@$VPS_IP

# ✅ Services rodando
docker-compose ps

# ✅ Database acessível
docker exec ifriend-postgres psql -U ifriend -d ifriend -c "SELECT 1;"

# ✅ Ollama pronto
curl http://localhost:11434/api/tags

# ✅ Nginx rodando
curl https://your-domain.com/health

Kubernetes

# ✅ Pods rodando
kubectl get pods -n ifriend

# ✅ Services
kubectl get svc -n ifriend

# ✅ Logs
kubectl logs deployment/api -n ifriend

# ✅ Port forward para testar
kubectl port-forward svc/api 8000:8000 -n ifriend
curl http://localhost:8000/health

🎯 Próximos Passos

  1. Agora: Teste docker-compose localmente
  2. ⏭️ Amanhã: Deploy em VPS
  3. ⏭️ Próxima semana: Setup Kubernetes
  4. ⏭️ Depois: Backup + monitoring + CI/CD

📚 Recursos Úteis