Docker Development WorkFlow - um guia com Flask e Postgres

Docker, uma das últimas manias, é uma ferramenta incrível e poderosa para empacotar, enviar e executar aplicativos. No entanto, entender e configurar o Docker para seu aplicativo específico pode levar um pouco de tempo. Uma vez que a Internet está repleta de guias conceituais, não irei me aprofundar muito conceitualmente sobre Containers. Em vez disso, explicarei o que significa cada linha que escrevo e como você pode aplicá-la ao seu aplicativo e configuração específicos.

Por que Docker?

Faço parte de uma organização sem fins lucrativos administrada por estudantes chamada Hack4Impact na UIUC, onde desenvolvemos projetos técnicos para organizações sem fins lucrativos para ajudá-las a realizar suas missões. A cada semestre, temos várias equipes de projeto de 5 a 7 alunos desenvolvedores de software, com uma variedade de níveis de habilidade, incluindo alunos que acabaram de terminar seu primeiro curso de ciência da computação de nível universitário.

Uma vez que muitas organizações sem fins lucrativos frequentemente pedem aplicativos da web, eu fiz a curadoria de um Flask Boilerplate para permitir que as equipes tenham rapidamente seus serviços de API REST de back-end em funcionamento. Funções de utilitários comuns, estrutura de aplicativo, wrappers de banco de dados e conexões são fornecidos junto com a documentação para configuração, melhores práticas de codificação e etapas para implantação do Heroku.

Problemas com ambiente de desenvolvimento e dependências

No entanto, como integramos novos desenvolvedores de software para alunos a cada semestre, as equipes gastariam muito tempo configurando e solucionando problemas de ambiente. Freqüentemente, tínhamos vários membros desenvolvendo em diferentes sistemas operacionais e enfrentávamos uma miríade de problemas (Windows, estou apontando para você). Embora muitos desses problemas fossem triviais, como iniciar a versão correta do banco de dados PostgreSQL com o usuário / senha corretos, foi uma perda de tempo que poderia ter sido colocado no próprio produto.

Além disso, eu só escrevi documentação para usuários de MacOS apenas com instruções bash (eu tenho um Mac) e, essencialmente, deixei os usuários de Windows e Linux secarem. Eu poderia ter rodado algumas máquinas virtuais e documentado a configuração novamente para cada sistema operacional, mas por que faria isso se houvesse o Docker?

Entrar no Docker

Com o Docker, todo o aplicativo pode ser isolado em contêineres que podem ser transferidos de máquina para máquina. Isso permite ambientes e dependências consistentes. Assim, você pode “construir uma vez, executar em qualquer lugar” e os desenvolvedores agora poderão instalar apenas uma coisa - Docker - e executar alguns comandos para fazer o aplicativo funcionar. Os recém-chegados poderão começar a desenvolver rapidamente sem se preocupar com o ambiente. As organizações sem fins lucrativos também poderão fazer alterações rapidamente no futuro.

O Docker também oferece muitos outros benefícios, como sua natureza portátil e eficiente em termos de recursos (em comparação com as máquinas virtuais) e como você pode configurar a integração contínua e implantar rapidamente seu aplicativo sem problemas.

Uma breve visão geral dos componentes principais do Docker

Existem muitos recursos online que explicam o Docker melhor do que eu, portanto, não os examinarei em muitos detalhes. Aqui está uma postagem de blog incrível sobre seus conceitos, e outra especificamente no Docker. No entanto, irei examinar alguns dos principais componentes do Docker que são necessários para entender o restante desta postagem do blog.

Imagens Docker

As imagens do Docker são modelos somente leitura que descrevem um Docker Container. Eles incluem instruções específicas escritas em um Dockerfile que define o aplicativo e suas dependências. Pense neles como um instantâneo de seu aplicativo em um determinado momento. Você obterá imagens quando você docker build.

Docker Containers

Docker Containers são instâncias de imagens Docker. Eles incluem o sistema operacional, código do aplicativo, tempo de execução, ferramentas do sistema, bibliotecas do sistema e assim por diante. Você pode conectar vários Docker Containers juntos, como ter um aplicativo Node.js em um contêiner que está conectado a um contêiner de banco de dados Redis. Você executará um Docker Container com docker start.

Docker Registries

Um Docker Registry é um lugar para você armazenar e distribuir imagens Docker. Usaremos imagens do Docker como imagens base do DockerHub, um registro gratuito hospedado pelo próprio Docker.

Docker Compose

O Docker Compose é uma ferramenta que permite construir e iniciar várias imagens do Docker de uma vez. Em vez de executar os mesmos comandos múltiplos sempre que quiser iniciar seu aplicativo, você pode executá-los todos em um comando - assim que fornecer uma configuração específica.

Exemplo do Docker com Flask e Postgres

