Packaging a log collector CLI

Table of Contents

I have a small log collector CLI and I want to package it for different environments. Some teams run Alpine, some run Debian, and the security team insists on distroless for anything facing the internet. The script is the same everywhere — only the base image changes.

Here is my collector — it scans a directory for .log files and prints a JSON summary:

"""Scan a directory for .log files and print a JSON summary."""
import json
import sys
from pathlib import Path

log_dir = Path(sys.argv[1])
summary = [
    {"file": p.name, "lines": len(p.read_text().splitlines())}
    for p in sorted(log_dir.glob("*.log"))
]
print(json.dumps(summary))

And some sample log files to test with:

2026-04-01 INFO  application started
2026-04-01 INFO  listening on port 8080
2026-04-01 INFO  ready
2026-04-01 ERROR connection refused
2026-04-01 ERROR retry failed

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. Each <<...>> reference is defined in the sections below:

from typing import Annotated

import dagger
from dagger import DefaultPath, dag, function, object_type


@object_type
class LogCollector:
    @function
    async def collect_on_alpine(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
        """Run the log collector on Alpine."""
        return await (
            dag.lib().alpine_python_user_venv()
            .with_file('/app/collector.py', src.file('collector.py'))
            .with_directory('/app/logs', src.directory('logs'))
            .with_exec(['python3', 'collector.py', 'logs'])
            .stdout()
        )
    @function
    async def collect_on_debian(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
        """Run the log collector on Debian."""
        return await (
            dag.lib().debian_python_user_venv()
            .with_file('/app/collector.py', src.file('collector.py'))
            .with_directory('/app/logs', src.directory('logs'))
            .with_exec(['python3', 'collector.py', 'logs'])
            .stdout()
        )
    @function
    async def collect_on_distroless(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
        """Run the log collector on distroless."""
        return await (
            dag.lib().distroless_python3_debian()
            .with_file('/app/collector.py', src.file('collector.py'))
            .with_directory('/app/logs', src.directory('logs'))
            .with_exec(['python3', '/app/collector.py', '/app/logs'])
            .stdout()
        )

1. Collecting on Alpine

Alpine is the go-to lightweight base. I inject my script and log files, then run the collector:

@function
async def collect_on_alpine(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
    """Run the log collector on Alpine."""
    return await (
        dag.lib().alpine_python_user_venv()
        .with_file('/app/collector.py', src.file('collector.py'))
        .with_directory('/app/logs', src.directory('logs'))
        .with_exec(['python3', 'collector.py', 'logs'])
        .stdout()
    )
dagger call collect-on-alpine

2. Collecting on Debian

Some dependencies need native libraries that are easier to get on Debian. Same script, different base:

@function
async def collect_on_debian(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
    """Run the log collector on Debian."""
    return await (
        dag.lib().debian_python_user_venv()
        .with_file('/app/collector.py', src.file('collector.py'))
        .with_directory('/app/logs', src.directory('logs'))
        .with_exec(['python3', 'collector.py', 'logs'])
        .stdout()
    )
dagger call collect-on-debian

3. Collecting on distroless

Distroless has no shell and no package manager — only the Python interpreter. I use absolute paths and call Python directly:

@function
async def collect_on_distroless(self, src: Annotated[dagger.Directory, DefaultPath(".")]) -> str:
    """Run the log collector on distroless."""
    return await (
        dag.lib().distroless_python3_debian()
        .with_file('/app/collector.py', src.file('collector.py'))
        .with_directory('/app/logs', src.directory('logs'))
        .with_exec(['python3', '/app/collector.py', '/app/logs'])
        .stdout()
    )
dagger call collect-on-distroless

Author: root

Created: 2026-04-18 Sat 21:17

Validate