SSD Nodes Learn
Guias Matt ConnorPor Matt Connor · Atualizado 2026-07-24

Como instalar Nextcloud com Docker e TLS

Guia para rodar Nextcloud no VPS usando Docker Compose, Postgres, Redis e Nginx. Aprenda a configurar TLS via Let's Encrypt e rotinas de backup consistentes.

O que você está construindo de fato

Este guia executa o Nextcloud em um VPS com Docker Compose, utiliza TLS do Let's Encrypt como proxy reverso e configura um backup que realmente funciona na restauração. São quatro containers e um proxy: a imagem oficial do nextcloud escutando em loopback, o Postgres armazenando todos os metadados dos arquivos, o Redis gerenciando os file locks, uma segunda cópia da imagem do Nextcloud rodando apenas o loop do cron, e o nginx no host realizando o TLS termination à frente de tudo. A instalação leva vinte minutos, mas isso não é o mais importante. Duas decisões tomadas na primeira hora determinam se você ainda terá seus arquivos daqui a um ano: o uso de um banco de dados real em vez de SQLite, e um backup que captura o diretório de dados, o banco de dados e o config.php como um conjunto consistente.

Este procedimento assume Ubuntu 24.04 LTS ou Debian 13, Docker Engine com o plugin Compose v2 instalado do repositório oficial do Docker, e um registro DNS A (mais AAAA se você tiver IPv6) já apontando cloud.example.com para o VPS. Tudo isso requer um servidor sob seu controle — não é possível realizar TLS termination e dump de banco de dados em um SaaS de terceiros.

Dimensionamento: o que realmente consome a memória

O uso de memória do Nextcloud é dominado por três fatores, e nenhum deles é o "Nextcloud" em si.

PHP workers. A imagem -apache atende cada requisição concorrente através de um processo worker que contém um interpretador PHP. Cada worker pode crescer até PHP_MEMORY_LIMIT antes que o PHP encerre a requisição. O seu pior cenário de memória residente é aproximadamente requisições concorrentes × o limite de memória, e um cliente de sincronização desktop abre várias conexões paralelas por usuário. A concorrência, e não o número de usuários, define o limite máximo.

O banco de dados. O Postgres cria um processo backend para cada conexão e mantém os shared buffers residentes. O conjunto de trabalho escala com o número de arquivos, não com o número de bytes: oc_filecache armazena uma linha por arquivo por usuário. Cem mil arquivos pequenos representam um banco de dados mais pesado do que cem arquivos grandes.

Geração de previews. Gerar uma miniatura decodifica a imagem de origem na memória em resolução total. Previews de vídeo executam comandos para o ffmpeg. Rodar o occ preview:generate-all causa esse pico de uso repetidamente e de forma contínua, sendo a causa mais comum para o OOM killer encerrar um VPS pequeno.

O Redis é comparativamente barato. Qualquer componente adicionado posteriormente — Collabora, busca de texto completo, um antivírus — é um serviço residente separado com sua própria pegada de memória, e deve ser incluído no seu planejamento de dimensionamento antes de ser habilitado.

Os ajustes, se você tiver pouca RAM: diminua o PHP_MEMORY_LIMIT, limite o preview_max_x / preview_max_y / preview_max_filesize_image, reduza o enabledPreviewProviders apenas para os formatos que você realmente visualiza, e configure o trashbin_retention_obligation e versions_retention_obligation para que o diretório de dados não cresça silenciosamente para várias vezes o tamanho dos seus arquivos. Adicione um arquivo de swap. O swap é lento, e um OOM kill durante um upgrade é pior.

Por que o SQLite falha

O Nextcloud possui suporte ao SQLite e a imagem oficial o utilizará por padrão. Não faça isso. O SQLite serializa escritas com um lock em todo o banco de dados: apenas um escritor por vez para o arquivo inteiro. O Nextcloud realiza escritas constantes — locks de arquivos, linhas de atividade, entradas de cache, estado de jobs — e um único cliente desktop sincronizando uma árvore de diretórios gera muitas requisições paralelas. Sob esse padrão, ocorrem SQLSTATE[HY000]: General error: 5 database is locked e erros HTTP 500, e a falha aparece exatamente quando a instância começa a ser útil.

A conversão posterior é possível com occ db:convert-type, mas é uma migração longa e de risco total em um dataset ativo. Comece com Postgres ou MariaDB.