Com todos os componentes do Docker em mente, vamos começar a configurar um ambiente de desenvolvimento Docker com o aplicativo Flask usando Postgres como seu armazenamento de dados. No restante desta postagem do blog, farei referência ao Flask Boilerplate, o repositório que mencionei anteriormente para o Hack4Impact.

Nesta configuração, usaremos o Docker para construir duas imagens:

  • app - o aplicativo Flask servido na porta 5000
  • postgres - o banco de dados Postgres servido na porta 5432

Quando você olha para o diretório superior, há três arquivos que definem esta configuração:

  • Dockerfile - um script composto de instruções para configurar os appcontêineres. Cada comando é automático e executado sucessivamente. Este arquivo estará localizado no diretório onde você executa o aplicativo ( python manage.py runserverou python app.pyou npm startsão alguns exemplos). No nosso caso, está no diretório superior (onde manage.pyestá localizado). Um Dockerfile aceita instruções do Docker.
  • .dockerignore - especifica quais arquivos não devem ser incluídos no Container. É .gitignoreigual, mas para os Docker Containers. Este arquivo está emparelhado com o Dockerfile.
  • docker-compose.yml - Arquivo de configuração para Docker Compose. Isso nos permitirá construir as imagens appe postgresao mesmo tempo, definir os volumes e o estado que appdependem postgrese definir as variáveis ​​ambientais necessárias.

Nota: Há apenas um Dockerfile para duas imagens, porque iremos pegar uma imagem Docker Postgres oficial do DockerHub! Você pode incluir sua própria imagem Postgres escrevendo seu próprio Dockerfile para ela, mas não adianta.

Dockerfile

Só para esclarecer novamente, este Dockerfile é para o appcontêiner. Como uma visão geral, aqui está todo o Dockerfile - ele basicamente obtém uma imagem de base, copia o aplicativo, instala dependências e define uma variável de ambiente específica.

FROM python:3.6
LABEL maintainer "Timothy Ko "
RUN apt-get update
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
ENV FLASK_ENV="docker"
EXPOSE 5000

Because this Flask Application uses Python 3.6, we want an environment that supports it and already has it installed. Fortunately, DockerHub has an official image that’s installed on top of Ubuntu. In one line, we will have a base Ubuntu image with Python 3.6, virtualenv, and pip. There are tons of images on DockerHub, but if you would like to start off with a fresh Ubuntu image and build on top of it, you could do that.

FROM python:3.6

I then note that I’m the maintainer.

LABEL maintainer "Timothy Ko "

Now it’s time to add the Flask application to the image. For simplicity, I decided to copy the application under the /app directory on our Docker Image.

RUN mkdir /app
COPY . /app
WORKDIR /app

WORKDIR is essentially a cd in bash, and COPY copies a certain directory to the provided directory in an image. ADD is another command that does the same thing as COPY , but it also allows you to add a repository from a URL. Thus, if you want to clone your git repository instead of copying it from your local repository (for staging and production purposes), you can use that. COPY, however, should be used most of the time unless you have a URL. Every time you use RUN, COPY, FROM, or CMD, you create a new layer in your docker image, which affects the way Docker stores and caches images. For more information on best practices and layering, see Dockerfile Best Practices.

Now that we have our repository copied to the image, we will install all of our dependencies, which is defined in requirements.txt

RUN pip install --no-cache-dir -r requirements.txt

But say you had a Node application instead of Flask — you would instead write RUN npm install. The next step is to tell Flask to use Docker Configurations that I hardcoded into config.py. In that configuration, Flask will connect to the correct database we will set up later on. Since I had production and regular development configurations, I made it so that Flask would choose the Docker Configuration whenever the FLASK_ENV environment variable is set to docker. So, we need to set that up in our app image.

ENV FLASK_ENV="docker"

Then, expose the port(5000) the Flask application runs on:

EXPOSE 5000

And that’s it! So no matter what OS you’re on, or how bad you are at following documentation instructions, your Docker image will be same as your team members’ because of this Dockerfile.

Anytime you build your image, these following commands will be run. You can now build this image with sudo docker build -t app .. However, when you run it with sudo docker run app to start a Docker Container, the application will run into a database connection error. This is is because you haven’t provisioned a database yet.

docker-compose.yml

Docker Compose will allow you to do that and build your app image at the same time. The entire file looks like this:

version: '2.1'services: postgres: restart: always image: postgres:10 environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} volumes: - ./postgres-data/postgres:/var/lib/postgresql/data ports: - "5432:5432" app: restart: always build: . ports: - 5000:5000 volumes: - .:/app

For this specific repository, I decided to use version 2.1 since I was more comfortable with it and it had a few more guides and tutorials on it — yeah, that’s my only reasoning for not using version 3. With version 2, you must provide “services” or images you want to include. In our case, it is app and postgres(these are just names that you can refer to when you use docker-compose commands. You call them database and api or whatever floats your boat).

Postgres Image

Looking at the Postgres Service, I specify that it is a postgres:10 image, which is another DockerHub Image. This image is an Ubuntu Image that has Postgres installed and will automatically start the Postgres server.

postgres: restart: always image: postgres:10 environment: - POSTGRES_USER=${USER} - POSTGRES_PASSWORD=${PASSWORD} - POSTGRES_DB=${DB} volumes: - ./postgres-data/postgres:/var/lib/postgresql/data ports: - "5432:5432"

If you want a different version, just change the “10” to something else. To specify what user, password, and database you want inside Postgres, you have to define environment variables beforehand — this is implemented in the official postgres Docker image’s Dockerfile. In this case, the postgres image will inject the $USER, $PASSWORD, and $DB environment variables and make them the POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB envrionment variables inside the postgres container. Note that $USER and the other environment variables injected are environment variables specified in your own computer (more specifically the command line process you are using to run the docker-compose up command. By injecting your credentials, this allows you to not commit your credentials into a public repository.

Docker-compose will also automatically inject environment variables if you have a .env file in the same directory as your docker-compose.yml file. Here’s an example of a .env file for this scenario:

USER=testusrPASSWORD=passwordDB=testdb

Thus our PostgreSQL database will be named testdb with a user called testusr with password password.

Our Flask application will connect to this specific database, because I wrote down its URL in the Docker Configurations I mentioned earlier.

Every time a container is stopped and removed, the data is deleted. Thus, you must provide a persistent data storage so none of the database data is deleted. There are two ways to do it:

  • Docker Volumes
  • Local Directory Mounts

I’ve chosen to mount it locally to ./postgres-data/postgres , but it can be anywhere. The syntax is always[HOST]:[CONTAINER]. This means any data from /var/lib/postgresql/data is actually stored in ./postgres-data.

volumes:- ./postgres-data/postgres:/var/lib/postgresql/data

We will use the same syntax for ports:

ports:- "5432:5432"

app Image

We will then define the app image.

app: restart: always build: . ports: - 5000:5000 volumes: - .:/app depends_on: - postgres entrypoint: ["python", "manage.py","runserver"]

We first define it to have restart: always. This means that it will restart whenever it fails. This is especially useful when we build and start these containers. app will generally start up before postgres, meaning that app will try to connect to the database and fail, since the postgres isn’t up yet. Without this property, app would just stop and that’s the end of it.

We then define that we want this build to be the Dockerfile that is in this current directory:

build: .

This next step is pretty important for the Flask server to restart whenever you change any code in your local repository. This is very helpful so you don’t need to rebuild your image over and over again every time to see your changes. To do this, we do the same thing we did for postgres : we state that the /app directory inside the container will be whatever is in .(the current directory). Thus, any changes in your local repo will be reflected inside the container.

volumes: - .:/app

After this, we need to tell Docker Compose that app depends on the postgres container. Note that if you change the name of the image to something else like database, you must replace that postgres with that name.

depends_on: - postgres

Finally, we need to provide the command that is called to start our application. In our case, it’s python manage.py runserver.

entrypoint: ["python", "manage.py","runserver"]

One caveat for Flask is that you must explicitly note which host (port) you want to run it in, and whether you want it to be in debug mode when you run it. So in manage.py, I do that with:

def runserver(): app.run(debug=True, host=’0.0.0.0', port=5000)

Finally, build and start your Flask app and Postgres Database using your Command Line:

docker-compose builddocker-compose up -ddocker-compose exec app python manage.py recreate_db

The last command essentially creates the database schema defined by my Flask app in Postgres.

And that’s it! You should be able to see the Flask application running on //localhost:5000!

Docker Commands

Remembering and finding Docker commands can be pretty frustrating in the beginning, so here’s a list of them! I’ve also written a bunch of commonly used ones in my Flask Boilerplate Docs if you want to refer to that.

Conclusion

O Docker realmente permite que as equipes se desenvolvam muito mais rápido com sua portabilidade e ambientes consistentes entre plataformas. Embora eu só tenha usado o Docker para desenvolvimento, o Docker se destaca quando você o usa para integração / teste contínuo e na implantação.

Eu poderia adicionar mais algumas linhas e ter uma configuração de produção completa com Nginx e Gunicorn. Se eu quisesse usar o Redis para cache de sessão ou como uma fila, poderia fazer isso muito rapidamente e todos em minha equipe poderiam ter o mesmo ambiente quando reconstruíssem suas imagens Docker.

Além disso, eu poderia girar 20 instâncias do aplicativo Flask em segundos, se quisesse. Obrigado por ler! :)

Se você tiver alguma opinião e comentário, sinta-se à vontade para deixar um comentário abaixo ou enviar um e-mail para [email protected]! Além disso, fique à vontade para usar meu código ou compartilhá-lo com seus colegas!