Skip to main content

How Buildr Works

Buildr uses a decorator pattern to transform your Python functions into containerized applications. When you decorate a function with @Containerize, Buildr intercepts the function call and orchestrates a complete build-and-run lifecycle.

Core Architecture

Buildr’s architecture consists of three main components:

Containerize

The main decorator that orchestrates the entire containerization workflow

Builder

Responsible for building container images from your code

Runner

Executes the containerized application in the target environment

The Decorator Pattern

Buildr leverages Python decorators to provide a seamless containerization experience. Here’s how it works:
containerize.py:48-86
class Containerize(object):

    def __init__(self, runtime={}, package={}):
        self.runtime = option.load(option.RuntimeOptions, runtime)
        self.package = option.load(option.PackageOptions, package)
        self.image = "{repo}/{name}:latest".format(
            repo=self.package.repository,
            name=self.package.name
        )

        self.builder = builder.select(self.package.builder)
        self.runner = runner.select(self.runtime.executor)

    def __call__(self, func):
        def wrapped(*args, **kwargs):
            if is_in_docker_container():
                return func(*args, **kwargs)

            exec_file = sys.argv[0]
            slash_ix = exec_file.find('/')
            if slash_ix != -1:
                exec_file = exec_file[slash_ix:]

            write_dockerfile(self.package, exec_file)
            self.builder.build(self.image)

            if self.package.publish:
                self.builder.publish(self.image)

            def signal_handler(signal, frame):
                self.runner.cancel(self.package.name)
                sys.exit(0)
            signal.signal(signal.SIGINT, signal_handler)

            self.runner.run(self.image, self.package.name, self.runtime)

            return self.runner.logs(self.package.name)
        return wrapped

Decorator Initialization

When you apply the @Containerize decorator, the __init__ method runs immediately:
  1. Load Options - Runtime and package options are validated and loaded
  2. Generate Image Name - Constructs the Docker image tag from repository and name
  3. Select Components - Initializes the appropriate builder and runner based on configuration
@Containerize(
    package={'name': 'my-app', 'repository': 'docker.io/myrepo'},
    runtime={'ports': [8080], 'executor': 'docker'}
)
def main():
    print('Hello from container!')

Build and Run Lifecycle

When your decorated function is called, Buildr executes a sophisticated lifecycle:
1

Container Detection

Buildr first checks if the code is already running inside a container using is_in_docker_container():
containerize.py:11-30
def is_in_docker_container():
    mp_in_container = os.getenv('METAPARTICLE_IN_CONTAINER', None)
    if mp_in_container in ['true', '1']:
        return True
    elif mp_in_container in ['false', '0']:
        return False

    try:
        with open('/proc/1/cgroup', 'r+t') as f:
            lines = f.read().splitlines()
            last_line = lines[-1]
            if 'docker' in last_line:
                return True
            elif 'kubepods' in last_line:
                return True
            else:
                return False

    except IOError:
        return False
If already in a container, the function executes normally and the lifecycle ends.
2

Dockerfile Generation

If not in a container, Buildr generates a Dockerfile tailored to your application:
containerize.py:33-45
def write_dockerfile(package, exec_file):
    if hasattr(package, 'dockerfile') and package.dockerfile is not None:
        shutil.copy(package.dockerfile, 'Dockerfile')
        return

    with open('Dockerfile', 'w+t') as f:
        f.write("""FROM python:{version}-alpine

COPY ./ /app/
RUN pip install --no-cache -r /app/requirements.txt

CMD python /app/{exec_file}
""".format(version=package.py_version, exec_file=exec_file))
You can provide a custom Dockerfile or use the auto-generated one.
3

Image Build

The selected builder (e.g., DockerBuilder) builds the container image:
containerize.py:72
self.builder.build(self.image)
4

Image Publish (Optional)

If publish: True is set, the image is pushed to the registry:
containerize.py:74-75
if self.package.publish:
    self.builder.publish(self.image)
5

Signal Handler Registration

Buildr sets up graceful shutdown handling:
containerize.py:77-80
def signal_handler(signal, frame):
    self.runner.cancel(self.package.name)
    sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
6

Container Execution

The selected runner starts the container:
containerize.py:82
self.runner.run(self.image, self.package.name, self.runtime)
7

Log Streaming

Finally, container logs are streamed to the console:
containerize.py:84
return self.runner.logs(self.package.name)

Component Selection

Buildr uses a factory pattern to select the appropriate builder and runner:
builder/__init__.py:4-7
def select(spec):
    if spec == 'docker':
        return DockerBuilder()
    raise Exception('Unknown spec {}'.format(spec))
Currently supports:
  • docker - Uses Docker API to build images (default)

Configuration System

Buildr uses named tuples for type-safe configuration:
option.py:21-25
class RuntimeOptions(namedtuple('Runtime', 'executor replicas ports public shardSpec jobSpec')):
    required_options = []

    def __new__(cls, executor='docker', replicas=0, ports=[], public=False, shardSpec=None, jobSpec=None):
        return super(RuntimeOptions, cls).__new__(cls, executor, replicas, ports, public, shardSpec, jobSpec)
  • executor: Runtime environment (docker/metaparticle)
  • replicas: Number of container replicas
  • ports: Port mappings for the container
  • public: Whether to expose the service publicly
  • shardSpec: Sharding configuration
  • jobSpec: Job execution configuration
option.py:42-47
class PackageOptions(namedtuple('Package', 'repository name builder publish verbose quiet py_version')):
    required_options = ['repository']

    def __new__(cls, repository, name, builder='docker', publish=False, verbose=True, quiet=False, py_version=3, dockerfile=None):
        name = name if name else os.path.basename(os.getcwd())
        return super(PackageOptions, cls).__new__(cls, repository, name, builder, publish, verbose, quiet, py_version)
  • repository: Required - Docker registry repository
  • name: Container name (defaults to current directory)
  • builder: Builder to use (default: ‘docker’)
  • publish: Whether to push to registry
  • py_version: Python version for base image (default: 3)
  • dockerfile: Path to custom Dockerfile (optional)

Usage Example

Here’s a complete example showing all components in action:
from metaparticle_pkg import Containerize
import time

@Containerize(
    package={
        'name': 'my-service',
        'repository': 'docker.io/myrepo',
        'publish': True
    },
    runtime={
        'ports': [8080],
        'executor': 'docker'
    }
)
def main():
    print('Running in container!')
    time.sleep(10)

if __name__ == '__main__':
    main()
When you run this script, Buildr will:
  1. Detect you’re not in a container
  2. Generate a Dockerfile
  3. Build the image as docker.io/myrepo/my-service:latest
  4. Push the image to the registry
  5. Run the container with port 8080 exposed
  6. Stream logs to your terminal

Next Steps

Containerization

Deep dive into the containerization process

Builders

Learn about the builder system

Runners

Explore different execution environments

Build docs developers (and LLMs) love