Registry Docker self-hosted na VPS

Um registry Docker self-hosted armazena suas imagens de container localmente, sem depender do Docker Hub ou GHCR, sem limites de pull, com latência mínima para a VPS que vai fazer o pull. Ideal para ambientes offline, compliance com soberania de dados ou pipelines de alta frequência.

Registry oficial Docker self-hosted

Instalar o registry Docker da distribuição oficial:

yaml
# docker-compose.yml — Registry Docker com autenticação básica:
services:
  registry:
    image: registry:2
    restart: always
    ports:
      - "127.0.0.1:5000:5000"
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: "Docker Registry"
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
      # Ativar interface de gerenciamento:
      REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin: '["*"]'
    volumes:
      - registry_data:/data
      - ./auth:/auth

volumes:
  registry_data:

# Criar arquivo de autenticação htpasswd:
# (instalar: apt install apache2-utils)
mkdir -p auth
htpasswd -Bbn meu-usuario minha-senha > auth/htpasswd

# Expor via Nginx com HTTPS (obrigatório para registry remoto):
# O Docker client exige HTTPS para registry remotos (exceto localhost)

Nginx como proxy com HTTPS

Expor o registry com HTTPS e certificado Let's Encrypt:

nginx
# /etc/nginx/conf.d/registry.conf:
server {
    listen 443 ssl http2;
    server_name registry.seudominio.com.br;

    ssl_certificate /etc/letsencrypt/live/registry.seudominio.com.br/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/registry.seudominio.com.br/privkey.pem;

    # Aumentar limite para push de imagens grandes:
    client_max_body_size 2G;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $http_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_read_timeout 900;
    }
}

# Login no registry:
docker login registry.seudominio.com.br
# (informar usuário e senha do htpasswd)

# Build e push para o registry self-hosted:
docker build -t registry.seudominio.com.br/minha-org/minha-api:latest .
docker push registry.seudominio.com.br/minha-org/minha-api:latest

# Pull na VPS:
docker pull registry.seudominio.com.br/minha-org/minha-api:latest

Gitea com Container Registry integrado

Gitea fornece git hosting + container registry em um único serviço:

yaml
# docker-compose.yml — Gitea com Container Registry:
services:
  gitea:
    image: gitea/gitea:latest
    restart: always
    environment:
      USER_UID: 1000
      USER_GID: 1000
      GITEA__packages__ENABLED: "true"  # ativar Container Registry
    ports:
      - "127.0.0.1:3000:3000"
      - "22:22"    # SSH para git push
    volumes:
      - gitea_data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro

volumes:
  gitea_data:

# Após criar conta e organização no Gitea:
# Login no Container Registry do Gitea:
docker login gitea.seudominio.com.br
# Usuário: seu usuário do Gitea
# Senha: Personal Access Token (Settings → Applications → Generate Token)

# Push de imagem:
docker tag minha-api gitea.seudominio.com.br/minha-org/minha-api:latest
docker push gitea.seudominio.com.br/minha-org/minha-api:latest

# O package aparece em: Gitea → Packages → Container

Integrar registry self-hosted com GitHub Actions

Push para o registry próprio no pipeline de CI/CD:

yaml
# .github/workflows/build.yml:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login no registry self-hosted
        uses: docker/login-action@v3
        with:
          registry: registry.seudominio.com.br
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Build e push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            registry.seudominio.com.br/minha-org/minha-api:latest
            registry.seudominio.com.br/minha-org/minha-api:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            # Na VPS — já autenticada no registry local:
            docker pull registry.seudominio.com.br/minha-org/minha-api:${{ github.sha }}
            cd /opt/minha-api
            sed -i 's|:.*|:${{ github.sha }}|' docker-compose.yml
            docker compose up -d api

Limpeza automática de imagens antigas

Evitar que o disco encha com imagens não utilizadas:

bash
#!/bin/bash
# /opt/scripts/cleanup-registry.sh

# Deletar manifests de mais de 30 dias via Registry API:
REGISTRY="http://localhost:5000"
AUTH="meu-usuario:minha-senha"

# Listar repositórios:
REPOS=$(curl -s -u "$AUTH" "$REGISTRY/v2/_catalog" | jq -r '.repositories[]')

for REPO in $REPOS; do
    # Listar todas as tags:
    TAGS=$(curl -s -u "$AUTH" "$REGISTRY/v2/$REPO/tags/list" | jq -r '.tags[]?')

    for TAG in $TAGS; do
        # Verificar data de criação pelo digest:
        DIGEST=$(curl -s -u "$AUTH" -I \
          -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
          "$REGISTRY/v2/$REPO/manifests/$TAG" | grep Docker-Content-Digest | awk '{print $2}' | tr -d '\r')

        # (lógica de idade da imagem via label ou timestamp)
        echo "  $REPO:$TAG → $DIGEST"
    done
done

# Limpeza mais simples — remover imagens não usadas na VPS:
docker image prune -a --filter "until=720h" -f   # imagens com mais de 30 dias
docker volume prune -f                             # volumes órfãos
docker system df                                   # ver uso de disco do Docker

# Crontab — executar toda semana:
# 0 2 * * 0 root /opt/scripts/cleanup-registry.sh

Não quer configurar manualmente?

Implante o VPS para Docker em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Docker Hub Free: 1 repositório privado, limite de pulls (200/6h). GHCR: integrado ao GitHub, gratuito para repositórios públicos, 500 MB no plano Free. Registry self-hosted: gratuito ilimitado, mas você gerencia o disco e disponibilidade. Para projetos com deploys frequentes na mesma VPS: self-hosted elimina latência e custos. Para open-source: GHCR ou Docker Hub são mais práticos.

Sim, para acesso remoto. O Docker client rejeita registries HTTP (inseguros) por padrão, exceto localhost (127.0.0.1). Para registry na mesma VPS: você pode usar http://localhost:5000 sem HTTPS na VPS. Para acessar de outro servidor ou do GitHub Actions: HTTPS é obrigatório. Use Let's Encrypt com certbot, gratuito e automático.

Zot (zotregistry.io) é uma alternativa ao registry oficial Docker, OCI-compliant, com interface web, autenticação LDAP/OIDC, garbage collection automático e menor consumo de memória. É mais moderno que o registry:2 oficial e tem features que o registry oficial não tem. Para novos deployments: considere Zot ou Harbor (mais completo, mas mais pesado).

O registry armazena dados em /data (ou no volume configurado). Backup simples: rsync ou tar do volume + cópia para S3/MinIO. Backup mais robusto: use skopeo para copiar imagens entre registries (skopeo copy docker://registry-local/app:tag docker://backup-registry/app:tag). Para disaster recovery: mantenha pelo menos uma cópia das imagens de produção em um registry externo (GHCR, ECR).

Docker Hub limita pulls anônimos a 100/6h por IP e autenticados a 200/6h no Free. Soluções: (1) Usar mirror local, configure daemon.json com "registry-mirrors": ["https://mirror.gcr.io"] para imagens públicas. (2) Pull as imagens base uma vez, tag para o registry self-hosted e use-as de lá. (3) Use GHCR ou Amazon ECR Public (sem limites de pull para imagens públicas). (4) Assine o Docker Hub (pago).