Na Kinsta, temos projetos de todos os tamanhos para hospedagem de aplicativos, hospedagem de banco de dados e hospedagem gerenciada de WordPress.

Com as soluções de hospedagem na nuvem da Kinsta, você pode implantar aplicativos em várias linguagens e frameworks, como NodeJS, PHP, Ruby, Go, Scala e Python. Com um Dockerfile, você pode implementar qualquer aplicativo. Você pode conectar seu repositório Git (hospedado no GitHub, GitLab ou Bitbucket) para implantar seu código diretamente na Kinsta.

Você pode hospedar bancos de dados MariaDB, Redis, MySQL e PostgreSQL prontos para uso, economizando tempo para se concentrar no desenvolvimento de seus aplicativos em vez de se preocupar com configurações de hospedagem.

E se você escolher nossa Hospedagem Gerenciada de WordPress, você experimentará o poder das máquinas do Google Cloud C2 em sua rede de nível Premium e a segurança integrada do Cloudflare, tornando seus sites WordPress os mais rápidos e seguros do mercado.

Resolvendo o desafio de criar aplicativos nativos da nuvem com uma equipe remota

Um dos maiores desafios do desenvolvimento e da manutenção de aplicativos nativos da nuvem de nível empresarial é ter uma experiência consistente durante todo o ciclo de vida do desenvolvimento. Isso é ainda mais difícil para empresas remotas com equipes distribuídas que trabalham em plataformas diferentes, com configurações diferentes e comunicação assíncrona. Precisamos fornecer uma solução consistente, confiável e escalável que funcione para:

  • Desenvolvedores e equipes de garantia de qualidade, independentemente de seus sistemas operacionais, criam uma configuração simples e mínima para desenvolver e testar recursos.
  • Equipes de DevOps, SysOps e Infra, para configurar e manter ambientes de teste e produção.

Na Kinsta, confiamos muito no Docker para que você tenha uma experiência consistente em cada etapa, do desenvolvimento à produção. Neste artigo, mostraremos a você:

  • Como aproveitar o Docker Desktop para aumentar a produtividade dos desenvolvedores.
  • Como criamos imagens do Docker e as enviamos para o Google Container Registry por meio de pipelines CI com CircleCI e GitHub Actions.
  • Como usamos pipelines CI para promover alterações incrementais na produção usando imagens do Docker, o Google Kubernetes Engine e o Cloud Deploy.
  • Como a equipe de controle de qualidade usa perfeitamente imagens pré-construídas do Docker em diferentes ambientes.

Usando o Docker Desktop para melhorar a experiência do desenvolvedor

A execução de um aplicativo localmente exige que os desenvolvedores preparem meticulosamente o ambiente, instalem todas as dependências, configurem servidores e serviços e certifiquem-se de que estejam configurados corretamente. Quando você executa vários aplicativos, isso pode ser complicado, especialmente quando se trata de projetos complexos com várias dependências. Quando você introduz nessa variável vários colaboradores com vários sistemas operacionais, o caos está instalado. Para evitar isso, usamos o Docker.

Com o Docker, você pode declarar as configurações de ambiente, instalar as dependências e criar imagens com tudo onde deveria estar. Qualquer pessoa, em qualquer lugar, com qualquer sistema operacional, pode usar as mesmas imagens e ter a mesma experiência que todos os outros.

Declare sua configuração com o Docker Compose

Para começar, crie um arquivo Docker Compose, docker-compose.yml. É um arquivo de configuração declarativo escrito no formato YAML que informa ao Docker qual é o estado desejado do seu aplicativo. O Docker usa essas informações para configurar o ambiente para o seu aplicativo.

Os arquivos Docker Compose são muito úteis quando você tem mais de um contêiner em execução e há dependências entre os contêineres.

Para criar seu arquivo docker-compose.yml:

  1. Comece escolhendo um arquivo image como base para o nosso aplicativo. Pesquise no Docker Hub e tente encontrar uma imagem do Docker que já contenha as dependências do seu aplicativo. Certifique-se de usar uma tag de imagem específica para evitar erros. O uso da tag latest pode causar erros imprevistos em seu aplicativo. Você pode usar várias imagens de base para várias dependências. Por exemplo, uma para o PostgreSQL e outra para Redis.
  2. Use volumes para manter os dados em seu host, se você precisar. A persistência de dados no computador host ajuda a evitar a perda de dados se os contêineres do docker forem excluídos ou se você precisar recriá-los.
  3. Use o networks para isolar sua configuração e evitar conflitos de rede com o host e outros contêineres. Isso também ajuda os contêineres a se localizarem e se comunicarem facilmente uns com os outros.