O arquivo Compose

Coloque isto em /srv/nextcloud/compose.yaml, com os secrets em um arquivo .env irmão no modo 600.

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - db:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${DB_PASSWORD}

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}

  app:
    image: nextcloud:31-apache
    restart: unless-stopped
    depends_on: [db, redis]
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - html:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data
    environment:
      POSTGRES_HOST: db
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
      NEXTCLOUD_ADMIN_USER: admin
      NEXTCLOUD_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com
      TRUSTED_PROXIES: 172.16.0.0/12
      OVERWRITEPROTOCOL: https
      OVERWRITECLIURL: https://cloud.example.com
      APACHE_DISABLE_REWRITE_IP: "1"
      PHP_MEMORY_LIMIT: 512M
      PHP_UPLOAD_LIMIT: 10G

  cron:
    image: nextcloud:31-apache
    restart: unless-stopped
    entrypoint: /cron.sh
    depends_on: [db, redis]
    volumes:
      - html:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data

volumes:
  db:
  html:

Fixe a tag principal e verifique a tag atual no Docker Hub antes de copiar 31 literalmente. O latest pode realizar um upgrade de versão principal em um futuro docker compose pull, e o Nextcloud não suporta isso.

O diretório de dados é um bind mount, não um volume nomeado, propositalmente: um caminho que você pode apontar diretamente para uma ferramenta de backup é mais importante que organização. Crie-o com o UID www-data da imagem e as permissões exigidas pelo Nextcloud:

sudo mkdir -p /srv/nextcloud/data
sudo chown -R 33:33 /srv/nextcloud/data
sudo chmod 0770 /srv/nextcloud/data

Observe a publicação da porta: 127.0.0.1:8080:80. O Docker publica portas escrevendo regras DNAT que são avaliadas antes que a chain INPUT do ufw veja o pacote — um 8080:80 expõe um Nextcloud sem criptografia na internet pública, independentemente das configurações do ufw. Vincular ao loopback mantém o serviço fora da interface pública. Assim, o firewall só precisa permitir o proxy — e se você preferir não deixar o SSH aberto para toda a internet, acessar a VPS via uma VPN WireGuard própria permite remover a porta 22 das regras públicas completamente:

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Inicie o serviço com docker compose up -d e monitore o docker compose logs -f app. O primeiro boot copia toda a árvore de arquivos da aplicação para o volume e executa o instalador; o container não responderá nada até que isso termine.

TLS e o reverse proxy

Instale o nginx e o certbot da distro, crie um server block simples na porta 80 com o server_name correto e deixe o certbot reescrevê-lo. A mecânica do desafio HTTP-01, o timer de renovação e os modos de falha são detalhados em emissão de certificados Let's Encrypt com certbot e nginx no Ubuntu 24.04:

sudo apt install nginx certbot python3-certbot-nginx
sudo certbot --nginx -d cloud.example.com

O certbot adiciona as linhas ssl_certificate e o redirect :80:443, e instala um timer do systemd para renovar o certificado de 90 dias. Confirme a existência com systemctl list-timers | grep certbot — um timer de renovação desativado é uma bomba relógio de 90 dias.

O bloco do proxy:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name cloud.example.com;

    # certbot manages ssl_certificate / ssl_certificate_key here

    add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;

    client_max_body_size 10G;
    client_body_timeout 300s;

    location = /.well-known/carddav { return 301 /remote.php/dav; }
    location = /.well-known/caldav  { return 301 /remote.php/dav; }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        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_set_header X-Forwarded-Host  $host;
        proxy_request_buffering off;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

No nginx 1.25 ou superior, adicione http2 on;. O Ubuntu 24.04 utiliza uma build mais antiga onde o equivalente é listen 443 ssl http2;. O comando nginx -t informará qual versão sua build aceita.

client_max_body_size e os timeouts de leitura longos evitam que uploads grandes falhem no meio do processo. O proxy_request_buffering off faz o streaming do upload em vez de salvar o arquivo inteiro no disco do proxy primeiro.

O nginx no host é a solução mais simples para apenas um app. Se o Nextcloud for compartilhar o VPS com outros containers, rodar o Traefik como reverse proxy Docker Compose para múltiplos apps move o roteamento e a emissão de certificados para labels de container; as mesmas preocupações com client_max_body_size e timeouts retornam lá como configurações de middleware e transport.

