Pular para o conteúdo
Docker

MinIO: seu próprio servidor S3 numa VPS

Ilustração colorida de um unicórnio de crina luminosa ao lado de baús etiquetados, numa biblioteca de prateleiras flutuantes, com uma coruja arquivista carregando pergaminhos e um castelo com velas ao fundo

Olá meus Unicórnios! 🦄✨

Todo projeto chega no dia em que precisa guardar arquivo em algum lugar que não seja o próprio servidor. Foto de produto, PDF de nota, backup do banco, áudio que o cliente mandou. E a resposta padrão da internet é sempre a mesma: joga no S3 da Amazon. 📦

Funciona muito bem, e eu não vou falar mal do S3. Mas ele cobra por GB guardado, cobra por GB que sai, cobra por requisição, e essa conta tem um jeito peculiar de crescer sem avisar. Fora que, para quem está aprendendo, criar conta na AWS e entender IAM antes de conseguir subir o primeiro arquivo é um desanimo e tanto. 😅

É aí que entra o MinIO: um servidor de armazenamento de objetos, de código aberto, que fala a mesma língua do S3. Não é "parecido com": é a mesma API. Você sobe ele na sua VPS, aponta o seu programa para lá trocando só o endereço, e o código que falava com a Amazon continua funcionando igualzinho.

Este artigo é o passo a passo completo: da VPS com Docker até o arquivo indo e voltando, com o painel web funcionando e as armadilhas que eu encontrei. E tem uma delas, logo no começo, que derruba praticamente todo mundo na primeira tentativa. 🙃

🧊 O que é "armazenamento de objetos", sem enrolação

Antes de instalar, vale entender o que a gente está subindo, porque o nome assusta mais que a coisa.

Você já conhece armazenamento de arquivos: pastas dentro de pastas, um caminho como /home/paloma/fotos/gato.jpg. É assim que o seu computador funciona.

O armazenamento de objetos é mais burro, e é de propósito. Ele tem só dois níveis:

  • o bucket, que é um balde com um nome (backups, fotos);
  • o objeto, que é o arquivo lá dentro, identificado por uma chave (2026/agosto/gato.jpg).

Repare que a chave parece ter pastas, mas não tem: é um nome só, com barras no meio. Essa simplicidade é o que permite o sistema crescer para bilhões de arquivos sem se perder, e é por isso que praticamente todo serviço de nuvem guarda arquivo assim hoje.

O S3 é o nome do serviço da Amazon que popularizou isso, e a API dele virou o padrão de fato. O MinIO implementa essa mesma API. Traduzindo: as ferramentas e bibliotecas escritas para a Amazon funcionam com o MinIO sem alterar o código, só o endereço. Isso vai ficar bem concreto mais para a frente, quando eu usar a ferramenta oficial da própria Amazon para conversar com o meu servidor. 😄

🖥️ O ponto de partida

Usei uma VPS Ubuntu 24.04 comum, dessas baratinhas, com o Docker já instalado. Se você ainda não tem Docker na sua máquina, é o script oficial que resolve:

apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

Esse script descobre a distribuição sozinho e instala tudo, inclusive o Docker Compose como plugin, que é o que a gente vai usar. Confira as duas versões:

docker --version
docker compose version

Repare que é docker compose com espaço, e não docker-compose com hífen. O hífen é a versão antiga, descontinuada. Se você achar um tutorial mandando instalar docker-compose por fora, ele é velho.

Para entrar na VPS, é o SSH de sempre. No Windows o próprio PowerShell já tem o comando embutido:

ssh root@SEU_IP_AQUI

Na primeira conexão ele pergunta se você confia na máquina. Responda yes, digite a senha, e você está dentro.

🔑 A senha primeiro, o arquivo depois

O MinIO nasce com um usuário administrador, e você escolhe qual é. Aqui vai o conselho que eu dou sempre: não invente senha na mão, e principalmente não deixe a minioadmin/minioadmin que aparece em metade dos tutoriais. Esse servidor vai ficar exposto na internet.

Primeiro crie a pasta onde tudo vai morar e entre nela:

mkdir -p /opt/minio
cd /opt/minio

O mkdir cria a pasta e o cd entra nela. Daqui em diante, todo comando roda de dentro de /opt/minio. Se você fechar o terminal e voltar depois, rode o cd de novo.

Agora a senha, gerada pelo próprio Ubuntu com o openssl, que já vem instalado:

openssl rand -hex 12

Ele cospe uma sequência aleatória de letras e números, mais ou menos assim:

7c4e91a2f8b35d06e1a7c94b

Copie a sua para um bloco de notas: ela vai ser a senha do administrador do seu S3.

📝 Criando arquivo no Ubuntu: o nano

Se você nunca mexeu num servidor Linux, esta é a parte que trava todo mundo: como é que eu crio um arquivo aqui, se não tem Bloco de Notas? 😅

Tem sim, chama-se nano, já vem instalado, e é o mais simples de todos. Você digita nano seguido do nome do arquivo:

nano .env

A tela do terminal vira o editor. Se o arquivo não existir, ele é criado na hora; se existir, abre para edição. Aí é só digitar (ou colar) o conteúdo normalmente.

Para colar, use Ctrl + Shift + V ou clique com o botão direito. O Ctrl + V comum não funciona no terminal, e essa é a primeira coisa que confunde. 🙃

Para salvar e sair, são três teclas em sequência:

Ctrl + O     grava o arquivo (a letra O, de "output")
Enter        confirma o nome
Ctrl + X     sai do editor

Repare no rodapé do nano: ele mostra os atalhos o tempo todo, com o ^ significando Ctrl. Ou seja, ^O é Ctrl + O e ^X é Ctrl + X. Depois de duas ou três vezes vira automático.

🔐 O arquivo .env, com o usuário e a senha

Com o nano na mão, crie o primeiro arquivo:

nano .env

E escreva estas duas linhas, trocando a senha pela que o openssl gerou para você:

MINIO_ROOT_USER=admin-unicornio
MINIO_ROOT_PASSWORD=SUA_SENHA_AQUI

Salve com Ctrl + O, Enter, Ctrl + X. Para conferir que ficou certo:

cat .env

O cat mostra o conteúdo de um arquivo na tela. Se aparecerem as duas linhas, está pronto.

Note que o usuário não precisa ser minioadmin: eu escolhi admin-unicornio justamente porque o nome padrão é a primeira coisa que qualquer robô tenta.

📦 O docker-compose.yml, e as duas portas

Agora o arquivo que descreve o serviço. Mesma coisa de antes: nano, cola, salva.

nano docker-compose.yml

E o conteúdo é este:

services:
  minio:
    image: quay.io/minio/minio:RELEASE.2025-04-22T22-12-26Z
    container_name: minio
    restart: always
    ports:
      - "9000:9000"   # API S3: e por aqui que os programas falam
      - "9001:9001"   # painel web: e por aqui que voce fala
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    # sem o --console-address o painel sorteia uma porta nova a cada reinicio
    command: server /data --console-address ":9001"
    volumes:
      - minio_data:/data

volumes:
  minio_data:

Salve e saia, como antes. E aqui vai o aviso mais importante para quem está começando:

Antes de subir, dá para perguntar ao próprio Docker se o arquivo está válido. São trinta segundos que evitam dor de cabeça:

docker compose config

Se ele imprimir o conteúdo de volta, está tudo certo. Se a indentação estiver errada, ele reclama apontando a linha e a coluna.

Agora, por que duas portas? Essa é a pergunta que mais aparece, e a resposta é simples: são dois serviços diferentes morando no mesmo programa.

A 9000 é a API S3. É por ela que os programas falam, no formato que o S3 definiu. Ela não é um site: se você abrir no navegador, vai receber um XML de erro, e isso é o comportamento correto.

