Skip to main content

Overview

Buildr’s containerization process is designed to be completely transparent to your application code. The entire workflow is triggered by a simple decorator, but underneath it orchestrates Dockerfile generation, image building, and container execution.

Container Detection

The first critical step in the containerization process is determining whether the code is already running inside a container. This prevents infinite recursion where a containerized app tries to containerize itself.

Detection Algorithm

Buildr uses a multi-layered approach to detect container execution:
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
1

Environment Variable Check

First checks the METAPARTICLE_IN_CONTAINER environment variable:
  • true or 1 → Running in container
  • false or 0 → Not in container
  • Not set → Continue to next check
2

Cgroup Analysis

Reads /proc/1/cgroup to inspect the init process’s control group:
  • Contains docker → Docker container
  • Contains kubepods → Kubernetes pod
  • Otherwise → Not containerized
3

Fallback

If /proc/1/cgroup doesn’t exist (e.g., on macOS/Windows), assumes not in container
If is_in_docker_container() returns True, the decorated function executes normally without any containerization steps.

Dockerfile Generation

When containerization is needed, Buildr generates a Dockerfile optimized for Python applications:

Auto-Generated Dockerfile

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))

Dockerfile Structure

The auto-generated Dockerfile follows best practices:
FROM python:{version}-alpine
  • Uses official Python Alpine Linux images for minimal size
  • Version is controlled by py_version in package options (default: 3)
  • Alpine variant reduces image size significantly
COPY ./ /app/
  • Copies the entire current directory to /app/ in the container
  • Includes your application code and requirements.txt
  • Preserves directory structure
RUN pip install --no-cache -r /app/requirements.txt
  • Installs Python dependencies from requirements.txt
  • Uses --no-cache to reduce image size
  • Requires a requirements.txt file in your project root
CMD python /app/{exec_file}
  • Executes your Python script when the container starts
  • exec_file is automatically determined from sys.argv[0]
  • Runs as the container’s main process (PID 1)

Custom Dockerfile

You can provide your own Dockerfile for advanced use cases:
@Containerize(
    package={
        'name': 'custom-app',
        'repository': 'myrepo',
        'dockerfile': './custom.Dockerfile'
    }
)
def main():
    print('Using custom Dockerfile!')
When a custom Dockerfile is provided, Buildr copies it to ./Dockerfile and uses it for the build. This allows you to:
  • Use different base images
  • Add custom build steps
  • Include system dependencies
  • Optimize layer caching

Executable Path Resolution

Buildr automatically determines which Python file to execute in the container:
containerize.py:66-69
exec_file = sys.argv[0]
slash_ix = exec_file.find('/')
if slash_ix != -1:
    exec_file = exec_file[slash_ix:]
When you run python app.py:
  • sys.argv[0] = app.py
  • No slash found, uses app.py as-is
  • Container CMD: python /app/app.py

The Wrapper Function

The @Containerize decorator wraps your function to intercept execution:
containerize.py:62-85
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

Execution Flow

1

Container Check

First line checks if already in container - if yes, calls original function
2

Path Resolution

Determines the executable file path for the Dockerfile CMD
3

Dockerfile Creation

Generates or copies the Dockerfile to current directory
4

Image Build

Delegates to the builder to create the container image
5

Optional Publish

If configured, pushes the image to the registry
6

Signal Handling

Sets up Ctrl+C handler for graceful shutdown
7

Container Run

Starts the container with the runner
8

Log Streaming

Streams container output to console and returns

Signal Handling

Buildr provides graceful shutdown when you press Ctrl+C:
containerize.py:77-80
def signal_handler(signal, frame):
    self.runner.cancel(self.package.name)
    sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
When SIGINT is received:
  1. Runner’s cancel() method is called to stop the container
  2. Python process exits cleanly with code 0
The signal handler is registered before the container starts, ensuring you can always interrupt the containerization process.

Complete Example

Here’s how all the pieces work together:
example.py
from metaparticle_pkg import Containerize
import time

@Containerize(
    package={'name': 'hello-world', 'repository': 'myrepo'},
    runtime={'ports': [8080]}
)
def main():
    print('Hello from container!')
    for i in range(10):
        print(f'Tick {i}')
        time.sleep(1)

if __name__ == '__main__':
    main()

Execution Trace

$ python example.py

# Buildr detects: not in container
# Generates Dockerfile:
FROM python:3-alpine
COPY ./ /app/
RUN pip install --no-cache -r /app/requirements.txt
CMD python /app/example.py

# Builds image: myrepo/hello-world:latest
# Starts container with port 8080 exposed
# Streams logs:
Hello from container!
Tick 0
Tick 1
...

Best Practices

Always Include requirements.txt

The auto-generated Dockerfile expects requirements.txt in your project root. Include all dependencies, even if just:
# Empty requirements.txt

Use .dockerignore

Exclude unnecessary files to speed up builds:
__pycache__
*.pyc
.git
.env

Pin Python Version

Specify py_version for reproducible builds:
package={'py_version': 3.9}

Test Locally First

Run your code without containerization before adding the decorator to catch issues early

Next Steps

Builders

Learn how DockerBuilder creates images

Runners

Explore execution environments

Build docs developers (and LLMs) love