trusted_proxies e overwriteprotocol

É aqui que a maioria das instâncias self-hosted do Nextcloud apresenta erros, e os sintomas parecem não ter relação com a causa.

O X-Forwarded-Proto: https só é aplicado quando a requisição vem de um endereço listado em trusted_proxies. Quando não é aplicado, o Nextcloud entende que a requisição é HTTP puro e gera URLs com http://; o proxy redireciona essas URLs para HTTPS; o navegador segue o redirecionamento; o Nextcloud gera http:// novamente. Isso causa o loop de redirecionamento. O OVERWRITEPROTOCOL: https fixa o scheme de qualquer forma.

O problema no TRUSTED_PROXIES é que o endereço que o Nextcloud enxerga não é o 127.0.0.1. O nginx roda no host e se conecta a uma porta publicada, então o container enxerga o gateway da bridge do Docker — algo na faixa de 172.x. Descubra a subnet real:

docker network inspect nextcloud_default \
  -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'

Coloque esse CIDR (ou o 172.16.0.0/12 correspondente) no TRUSTED_PROXIES. Se configurar de forma muito ampla, qualquer cliente pode falsificar o X-Forwarded-For; se configurar incorretamente, todos os logins parecerão vir do endereço do gateway, a proteção contra brute-force bloqueará toda a sua instância de uma vez, e o overview do admin exibirá "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy."

O OVERWRITECLIURL é importante para o container do cron, que não recebe requisições externas para inferir um hostname. Sem ele, tarefas em segundo plano geram links para localhost e as notificações por e-mail enviam URLs inutilizáveis.

Background jobs: cron, não AJAX

O executor de tarefas padrão do Nextcloud é o AJAX: as tarefas são executadas como consequência do carregamento de uma página por um usuário. Ninguém navega às 04:00, portanto a expiração de lixeira, limpeza de versões, previews e tentativas de reenvio federado ficam travadas. O primeiro sintoma é um diretório de dados que cresce continuamente. O serviço cron mencionado acima executa o loop oficial do /cron.sh nos mesmos volumes. Configure o Nextcloud para utilizá-lo:

docker compose exec -u www-data app php occ background:cron

Cada comando occ segue este formato: docker compose exec -u www-data app php occ <command>. Recomenda-se criar um alias.

Backups: três itens, ou nenhum

Um backup apenas do filesystem não restaura uma instância corrompida. O diretório de dados contém os bytes; o Postgres contém o cache de arquivos, compartilhamentos, usuários e o estado do app; o config.php contém as credenciais do banco de dados, o instance ID e o password salt. Se você restaurar apenas os arquivos sem o banco de dados, o Nextcloud não conseguirá visualizá-los. Se restaurar o banco de dados sem o config.php, ele não conseguirá abrir o banco. Se restaurar um banco de dados antigo em um diretório de dados mais recente, os compartilhamentos apontarão para arquivos que mudaram de lugar.

Faça o backup dos três itens, a partir de uma instância em quiescence:

#!/usr/bin/env bash
set -euo pipefail
cd /srv/nextcloud
DEST="/var/backups/nextcloud/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$DEST"

occ() { docker compose exec -T -u www-data app php occ "$@"; }

occ maintenance:mode --on
trap 'occ maintenance:mode --off' EXIT

docker compose exec -T db \
  pg_dump -U nextcloud --clean --if-exists nextcloud | gzip > "$DEST/db.sql.gz"

docker compose exec -T app \
  tar -C /var/www/html -cf - config custom_apps themes > "$DEST/app.tar"

rsync -a --delete /srv/nextcloud/data/ /var/backups/nextcloud/data/

O maintenance mode garante que o dump e a cópia de arquivos estejam sincronizados. Se pular essa etapa, você acabará capturando um banco de dados que referencia um arquivo que o rsync ainda não alcançou. Note que o script mantém dumps de banco de dados com timestamp, mas apenas um espelho rotativo do diretório de dados — o rsync --delete sobrescreve o diretório a cada execução — portanto, apenas o dump mais recente é compatível com a cópia de arquivos.

Depois, mova o backup para fora da máquina. Um backup que reside no mesmo VPS do sistema original é apenas uma cópia, não um backup. O uso de restic contra object storage ou um segundo host é a solução padrão; a deduplicação dele lida com o diretório de dados muito melhor do que um tarball diário. A configuração completa, desde a inicialização do repositório até o timer diário e o teste de restore, está em backups de VPS off-box com restic.

O restore não é apenas o processo inverso. Uma stack recém-iniciada executa o instalador e gera um novo config.php — um novo instance ID e password salt — e importar o dump sobre essa nova identidade resulta em sessões e tokens de compartilhamento corrompidos. Restaure a identidade antiga primeiro, nesta ordem:

docker compose up -d && docker compose stop app cron    # create the volumes, then halt the app
sudo rsync -a --delete /var/backups/nextcloud/data/ /srv/nextcloud/data/
docker compose run --rm -T --entrypoint "" app \
  tar -C /var/www/html -xf - < app.tar                  # the original config.php returns
gunzip -c db.sql.gz | docker compose exec -T db psql -U nextcloud -d nextcloud
docker compose start app cron
docker compose exec -T -u www-data app php occ maintenance:mode --off
docker compose exec -T -u www-data app php occ files:scan --all

O files:scan reconcilia o cache de arquivos com o que realmente está no disco. Pratique isso uma vez, em um VPS reserva, antes de precisar do procedimento.

Upgrades: uma versão major por vez

O Nextcloud suporta o upgrade de apenas uma versão major por vez. Pular da 29 para a 31 não funciona corretamente — o processo falha com Exception: Updates between multiple major versions and downgrades are unsupported. e deixa o sistema em maintenance mode.

O upgrade via Docker consiste em: fazer um backup, alterar a tag de 31 para 32 nos serviços app e cron, depois docker compose pull && docker compose up -d, e então docker compose logs -f app. O entrypoint da imagem detecta o código novo comparando-o aos dados existentes e executa o occ upgrade automaticamente. Não interrompa o processo. Quando os logs pararem de registrar atividades, execute docker compose exec -u www-data app php occ status e verifique versionstring e se os apps voltaram a ficar habilitados.

Duas regras para evitar problemas: atualize uma versão major, verifique, e então atualize a próxima. Nunca altere a tag no serviço app sem alterar o cron para corresponder — usar duas versões diferentes do Nextcloud contra um único banco de dados causa corrupção de dados.

Os erros que você realmente verá

"Your data directory is readable by other users. Please change the permissions to 0770." O diretório montado via bind-mount possui permissões de leitura para grupo ou outros usuários. sudo chmod 0770 /srv/nextcloud/data e sudo chown -R 33:33 /srv/nextcloud/data.

"Your data directory is invalid. Ensure there is a file called .ocdata in the root." O bind mount aponta para um local que o Nextcloud nunca inicializou — um erro de digitação no caminho ou um diretório vazio substituído em uma instância funcional. Verifique se o caminho no host coincide com a linha do volume.

"Access through untrusted domain." O hostname na requisição não está em trusted_domains. NEXTCLOUD_TRUSTED_DOMAINS aplica-se apenas na primeira instalação; depois, defina-o em produção: occ config:system:set trusted_domains 1 --value=cloud.example.com.

502 Bad Gateway, com connect() failed (111: Connection refused) while connecting to upstream em /var/log/nginx/error.log. O nginx não alcançou nada em 127.0.0.1:8080. O container ainda está inicializando (verifique docker compose logs app), ele parou (docker compose ps) ou a linha de publicação não coincide com a porta proxy_pass. Confirme com ss -ltnp | grep 8080.

Um loop de redirecionamento ou avisos de "insecure" no overview de admin. OVERWRITEPROTOCOL: https está ausente ou TRUSTED_PROXIES não contém a subnet do gateway do Docker. Veja a seção de proxy acima.

LockedException: "files/..." is locked. Com REDIS_HOST configurado, a imagem define o Redis como backend de locking e locks obsoletos são raros. Sem isso, os locks residem na tabela de banco de dados oc_file_locks e uma requisição interrompida durante a escrita deixa linhas residuais. Confirme se o Redis está realmente em uso — occ config:system:get memcache.locking deve retornar a classe Redis — antes de limpar linhas de lock manualmente.

"The PHP memory limit is below the recommended value of 512MB." Aumente PHP_MEMORY_LIMIT e recrie o container. Lembre-se do impacto disso no seu limite máximo de uso.