A 9001 é o painel web, feito para gente. Tela de login, lista de arquivos, botão de upload.

Poderiam ser uma só? Poderiam, mas aí toda requisição precisaria adivinhar se quem chamou foi um navegador ou um programa. Separar em duas portas deixa cada uma com um trabalho só, e permite uma coisa muito útil lá na frente: publicar a API para a internet e deixar o painel fechado, ou o contrário.

🚀 Subindo o MinIO

Com o arquivo pronto, é uma linha:

docker compose up -d

O -d é de detached: ele sobe e devolve o terminal para você. Na primeira vez demora um pouco, porque precisa baixar a imagem. Depois:

docker compose ps
NAME      STATUS
minio     Up 8 minutes

De pé. Mas não confie no "Up": ele diz que o contêiner está rodando, não que a aplicação lá dentro terminou de subir. Quem conta a verdade é o log:

docker logs minio
MinIO Object Storage Server
Copyright: 2015-2026 MinIO, Inc.
License: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html
Version: RELEASE.2025-04-22T22-12-26Z (go1.24.2 linux/amd64)

API: http://172.21.0.2:9000  http://127.0.0.1:9000
WebUI: http://172.21.0.2:9001 http://127.0.0.1:9001

Docs: https://docs.min.io

Essas duas linhas, API e WebUI, são o que você quer ver, e são elas que denunciam a armadilha da próxima seção. Guarde bem o número que aparece no WebUI. 👀

💀 A armadilha: o painel numa porta sorteada

Se você só for ler um pedaço deste artigo, leia este. 🙏

Na minha primeira tentativa eu escrevi o compose sem o --console-address, assim, que é como a maioria dos exemplos antigos mostra:

    command: server /data

O contêiner subiu bonito, o docker compose ps disse Up, e o log não trouxe erro nenhum. Aí eu abri o painel no navegador e... nada. A página simplesmente não conectava.

Testando pelo terminal da própria VPS, o retrato do problema:

curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:9001/
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:9000/
HTTP 000
HTTP 403

Repare na diferença, porque ela é a chave do diagnóstico. O HTTP 403 da porta 9000 é ótima notícia: significa que tem alguém atendendo e respondendo "você não tem permissão". Já o HTTP 000 não é um código HTTP: é o jeito do curl dizer que não conseguiu conectar com ninguém. Não tem serviço ali.

E a explicação estava no log o tempo todo, naquela linha que eu mandei você guardar:

WebUI: http://172.21.0.2:45195 http://127.0.0.1:45195

45195. Não 9001. O MinIO tinha escolhido uma porta aleatória para o painel, e a 9001 que eu publiquei no Docker não tinha ninguém do outro lado. 😳

E não é uma porta fixa esquisita: é sorteada de novo a cada reinício. Reiniciei o contêiner só para conferir:

docker restart minio
docker logs minio | grep WebUI
WebUI: http://172.21.0.2:45195 http://127.0.0.1:45195
WebUI: http://172.21.0.2:33823 http://127.0.0.1:33823

Era 45195, virou 33823. Ou seja: mesmo que você descobrisse a porta e a publicasse no compose, ela mudaria no próximo restart e o painel sumiria de novo. É por isso que esse erro deixa tanta gente maluca: o comportamento é inconsistente, e nada no log parece um erro.

A correção é a linha que já está no arquivo lá em cima:

    command: server /data --console-address ":9001"

Ela manda o painel ficar sempre na 9001. Aplique com:

docker compose up -d

Não precisa parar nada antes: o up -d percebe sozinho que o arquivo mudou e recria o contêiner. Agora o log diz o que a gente queria:

API: http://172.21.0.2:9000  http://127.0.0.1:9000
WebUI: http://172.21.0.2:9001 http://127.0.0.1:9001

E a porta responde:

curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:9001/
HTTP 200

