Docker Compose

Docker Compose simplifies managing multi-container applications by allowing you to define and run them together using a single YAML configuration file.

Below is an example of a docker compose file to run a flask APP, it is used in the project Rest Flask Template.

networks:
  local_flask_network:
    driver: bridge


x-app-service: &app-service
  image: flask-app:latest
  environment:
    FLASK_ENV: ${FLASK_ENV}
    EMAIL_SECRET_KEY: ${EMAIL_SECRET_KEY}
    TWO_FACTOR_AUTH_SECRET_KEY: ${TWO_FACTOR_AUTH_SECRET_KEY}
    JWT_SECRET_KEY: ${JWT_SECRET_KEY}
    MAIL_SERVER: ${MAIL_SERVER}
    MAIL_PORT: ${MAIL_PORT}
    MAIL_USERNAME: ${MAIL_USERNAME}
    MAIL_PASSWORD: ${MAIL_PASSWORD}
    MAIL_DEFAULT_SENDER: ${MAIL_DEFAULT_SENDER}
    DATABASE_URI: "postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB}"
    CELERY_BROKER_URL: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@rabbit:5672//"


services:

  flask-api:
    <<: *app-service
    ports:
      - "5000:5000"
    networks:
      - local_flask_network
    depends_on:
      - database
      - database-migration
      - celery

  celery:
    <<: *app-service
    command: >
      sh -c "
        mkdir -p /home/appuser/celery_data &
        celery -A rest_flask_template.run_celery.celery beat -s /home/appuser/celery_data/celerybeat-schedule --loglevel=info &
        celery -A rest_flask_template.run_celery.celery worker --loglevel=info &
        tail -f /dev/null;
      "
    networks:
      - local_flask_network
    depends_on:
      - database
      - rabbit
    volumes:
      - celery_data:/home/appuser/celery_data


  database-migration:
    <<: *app-service
    command: >
      sh -c "
        python -m rest_flask_template.manage db_upgrade &
        python -m rest_flask_template.manage db_check;
      "
    networks:
      - local_flask_network
    depends_on:
      - database

  # Default port number used by Postgres: 5432
  database:
    image: "postgres:17-bookworm"
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_DB: ${POSTGRES_DB}
    networks:
      - local_flask_network
    volumes:
      - database_data:/var/lib/postgresql/data

  # Default port number used by RabbitMQ: 5672
  rabbit:
    image: "rabbitmq:4.0"
    environment:
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS}
    networks:
      - local_flask_network

# Docker manage these volume data
volumes:
  database_data:
  celery_data:

There are multiple interesting features used:

  • The services are all part of a same bridge network local_flask_network

    • This allows them to easily communicate between each other (the name of the services can be directly used in the URLs and there is no need to map ports).

    • This also prevent port conflicts with ports already used by the HOST machine.

    • networks can be configured externally by adding the option external: true, it means you’ll have to create them manually before running the docker compose up command.

    • A commonly used network mode used is the network_mode: "host" (in this case, ports don’t need to be mapped with HOST because the services shared the same network as the HOST machine)

    • In our case, because we use the bridge network, we map the port 5000 of service flask-api with the HOST port 5000 (- "<host_port_number>:<container_port_number>")

  • This docker compose uses environment variables to avoid setting values directly in the docker compose file. Careful as if you want to use the automatically loaded .env next to the docker compose file (or using the command argument --env-file env_vars.env), the environment variables set in the HOST machine take precedence over the variables set in the env file!! docker compose will take the values in the .env file only if the variable is unset in the HOST.

  • We use the Anchors and Aliases feature of yaml (&, * :code:` <<:) combined with the `extension (x-) feature of docker compose to avoid duplicating code.

  • expose is only informative, so we are not using it, the services don’t need it to be able to communicate between each other.

  • In this example, we let docker create the volumes for us, but we can also add external: true in the docker compose and create them manually before running the docker compose up command

Using Multiple docker compose files

Docker compose provides multiple option to customize a Compose application for different environments or workflows: https://docs.docker.com/compose/how-tos/multiple-compose-files/ We can extend or include a docker file directly in the docker compose file or we can also merge multiple docker compose file with the -f argument.

Below is an example showing the merge method to add a service only when necessary (only in dev environement in this case). Considering the previous docker-compose.yml, we can create another docker compose file docker-compose-dev.yml as below:

services:
  smtp-mail-server:
    image: python:3.12-slim-bookworm
    environment:
      PYTHONUNBUFFERED: 1
    command: >
      sh -c "
        pip install aiosmtpd;
        python -m aiosmtpd -n --listen 0.0.0.0:${MAIL_PORT} --debug;
      "
    networks:
      - local_flask_network

We can now run our app with the command:

docker compose -f docker-compose.yml -f docker-compose-dev.yml up

Note

The rules used to merge the files are described here: https://docs.docker.com/reference/compose-file/merge/


Sources: