WorkFlow de desarrollo de Docker: una guía con Flask y Postgres

Docker, una de las últimas modas, es una herramienta increíble y poderosa para empaquetar, enviar y ejecutar aplicaciones. Sin embargo, comprender y configurar Docker para su aplicación específica puede llevar un poco de tiempo. Dado que Internet está lleno de guías conceptuales, no profundizaré conceptualmente en los contenedores. En cambio, explicaré qué significa cada línea que escribo y cómo puede aplicar eso a su aplicación y configuración específicas.

¿Por qué Docker?

Soy parte de una organización sin fines de lucro dirigida por estudiantes llamada Hack4Impact en UIUC, donde desarrollamos proyectos técnicos para organizaciones sin fines de lucro para ayudarlos a promover sus misiones. Cada semestre, tenemos varios equipos de proyectos de 5 a 7 estudiantes desarrolladores de software, con una variedad de niveles de habilidad, incluidos los estudiantes que solo han terminado su primer curso de informática a nivel universitario.

Dado que muchas organizaciones sin fines de lucro a menudo solicitaban aplicaciones web, seleccioné un Flask Boilerplate para permitir a los equipos poner rápidamente en funcionamiento sus servicios de API REST de backend. Las funciones de utilidad comunes, la estructura de la aplicación, los envoltorios de la base de datos y las conexiones se proporcionan junto con la documentación para la configuración, las mejores prácticas de codificación y los pasos para la implementación de Heroku.

Problemas con el entorno de desarrollo y las dependencias

Sin embargo, dado que incorporamos nuevos desarrolladores de software para estudiantes cada semestre, los equipos dedicarían mucho tiempo a configurar y solucionar problemas del entorno. A menudo teníamos varios miembros que se desarrollaban en diferentes sistemas operativos y nos encontrábamos con una gran cantidad de problemas (Windows, te estoy señalando). Aunque muchos de esos problemas eran triviales, como iniciar la versión correcta de la base de datos PostgreSQL con el usuario / contraseña correctos, fue una pérdida de tiempo que podría haberse puesto en el producto en sí.

Además de eso, solo escribí documentación para usuarios de MacOS con solo instrucciones bash (tengo una Mac), y esencialmente dejé a los usuarios de Windows y Linux afuera para secar. Podría haber creado algunas máquinas virtuales y documentar la configuración nuevamente para cada sistema operativo, pero ¿por qué haría eso si hay Docker?

Ingrese Docker

Con Docker, toda la aplicación se puede aislar en contenedores que se pueden transferir de una máquina a otra. Esto permite entornos y dependencias consistentes. Por lo tanto, puede "compilar una vez, ejecutar en cualquier lugar" y los desarrolladores ahora podrán instalar solo una cosa, Docker, y ejecutar un par de comandos para que la aplicación se ejecute. Los recién llegados podrán comenzar a desarrollarse rápidamente sin preocuparse por su entorno. Las organizaciones sin fines de lucro también podrán realizar cambios rápidamente en el futuro.

Docker también tiene muchos otros beneficios, como su naturaleza portátil y eficiente en el uso de recursos (en comparación con las máquinas virtuales), y cómo puede configurar la integración continua sin problemas e implementar rápidamente su aplicación.

Una breve descripción general de los componentes principales de Docker

Hay muchos recursos en línea que explicarán Docker mejor que yo, así que no los revisaré con demasiados detalles. Aquí hay una publicación de blog increíble sobre sus conceptos y otra sobre Docker específicamente. Sin embargo, repasaré algunos de los componentes principales de Docker que se requieren para comprender el resto de esta publicación de blog.

Imágenes de Docker

Las imágenes de Docker son plantillas de solo lectura que describen un contenedor de Docker. Incluyen instrucciones específicas escritas en un Dockerfile que define la aplicación y sus dependencias. Piense en ellos como una instantánea de su aplicación en un momento determinado. Obtendrá imágenes cuando usted docker build.

Contenedores Docker

Los contenedores de Docker son instancias de imágenes de Docker. Incluyen el sistema operativo, el código de la aplicación, el tiempo de ejecución, las herramientas del sistema, las bibliotecas del sistema, etc. Puede conectar varios contenedores Docker juntos, como tener una aplicación Node.js en un contenedor que está conectado a un contenedor de base de datos de Redis. Ejecutará un contenedor Docker con docker start.

Registros de Docker

Un registro de Docker es un lugar para almacenar y distribuir imágenes de Docker. Usaremos Docker Images como nuestras imágenes base de DockerHub, un registro gratuito alojado por el propio Docker.

Docker Compose

Docker Compose es una herramienta que le permite crear e iniciar varias imágenes de Docker a la vez. En lugar de ejecutar los mismos comandos múltiples cada vez que desee iniciar su aplicación, puede hacerlo todos en un comando, una vez que proporcione una configuración específica.

Ejemplo de Docker con Flask y Postgres

Con todos los componentes de Docker en mente, empecemos a configurar un entorno de desarrollo de Docker con la aplicación Flask utilizando Postgres como su almacén de datos. Durante el resto de esta publicación de blog, haré referencia a Flask Boilerplate, el repositorio que mencioné anteriormente para Hack4Impact.

En esta configuración, usaremos Docker para construir dos imágenes:

  • app - la aplicación Flask servida en el puerto 5000
  • postgres - la base de datos de Postgres servida en el puerto 5432

Cuando miras el directorio superior, hay tres archivos que definen esta configuración:

  • Dockerfile : un script compuesto por instrucciones para configurar los appcontenedores. Cada comando es automático y se ejecuta sucesivamente. Este archivo se encuentra en el directorio donde se ejecuta la aplicación ( python manage.py runservero python app.py, o npm startson algunos ejemplos). En nuestro caso, está en el directorio superior (donde manage.pyse encuentra). Un Dockerfile acepta las instrucciones de Docker.
  • .dockerignore : especifica qué archivos no se incluirán en el contenedor. Es como .gitignorepero para los contenedores Docker. Este archivo está emparejado con el Dockerfile.
  • docker-compose.yml : archivo de configuración para Docker Compose. Esto nos permitirá construir imágenes appe postgresimágenes a la vez, definir volúmenes y estados que appdependen postgresy establecer las variables ambientales requeridas.

Nota: ¡ Solo hay un Dockerfile para dos imágenes porque tomaremos una imagen oficial de Docker Postgres de DockerHub! Puede incluir su propia imagen de Postgres escribiendo su propio Dockerfile, pero no tiene sentido.

Dockerfile

Solo para aclarar nuevamente, este Dockerfile es para el appcontenedor. Como descripción general, aquí está el Dockerfile completo: esencialmente obtiene una imagen base, copia la aplicación, instala dependencias y establece una variable de entorno 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

Docker truly allows teams to develop much faster with its portability and consistent environments across platforms. Although I’ve only gone through using Docker for development, Docker excels when you use it for Continuous Integration/testing and in Deployment.

I could add a couple more lines and have a full production setup with Nginx and Gunicorn. If I wanted to use Redis for session caching or as a queue, I could do that very quickly and everyone on my team would be able to have the same environment when they rebuilt their Docker Images.

Not only that, I could spin up 20 instances of the Flask Application in seconds if I wanted to. Thanks for reading! :)

If you have any thoughts and comments, feel free to leave a comment below or email me at [email protected]! Also, feel free to use my code or share this with your peers!