Juntando tudo, temos um docker-compose.yml que se parece com isto:

version: '3.8'services:
  db:
    image: postgres:14.7-alpine3.17
    hostname: mk_db
    restart: on-failure
    ports:
      - ${DB_PORT:-5432}:5432
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER:-user}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
      POSTGRES_DB: ${DB_NAME:-main}
    networks:
      - mk_network
  redis:
    image: redis:6.2.11-alpine3.17
    hostname: mk_redis
    restart: on-failure
    ports:
      - ${REDIS_PORT:-6379}:6379
    networks:
      - mk_network
      
volumes:
  db_data:

networks:
  mk_network:
    name: mk_network

Containerização do aplicativo

Crie uma imagem do Docker para o seu aplicativo

Primeiro, precisamos criar uma imagem do Docker usando Dockerfile e, em seguida, chamá-lo a partir do docker-compose.yml.

Para criar seu arquivo Dockerfile:

  1. Comece escolhendo uma imagem como base. Use a menor imagem de base que funcione para o aplicativo. Normalmente, as imagens alpine são mínimas, com quase nenhum pacote extra instalado. Você pode começar com uma imagem alpine e construir em cima dela:
    FROM node:18.15.0-alpine3.17
    
  2. Às vezes, você precisa usar uma arquitetura de CPU específica para evitar conflitos. Por exemplo, suponha que você use um processador arm64-based, mas precise criar uma imagem amd64. Você pode fazer isso especificando o -- platform em Dockerfile:
    FROM --platform=amd64 node:18.15.0-alpine3.17
    
  3. Defina o diretório do aplicativo, instale as dependências e copie a saída para o diretório raiz:
    WORKDIR /opt/app 
    COPY package.json yarn.lock ./ 
    RUN yarn install 
    COPY . .
  4. Chame o Dockerfile de docker-compose.yml:
    services:
      ...redis
      ...db
      
      app:
        build:
          context: .
          dockerfile: Dockerfile
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        networks:
          - mk_network
        depends_on:
          - redis
          - db
  5. Implemente o recarregamento automático para que, ao alterar algo no código-fonte, você possa visualizar as alterações imediatamente sem precisar reconstruir o aplicativo manualmente. Para fazer isso, crie a imagem primeiro e, em seguida, execute em um serviço separado:
    services:
      ... redis
      ... db
      
      build-docker:
        image: myapp
        build:
          context: .
          dockerfile: Dockerfile
      app:
        image: myapp
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        volumes:
          - .:/opt/app
          - node_modules:/opt/app/node_modules
        networks:
          - mk_network
        depends_on:
          - redis
          - db
          - build-docker
          
    volumes:
      node_modules:

Dica profissional: Observe que o node_modules também está montado explicitamente para evitar problemas específicos da plataforma com os pacotes. Isso significa que, em vez de usar o node_modules no host, o contêiner Docker usa o seu próprio, mas o mapeia no host em um volume separado.

Otimizando a criação de imagens de produção com Integração Contínua

A maioria dos nossos aplicativos e serviços usa CI/CD para implantação. O Docker desempenha um papel importante no processo. Cada alteração na branch principal aciona imediatamente um pipeline de criação por meio do GitHub Actions ou do CircleCI. O fluxo de trabalho geral é muito simples: ele instala as dependências, executa os testes, cria a imagem do Docker e a envia para o Google Container Registry (ou Artifact Registry). A parte que discutiremos neste artigo é a etapa de build.

Criação das imagens do Docker

Usamos builds de múltiplos estágios por razões de segurança e desempenho.

Etapa 1: Builder

Nesta etapa, copiamos toda a base de código com todo o código-fonte e a configuração, instalamos todas as dependências, inclusive as dependências de desenvolvimento, e criamos o aplicativo. Você cria uma pasta dist/ e copia a versão construída do código para lá. Mas essa imagem é muito grande, com um grande conjunto de rastros para ser usado em produção. Além disso, como usamos registros NPM privados, também usamos nosso NPM_TOKEN privado nesse estágio. Portanto, definitivamente não queremos que esse estágio seja exposto ao mundo externo. A única coisa de que precisamos nesse estágio é a pasta dist/.