A lição que fica, e vale para qualquer contêiner: quando a porta publicada não responde, compare o número que o programa diz estar usando com o número que você publicou. Publicar uma porta no Docker não obriga o programa lá dentro a usá-la; são duas decisões separadas, e ninguém avisa quando elas discordam.

🔥 Abrindo as portas no firewall

Até agora tudo foi testado de dentro da própria VPS, com localhost. Para acessar de fora, o firewall precisa deixar. A minha VPS usa o ufw, que é o padrão do Ubuntu:

ufw allow 9000/tcp
ufw allow 9001/tcp

E confira como ficou:

ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
9000/tcp                   ALLOW       Anywhere
9001/tcp                   ALLOW       Anywhere

Agora, do seu computador, o teste que importa:

curl -s -o /dev/null -w "HTTP %{http_code}\n" http://SEU_IP_AQUI:9000/
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://SEU_IP_AQUI:9001/
HTTP 403
HTTP 200

Os dois respondendo, cada um do seu jeito. E aquele 403 da API merece um olhar de perto, porque ele explica o que essa porta é:

curl http://SEU_IP_AQUI:9000/
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>AccessDenied</Code>
    <Message>Access Denied.</Message>
    <Resource>/</Resource>
    <RequestId>18CCF9884A430C12</RequestId>
</Error>

Esse XML é exatamente o formato de erro do Amazon S3. Não é o MinIO imitando de longe: é a mesma estrutura, com os mesmos nomes de campo, porque a API é a mesma. Primeira pista concreta de que a compatibilidade é real. 😄

🖱️ Entrando no painel

Agora a parte visual. Abra no navegador, trocando pelo IP da sua VPS:

http://SEU_IP_AQUI:9001

Aparece a tela de login do MinIO, pedindo usuário e senha, que são aqueles do .env. E antes de acertar, deixa eu te mostrar o que acontece quando erra, porque essa mensagem merece um aviso:

Tela de login do MinIO com uma tarja vermelha no topo escrita invalid login, com os campos de usuário e senha preenchidos

"invalid login." e mais nada. 🫠 Repare no que ela não diz: não diz se o usuário está errado, se a senha está errada, ou se o servidor está com problema. E isso é proposital, é uma prática de segurança conhecida: contar qual dos dois errou ajudaria alguém a descobrir nomes de usuário válidos por tentativa.

Prático para quem ataca, chato para você às 23h. Se levar esse recado, confira o .env com cat .env, e lembre que o MinIO diferencia maiúscula de minúscula nos dois campos.

Com a senha certa, o painel abre no Object Browser:

Painel do MinIO logado, mostrando o Object Browser com o bucket backups na lista, com 2 objetos e 50 bytes

À esquerda, o menu dividido em User (o que você usa no dia a dia) e Administrator (a configuração). É uma interface enxuta, e isso é elogio: dá para achar tudo sem manual.

🪣 Criando o primeiro bucket

Bucket é o balde onde os arquivos ficam. Clique em Buckets, no menu da esquerda, e depois no botão Create Bucket.

Aqui mora a segunda armadilha do artigo, e ela é bem mais fácil de resolver que a primeira. Eu tentei chamar o meu de Meus_Backups, que me pareceu um nome perfeitamente razoável:

Formulário Create Bucket do MinIO com o nome Meus_Backups em vermelho e a mensagem Invalid bucket name abaixo do campo

"Invalid bucket name", campo vermelho, e repare que o botão Create Bucket nem habilita. O painel barra antes de mandar para o servidor.

O motivo é que nome de bucket não é nome de pasta: ele segue a regra do S3, porque um dia ele já foi parte de um endereço de internet. As regras que pegam todo mundo:

  • só letras minúsculas, números, ponto e hífen;
  • nada de maiúscula e nada de underline (_);
  • de 3 a 63 caracteres, começando e terminando com letra ou número.

Ou seja: Meus_Backups errou duas vezes de uma só. O nome certo é meus-backups, com minúsculas e hífen. Trocando, o campo fica normal e o botão acende:

Tela de Buckets do MinIO listando dois buckets, backups e meus-backups, com data de criação e acesso R/W

Bucket criado. 🎉 E pelo terminal a regra é a mesma, com uma mensagem até mais direta:

mc: <ERROR> Unable to make bucket `meuminio/MeusBackups`.
Bucket name contains invalid characters

Aquele Versioning que aparece no formulário de criação, se você tiver curiosidade, guarda todas as versões de um mesmo arquivo em vez de sobrescrever. É útil e dá artigo próprio; para começar, deixe desligado.

🛠️ O mc: o terminal do MinIO

O painel é ótimo para olhar, mas o dia a dia é no terminal. O MinIO tem um programa próprio para isso, o mc (de MinIO Client). Instalar é baixar um arquivo e dar permissão de execução:

curl -sSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
chmod +x /usr/local/bin/mc
mc --version

O chmod +x é o que diz ao Linux "este arquivo é um programa, pode executar". Sem ele, o sistema recusa com um Permission denied que parece problema de senha e não é.

Agora o mc precisa saber onde fica o seu servidor e quem é você. Isso se chama apelido (alias): você registra uma vez e usa o nome curto para sempre.

mc alias set meuminio http://localhost:9000 admin-unicornio SUA_SENHA_AQUI

São quatro informações na ordem: o apelido que você inventou (meuminio), o endereço da API (a 9000, não a do painel!), o usuário e a senha. Se der certo:

Added `meuminio` successfully.

E se a senha estiver errada, a mensagem é bem mais informativa que a do painel:

mc: <ERROR> Unable to initialize new alias from the provided credentials.
The request signature we calculated does not match the signature you provided.
Check your key and signing method.

Essa frase, "a assinatura que calculamos não bate com a que você enviou", é herança direta do S3 e vale entender: a sua senha nunca viaja pela rede. Ela é usada para assinar cada requisição, e o servidor refaz a mesma conta do lado dele para comparar. Se as contas não batem, ou a senha está errada, ou algo mudou no caminho.

📤 Mandando e trazendo arquivo

Com o apelido pronto, os comandos são quase iguais aos do Linux que você já conhece. Criar um bucket:

mc mb meuminio/backups
Bucket created successfully `meuminio/backups`.

Listar o que existe:

mc ls meuminio
[2026-08-18 20:25:51 CEST]     0B backups/
[2026-08-18 20:30:37 CEST]     0B meus-backups/

Enviar um arquivo (o cp é o mesmo verbo de copiar do Linux):

echo "Ola meus Unicornios!" > teste.txt
mc cp teste.txt meuminio/backups/
teste.txt: 21 B / 21 B  100.00%

Ver o que tem dentro do bucket:

mc ls meuminio/backups
[2026-08-18 20:25:52 CEST]    21B STANDARD teste.txt

E trazer o conteúdo de volta, para provar que o arquivo está inteiro:

mc cat meuminio/backups/teste.txt
Ola meus Unicornios!

Foi, voltou, chegou igual. 🦄 No painel, os arquivos aparecem na mesma hora:

Object Browser do MinIO mostrando o conteúdo do bucket backups, com os arquivos aws.txt e teste.txt, e o acesso marcado como PRIVATE

Repare no Access: PRIVATE lá em cima. É o padrão, e é o padrão certo: bucket novo nasce fechado. Quem tentar abrir o arquivo pela URL, sem credencial, leva o mesmo AccessDenied de antes:

curl -s -o /dev/null -w "HTTP %{http_code}\n" http://SEU_IP_AQUI:9000/backups/teste.txt
HTTP 403

🔗 O link temporário, que é a graça do S3

"Mas e se eu quiser mandar esse arquivo para alguém?" É aqui que o S3 mostra por que virou padrão, com um recurso chamado link assinado (presigned URL).

A ideia é linda: em vez de abrir o bucket para o mundo, você gera um link que funciona sozinho e expira.

