Cache no GitHub Actions: npm, pip e Docker

Sem cache, cada workflow reinstala todas as dependências do zero, um npm ci simples pode demorar 60-90 segundos. Com cache configurado corretamente, esse tempo cai para 5-10 segundos. GitHub Actions oferece 10 GB de cache por repositório e a actions/cache gerencia invalidação automática por hash de arquivo.

Cache de dependências Node.js

actions/setup-node tem cache integrado, a forma mais simples de cachear npm:

yaml
# Forma 1: cache integrado no setup-node (recomendado)
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'         # ou 'yarn' ou 'pnpm'
          # Chave automática baseada no hash de package-lock.json

      - run: npm ci            # usa cache se disponível (~5s vs ~60s)

# Forma 2: cache manual com actions/cache (mais controle)
      - name: Cache node_modules
        uses: actions/cache@v4
        id: npm-cache
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-

      - run: npm ci

# pnpm (mais eficiente que npm):
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile

Cache de dependências Python

Cache de pip para projetos Python:

yaml
# Forma 1: cache integrado no setup-python
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
          cache-dependency-path: 'requirements*.txt'

      - run: pip install -r requirements.txt -r requirements-dev.txt

# Forma 2: cache manual (mais controle)
      - name: Cache pip
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: pip-${{ runner.os }}-${{ hashFiles('requirements*.txt') }}
          restore-keys: |
            pip-${{ runner.os }}-

# Poetry — cache do virtualenv:
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - uses: snok/install-poetry@v1
      - name: Cache Poetry venv
        uses: actions/cache@v4
        with:
          path: ~/.cache/pypoetry
          key: poetry-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
      - run: poetry install

Cache de layers Docker

Cache de build Docker com GitHub Actions Cache para acelerar builds:

yaml
      - name: Configurar Buildx com cache
        uses: docker/setup-buildx-action@v3

      - name: Build com cache GHA
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/usuario/app:latest
          # Cache type=gha usa GitHub Actions Cache automaticamente
          cache-from: type=gha
          cache-to: type=gha,mode=max

# mode=max: cacheia todas as camadas (incluindo intermediárias)
# mode=min: cacheia apenas a camada final (menor cache, menos eficiente)

# Para repos com muitos builds simultâneos — scope por branch:
          cache-from: type=gha,scope=${{ github.ref_name }}
          cache-to: type=gha,mode=max,scope=${{ github.ref_name }}

Cache de build TypeScript/turbo

Cache para monorepos com Turborepo ou NX:

yaml
# Turborepo — cache remoto no GitHub Actions:
      - name: Cache Turbo
        uses: actions/cache@v4
        with:
          path: .turbo
          key: turbo-${{ runner.os }}-${{ github.sha }}
          restore-keys: |
            turbo-${{ runner.os }}-

      - name: Build e test (apenas módulos afetados)
        run: npx turbo run build test --cache-dir=.turbo

# NX — cache remoto:
      - name: Cache NX
        uses: actions/cache@v4
        with:
          path: .nx/cache
          key: nx-${{ runner.os }}-${{ hashFiles('nx.json', '**/project.json') }}
          restore-keys: |
            nx-${{ runner.os }}-

      - run: npx nx affected --target=build,test

Estratégia de chaves de cache

A chave correta garante cache útil sem stale data:

yaml
# Estrutura de chave recomendada:
# {runtime}-{os}-{hash-dos-lockfiles}

# Node.js:
key: node-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

# Python:
key: python-3.12-${{ runner.os }}-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}

# Docker (por branch para isolamento):
key: docker-${{ runner.os }}-${{ github.ref_name }}-${{ hashFiles('Dockerfile', '.dockerignore') }}

# restore-keys: lista de prefixos para fallback progressivo
restore-keys: |
  docker-${{ runner.os }}-${{ github.ref_name }}-
  docker-${{ runner.os }}-
  docker-

# Verificar hit/miss de cache:
      - name: Cache deps
        id: cache-deps
        uses: actions/cache@v4
        with: ...

      - name: Instalar deps (apenas se cache miss)
        if: steps.cache-deps.outputs.cache-hit != 'true'
        run: npm ci

Não quer configurar manualmente?

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

Perguntas frequentes

10 GB por repositório (não por workflow). Caches que não são acessados por 7 dias são deletados automaticamente. Quando o limite de 10 GB é atingido, os caches mais antigos são removidos para dar espaço. Monitore em Actions → Caches no repositório.

Causas comuns: (1) chave muito específica (sha do commit muda em cada push); (2) runner OS diferente (cache gerado em ubuntu-latest não funciona em windows-latest); (3) cache expirado (7 dias sem acesso); (4) modo de cache não compatível com a action. Use restore-keys como fallback para casos de miss.

Cachear ~/.npm (cache do npm) é mais correto: npm ci sempre lê do cache local mas instala em node_modules/ limpo, garante consistência. Cachear node_modules/ é mais rápido mas pode ter problemas com binários nativos ou symlinks entre runners. A recomendação oficial é cachear ~/.npm via setup-node.

Via interface: Actions → Caches → selecione e delete. Via GitHub CLI: gh cache list e gh cache delete CACHE_ID. Via API: DELETE /repos/{owner}/{repo}/actions/caches/{cache_id}. Para forçar rebuild completo: altere a chave do cache ou delete o cache existente.

Caches de um branch ficam disponíveis para branches derivados (pull requests). Um PR pode usar o cache do branch base (main). Mas caches de um branch não ficam automaticamente disponíveis para outros branches não relacionados. Isso é intencional: evita que um branch comprometa o cache de outro.