Estágio 2: Produção

A maioria das pessoas usa esse estágio para o tempo de execução, por estar muito próximo do que precisamos para executar o aplicativo. No entanto, ainda precisamos instalar as dependências de produção, o que significa que deixamos rastros e precisamos do NPM_TOKEN. Portanto, esse estágio ainda não está pronto para ser exposto. Além disso, preste atenção em yarn cache clean na linha 19. Esse pequeno comando reduz o tamanho da nossa imagem em até 60%.

Estágio 3: Tempo de execução

O último estágio precisa ser o mais fino possível, com o mínimo de rastros. Então, copiamos o aplicativo totalmente pronto da produção e seguimos em frente. Deixamos de lado todas as coisas do yarn e do NPM_TOKEN e executamos apenas o aplicativo.

Esta é a versão final do Dockerfile.production:

# Stage 1: build the source code 
FROM node:18.15.0-alpine3.17 as builder 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install 
COPY . . 
RUN yarn build 

# Stage 2: copy the built version and build the production dependencies FROM node:18.15.0-alpine3.17 as production 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install --production && yarn cache clean 
COPY --from=builder /opt/app/dist/ ./dist/ 

# Stage 3: copy the production ready app to runtime 
FROM node:18.15.0-alpine3.17 as runtime 
WORKDIR /opt/app 
COPY --from=production /opt/app/ . 
CMD ["yarn", "start"]

Observe que, em todas as etapas, começamos a copiar os arquivos package.json e yarn.lock primeiro, instalamos as dependências e depois copiamos o restante da base de código. O motivo é que o Docker constrói cada comando como uma camada sobre a anterior. E cada build pode usar as camadas anteriores, se disponíveis, e apenas construir as novas camadas para fins de desempenho.

Digamos que você tenha alterado algo em src/services/service1.ts sem tocar nos pacotes. Isso significa que as primeiras quatro camadas do construtor não foram tocadas e podem ser reutilizadas. Isso torna o processo de build incrivelmente mais rápido.

Enviando o aplicativo para o Google Container Registry por meio de pipelines CircleCI

Há várias maneiras de criar uma imagem do Docker nos pipelines do CircleCI. Em nosso caso, optamos por usar circleci/gcp-gcr orbs:

executors:
  docker-executor:
    docker:
      - image: cimg/base:2023.03
orbs:
  gcp-gcr: circleci/[email protected]
jobs:
  ...
  deploy:
    description: Build & push image to Google Artifact Registry
    executor: docker-executor
    steps:
      ...
      - gcp-gcr/build-image:
          image: my-app
          dockerfile: Dockerfile.production
          tag: ${CIRCLE_SHA1:0:7},latest
      - gcp-gcr/push-image:
          image: my-app
          tag: ${CIRCLE_SHA1:0:7},latest

Graças ao Docker, você precisa de uma configuração mínima para criar e enviar nosso aplicativo.

Enviando o aplicativo para o Google Container Registry por meio do GitHub Actions

Como alternativa ao CircleCI, podemos usar o GitHub Actions para implantar o aplicativo continuamente. Configuramos o site gcloud, criamos e enviamos a imagem do Docker para gcr.io:

jobs:
  setup-build:
    name: Setup, Build
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Get Image Tag
      run: |
        echo "TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

    - uses: google-github-actions/setup-gcloud@master
      with:
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        project_id: ${{ secrets.GCP_PROJECT_ID }}

    - run: |-
        gcloud --quiet auth configure-docker

    - name: Build
      run: |-
        docker build 
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG" 
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest" 
          .

    - name: Push
      run: |-
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG"
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest"

A cada pequena alteração enviada para a branch principal, criamos e enviamos uma nova imagem do Docker para o registro.

Implantando mudanças no Google Kubernetes Engine usando os pipelines de entrega do Google

Ter imagens do Docker prontas para uso para cada alteração também facilita a implantação na produção ou a reversão caso algo dê errado. Usamos o Google Kubernetes Engine para gerenciar e servir nossos aplicativos e usamos o Google Cloud Deploy e o Pipelines de entrega para nosso processo de implantação contínua.

Quando a imagem do Docker é criada após cada pequena alteração (com o pipeline de CI mostrado acima), damos um passo adiante e implantamos a alteração em nosso cluster de desenvolvimento usando gcloud. Vamos dar uma olhada nessa etapa do pipeline do CircleCI:

- run:
    name: Create new release
    command: gcloud deploy releases create release-${CIRCLE_SHA1:0:7} --delivery-pipeline my-del-pipeline --region $REGION --annotations commitId=$CIRCLE_SHA1 --images my-app=gcr.io/${PROJECT_ID}/my-app:${CIRCLE_SHA1:0:7}

Isso aciona um processo de lançamento para implementar as alterações em nosso cluster Kubernetes de desenvolvimento. Depois de testar e obter as aprovações, promovemos a alteração para o ambiente de teste e, em seguida, para o ambiente de produção. Tudo isso é possível porque temos uma imagem do Docker fina e isolada para cada alteração que tem quase tudo o que precisa. Só precisamos informar à implantação qual tag você deve usar.

Como a equipe de garantia de qualidade se beneficia desse processo

A equipe de controle de qualidade precisa principalmente de uma versão de pré-produção na nuvem dos aplicativos a serem testados. No entanto, às vezes eles precisam executar um aplicativo pré-criado localmente (com todas as dependências) para testar um determinado recurso. Nesses casos, eles não querem ou não precisam passar por todo o trabalho de clonar o projeto inteiro, instalar pacotes npm, criar o aplicativo, enfrentar erros de desenvolvedor e revisar todo o processo de desenvolvimento para colocar o aplicativo em funcionamento. Agora que tudo já está disponível como uma imagem do Docker no Google Container Registry, tudo o que eles precisam é de um serviço no arquivo Docker compose:

services:
  ...redis
  ...db
  
  app:
    image: gcr.io/${PROJECT_ID}/my-app:latest
    restart: on-failure
    ports:
      - ${PORT:-4000}:${PORT:-4000}
    environment:
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379
      - DATABASE_URL=postgresql://${DB_USER:-user}:${DB_PASSWORD:-password}@db:5432/main
    networks:
      - mk_network
    depends_on:
      - redis
      - db

Com esse serviço, eles podem ativar o aplicativo em suas máquinas locais usando contêineres do Docker, executando:

docker compose up

Esse é um grande passo para simplificar os processos de teste. Mesmo que o controle de qualidade decida testar uma tag específica do aplicativo, ele poderá alterar facilmente a tag da imagem na linha 6 e executar novamente o comando Docker compose. Mesmo que você decida comparar diferentes versões do aplicativo simultaneamente, poderá fazer isso facilmente com alguns ajustes. A maior vantagem é manter nossa equipe de controle de qualidade longe dos desafios do desenvolvedor.

Vantagens de usar o Docker

  • Quase zero de rastros para dependências: Se você decidir atualizar a versão do Redis ou do Postgres, basta alterar uma linha e executar o aplicativo novamente. Não há necessidade de mudar nada em seu sistema. Além disso, se você tiver dois aplicativos que precisam do Redis (talvez até com versões diferentes), você pode ter ambos rodando em seu próprio ambiente isolado, sem conflitos entre eles.
  • Várias instâncias do aplicativo: Há muitos casos em que precisamos executar o mesmo aplicativo com um comando diferente. Por exemplo, inicializar o banco de dados, executar testes, monitorar alterações no banco de dados ou ouvir mensagens. Em cada um desses casos, como já temos a imagem construída pronta, basta adicionar outro serviço ao arquivo de composição do Docker com um comando diferente e pronto.
  • Ambiente de teste mais simples: Na maioria das vezes, você só precisa executar o aplicativo. Você não precisa do código, dos pacotes ou de nenhuma conexão com o banco de dados local. Você só quer ter certeza de que o aplicativo funciona corretamente ou precisa de uma instância em execução como um serviço de backend enquanto trabalha em seu próprio projeto. Esse também pode ser o caso de QA, revisores de Pull Request ou até mesmo pessoal de UX que queira ter certeza de que seu design foi implementado corretamente. Nossa configuração com Docker facilita muito para todos eles prosseguirem sem ter que lidar com muitos problemas técnicos.

Este artigo foi originalmente publicado no Docker.

Amin Choroomi

Desenvolvedor de software na Kinsta. Apaixonado por Docker e Kubernetes, ele é especialista em desenvolvimento de aplicativos e práticas de DevOps.