mc share download --expire=1h meuminio/backups/teste.txt
URL: http://SEU_IP_AQUI:9000/backups/teste.txt
Expire: 1 hours 0 minutes 0 seconds
Share: http://SEU_IP_AQUI:9000/backups/teste.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=admin-unicornio%2F20260811%2Fus-east-1%2Fs3%2Faws4_request
&X-Amz-Date=20260811T232628Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host
&X-Amz-Signature=6c7dfca8811813d6b6f6686df70e96e95...

Olhe os nomes dos parâmetros: X-Amz-Algorithm, X-Amz-Credential, us-east-1. Amz de Amazon. Isso não é o MinIO copiando o estilo: é a mesma especificação de assinatura, com os mesmos nomes de campo e até a mesma região padrão da AWS.

E o link funciona sem credencial nenhuma. Colando ele num curl:

Ola meus Unicornios!

Aquele mesmo arquivo que devolvia 403 agora abre, porque a assinatura na URL é a permissão. Passada a hora, o link morre sozinho e o arquivo volta a ser privado. É assim que serviço grande entrega download sem deixar nada aberto. 🔐

🧪 A prova real: a ferramenta da Amazon falando com o MinIO

Chegou a hora de provar a promessa do começo. O artigo inteiro diz que o MinIO é compatível com o S3, então vamos testar com o juiz menos suspeito possível: a aws CLI, o programa oficial da própria Amazon. 😏

Instalando na VPS:

curl -sSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
apt-get install -y unzip
unzip -q awscliv2.zip
./aws/install
aws --version
aws-cli/2.36.25 Python/3.14.6 Linux/6.8.0-136-generic

Ela precisa saber a credencial, e o jeito mais simples é por variável de ambiente. São três linhas, e repare que os nomes são os da Amazon, sem nada de MinIO neles:

export AWS_ACCESS_KEY_ID=admin-unicornio
export AWS_SECRET_ACCESS_KEY=SUA_SENHA_AQUI
export AWS_DEFAULT_REGION=us-east-1

Agora o comando, e toda a mágica está no --endpoint-url: é ele que diz "em vez da Amazon, fale com este endereço aqui".

aws --endpoint-url http://localhost:9000 s3 ls
2026-08-18 20:25:51 backups
2026-08-18 20:30:37 meus-backups

Funcionou. 🎉 A ferramenta da Amazon listou os buckets do meu servidor na minha VPS, sem saber que não estava falando com a AWS. Enviando um arquivo:

aws --endpoint-url http://localhost:9000 s3 cp aws.txt s3://backups/
upload: ./aws.txt to s3://backups/aws.txt

Repare no s3:// ali. É o mesmo endereço que você escreveria para a Amazon de verdade.

E o que acontece se você esquecer o --endpoint-url? Esse erro vale ouro, porque ele confunde muita gente:

aws s3 ls
An error occurred (InvalidAccessKeyId) when calling the ListBuckets operation:
The AWS Access Key Id you provided does not exist in our records.

Lendo rápido, parece que a sua credencial está errada. Não está: sem o --endpoint-url, o comando foi para a Amazon de verdade, que obviamente nunca ouviu falar do seu usuário. O "our records" da mensagem é o cadastro da AWS. 😅

A lição prática: se um programa seu começar a dar erro de credencial do nada, confira primeiro para onde ele está apontando. Na maioria das bibliotecas essa configuração se chama endpoint ou endpoint_url, e é a única linha que muda entre usar a Amazon e usar o seu MinIO.

🔑 Não use a senha de administrador nos seus programas

Até agora eu usei o usuário administrador em tudo, porque é o que existia. Mas para valer, não é ele que vai no seu sistema.

Pense assim: essa credencial é a de dono do servidor inteiro. Se ela estiver escrita em três programas diferentes e um deles vazar, você precisa trocar a senha do administrador, e aí os outros dois param junto. 😬

O jeito certo é criar uma chave de acesso separada para cada programa. No painel, clique em Access Keys e depois em Create access key. O MinIO já gera o par para você:

