Aplicação web em três camadas (frontend, backend e banco) containerizada com Docker e implantada em Kubernetes local (Minikube) via Helm Chart, entregue como trabalho da disciplina de DevOps.
O Garimpo Musical é uma plataforma para descobrir artistas independentes: mostra os lançamentos atuais, sugere artistas a partir da localização do usuário e tem perfil próprio para cada artista, com músicas e link para o Spotify. O acesso é protegido por uma tela de login, com cadastro próprio e escolha de avatar. Cada usuário pode seguir artistas e ver depois sua lista em uma aba "Seguindo".
O foco do trabalho não é a aplicação em si, e sim empacotá-la em contêineres, isolando frontend, backend e banco em camadas independentes que sobem com um único comando.
A primeira versão era só leitura: a home listava álbuns e artistas vindos do banco e o perfil do artista mostrava as músicas. Para a segunda entrega o projeto ganhou duas funcionalidades novas, ambas com gravação no banco:
- Autenticação completa: tela de login com cadastro próprio (nome, email, senha com indicador de força e escolha de avatar), sessão guardada no Postgres em vez de JWT, senhas com BCrypt e logout que apaga a sessão.
- Seguir artistas: o botão "Seguir" no perfil do artista virou funcional (alterna para "Deixar de seguir" quando já segue) e foi adicionada uma nova aba "Seguindo" no menu lateral, listando os artistas seguidos.
Junto disso, as credenciais do banco e a porta do frontend foram
movidas para um .env (com .env.example versionado).
Nesta terceira entrega, o projeto foi migrado do Docker Compose para
Kubernetes (rodando localmente no Minikube), com um Helm Chart
para orquestrar os três componentes, Ingress publicando o site em
http://k8s.local e as imagens exportadas para o Docker Hub. A base
com Docker Compose continua funcional em paralelo — a seção
"Rodando no Kubernetes (Minikube)" mais abaixo explica tudo em detalhes.
O frontend é uma aplicação React (Create React App) servida por Nginx. O
mesmo Nginx atua como reverse proxy, encaminhando as chamadas /api/* para o
backend.
O backend é um serviço Spring Boot 3.5 em Java 21, com Spring Data JPA sobre Hibernate. O build é feito com Maven dentro de um Dockerfile multi-stage, de modo que a imagem final só carrega o JAR e a JDK.
O banco é PostgreSQL 16 (postgres:16-alpine), com volume nomeado
(postgres_data) para persistência dos dados.
A orquestração é feita pelo Docker Compose v2, no arquivo compose.yml da
raiz do repositório.
A decisão de design mais importante foi expor apenas uma porta ao host: a do
frontend (localhost:3000). Backend e banco vivem dentro de uma rede Docker
privada (garimpo-net) e não são acessíveis de fora.
Quando o navegador abre o site, o Nginx do frontend devolve o build do React.
O JavaScript faz chamadas relativas para /api/..., que o Nginx encaminha
internamente para http://backend:8080. O backend, por sua vez, fala com o
banco em postgres:5432. Esses hostnames são resolvidos pelo DNS interno do
Docker, sem precisar configurar IPs.
Esse arranjo dá três benefícios práticos: reduz a superfície de ataque (apenas uma porta exposta), elimina a necessidade de configurar CORS no backend (tudo vem do mesmo origin do ponto de vista do navegador) e permite usar URLs relativas no React, sem variável de ambiente.
A ordem de inicialização também está garantida: o Postgres tem um
healthcheck com pg_isready, e o backend só sobe depois que o healthcheck
passa.
Apenas Docker Desktop (ou Docker Engine + Compose v2). Não é preciso ter Node, Java, Maven nem Postgres na máquina.
As credenciais do banco e a porta exposta do frontend são lidas de um
arquivo .env na raiz do projeto. O repositório traz um .env.example com
os valores padrão; basta copiá-lo:
cp .env.example .env
O .env está no .gitignore e não é versionado — só o .env.example,
que serve de referência.
git clone https://github.com/digo-smithh/pratica-devops-garimpo-musical.git
cd pratica-devops-garimpo-musical
cp .env.example .env
docker compose up --build
Esse comando baixa a imagem do Postgres, constrói as imagens do backend e do
frontend, cria a rede e o volume e sobe os três contêineres na ordem certa.
Quando os logs estabilizarem (o Spring Boot vai imprimir Started BackendJavaApplication), abra http://localhost:3000.
Para parar, docker compose down. Para parar e apagar o volume com os dados
do banco, docker compose down -v.
O Postgres não é exposto no host (só vive na rede garimpo-net), então
não dá para conectar com localhost:5432. Para inspecionar o banco, use
docker exec direto no contêiner:
docker exec -it garimpo-postgres psql -U postgres -d garimpo-db
Credenciais padrão (definidas no .env):
- host (interno do compose):
postgres - porta:
5432 - database:
garimpo-db - usuário:
postgres - senha:
password
Ao abrir http://localhost:3000 o site cai numa tela de login. Há duas
opções para entrar:
Usar a conta de demonstração (criada automaticamente pelo seed):
- email:
demo@garimpo.test - senha:
demo123
Criar uma conta nova pelo botão "Criar conta", informando nome, email, senha (com indicador de força) e escolhendo um dos seis avatares disponíveis.
A autenticação é baseada em sessão guardada no banco: o POST /api/auth/login
gera um token UUID, persistido na tabela session com expiração de 7 dias,
e retornado para o frontend, que o armazena em localStorage. Cada chamada
subsequente envia Authorization: Bearer <token>. Ao clicar em "Sair", o
backend apaga a linha da session.
As senhas são guardadas com BCrypt na coluna password_hash da tabela
app_user.
Todas as chamadas passam pelo Nginx do frontend (/api/...). Não há acesso
direto ao backend a partir do host.
Públicos (sem token):
GET /api/home/albums— lista os álbuns em destaque.GET /api/home/artists?city=&state=— lista artistas, com filtro opcional por cidade e estado.GET /api/home/artists/{id}— detalhes de um artista, com as músicas.
Autenticação (escrevem no banco):
POST /api/auth/register— cria umapp_usere já abre umasession.POST /api/auth/login— verifica a senha (BCrypt) e abre uma novasession.POST /api/auth/logout— apaga asessioncorrespondente ao token enviado.GET /api/auth/me— retorna o usuário do token (sem gravar nada).
Seguindo artistas (também autenticados, escrevem no banco):
GET /api/me/following— lista os artistas que o usuário segue.GET /api/me/following/ids— só os IDs, usado pelo frontend para marcar o botão "Seguir/Deixar de seguir".POST /api/me/following/{artistId}— começa a seguir um artista (retorna201se for novo,204se já seguia,404se o artista não existe).DELETE /api/me/following/{artistId}— para de seguir (idempotente, sempre204).
Os endpoints autenticados esperam o header Authorization: Bearer <token>,
onde <token> é o UUID retornado por /login ou /register.
A aplicação não é só leitura — várias operações escrevem no Postgres em tempo real, em três tabelas criadas pelo Hibernate no startup:
app_user— uma linha por usuário cadastrado (nome, email, hash BCrypt da senha, avatar e data de criação).session— uma linha por login ativo (token UUID, FK para o usuário, data de criação e expiração).follow— uma linha por par (usuário, artista) seguido, com FKs paraapp_usereartiste UNIQUE em(user_id, artist_id)para evitar duplicatas.
O mapeamento de cada endpoint para o efeito no banco:
POST /auth/registerfazINSERTemapp_usereINSERTemsession.POST /auth/loginfazINSERTemsession.POST /auth/logoutfazDELETEemsession.POST /me/following/{id}fazINSERTemfollow(ignora se já existe).DELETE /me/following/{id}fazDELETEemfollow.
Os dados sobrevivem entre execuções porque o compose.yml declara um volume
nomeado para o diretório de dados do Postgres:
volumes:
- postgres_data:/var/lib/postgresql/datadocker compose ps deve mostrar três contêineres no ar: o frontend com
mapeamento 0.0.0.0:3000->80/tcp, e o backend e o Postgres mostrando apenas
a porta interna, sem mapeamento. Isso é o esperado.
Como confirmação, uma chamada direta ao backend a partir do host falha
(curl http://localhost:8080 retorna Connection refused), mas a mesma
chamada feita através do proxy do frontend funciona normalmente
(curl http://localhost:3000/api/home/albums devolve a lista de álbuns em
JSON).
A versão do Postgres foi fixada em 16-alpine em vez da tag latest, porque
o Postgres 18 introduziu mudanças incompatíveis na estrutura do diretório de
dados.
A dependência spring-boot-docker-compose do Spring Boot foi desabilitada
explicitamente (SPRING_DOCKER_COMPOSE_ENABLED=false). Esse plugin tenta
subir os serviços do compose automaticamente quando detecta o arquivo, o que
faz sentido em desenvolvimento local mas atrapalha quando o backend já está
rodando dentro de um container.
O npm install do frontend foi mantido em vez de npm ci porque o
package-lock.json original estava dessincronizado de algumas dependências —
como o foco do trabalho é Docker, deixei a instalação mais permissiva no
Dockerfile.
Toda a stack também pode ser executada dentro de um cluster Kubernetes
local, usando Minikube como cluster e Helm como gerenciador de
pacotes. A aplicação é publicada por meio de um Ingress, acessível em
http://k8s.local.
No macOS: brew install minikube kubectl helm.
Adicione a linha abaixo em /etc/hosts para que o navegador resolva
k8s.local para o loopback (necessário porque o Minikube com driver
Docker no macOS expõe o Ingress via minikube tunnel em 127.0.0.1):
127.0.0.1 k8s.local
Pela primeira vez (inicializa o cluster, builda as imagens e instala o chart):
./scripts/helm-up.sh -i -b
Depois, em outro terminal (precisa continuar rodando para o Ingress ficar acessível):
sudo minikube tunnel
Abra http://k8s.local no navegador — o site é servido pelo frontend do
cluster, que fala com o backend via Service interno, que por sua vez fala
com o Postgres.
Nas próximas vezes (cluster já inicializado, sem precisar buildar imagens de novo):
./scripts/helm-up.sh
./scripts/helm-down.sh # remove só o release do Helm (dados persistem)
./scripts/minikube-down.sh # para o cluster (mantém o disco)
Para apagar tudo e recomeçar do zero: minikube delete.
O chart fica em garimpo-chart/ e é organizado como um chart guarda-chuva
com um subchart por componente:
garimpo-chart/Chart.yaml— declara as dependênciasbackend,frontendedb.garimpo-chart/values.yaml— valores globais compartilhados (nomes de serviço, portas, host do Ingress).garimpo-chart/templates/ingress.yaml— o Ingress que expõek8s.local.garimpo-chart/charts/db/— subchart do Postgres (Secret, ConfigMap, PV, PVC, Deployment, Service).garimpo-chart/charts/backend/— subchart do backend (Deployment e Service).garimpo-chart/charts/frontend/— subchart do frontend (Deployment e Service).
Cada subchart tem seu próprio values.yaml com image.repository,
image.tag, resources, etc.
O chart renderiza os seguintes objetos no cluster:
- Deployment (3): um por serviço (
garimpo-db,backend,frontend), cada um com uma réplica. - Service (3):
garimpo-db:5432,backend:8080efrontend:80, todos do tipoClusterIP— o cluster resolve os hostnames internamente via DNS. - ConfigMap (1):
garimpo-db-configmap, comhost,portedatabase, consumido pelo backend. - Secret (1):
garimpo-db-secret, com usuário e senha do Postgres em base64, injetados como variáveis de ambiente. - PersistentVolume + PersistentVolumeClaim:
garimpo-db-pv(hostPath, 2 Gi) egarimpo-db-pvc, montados em/var/lib/postgresql/datapara que os dados sobrevivam a restarts. - Ingress:
garimpo-ingress, comingressClassName: nginxe hostk8s.local, roteando/para o Service do frontend.
Os mesmos manifests também estão disponíveis em K8s/ como YAMLs brutos
(sem Helm), servindo de referência didática do que o chart produz.
O backend recebe do cluster cinco variáveis (DB_HOST, DB_PORT,
DB_NAME, DB_USERNAME, DB_PASSWORD) — as três primeiras vêm do
ConfigMap e as duas últimas do Secret. O Helm/K8s compõe a URL JDBC via
interpolação $(VAR) e injeta o resultado em SPRING_DATASOURCE_URL, que
o Spring Boot usa direto sem depender de nenhuma outra configuração.
As imagens são publicadas no Docker Hub em:
docker.io/rdgsmithh/garimpo-backend:1.0docker.io/rdgsmithh/garimpo-frontend:1.0
O chart já aponta para elas. Para regerar e publicar as imagens (multi-arch
amd64 + arm64):
docker login
./scripts/docker-hub-push.sh rdgsmithh 1.0
Localmente, para desenvolvimento, o helm-up.sh -b builda essas mesmas
tags e faz minikube image load — como o imagePullPolicy é
IfNotPresent, o cluster prefere a imagem local se ela existir.
Todos ficam em scripts/ e seguem o padrão de flags -i (inicializa
Minikube) e -b (builda imagens):
minikube-up.sh [-i] [-b]— inicia o cluster e/ou builda as imagens, sem instalar nada.minikube-down.sh— para o cluster (dados no disco preservados).helm-up.sh [-i] [-b]— mesmo do anterior +helm installouhelm upgradedo releasegarimpo.helm-down.sh—helm uninstall garimpo(mantém PV/PVC).docker-hub-push.sh <usuário> [tag]— build multi-arch e push das duas imagens.
Depois de subir, os comandos abaixo devem funcionar:
kubectl get pods # três pods Running: garimpo-db, backend, frontend
kubectl get svc # três Services ClusterIP
kubectl get ingress # garimpo-ingress com host k8s.local
curl http://k8s.local/api/home/albums # JSON com os álbuns
A tela de login abre em http://k8s.local, e a conta demo
(demo@garimpo.test / demo123) continua funcionando exatamente como no
Docker Compose — inclusive as gravações no banco (registro, login, seguir
artistas), já que o Postgres tem PV persistente.
Rodrigo Smith