Apache/Airflow and PostgreSQL with Docker and Docker Compose | by Ivan Rezic | Towards Data Science

Hello, in this post I will show you how to set up official Apache/Airflow with PostgreSQL and LocalExecutor using docker and docker-compose. In this post, I won’t be going through Airflow, what it is, and how it is used. Please check the official documentation for more information about that.
Before setting up and running Apache Airflow, please install Docker and Docker Compose.
In this chapter, I will show you files and directories which are needed to run airflow and in the next chapter, I will go file by file, line by line explaining what is going on.
Firstly, in the root directory create three more directories: dags, logs, and scripts. Further, create following files: .env, docker-compose.yml, entrypoint.sh and dummy_dag.py. Please make sure those files and directories follow the structure below.
#project structureroot/├── dags/│   └── dummy_dag.py├── scripts/│   └── entrypoint.sh├── logs/├── .env└── docker-compose.yml
Created files should contain the following:
#docker-compose.ymlversion: '3.8'services:    postgres:        image: postgres        environment:            - POSTGRES_USER=airflow            - POSTGRES_PASSWORD=airflow            - POSTGRES_DB=airflow    scheduler:        image: apache/airflow        command: scheduler        restart_policy:            condition: on-failure        depends_on:            - postgres        env_file:            - .env        volumes:            - ./dags:/opt/airflow/dags            - ./logs:/opt/airflow/logs    webserver:        image: apache/airflow        entrypoint: ./scripts/entrypoint.sh        restart_policy:            condition: on-failure        depends_on:            - postgres            - scheduler        env_file:            - .env        volumes:            - ./dags:/opt/airflow/dags            - ./logs:/opt/airflow/logs            - ./scripts:/opt/airflow/scripts        ports:            - "8080:8080"
#entrypoint.sh#!/usr/bin/env bashairflow initdbairflow webserver
#.envAIRFLOW__CORE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres/airflowAIRFLOW__CORE__EXECUTOR=LocalExecutor
#dummy_dag.pyfrom airflow import DAGfrom airflow.operators.dummy_operator import DummyOperatorfrom datetime import datetimewith DAG('example_dag', start_date=datetime(2016, 1, 1)) as dag:    op = DummyOperator(task_id='op')
Positioning in the root directory and executing “docker-compose up” in the terminal should make airflow accessible on localhost:8080. Image bellow shows the final result.
If you encounter permission errors, please run “chmod -R 777” on all subdirectories, e.g. “chmod -R 777 logs/”
In Leyman’s terms, docker is used when managing individual containers and docker-compose can be used to manage multi-container applications. It also moves many of the options you would enter on the docker run into the docker-compose.yml file for easier reuse. It works as a front end "script" on top of the same docker API used by docker. You can do everything docker-compose does with docker commands and a lot of shell scripting.
Before running our multi-container docker applications, docker-compose.yml must be configured. With that file, we define services that will be run on docker-compose up.
The first attribute of docker-compose.yml is version, which is the compose file format version. For the most recent version of file format and all configuration options click here.
Second attribute is services and all attributes one level bellow services denote containers used in our multi-container application. These are postgres, scheduler and webserver. Each container has image attribute which points to base image used for that service.
For each service, we define environment variables used inside service containers. For postgres it is defined by environment attribute, but for scheduler and webserver it is defined by .env file. Because .env is an external file we must point to it with env_file attribute.
By opening .env file we can see two variables defined. One defines executor which will be used and the other denotes connection string. Each connection string must be defined in the following manner:
dialect+driver://username:password@host:port/database
Dialect names include the identifying name of the SQLAlchemy dialect, a name such as sqlite, mysql, postgresql, oracle, or mssql. Driver is the name of the DBAPI to be used to connect to the database using all lowercase letters. In our case, connection string is defined by:
postgresql+psycopg2://airflow:airflow@postgres/airflow
Omitting port after host part denotes that we will be using default postgres port defined in its own Dockerfile.
Every service can define command which will be run inside Docker container. If one service needs to execute multiple commands it can be done by defining an optional .sh file and pointing to it with entrypoint attribute. In our case we have entrypoint.sh inside the scripts folder which once executed, runs airflow initdb and airflow webserver. Both are mandatory for airflow to run properly.
Defining depends_on attribute, we can express dependency between services. In our example, webserver starts only if both scheduler and postgres have started, also the scheduler only starts after postgres have started.
In case our container crashes, we can restart it by restart_policy. The restart_policy configures if and how to restart containers when they exit. Additional options are condition, delay, max_attempts, and window.
Once service is running, it is being served on containers defined port. To access that service we need to expose the containers port to the host's port. That is being done by ports attribute. In our case, we are exposing port 8080 of the container to TCP port 8080 on 127.0.0.1 (localhost) of the host machine. Left side of : defines host machines port and the right-hand side defines containers port.
Lastly, the volumes attribute defines shared volumes (directories) between host file system and docker container. Because airflows default working directory is /opt/airflow/ we need to point our designated volumes from the root folder to the airflow containers working directory. Such is done by the following command:
#general case for airflow- ./<our-root-subdir>:/opt/airflow/<our-root-subdir>#our case- ./dags:/opt/airflow/dags- ./logs:/opt/airflow/logs- ./scripts:/opt/airflow/scripts           ...
This way, when the scheduler or webserver writes logs to its logs directory we can access it from our file system within the logs directory. When we add a new dag to the dags folder it will automatically be added in the containers dag bag and so on.
That's it for today, thank you for reading this story, I will be posting more soon. If you notice any mistakes, please let me know.
EDIT: “deploy:” is needed before “restart_policy:” as suggested by comments