Docker Development WorkFlow - en guide med Flask og Postgres

Docker, en af ​​de seneste dille, er et fantastisk og kraftfuldt værktøj til pakning, forsendelse og kørsel af applikationer. At forstå og opsætte Docker til din specifikke applikation kan dog tage lidt tid. Da internettet er fyldt med konceptuelle guider, går jeg ikke alt for dybt konceptuelt om containere. I stedet forklarer jeg, hvad hver linje, jeg skriver, betyder, og hvordan du kan anvende det på din specifikke applikation og konfiguration.

Hvorfor Docker?

Jeg er en del af et studerende-drevet nonprofit kaldet Hack4Impact på UIUC, hvor vi udvikler tekniske projekter til nonprofitorganisationer for at hjælpe dem med at fremme deres missioner. Hvert semester har vi flere projektteam med 5-7 studerende softwareudviklere med en række færdighedsniveauer inklusive studerende, der kun har afsluttet deres første datalogikursus på college-niveau.

Da mange nonprofitorganisationer ofte bad om webapplikationer, kuraterede jeg en Flask Boilerplate for at give teams mulighed for hurtigt at få deres backend REST API-tjenester i gang. Almindelige hjælpefunktioner, applikationsstruktur, databaseindpakninger og forbindelser leveres sammen med dokumentation til opsætning, bedste kodningspraksis og trin til implementering af Heroku.

Problemer med udviklingsmiljø og afhængigheder

Men da vi ombord på nye studentsoftwareudviklere hvert semester, ville hold bruge meget tid på at konfigurere og fejlfinde miljøproblemer. Vi havde ofte flere medlemmer, der udviklede sig på forskellige operativsystemer, og løb ind i et utal af problemer (Windows, jeg peger på dig). Selvom mange af disse problemer var trivielle, såsom at starte den korrekte PostgreSQL-databaseversion med den rigtige bruger / adgangskode, spildte det tid, der kunne have været sat i selve produktet.

Derudover skrev jeg kun dokumentation til MacOS-brugere med kun bash-instruktioner (jeg har en Mac) og lod i det væsentlige Windows- og Linux-brugere ude for at tørre. Jeg kunne have spundet nogle virtuelle maskiner op og dokumenteret opsætningen igen for hvert operativsystem, men hvorfor ville jeg gøre det, hvis der er Docker?

Indtast Docker

Med Docker kan hele applikationen isoleres i containere, der kan porteres fra maskine til maskine. Dette giver mulighed for ensartede miljøer og afhængigheder. Således kan du "bygge en gang, køre hvor som helst", og udviklere kan nu kun installere en ting - Docker - og køre et par kommandoer for at få applikationen til at køre. Nybegyndere vil hurtigt kunne begynde at udvikle sig uden at bekymre sig om deres miljø. Nonprofits vil også være i stand til hurtigt at foretage ændringer i fremtiden.

Docker har også mange andre fordele, såsom dets bærbare og ressourceeffektive karakter (sammenlignet med virtuelle maskiner), og hvordan du smertefrit kan opsætte kontinuerlig integration og hurtigt implementere din applikation.

En kort oversigt over Docker Core-komponenter

Der er mange ressourcer online, der forklarer Docker bedre, end jeg kan, så jeg vil ikke gå over dem for meget detaljeret. Her er et fantastisk blogindlæg om dets koncepter og et andet specifikt om Docker. Jeg vil dog gå over nogle af kernekomponenterne i Docker, der kræves for at forstå resten af ​​dette blogindlæg.

Docker-billeder

Docker-billeder er skrivebeskyttede skabeloner, der beskriver en Docker Container. De inkluderer specifikke instruktioner skrevet i en Dockerfile, der definerer applikationen og dens afhængigheder. Tænk på dem som et øjebliksbillede af din applikation på et bestemt tidspunkt. Du får billeder, når du docker build.

Docker-containere

Docker-containere er forekomster af Docker-billeder. De inkluderer operativsystemet, applikationskode, runtime, systemværktøjer, systembiblioteker og så videre. Du er i stand til at forbinde flere Docker-containere sammen, såsom at have et Node.js-program i en container, der er forbundet til en Redis-databasebeholder. Du kører en Docker Container med docker start.

Docker-registre

Et Docker-register er et sted for dig at gemme og distribuere Docker-billeder. Vi bruger Docker Images som vores basisbilleder fra DockerHub, en gratis registreringsdatabase, der hostes af Docker selv.

Docker komponere

Docker Compose er et værktøj, der giver dig mulighed for at opbygge og starte flere Docker-billeder på én gang. I stedet for at køre de samme flere kommandoer hver gang du vil starte din applikation, kan du udføre dem alle i en kommando - når du først har angivet en bestemt konfiguration.

Docker-eksempel med Flask og Postgres

Med alle Docker-komponenterne i tankerne, lad os komme i gang med at oprette et Docker-udviklingsmiljø med Flask Application ved hjælp af Postgres som datalager. I resten af ​​dette blogindlæg vil jeg henvise til Flask Boilerplate, det lager, jeg nævnte tidligere for Hack4Impact.

I denne konfiguration bruger vi Docker til at oprette to billeder:

  • app - Flaskeansøgningen serveres i port 5000
  • postgres - Postgres-databasen serveres i port 5432

Når du ser på den øverste mappe, er der tre filer, der definerer denne konfiguration:

  • Dockerfile - et script sammensat af instruktioner til opsætning af appcontainerne. Hver kommando er automatisk og udføres successivt. Denne fil findes i det bibliotek, hvor du kører appen ( python manage.py runservereller python app.pyeller npm starter nogle eksempler). I vores tilfælde er det i den øverste mappe (hvor manage.pyer placeret). En Dockerfil accepterer Docker-instruktioner.
  • .dockerignore - specificerer hvilke filer der ikke skal medtages i containeren. Det er ligesom .gitignoremen for Docker Containers. Denne fil er parret med Dockerfile.
  • docker-compose.yml - Konfigurationsfil til Docker Compose. Dette giver os mulighed for at bygge både appog postgresbilleder på én gang, definere volumener og tilstand, der appafhænger af postgres, og indstille krævede miljøvariabler.

Bemærk: Der er kun en Dockerfil til to billeder, fordi vi tager et officielt Docker Postgres-billede fra DockerHub! Du kan medtage dit eget Postgres-billede ved at skrive din egen Dockerfile til det, men der er ingen mening.

Dockerfil

Bare for at afklare igen er denne Dockerfile til appcontaineren. Som en oversigt er her hele Dockerfile - den får i det væsentlige et basisbillede, kopierer applikationen over, installerer afhængigheder og indstiller en bestemt miljøvariabel.

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 tillader virkelig teams at udvikle sig meget hurtigere med sin bærbarhed og ensartede miljøer på tværs af platforme. Selvom jeg kun har gennemgået at bruge Docker til udvikling, udmærker Docker sig, når du bruger det til kontinuerlig integration / test og i implementering.

Jeg kunne tilføje et par flere linjer og have en fuld produktionsopsætning med Nginx og Gunicorn. Hvis jeg ønskede at bruge Redis til caching i sessioner eller som en kø, kunne jeg gøre det meget hurtigt, og alle på mit team kunne have det samme miljø, når de genopbyggede deres Docker-billeder.

Ikke kun det, jeg kunne spinde op på 20 forekomster af Flask Application på få sekunder, hvis jeg ville. Tak for læsningen! :)

Hvis du har nogle tanker og kommentarer, er du velkommen til at efterlade en kommentar nedenfor eller e-maile mig på [email protected]! Du er også velkommen til at bruge min kode eller dele denne med dine jævnaldrende!