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

Author: root

Created: 2026-04-18 Sat 21:17

Validate