Tela Access Keys do MinIO listando uma chave de acesso criada, com validade no-expiry e status Enabled

Duas coisas importantes nessa tela. A primeira é o aviso que aparece na hora da criação: o segredo é mostrado uma única vez. Copie ali, porque não tem como consultar depois; se perder, o caminho é apagar a chave e criar outra.

A segunda é que a lista mostra só o identificador, nunca o segredo. É por isso que essa tela é segura de deixar aberta, e é por isso que ela é o lugar certo para revogar: uma chave que vazou se apaga com um clique, sem mexer na senha do administrador nem derrubar os outros programas.

Essa chave se usa exatamente como a outra, trocando usuário e senha pelos dois valores gerados:

mc alias set appteste http://localhost:9000 SUA_CHAVE_AQUI SEU_SEGREDO_AQUI
Added `appteste` successfully.

📊 Quanto isso consome, de verdade

A pergunta que sempre vem: será que preciso de máquina grande? Com o MinIO de pé, sem ninguém usando:

docker stats --no-stream
NAME      CPU %     MEM USAGE / LIMIT
minio     0.00%     554.3MiB / 7.755GiB

Uns 550 MB de RAM e CPU zerada. Numa máquina de 7,8 GB isso é 7%. A imagem do contêiner é pequena também, o que faz diferença na primeira subida:

docker images quay.io/minio/minio
quay.io/minio/minio:RELEASE.2025-04-22T22-12-26Z   250MB

250 MB. Baixa rápido até em conexão ruim.

E a velocidade, que é o que interessa quando o arquivo é grande. Gerei um arquivo de 100 MB e mandei para o bucket:

dd if=/dev/urandom of=grande.bin bs=1M count=100
mc cp grande.bin meuminio/meus-backups/
Total      │ Transferred │ Duration │ Speed
100.00 MiB │ 100.00 MiB  │ 00m01s   │ 78.72 MiB/s

100 MB em um segundo e meio, a 78,72 MiB/s, e a memória nem se mexeu (foi de 554 MB para 557 MB). Esse número é do disco da VPS, não da internet: quando o arquivo vier de fora, quem manda é a sua banda. Mas serve para matar a dúvida de que o MinIO seria o gargalo. 🚀

Dá para ver o estado do servidor pelo mc também:

mc admin info meuminio
●  localhost:9000
   Uptime: 8 minutes
   Version: 2025-04-22T22:12:26Z
   Network: 1/1 OK
   Drives: 1/1 OK

┌──────┬───────────────────────┬─────────────────────┬──────────────┐
│ Pool │ Drives Usage          │ Erasure stripe size │ Erasure sets │
│ 1st  │ 23.3% (total: 96 GiB) │ 1                   │ 1            │
└──────┴───────────────────────┴─────────────────────┴──────────────┘

Aquele Drives: 1/1 diz que o MinIO está usando um disco só, que é o caso de quem roda numa VPS. Ele sabe trabalhar com vários discos e várias máquinas, distribuindo os dados de forma a sobreviver à perda de um deles, mas isso é outro assunto e outro artigo.

🔁 Os comandos do dia a dia

Guardando os que eu realmente uso, todos de dentro de /opt/minio:

docker compose ps          # o que está de pé
docker compose logs -f     # acompanhar os logs ao vivo (Ctrl+C sai)
docker compose restart     # reiniciar sem perder nada
docker compose down        # parar (os dados ficam no volume)
docker compose up -d       # subir de novo

Uma diferença que vale gravar: docker compose down remove o contêiner mas preserva o volume, então os seus arquivos continuam lá quando você subir de novo. Já docker compose down -v, com o -v, apaga o volume junto, e aí os arquivos somem de verdade. Esse -v é pequenininho e faz um estrago enorme. ⚠️

E os do mc, que são os que você vai usar mais:

mc ls meuminio                      # listar os buckets
mc ls meuminio/backups              # listar o que tem dentro
mc cp arquivo.pdf meuminio/backups/ # enviar
mc cp meuminio/backups/arquivo.pdf . # baixar
mc rm meuminio/backups/arquivo.pdf  # apagar
mc du meuminio/backups              # ver quanto o bucket ocupa

Para atualizar o MinIO quando sair versão nova, troque a etiqueta da imagem no compose e:

docker compose pull
docker compose up -d

🔒 Duas coisas antes de usar isso para valer

O que está aqui funciona, mas roda em HTTP puro. Para um teste está ótimo; para uso real, duas mudanças importam.

A primeira é colocar HTTPS na frente, com um domínio e um proxy reverso. Sem isso, tudo que você envia e baixa viaja em texto aberto pela rede. A assinatura protege a sua senha, mas não esconde o conteúdo dos arquivos.

A segunda é não deixar as duas portas abertas para o mundo. Se só o seu servidor de aplicação vai conversar com o MinIO, restrinja o firewall ao IP dele em vez de liberar para todos:

ufw delete allow 9000/tcp
ufw allow from IP_DO_SEU_APP to any port 9000

E é aqui que aquela separação em duas portas fica útil de verdade: dá para deixar a API acessível para os seus sistemas e fechar o painel para todo mundo menos você, porque são portas independentes. Se fosse tudo na mesma, essa escolha não existiria.

No fim das contas, o que o MinIO entrega é isto: o mesmo jeito de guardar arquivo que a nuvem inteira usa, com as mesmas ferramentas e as mesmas bibliotecas, rodando numa VPS que você já paga e num disco que é seu. Para quem está aprendendo, é a chance de entender o S3 sem cartão de crédito na jogada; para quem já tem sistema rodando, é trocar uma linha de configuração e parar de pagar por GB. 🦄

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Perguntas frequentes

O MinIO é realmente compatível com o Amazon S3?
Sim, e dá para provar sem acreditar em ninguém: a aws CLI oficial da Amazon lista os buckets, envia e baixa arquivos do MinIO trocando apenas o parâmetro --endpoint-url. O link temporário que o MinIO gera vem assinado com AWS4-HMAC-SHA256 e região us-east-1, exatamente o formato da Amazon.
Por que o MinIO precisa de duas portas?
São dois serviços diferentes. A porta 9000 é a API S3, por onde os programas conversam: ela não é um site, e abrir no navegador devolve um XML de AccessDenied. A porta 9001 é o painel web, feito para gente, com a tela de login e o navegador de arquivos.
Por que o painel do MinIO não abre na porta 9001?
Quase sempre é a falta do --console-address ":9001" no comando. Sem essa opção o MinIO escolhe uma porta aleatória para o painel, diferente a cada reinício, e a porta 9001 que você publicou no Docker não tem ninguém atendendo. O sintoma é o navegador não conectar, sem erro nenhum no log.
Posso usar o usuário e a senha de administrador no meu sistema?
Pode, mas não deve. Essa credencial é a de dono do servidor inteiro, e trocá-la depois obriga a mexer em todos os programas que a usam. O certo é criar uma access key na tela Access Keys do painel: ela funciona igual nos programas, pode ser apagada sozinha se vazar, e não serve para entrar no painel.
Por que o MinIO recusa o nome do meu bucket?
Porque nome de bucket segue a regra do S3, não a de pasta: só letras minúsculas, números, ponto e hífen, de 3 a 63 caracteres. Maiúscula e underline são recusados, com a mensagem Bucket name contains invalid characters. No painel, o campo fica vermelho com Invalid bucket name e o botão de criar nem habilita.
Quanto o MinIO consome de servidor?
Pouco para começar. A imagem do contêiner tem 250 MB, e em repouso o MinIO ficou em torno de 550 MB de RAM com a CPU zerada, numa VPS de 7,8 GB. O envio de um arquivo de 100 MB levou 1,5 segundo, a 78,72 MiB/s, sem a memória subir de forma relevante.

Leia também