docker-compose
Docker Compose is a powerful tool for defining and running multi-container Docker applications. It allows you to orchestrate multiple containers, manage their configurations, and streamline development workflows using a single YAML file.
Let’s create a simple client-server application using two docker containers: one runs a Flask server and the other one runs a client that sends requests to the server. We will use docker compose to configure services, networks, and volumes and finally spin up the entire application stack.
Server
requirements.txt
Flask==3.1.1
server.py
# Flask Server
# python server.py
from flask import Flask, jsonify
app = Flask(__name__)
# http://127.0.0.1:5000
@app.route('/hello')
def hello():
return jsonify(message="Hello World! from the server")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug = True)
else:
pass
Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
EXPOSE 5000
CMD ["python", "server.py"]
Client
requirements.txt
requests==2.32.3
python-dotenv==1.0.1
client.py
# Client
# python client.py
import requests
import time
from dotenv import load_dotenv
import os
# Load environment variables
load_dotenv()
# Get server host from environment variable
SERVER_HOST = os.getenv('SERVER_HOST')
SERVER_URL = f'http://{SERVER_HOST}:5000/hello'
if __name__ == '__main__':
print("Client")
while True:
try:
response = requests.get(SERVER_URL)
print(f"Response from server: {response.json()['message']}", flush=True)
except requests.exceptions.RequestException as e:
print(f"Error connecting to server: {e}")
time.sleep(5)
else:
pass
Note that we use the parameter flush=True to forces the output to be written immediately, bypassing the buffer.
Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY client.py .
CMD ["python", "client.py"]
Also we need a .env file:
SERVER_HOST=127.0.0.1
In this way we can run client.py also locally and it will pick up this value SERVER_HOST and when we build with docker-compose we can inject another value. This is needed because otherwise the client will fail when running in a Docker container, as 127.0.0.1 refers to the client container itself.
Now we need a docker-compose.yml
services:
server:
build: ./server
container_name: server
ports:
- "5000:5000"
networks:
- app-network
client:
build: ./client
container_name: client
depends_on:
- server
environment:
- SERVER_HOST=server
networks:
- app-network
networks:
app-network:
driver: bridge
Note the SERVER_HOST injected value in client and the port mapping in the server.
You can find more information on Bridge network driver here.
Once everything is in place we can run docker-compose up. You will see that the images are build and the containers executed.
Then we can press CTRL+C to quit.
Outro
I hope the post was interesting and thank you for taking the time to read it. On my Medium you can find a more in depth story and on my Blogspot you can find the same post in italian. Let me know if you have any question and if you like the content that I create feel free to buy me a coffee.