Boas-vindas à Alura, sou Vinicius Dias e este curso será sobre integração contínua com Docker.
Audiodescrição: Vinicius é um homem branco, cabelo curto e escuro, bigode e cavanhaque. Utiliza uma camiseta preta e, atrás de si, há uma parede branca iluminada com luzes de tonalidades rosa e roxa.
Vamos conhecer um pouco mais sobre esse mundo de integração contínua, principalmente em ambientes que utilizam Docker. Ou seja, entenderemos como incluir a construção de uma imagem Docker na nossa esteira de integração contínua. Essa imagem precisa estar pronta para um ambiente de produção.
Aprenderemos bastante sobre construção de imagens, como garantir que essa construção seja focada num ambiente de produção, como diminuir o tamanho das imagens geradas, e como automatizar a geração de imagens, tratando inclusive do Docker Hub.
Ao final do curso, quando um Pull Request (PR) for mesclado à main, teremos uma imagem latest sendo atualizada e uma nova imagem sendo criada para manter um histórico. Vamos conversar sobre o motivo dessa abordagem e entender todo o processo mas, basicamente, teremos uma imagem Docker sendo construída de forma automática em GitHub Actions.
Para isso, é necessário um bom conhecimento em Docker, além de assuntos vistos em cursos anteriores de integração contínua com GitHub Actions. Também é interessante saber bem de Git e ter familiaridade com o terminal.
Caso haja alguma dúvida no processo, não hesite em abrir um tópico no nosso fórum ou, então, acessar nosso servidor do Discord. Lá, as discussões são um pouco mais dinâmicas. Te convidamos também para, além de fazer perguntas, responder as dúvidas de outras pessoas, pois isso é uma ótima forma de fixar o conhecimento.
Começaremos a criar uma imagem Docker adiante. Até lá.
Começaremos a colocar a mão na massa utilizando o Docker para além do ambiente de desenvolvimento, um objetivo a mais na nossa integração contínua, evoluindo na infraestrutura!
Temos um serviço do banco de dados no docker-compose.yml
utilizado no ambiente de desenvolvimento para subirmos a aplicação. Conforme conversamos anteriormente, num cenário mais real, num ambiente de produção, teremos um banco de dados "de pé", isto é, rodando, e poderíamos ter um banco de dados somente para testes.
Deixaremos o banco de dados da forma como está atualmente por realmente só ser usado no nosso docker-compose.yml
para o ambiente de desenvolvimento. Vamos focar no nosso serviço, que vai virar um contêiner de aplicação, isto é, o que realmente estamos executando.
Estamos utilizando uma imagem que possui o compilador de Go, com a qual criamos um volume. Isto porque enviamos o nosso código para dentro do container, para ser compilado e executado. Porém, em um ambiente de execução de containers, quando colocamos essa aplicação para rodar utilizando uma imagem para criar containers, não teremos o nosso código lá, e sim esta imagem dela.
Precisamos, então, que uma imagem Docker esteja previamente pronta, sem necessitar de um volume ou detalhes externos. Assim, nosso código, ou então o executável, já que ele foi compilado, precisa estar na imagem a ser utilizada em produção.
O mesmo deve ser considerado em relação ao working_dir
(, working directory, ou "diretório de trabalho"), em que a execução acontecerá. Além disso, estamos expondo uma porta de acordo com o banco de dados, algo que também não será necessário. E então estamos definindo algumas variáveis de ambiente em environment
.
Dito isso, não podemos depender de volumes para a imagem a ser utilizada em produção. Portanto, por enquanto teremos não o código, e sim somente o executável. Neste caso, teoricamente nem precisaríamos depender da imagem de golang
, mas por ora manteremos assim, adiante pensaremos em outras alternativas.
Na raiz do projeto, criaremos o "Dockerfile". Ele vai ser FROM golang
na mesma versão utilizada no docker-compose.yml
, "1.22". Mais uma vez, é possível conversarmos sobre essa imagem base mais para frente.
Já que em docker-compose.yml
estamos expondo uma porta 8080
, então repetiremos esta informação para efeito de documentação, uma vez que o EXPOSE
não possui efeito muito prático. Se quisermos expor a porta para o host na criação de contêineres a partir dessa imagem, precisaremos informar, seja no docker-compose.yml
via ports
, seja pela linha de comando com o parâmetro -p
. Assim, o EXPOSE
serve muito mais para fins documentais de qual porta a imagem que cria contêineres expõe.
Trabalharemos na pasta /app
, de acordo com o working directory. E copiaremos de host o arquivo já compilado ./main
para a /app/main
. Não precisaríamos indicar o caminho completo por estarmos na pasta correta, mas é uma boa prática colocar caminhos absolutos quando eles são curtos por trazer maior clareza.
Então, poderemos definir nosso ENTRYPOINT
como ./main
, ou nosso CMD
(command), como ./main
. No nosso cenário, não vai fazer nenhuma diferença. A diferença entre essas opções aparece nos cursos de Docker. Como o nosso ./main
não espera nenhum parâmetro, não faz diferença qual vamos utilizar. Em Dockerfile
, então, teremos bom início para a criação da nossa imagem Docker.
FROM golang:1.22
EXPOSE 8080
WORKDIR /app
COPY ./main /app/main
CMD [ "./main" ]
No docker-compose.yml
, vamos parar de utilizar a imagem golang
, e construir a nossa própria imagem a partir do Dockerfile
. Inseriremos após o app
um build: .
, que procurará um arquivo denominado Dockerfile
na mesma pasta que o docker-compose.yml
está sendo executado, e então construirá uma imagem a partir desse arquivo, que gerará um contêiner para o nosso app
.
app:
build: .
command:
- go
- run
- main.go
Ppossivelmente teremos alguns problemas neste momento, mas vamos tentar rodar o código. Antes disso, removeremos o seguinte trecho, já que passamos a ter o executável, e também o command
, pois definimos que command
será a execução do ./main
previamente compilado. Também removeremos o working_dir
:
app:
build: .
ports:
- 8080:8080
depends_on:
- postgres
Como mudamos a definição da imagem, rodaremos docker compose up --build
no terminal, para que a imagem seja construída. Caso alteremos o Dockerfile
durante o curso, precisaremos rodar com o --build
, se não o docker-compose.yml
utilizará a versão anterior da imagem.
Rodaremos em modo de debug, como esperado, pois estamos no ambiente de desenvolvimento, referente a um detalhe de Go. Vamos falar mais sobre modo de produção em breve. Temos um erro de execução porque faltam algumas implementações no nosso Dockerfile
, e podemos dizer que começamos a criar um Dockerfile
que teoricamente ainda não está pronto para produção, o cenário ainda não é muito realista.
Copiamos o arquivo ./main
previamente compilado, e o enviamos para o contêiner. Tivemos a construção da imagem no Dockerfile
em um ambiente de integração contínua, e talvez precisemos construi-la novamente em um ambiente de produção, isto é, podemos ter múltiplas construções.
Então nem sempre, ou não necessariamente teremos o arquivo compilado. Precisamos considerar um cenário em que enviaríamos o código para o contêiner, que então seria compilado, e só depois rodaríamos o executável. Este não é o único ponto que precisamos ajustar, mas é um bom próximo passo para a melhoria do nosso Dockerfile
.
A seguir, evoluiremos um pouco mais o Dockerfile
, mesmo que tenhamos alguns erros ainda. Até lá!
Vamos recapitular um dos problemas que temos até agora: se executarmos a construção de uma imagem utilizando o Dockerfile
em outro ambiente, precisaremos que esse outro ambiente tenha o arquivo ./main
já compilado na pasta principal. No entanto, o local onde formos executar a construção de tal imagem não precisa necessariamente ter Go instalado.
Da mesma forma, não precisamos garantir que o ambiente tenha a mesma versão do Go. A utilização de Docker, ou de algum outro sistema de contêiners traz justamente essa flexibilidade de execuções a partir da imagem, independentemente de onde ela esteja. E a construção das imagens também segue um princípio parecido. Não podemos depender muito do ambiente para termos a construção correta.
Então, em vez de copiar o arquivo já compilado, vamos copiar o código do projeto para o Dockerfile
. Tendo o código, podemos rodar a etapa de compilação, e inclusive trocar o comando para go
, utilizando a sintaxe de array run
e main.go
.
FROM golang:1.22
EXPOSE 8080
WORKDIR /app
COPY ./main /app/main
CMD [ "go", "run", "main.go" ]
Obviamente ainda não temos o arquivo main.go
. No Dockerfile
de produção, por enquanto não precisaremos rodar os testes. Vamos copiar primeiro cada uma das pastas: assets/
, controllers/
, database/
, models/
, routes/
. Reparem que estamos pulando algumas pastas. A pasta pkg
, por exemplo, será criada ao compilarmos o projeto, quando as dependências forem baixadas.
A postgres-data
se refere ao banco de dados, que não é necessária também. Não estamos copiando tudo da nossa raiz para dentro do nosso container. Em um projeto mais organizado, o ideal seria que todas as pastas estivessem dentro de uma pasta src
, ou algo do tipo, que é a organização mais usual. De novo, isso é detalhe de código, e por ora focaremos na parte de operações. Continuando, copiaremos também os templates
, o main.go
, e go.mod
, que são as definições do módulo, das nossas dependências.
Inclusive, o erro que está acontecendo na execução é justamente por não termos a pasta templates
no contêiner. Assim, o Go não consegue encontrar os arquivos da visualização, do HTML. Os arquivos HTML que estão na pasta "templates", portanto, não são compilados, nem vão para o nosso binário, porém precisam ser encontrados.
WORKDIR /app
COPY ./assets/ /app/assets/
COPY ./controllers/ /app/controllers/
COPY ./database/ /app/database/
COPY ./models/ /app/models/
COPY ./routes/ /app/routes/
COPY ./templates/ /app/templates/
COPY ./main.go /app/main.go/
COPY ./go.mod /app/go.mod/
COPY ./go.sum /app/go.sum/
CMD [ "go", "run", "main.go" ]
O go.sum
"guarda" o que for baixado, todas as dependências que foram utilizadas, na versão exata. Algumas linguagens vão ter arquivos diferentes com a gestão de dependência. Por exemplo, no PHP, há o composer.json
e o composer.lock
. No JavaScript, temos package.json
, package.lock
ou yarn.lock
. Ou seja, cada linguagem terá alguns arquivos além do seu código, indicando as dependências necessárias. Para cada linguagem, será preciso conversar com a equipe de desenvolvimento e entender o que precisa ser enviado para o contêiner.
Quando falarmos melhor sobre as etapas de construção, podemos conversar sobre o que realmente precisaria ser enviado para o contêiner, etc. Por enquanto, não precisaremos enviar o main_test.go
, já que não executaremos os testes no contêiner.
O ideal é termos o mínimo necessário, para as imagens na produção serem menores. Isso porque podemos acabar armazenando-as em algum lugar que cobre pelo seu tamanho. Ter uma imagem menor agiliza o processo de deploy, pois ela precisará ser baixada de algum lugar. Então, quanto menor, melhor.
Ao executarmos e tentamos rodar docker compose up
, teremos que o nosso banco de dados está "de pé", mas a aplicação não. Então, com "Ctrl + C" interromperemos todo o processo e em seguida rodaremos docker compose up --build
, para reconstruirmos a imagem. Os arquivos são devidamente copiados, o banco de dados sobe, enquanto isso, não notamos nada de diferente. As dependências do Go são baixadas, esperaremos um pouco e, finalmente, nossa aplicação sobe. Há alguns avisos, principalmente, de debug.
Basicamente, o problema ocorre porque não definimos uma variável de ambiente, PORT
, que é o que o Go utiliza para saber em qual porta subir o servidor, por meio do GIN. Porém, o padrão é o que já estava sendo usado anteriormente, ou seja, esse aviso já existia. Teoricamente, a nossa aplicação está pronta para ser acessada!
Tentaremos acessar https://localhost:8080
, e encontraremos nossa aplicação "de pé", com o nosso template rodando. Se acessarmos https://localhost:8080/Vinicius
, teremos a nossa saudação personalizada na tela. Em https://localhost:8080/alunos
, esperamos encontrar o array com os alunos criados, contidos no nosso banco de dados. Aparentemente, tudo está funcionando conforme esperado, nossa API está perfeitamente funcional com o nosso novo Dockerfile
.
Ainda existem alguns detalhes a serem ajustados, principalmente documentais. Depois, poderemos lidar com outras questões para além daquelas encontradas por quem lê nosso Dockerfile
, como diminuir o tamanho da imagem, pensar em quais arquivos precisam estar no contêiner, e assim por diante.
Temos uma imagem criada a partir de um Dockerfile
, que pode ser utilizada no ambiente de integração contínua como o GitHub Actions, em produção, que poderíamos ter com o ambiente de entrega contínua e deploy contínuo (CI/CD). Basicamente, significa que temos um bom primeiro passo.
Então, adiante, vamos recapitular alguns conceitos e melhorar um pouco mais a nossa imagem, para garantir que ela consiga ser executada em outros ambientes que não seriam possíveis no momento.
O curso Docker: construindo imagens para produção possui 100 minutos de vídeos, em um total de 36 atividades. Gostou? Conheça nossos outros cursos de Builds em DevOps, ou leia nossos artigos de DevOps.
Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.