Rodar Large Language Models (LLMs) locais em CPU é lento. Se o seu servidor de homelab tem uma APU AMD Ryzen, você pode usar essa GPU integrada para acelerar a inferência.
Rodar tudo dentro de um container LXC leve do Proxmox, em vez de uma VM completa, economiza recursos. No entanto, as permissões de containers sem privilégios e as peculiaridades dos drivers da AMD costumam quebrar a configuração.
Meu hardware:
- CPU: AMD Ryzen 5 4600G com Radeon Graphics (1 Socket)
- Memória: 128GB
- SO: Proxmox Virtual Environment 9.1.6
Isto é para testes, aprendizado e self-hosting de modelos leves em hardware de homelab, e não para montar uma estação de trabalho de IA gigante.
Veja como repassar a sua APU AMD a um container LXC, instalar o Docker e implantar uma stack de IA com aceleração por hardware usando Ollama e Open WebUI.
Fase 1: Preparar o host Proxmox#
Todos os comandos desta fase devem ser executados no shell do host Proxmox (root@pve1).
1. Instalar o firmware da AMD (o repositório non-free)#
O Proxmox (baseado em Debian) não inclui por padrão o firmware gráfico da AMD necessário. Precisamos habilitar os repositórios de firmware non-free para instalar o pacote.
nano /etc/apt/sources.listGaranta que as linhas do seu repositório principal do Debian incluam non-free e non-free-firmware. Deve ficar mais ou menos assim:
deb http://deb.debian.org/debian trixie main contrib non-free non-free-firmware
deb http://security.debian.org/debian-security trixie-security main contrib non-free non-free-firmware
deb http://deb.debian.org/debian trixie-updates main contrib non-free non-free-firmware(Nota: se você está em uma versão mais antiga do Proxmox, substitua trixie por bookworm).
Atualize os pacotes e instale o firmware:
apt update
apt install -y firmware-amd-graphics2. Configurar o GRUB para IOMMU#
Primeiro, habilite o IOMMU para que o host possa acessar diretamente o hardware da GPU.
nano /etc/default/grubModifique a linha GRUB_CMDLINE_LINUX_DEFAULT para ficar exatamente assim (remova qualquer flag video=efifb:off ou nomodeset, se existir):
GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt"Atualize o GRUB para aplicar:
update-grub3. Desativar o “sequestro” do VFIO#
Se você já configurou passthrough de VM antes, pode ter colocado os drivers da GPU na blacklist. Precisamos que o host os use para o LXC.
nano /etc/modprobe.d/vfio.confComente (adicione # no início) todas as linhas, para que fique assim:
# options vfio-pci ids=1002:1636...
# softdep amdgpu pre: vfio-pci
# softdep snd_hda_intel pre: vfio-pci4. Forçar o carregamento do módulo amdgpu#
Garanta que o host carregue os drivers da AMD no boot.
nano /etc/modulesAdicione estas linhas no final:
vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd
amdgpu5. Verificar blacklists ocultas da AMD#
Antes de reiniciar, garanta que o Proxmox não esteja bloqueando ativamente o driver. Execute a seguinte verificação:
grep -ir "amdgpu" /etc/modprobe.d/Se você vir um arquivo como blacklist-amdgpu.conf, será preciso renomeá-lo, ou o driver nunca vai carregar, independentemente da sua configuração em /etc/modules:
mv /etc/modprobe.d/blacklist-amdgpu.conf /etc/modprobe.d/blacklist-amdgpu.conf.bak6. Aplicar as alterações e reiniciar#
Regenere as imagens de boot e reinicie o seu servidor Proxmox.
update-initramfs -u -k all
reboot7. Verificar se os drivers carregaram#
Quando o host voltar, verifique os arquivos de dispositivo da GPU:
ls -l /dev/driVocê deve ver card0 e renderD128.
Fase 2: Corrigir as permissões do container (a correção com Udev)#
Por padrão, containers LXC sem privilégios mapeiam o usuário root para um usuário “nobody” de número alto no host. Se você repassar a GPU agora, o container terá o acesso negado. Corrigimos isso com uma regra Udev no host.
Execute isto no host Proxmox:
# Create the rule to make the GPU globally readable/writable
echo 'KERNEL=="renderD128", MODE="0666"' > /etc/udev/rules.d/99-render.rules
echo 'KERNEL=="card0", MODE="0666"' >> /etc/udev/rules.d/99-render.rules
# Apply the rule immediately
udevadm control --reload-rules && udevadm triggerVerifique se as permissões mudaram (procure por crw-rw-rw-):
ls -l /dev/dri/renderD128Fase 3: Criar e configurar o container LXC#
1. Provisionar o container#
Crie um novo container LXC sem privilégios usando a CLI do Proxmox. Para a stack de IA, o Ubuntu 24.04 é o template recomendado. Primeiro, atualize o cache de templates e baixe a imagem:
pveam update
pveam download local ubuntu-24.04-standard_24.04-2_amd64.tar.zstCrie o container (usamos o ID 100 como exemplo), atribuindo 4 núcleos de CPU, 4GB de RAM e 30GB de armazenamento:
pct create 100 local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \
--unprivileged 1 \
--hostname local-ai \
--cores 6 \
--memory 49152 \
--swap 0 \
--rootfs local-lvm:30 \
--net0 name=eth0,bridge=vmbr0,ip=10.1.99.16/16,gw=10.1.0.1,tag=99Não inicie o container ainda.
2. Obter os IDs de grupo do host (opcional)#
Como a regra Udev da Fase 2 torna a GPU gravável por qualquer usuário (0666), alinhar os IDs de grupo não é estritamente necessário para o container iniciar. Mesmo assim, mantê-los alinhados continua sendo uma boa prática estrutural. Encontre os IDs dos grupos render e video no seu host:
getent group render | cut -d: -f3 # Example: 104
getent group video | cut -d: -f3 # Example: 443. Habilitar o nesting do LXC#
Na interface web do Proxmox, selecione o seu container -> Options -> Features. Marque a caixa Nesting. Isso é obrigatório para rodar Docker dentro do LXC.
4. Editar a configuração do LXC#
Encontre o ID do seu container (por exemplo, 100). Edite o arquivo de configuração dele no host:
nano /etc/pve/lxc/100.confAcrescente as linhas a seguir para repassar o hardware (atualize os GIDs se os seus forem diferentes de 104 e 44):
# GPU Passthrough
lxc.cgroup2.devices.allow: c 226:0 rwm
lxc.cgroup2.devices.allow: c 226:128 rwm
lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=fileInicie (ou reinicie) o seu container LXC.
Fase 4: Implantando o Ollama e o Open WebUI com Docker Compose#
Rodar containers avulsos exige vários comandos e deixa a rede frágil. Em vez disso, definimos a stack de IA com o Docker Compose.
Entre no console do seu container LXC (pct enter 100 ou SSH).
1. Instalar o Docker#
Primeiro, pegue o script oficial para instalar o Docker (que inclui o plugin do Compose) no seu container Ubuntu novo:
apt update && apt upgrade -y
curl -fsSL https://get.docker.com | sh2. Definir a configuração da stack#
Crie um novo diretório para os serviços de IA e escreva o arquivo docker-compose.yml:
mkdir -p ~/ai-stack && cd ~/ai-stack
nano docker-compose.ymlCole o YAML a seguir no arquivo:
services:
# -------------------------------------
# 1. Reverse Proxy (Traefik)
# -------------------------------------
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- "--api.insecure=true" # Enables the Traefik dashboard on port 8080 (optional)
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false" # Security: deny mapping unless explicitly labeled
# Define both HTTP (80) and HTTPS (443) entrypoints
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Automatically redirect all HTTP traffic to HTTPS (websecure)
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80" # Standard HTTP entrypoint
- "443:443" # Secure HTTPS entrypoint
- "8080:8080" # Traefik Dashboard
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" # Secure read-only access to Docker events
networks:
- proxy-net
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# -------------------------------------
# 2. LLM Backend (Ollama)
# -------------------------------------
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
user: "root" # Force hardware permissions
privileged: true # Bypass Docker AppArmor blocking IO controls to /dev/dri
networks:
- ai-net
devices:
- /dev/dri:/dev/dri
volumes:
- ollama_data:/root/.ollama
environment:
- OLLAMA_HOST=0.0.0.0
- OLLAMA_VULKAN=1
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
deploy:
resources:
limits:
memory: 48G
logging:
driver: "json-file"
options:
max-size: "20m"
max-file: "3"
# -------------------------------------
# 3. Frontend UI (Open-WebUI)
# -------------------------------------
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
restart: unless-stopped
networks:
- ai-net # Internal network to reach Ollama
- proxy-net # Edge network facing Traefik
volumes:
- open_webui_data:/app/backend/data
environment:
- OLLAMA_BASE_URL=http://ollama:11434
depends_on:
ollama:
condition: service_healthy # Will not try to start before Ollama's API responds
# Principle of Least Privilege
cap_drop:
- ALL
deploy:
resources:
limits:
memory: 2G
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels:
- "traefik.enable=true"
# Tell Traefik exactly which network to use to reach this container!
- "traefik.docker.network=ai-stack_proxy-net"
# Catch-all rule: Allows ANY IP or domain to reach Open-WebUI
- "traefik.http.routers.openwebui.rule=PathPrefix(`/`)"
# Route traffic through HTTPS and generate a self-signed certificate
- "traefik.http.routers.openwebui.entrypoints=websecure"
- "traefik.http.routers.openwebui.tls=true"
- "traefik.http.services.openwebui.loadbalancer.server.port=8080"
# Traefik Security Middleware
- "traefik.http.middlewares.secure-headers.headers.browserXssFilter=true"
- "traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.secure-headers.headers.forceSTSHeader=true"
- "traefik.http.routers.openwebui.middlewares=secure-headers"
# -------------------------------------
# Networks & Volumes
# -------------------------------------
networks:
ai-net:
driver: bridge # Secure internal layer
proxy-net:
driver: bridge # External ingress layer
volumes:
ollama_data:
open_webui_data:Por que essa arquitetura importa#
Rodar comandos docker run puros funciona para uma demo rápida, mas não se sustenta em um servidor de homelab permanente. Veja por que estruturamos o docker-compose.yml desta forma:
- API do LLM isolada (air-gapped): O Ollama está vinculado exclusivamente à bridge interna
ai-net. Não há nenhuma porta exposta no host. A única forma de falar com o modelo é pelo container do Open WebUI, por meio da rede isolada. - Ingress dinâmico com Traefik: Em vez de vincular portas do host de forma estática, o Traefik age como um reverse proxy inteligente. Ele roteia o tráfego de entrada nas portas 80/443 diretamente para o Open WebUI usando labels do Docker, força o redirecionamento automático para HTTPS e injeta cabeçalhos de segurança rígidos (
browserXssFilter,contentTypeNosniff). - A armadilha do “Memory Backoff”: Como a APU compartilha a RAM do sistema, o driver Vulkan impõe um limite estrito às alocações únicas de memória. Se você tenta carregar um modelo grande demais, a GPU falha na alocação inicial e “recua” (backs off), jogando essas camadas para a CPU. Se você vê 0% de uso da GPU durante a inferência, é por isso. Ao aumentar o limite de memória do LXC e do Ollama (no nosso caso, para 48GB), damos a folga necessária para o buffer do Vulkan.
- A tag
latestvsrocm: Muitos usuários de AMD puxam por precaução a tag de imagemrocm. Não faça isso com APUs. Em uma APU Ryzen, a abstração do ROCm muitas vezes falha ao inicializar a GPU integrada e recua silenciosamente para a execução na CPU. Para a aceleração Vulkan em APU, a imagemlatestsimplesmente funciona. - Princípio do menor privilégio: Removemos todas as capabilities de root do Linux desnecessárias do container do Open WebUI (
cap_drop: ALL). Se surgir uma vulnerabilidade no frontend, o raio de impacto fica contido. - Resolvendo o crash do erro 500: Normalmente, o Open WebUI inicia mais rápido do que o Ollama consegue inicializar os drivers da GPU AMD. Quando a interface tenta conectar e falha, ela gera um crash permanente
500: Internal Error. Corrigimos isso construindo um health check nativo comCMD ollama listque sonda a API. O Traefik e o WebUI esperam estruturalmente que o Ollama fiquehealthyantes de tentar a conexão.
3. Implantar a stack#
Suba o ambiente inteiro em segundo plano:
docker compose up -d4. Verificar a aceleração por hardware#
Verifique se o Ollama detecta a placa gráfica da AMD antes de baixar qualquer modelo de vários gigabytes.
docker logs ollama | grep "AMD"Saída esperada: ggml_vulkan: Found AMD Radeon Graphics
Bônus: como monitorar o uso da GPU (radeontop)#
Se você executa inferências pela CLI, pode monitorar as métricas da APU em tempo real com o radeontop, uma alternativa da AMD ao nvidia-smi.
Como você configurou o container para acessar /dev/dri, dá para executá-lo diretamente de dentro do ambiente LXC.
Primeiro, instale o utilitário:
apt install -y radeontopEm seguida, basta executá-lo para ver a atividade de renderização, as alocações de VRAM e as frequências de clock dos shaders:
radeontopQuando você gera uma resposta no Open WebUI, a utilização do Graphics pipe sobe brevemente a 100% enquanto a GPU processa os tokens.

O resultado final#
Quando estiver rodando, você tem um ambiente de IA local e privado. A aceleração por hardware torna rápidas tarefas como resumir artigos inteiros.
Acessando o Open WebUI#
Você pode acessar a interface web navegando até https://10.1.99.16 (ou o IP que você atribuiu ao seu container LXC) no navegador. Como o Traefik cuida do roteamento, ele responde automaticamente na porta 443 com um certificado autoassinado.
Aqui está o Open WebUI resumindo um link usando a APU:

