Shipping a webhook receiver
Table of Contents
I want to build a webhook receiver that listens for GitHub events and triggers deployments. It's a small Flask app — a single file with a few routes. I need to package it in a container, and I don't want to write the venv setup from scratch every time I start a new microservice like this.
Here is my app:
from flask import Flask
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
return "deployment triggered\n"
@app.route("/health")
def health():
return "ok\n"
First I set up my Dagger project and install daggerlib:
dagger init --sdk=python
dagger install github.com/Konubinix/daggerlib
The following is only for testing purposes — it makes the project use the local daggerlib checkout instead of the published version:
sed -i '/pin/d; s|"github.com/Konubinix/daggerlib@main"|"../.."|; s|"\.\./\.\.",|"../.."|' dagger.json
Then I write my Dagger module. The <<...>> references below are
filled in by the function definitions that follow — each section
defines one function and shows what it does when called:
from typing import Annotated
import dagger
from dagger import DefaultPath, dag, function, object_type
@object_type
class WebhookReceiver:
@function
async def flask_routes(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
"""Verify the Flask app loads and exposes the expected routes."""
return await (
dag.lib().alpine_python_user_venv(pip_packages=['flask'])
.with_file('/app/app.py', src.file('app.py'))
.with_exec(['python3', '-c', "from app import app; print(sorted(r.rule for r in app.url_map.iter_rules() if r.rule != '/static/<path:filename>'))"])
.stdout()
)
@function
async def gunicorn_check(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
"""Verify gunicorn can find the Flask app."""
return await (
dag.lib().alpine_python_user_venv(pip_packages=['flask', 'gunicorn'])
.with_file('/app/app.py', src.file('app.py'))
.with_exec(['gunicorn', '--check-config', 'app:app'])
.with_exec(['sh', '-c', 'echo gunicorn config ok'])
.stdout()
)
1. Packaging the app in a container
I write a Dagger function that gets a Python container with Flask installed, injects my app, and verifies it loads:
@function
async def flask_routes(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
"""Verify the Flask app loads and exposes the expected routes."""
return await (
dag.lib().alpine_python_user_venv(pip_packages=['flask'])
.with_file('/app/app.py', src.file('app.py'))
.with_exec(['python3', '-c', "from app import app; print(sorted(r.rule for r in app.url_map.iter_rules() if r.rule != '/static/<path:filename>'))"])
.stdout()
)
dagger call flask-routes
2. Running with gunicorn in production
In production, I don't use the Flask dev server — I need gunicorn
as the WSGI server. I add it to the same container and verify it
can find my app:
@function
async def gunicorn_check(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
"""Verify gunicorn can find the Flask app."""
return await (
dag.lib().alpine_python_user_venv(pip_packages=['flask', 'gunicorn'])
.with_file('/app/app.py', src.file('app.py'))
.with_exec(['gunicorn', '--check-config', 'app:app'])
.with_exec(['sh', '-c', 'echo gunicorn config ok'])
.stdout()
)
dagger call gunicorn-check