O que falha em escala

O primeiro limite é o diretório de dados exceder o tamanho do volume. Aumentar um volume em um VPS exige o redimensionamento do volume e o crescimento do filesystem; fazer isso com o disco em 100% de uso é muito mais difícil do que agendar uma manutenção — configure alertas de uso de disco agora, não depois.

O segundo limite é o oc_filecache. Listagens de arquivos e varreduras de sincronização ficam lentas conforme o número de linhas aumenta. A solução exige manutenção no banco de dados: mantenha o Postgres em armazenamento rápido, garanta memória compartilhada suficiente e utilize configurações de retenção para limpar arquivos temporários e versões, evitando o acúmulo infinito.

O terceiro limite é a geração de previews competindo por recursos. Em máquinas pequenas, limite os provedores de preview e nunca execute o occ preview:generate-all durante o horário de trabalho.

Além disso, a resposta direta é que os recursos extras precisam de máquinas próprias. Collabora e busca de texto completo são serviços residentes com perfis de memória distintos; rodá-los na mesma máquina que armazena sua única cópia de arquivos aumenta o domínio de falha sem trazer benefícios. Mova o armazenamento de arquivos para um storage primário compatível com S3 quando o volume não for mais adequado — e note que isso torna os backups mais complexos: o banco de dados ainda contém os metadados e deve ser exportado em sincronia com o bucket.

Assim que a instância estiver atendendo usuários reais, coloque o Uptime Kuma à frente dela para ser notificado sobre indisponibilidades antes dos clientes de sincronização. Uma nuvem privada funciona bem com seu próprio servidor de e-mail e, se preferir não configurar os serviços manualmente, Cloudron, CasaOS e Coolify são plataformas que automatizam esse processo.

FAQ

Posso rodar o Nextcloud com SQLite em vez de Postgres?

Sim, a imagem oficial permite, mas um cliente de sincronização desktop emitindo requisições paralelas causará SQLSTATE[HY000]: General error: 5 database is locked e erros HTTP 500. O SQLite aplica um lock de escrita em todo o banco de dados, e o Nextcloud realiza escritas constantes — locks de arquivos, linhas de atividade e estado de jobs. Comece com Postgres ou MariaDB; o occ db:convert-type existe, mas é uma migração demorada e de risco para dados em produção.

De quanta RAM um VPS para Nextcloud realmente precisa?

Dimensionar pela concorrência, não pelo número de usuários. O uso de memória residente no pior cenário é aproximadamente o número de requisições simultâneas multiplicado por PHP_MEMORY_LIMIT, somado aos shared buffers do Postgres, um processo de backend por conexão e os picos de geração de previews. Uma máquina de 2 GB suporta uma instância doméstica pequena se você limitar os previews e adicionar swap; ao adicionar Collabora ou busca de texto completo, você estará dimensionando um segundo conjunto de serviços residentes.

Por que uploads grandes falham atrás do proxy reverso nginx?

Duas configurações no proxy geralmente explicam isso: client_max_body_size no padrão de 1 MB trunca a requisição, e valores baixos de proxy_read_timeout / proxy_send_timeout interrompem transferências longas no meio do processo. Configure ambos com valores generosos, altere proxy_request_buffering off para stream em vez de spool e aumente o PHP_UPLOAD_LIMIT no container da aplicação para corresponder ao proxy.

Por que o Nextcloud entra em loop de redirecionamento ou avisa sobre o proxy reverso?

O container não vê o nginx em 127.0.0.1 — ele vê o gateway da bridge do Docker, em algum lugar em 172.x. Quando esse endereço não está em TRUSTED_PROXIES, o header X-Forwarded-Proto: https é ignorado, o Nextcloud gera URLs com http:// e o proxy as devolve em loop. Configure TRUSTED_PROXIES para a subnet real da bridge e fixe o OVERWRITEPROTOCOL: https.

Posso atualizar o Nextcloud da versão 29 direto para a 31?

Não. O Nextcloud suporta apenas uma versão principal por atualização. Pular versões para em Updates between multiple major versions and downgrades are unsupported., deixando a instância em modo de manutenção. Faça backup, atualize a tag em uma versão principal tanto nos serviços app quanto cron, execute docker compose pull && docker compose up -d, verifique com occ status e repita o processo.