Como Instalar o Dify no Portainer com Docker Swarm (Passo a Passo Sem Erros)
Nesta aula vamos instalar o Dify do zero na sua VPS, usando Portainer, Docker Swarm e Traefik. Diferente de tutoriais genéricos, aqui cada passo existe porque resolve um erro real que trava a instalação — então siga na ordem, mesmo que pareça um passo “óbvio”.
Pré-requisitos
Antes de começar, seu ambiente já precisa ter rodando:
- Portainer (gerenciador do Docker Swarm)
- Traefik (proxy reverso, com rede
traefik_publice resolver de SSL configurado, ex:le) - PostgreSQL com pgvector (imagem
pgvector/pgvector:pg16), acessível na rededigital_networkpelo hostnamepostgres - Redis, acessível na mesma rede pelo hostname
redis
Se você ainda não tem esses serviços, monte-os antes de seguir. O Dify depende deles para subir.
Se for usar um servidor novo: prefira datacenter no Brasil para reduzir a latência da aplicação.
Passo 1: Autenticar no Docker Hub (evita o erro mais comum)
Antes de qualquer coisa, faça login no Docker Hub direto no terminal do servidor:
docker login
Use uma conta gratuita (crie em hub.docker.com se não tiver).
Por que esse passo existe: o Docker Hub limita quantas imagens você pode baixar por hora sem estar logado. Como o Dify usa várias imagens grandes (api, web, sandbox, plugin_daemon), é fácil bater nesse limite durante os testes e receber o erro toomanyrequests. Fazer login evita isso desde o início.
Passo 2: Baixar as imagens manualmente antes do deploy
Este é o passo que mais gente pula — e é o que mais causa erro. O Docker Swarm não baixa a imagem sozinho de forma confiável ao subir uma stack; ele espera que ela já esteja no servidor. Se não estiver, todos os serviços ficam com status rejected e a mensagem No such image.
Baixe as quatro imagens do Dify manualmente, uma por vez:
docker pull langgenius/dify-api:1.13.3
docker pull langgenius/dify-web:1.13.3
docker pull langgenius/dify-sandbox:0.2.14
docker pull langgenius/dify-plugin-daemon:0.5.3-local
Espere cada uma terminar antes de seguir para a próxima. Se aparecer erro de rate limit aqui, volte ao Passo 1.
Nota sobre versões: as tags acima são as que testamos e confirmamos que funcionam juntas. Evite usar
:latest— tags fixas garantem que a API, o Worker e o Plugin Daemon sejam sempre versões compatíveis entre si.
Passo 3: Criar os bancos de dados no PostgreSQL
O Dify precisa de dois bancos separados dentro do mesmo Postgres: um para a aplicação e outro exclusivo para o sistema de plugins.
Acesse o container do Postgres:
PG=$(docker ps -qf name=postgres)
Crie os dois bancos:
docker exec -i $PG psql -U postgres -c "CREATE DATABASE difydb;"
docker exec -i $PG psql -U postgres -c "CREATE DATABASE dify_plugin;"
Habilite a extensão vetorial no banco principal (necessária para embeddings e busca semântica):
docker exec -i $PG psql -U postgres -d difydb -c "CREATE EXTENSION IF NOT EXISTS vector;"
Por que dois bancos: o
plugin_daemon(serviço que gerencia plugins e integrações) mantém seu próprio schema, separado da aplicação principal. Colocar tudo no mesmo banco causa conflito de migrações.
Passo 4: Criar os volumes externos
O Dify guarda arquivos de usuário e chaves de criptografia em volumes persistentes. Crie-os antes do deploy:
docker volume create dify_storage_data
docker volume create dify_plugin_storage
Passo 5: Gerar as chaves de segurança
Gere quatro chaves aleatórias, uma de cada vez:
openssl rand -base64 42
Guarde os quatro valores gerados — você vai colar cada um em um lugar específico da stack no próximo passo. Atenção: algumas dessas chaves precisam ser idênticas em dois pontos diferentes da stack (a própria stack indica isso nos comentários). Se os valores não baterem, a API e o Plugin Daemon não conseguem se autenticar entre si.
Passo 6: Colar e ajustar a stack no Portainer
Vá em Stacks → Add stack, dê um nome (ex: dify), e cole a stack abaixo no editor.
Antes de clicar em Deploy, revise três coisas:
- Domínio: troque todas as ocorrências de
seudominio.com.brpelo subdomínio real que você vai usar (ex:dify.suaempresa.com.br). Isso aparece tanto nas variáveis de ambiente quanto nas labels do Traefik — troque em todos os lugares. - Chaves: substitua os placeholders pelas chaves geradas no Passo 5, respeitando os pareamentos indicados nos comentários.
- Senha do Postgres: use a senha real do seu banco compartilhado.
# ============================================================
# COLE AQUI A SUA STACK FINAL
# ============================================================
Clique em Deploy the stack.
Passo 7: Corrigir a permissão do volume de storage
Este passo evita um erro que só aparece na hora de criar a conta de administrador (PermissionDenied: permission denied ao salvar as chaves de criptografia).
Descubra qual usuário o container da API usa internamente:
docker exec $(docker ps -qf name=dify_api) id
O resultado normalmente é uid=1001(dify) gid=1001(dify). Ajuste a permissão do volume para esse mesmo usuário:
docker volume inspect dify_storage_data
Copie o valor de "Mountpoint" da saída e rode (troque 1001 se o id retornar outro número):
chown -R 1001:1001 /var/lib/docker/volumes/dify_storage_data/_data
Force o serviço a reiniciar com a permissão corrigida:
docker service update --force dify_api
docker service update --force dify_worker
Por que isso acontece: o volume é criado vazio, geralmente com dono
root. Mas o processo dentro do container roda com um usuário sem privilégios (dify, uid 1001) por segurança. Sem essa correção, a API não consegue gravar as chaves privadas de criptografia do primeiro workspace.
Passo 8: Acompanhar a migração do banco
Antes de acessar a tela de instalação, confira se a API terminou de criar as tabelas:
docker service logs dify_api --tail 50 -f
Espere aparecer a linha:
Database migration successful!
seguida de:
[INFO] Listening at: http://0.0.0.0:5001
Isso confirma que o backend está pronto. Se aparecer algum ERROR durante a migração (por exemplo Can't locate revision), o banco provavelmente foi tocado por uma tentativa anterior e precisa ser recriado do zero:
docker service scale dify_api=0
docker service scale dify_worker=0
docker exec -i $PG psql -U postgres -c "DROP DATABASE IF EXISTS difydb;"
docker exec -i $PG psql -U postgres -c "CREATE DATABASE difydb;"
docker exec -i $PG psql -U postgres -d difydb -c "CREATE EXTENSION IF NOT EXISTS vector;"
docker service scale dify_api=1
docker service scale dify_worker=1
Passo 9: Primeiro acesso e criação da conta de administrador
Acesse https://seudominio.com.br/install.
Se aparecer uma tela pedindo “Senha de inicialização do administrador”, significa que a variável INIT_PASSWORD da sua stack não está vazia. O valor mais simples é deixá-la em branco (INIT_PASSWORD: ""), o que faz o Dify pular direto para a criação da conta.
Na tela de configuração, preencha:
- E-mail: use o mesmo que você configurará depois no SMTP
- Nome de usuário
- Senha: precisa ter letras, números e mais de 8 caracteres (ex:
Empresa2026Admin)
Clique em Configuração. Se os Passos 3 e 7 foram feitos corretamente, a conta é criada sem erro e você cai direto no painel do Dify.
Resolução de problemas comuns
| Sintoma | Causa | Solução |
|---|---|---|
Serviço fica em 0/1, status rejected, erro No such image | Imagem não foi baixada no servidor | Rode os docker pull do Passo 2 |
toomanyrequests ao puxar imagem | Limite de download anônimo do Docker Hub | docker login (Passo 1) |
| Página trava no spinner de carregamento | URL da API não bate com o domínio real | Revise as variáveis de ambiente e labels do Traefik (Passo 6) |
| Bad Gateway ao acessar o domínio | Traefik não sabe qual rede usar (serviço está em 2 redes) | Adicione a label traefik.docker.network=traefik_public no serviço |
Can't locate revision nos logs da API | Banco de dados foi tocado por uma tentativa anterior | Recrie o banco do zero (Passo 8) |
PermissionDenied: permission denied ao criar conta admin | Volume de storage com dono errado | Ajuste o chown do volume (Passo 7) |
Depois do deploy
Com a conta criada, você já pode configurar os provedores de LLM (OpenAI, Anthropic, etc.), criar seu primeiro workflow e conectar o Dify às suas automações — por exemplo, via webhook no n8n usando a Service API URL.
version: "3.8"
# ==========================================================================
# DIFY - Stack para Portainer + Docker Swarm + Traefik
# Domínio: agentedify.seu_dominio.com.br
# --------------------------------------------------------------------------
# SEM nginx: o Traefik roteia direto para 'web' (frontend) e 'api' (backend),
# usando as regras de path que o Dify exige (/api, /v1, /console/api, /files, etc).
#
# PRÉ-REQUISITOS (rode via SSH UMA VEZ antes do deploy):
# docker login # resolve o rate limit do Docker Hub
# PG=$(docker ps -qf name=postgres)
# docker exec -i $PG psql -U postgres -c "CREATE DATABASE difydb;"
# docker exec -i $PG psql -U postgres -c "CREATE DATABASE dify_plugin;"
# docker exec -i $PG psql -U postgres -d difydb -c "CREATE EXTENSION IF NOT EXISTS vector;"
# docker volume create dify_storage_data
# docker volume create dify_plugin_storage
#
# CHAVES: troque os placeholders __TROQUE__ por strings geradas com:
# openssl rand -base64 42
# Respeite os pareamentos indicados nos comentários (mesmo valor nos dois lados).
#
# CLOUDFLARE: apontar o registro agentedify.seu_dominio.com.br para o IP do servidor
# (proxy pode ficar ativo/laranja; o Traefik faz o SSL via Let's Encrypt).
# ==========================================================================
x-shared-env: &shared-env
CONSOLE_API_URL: "https://agentedify.seu_dominio.com.br"
CONSOLE_WEB_URL: "https://agentedify.seu_dominio.com.br"
SERVICE_API_URL: "https://agentedify.seu_dominio.com.br"
APP_API_URL: "https://agentedify.seu_dominio.com.br"
APP_WEB_URL: "https://agentedify.seu_dominio.com.br"
FILES_URL: "https://agentedify.seu_dominio.com.br"
LOG_LEVEL: "INFO"
SECRET_KEY: "API_KEY"
INIT_PASSWORD: ""
MIGRATION_ENABLED: "true"
DEPLOY_ENV: "PRODUCTION"
# Postgres compartilhado (stack pgvector já existente, hostname: postgres)
DB_TYPE: "postgresql"
DB_USERNAME: "postgres"
DB_PASSWORD: "SUA_SENHA_POSTGRES"
DB_HOST: "postgres"
DB_PORT: "5432"
DB_DATABASE: "difydb"
# Redis compartilhado, sem senha
REDIS_HOST: "redis"
REDIS_PORT: "6379"
REDIS_USERNAME: ""
REDIS_PASSWORD: ""
REDIS_USE_SSL: "false"
REDIS_DB: "0"
CELERY_BROKER_URL: "redis://redis:6379/1"
WEB_API_CORS_ALLOW_ORIGINS: "*"
CONSOLE_CORS_ALLOW_ORIGINS: "*"
STORAGE_TYPE: "opendal"
OPENDAL_SCHEME: "fs"
OPENDAL_FS_ROOT: "storage"
# Vetorial no mesmo Postgres (pgvector já instalado na imagem pgvector/pgvector:pg16)
VECTOR_STORE: "pgvector"
PGVECTOR_HOST: "postgres"
PGVECTOR_PORT: "5432"
PGVECTOR_USER: "postgres"
PGVECTOR_PASSWORD: "SUA_SENHA_POSTGRES"
PGVECTOR_DATABASE: "difydb"
MAIL_TYPE: "smtp"
MAIL_DEFAULT_SEND_FROM: "Dify Admin "
SMTP_SERVER: "smtp.mailgun.org"
SMTP_PORT: "587"
SMTP_USERNAME: "contato@seu_dominio.com.br"
SMTP_PASSWORD: "API_KEY"
SMTP_USE_TLS: "true"
CODE_EXECUTION_ENDPOINT: "http://sandbox:8194"
CODE_EXECUTION_API_KEY: "API_KEY"
SSRF_PROXY_HTTP_URL: "http://ssrf_proxy:3128"
SSRF_PROXY_HTTPS_URL: "http://ssrf_proxy:3128"
PLUGIN_DAEMON_URL: "http://plugin_daemon:5002"
PLUGIN_DAEMON_KEY: "API_KEY"
INNER_API_KEY_FOR_PLUGIN: "API_KEY"
PLUGIN_REMOTE_INSTALL_HOST: "plugin_daemon"
PLUGIN_REMOTE_INSTALL_PORT: "5003"
PLUGIN_MAX_PACKAGE_SIZE: "52428800"
MARKETPLACE_API_URL: "https://marketplace.dify.ai"
MARKETPLACE_URL: "https://marketplace.dify.ai"
services:
# ======================= API (backend) =======================
api:
image: langgenius/dify-api:1.13.3
environment:
<<: *shared-env
MODE: "api"
volumes:
- dify_storage_data:/app/api/storage
networks:
- traefik_public
- digital_network
depends_on:
- plugin_daemon
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
resources:
limits:
cpus: "1"
memory: 2048M
restart_policy:
condition: on-failure
labels:
- traefik.enable=true
# Diz ao Traefik para usar a rede traefik_public (o serviço está em 2 redes)
- traefik.docker.network=traefik_public
# Rotas de backend do Dify -> serviço api (porta 5001)
- "traefik.http.routers.dify-api.rule=Host(`agentedify.seu_dominio.com.br`) && (PathPrefix(`/console/api`) || PathPrefix(`/api`) || PathPrefix(`/v1`) || PathPrefix(`/files`) || PathPrefix(`/e/`) || PathPrefix(`/mcp`))"
- traefik.http.routers.dify-api.entrypoints=websecure
- traefik.http.routers.dify-api.tls.certresolver=le
- traefik.http.routers.dify-api.priority=10
- traefik.http.routers.dify-api.service=dify-api
- traefik.http.services.dify-api.loadbalancer.server.port=5001
- traefik.http.services.dify-api.loadbalancer.passHostHeader=true
# ======================= WORKER (Celery) =======================
worker:
image: langgenius/dify-api:1.13.3
environment:
<<: *shared-env
MODE: "worker"
volumes:
- dify_storage_data:/app/api/storage
networks:
- digital_network
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
resources:
limits:
cpus: "1"
memory: 2048M
restart_policy:
condition: on-failure
# ======================= WEB (frontend) =======================
web:
image: langgenius/dify-web:1.13.3
environment:
CONSOLE_API_URL: "https://agentedify.seu_dominio.com.br"
APP_API_URL: "https://agentedify.seu_dominio.com.br"
MARKETPLACE_API_URL: "https://marketplace.dify.ai"
MARKETPLACE_URL: "https://marketplace.dify.ai"
networks:
- traefik_public
- digital_network
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
resources:
limits:
cpus: "1"
memory: 1024M
restart_policy:
condition: on-failure
labels:
- traefik.enable=true
# Diz ao Traefik para usar a rede traefik_public (o serviço está em 2 redes)
- traefik.docker.network=traefik_public
# Catch-all do domínio -> frontend (porta 3000). Prioridade menor que a api.
- "traefik.http.routers.dify-web.rule=Host(`agentedify.seu_dominio.com.br`)"
- traefik.http.routers.dify-web.entrypoints=websecure
- traefik.http.routers.dify-web.tls.certresolver=le
- traefik.http.routers.dify-web.priority=1
- traefik.http.routers.dify-web.service=dify-web
- traefik.http.services.dify-web.loadbalancer.server.port=3000
- traefik.http.services.dify-web.loadbalancer.passHostHeader=true
# ======================= PLUGIN DAEMON =======================
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.5.3-local
environment:
DB_TYPE: "postgresql"
DB_HOST: "postgres"
DB_PORT: "5432"
DB_USERNAME: "postgres"
DB_PASSWORD: "SUA_SENHA_POSTGRES"
DB_DATABASE: "dify_plugin"
DB_SSL_MODE: "disable"
REDIS_HOST: "redis"
REDIS_PORT: "6379"
REDIS_PASSWORD: ""
SERVER_PORT: "5002"
SERVER_KEY: "API_KEY"
DIFY_INNER_API_URL: "http://api:5001"
DIFY_INNER_API_KEY: "API_KEY"
PLUGIN_REMOTE_INSTALLING_HOST: "0.0.0.0"
PLUGIN_REMOTE_INSTALLING_PORT: "5003"
PLUGIN_WORKING_PATH: "/app/storage/cwd"
FORCE_VERIFYING_SIGNATURE: "true"
PLUGIN_STORAGE_TYPE: "local"
PLUGIN_STORAGE_LOCAL_ROOT: "/app/storage"
PLUGIN_INSTALLED_PATH: "plugin"
PLUGIN_PACKAGE_CACHE_PATH: "plugin_packages"
PLUGIN_MEDIA_CACHE_PATH: "assets"
MAX_PLUGIN_PACKAGE_SIZE: "52428800"
PYTHON_ENV_INIT_TIMEOUT: "120"
PLUGIN_MAX_EXECUTION_TIMEOUT: "600"
PLUGIN_STDIO_BUFFER_SIZE: "1024"
PLUGIN_STDIO_MAX_BUFFER_SIZE: "5242880"
volumes:
- dify_plugin_storage:/app/storage
networks:
- digital_network
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
resources:
limits:
cpus: "0.5"
memory: 1024M
restart_policy:
condition: on-failure
# ======================= SANDBOX =======================
sandbox:
image: langgenius/dify-sandbox:0.2.14
environment:
API_KEY: "API_KEY"
GIN_MODE: "release"
WORKER_TIMEOUT: "15"
ENABLE_NETWORK: "true"
HTTP_PROXY: "http://ssrf_proxy:3128"
HTTPS_PROXY: "http://ssrf_proxy:3128"
SANDBOX_PORT: "8194"
networks:
- digital_network
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
resources:
limits:
cpus: "0.5"
memory: 512M
restart_policy:
condition: on-failure
# ======================= SSRF PROXY =======================
ssrf_proxy:
image: ubuntu/squid:latest
networks:
- digital_network
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
resources:
limits:
cpus: "0.2"
memory: 256M
restart_policy:
condition: on-failure
volumes:
dify_storage_data:
external: true
dify_plugin_storage:
external: true
networks:
traefik_public:
external: true
digital_network:
external: true
# ==========================================
# CONFIGURAÇÕES AVANÇADAS DE ROTAS E ACESSO
# ==========================================
# REGRA GERAL PARA ALUNOS: Em instalações padrão (tudo no mesmo servidor),
# deixe estas variáveis em branco! O Dify é inteligente e calculará as rotas
# automaticamente com base no seu domínio principal (APP_WEB_URL).
# Só preencha se for separar o sistema em vários servidores diferentes.
# Senha do primeiro Administrador.
# Recomendação: Deixe vazio. Assim, o Dify pedirá para você criar a senha
# na tela no seu primeiro acesso, evitando que ela vaze neste arquivo.
- INIT_PASSWORD= # Ex: SenhaAdminForte2026!
# URL onde o painel de criação de IA será acessado.
# Útil apenas se a empresa quiser o painel em um domínio isolado (ex: admin.empresa.com)
- CONSOLE_WEB_URL= # Ex: https://dify.suaempresa.com.br
# Caminho da API interna do painel.
# Preencher apenas se o frontend visual e o backend rodarem em máquinas diferentes.
- CONSOLE_API_URL= # Ex: https://dify.suaempresa.com.br/console/api
# A porta de entrada para integrações externas (onde seu n8n vai bater!).
# Se vazio, ele assume seu domínio principal + a rota /v1.
- SERVICE_API_URL= # Ex: https://dify.suaempresa.com.br/v1
# Subdomínio exclusivo para carregar e exibir os PDFs e imagens (CDN).
# Útil apenas para aliviar o servidor principal quando houver milhares de acessos.
- FILES_URL= # Ex: https://arquivos.suaempresa.com.br
Passo 5: Criando o Banco de Dados no PostgreSQL
Para que o Dify consiga iniciar corretamente, o banco de dados principal já deve existir dentro do seu PostgreSQL. Vamos criá-lo via terminal.
Acesse o console/terminal do contêiner do seu PostgreSQL.
Certifique-se de acessar com o usuário
postgres(e não comoroot).Abra a interface de comandos do banco digitando:
psql
Crie o banco de dados rodando o comando SQL abaixo:
CREATE DATABASE difydb;
Conclusão: Após o Deploy
Aguarde o processamento: Após clicar em “Deploy the stack” no Portainer, aguarde de 5 a 10 minutos. O Dify possui vários serviços que precisam inicializar e criar as tabelas no banco de dados na primeira execução.
Primeiro Acesso: Ao acessar a URL configurada pela primeira vez, você precisará definir a conta de Administrador. Recomendamos usar o mesmo endereço de e-mail que você cadastrou no servidor SMTP na etapa de configuração. Guarde sua senha com segurança!