Skip to main content
Buildr automatically generates Dockerfiles for containerizing Python applications. This guide explains how the auto-generation works and current limitations regarding custom Dockerfiles.

Default Auto-Generated Dockerfile

Buildr creates a simple Dockerfile based on your PackageOptions configuration:
FROM python:{version}-alpine

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

CMD python /app/{exec_file}
The template uses:
  • {version}: Value from PackageOptions.py_version (default: 3)
  • {exec_file}: Your script’s filename
Source reference: containerize.py:38-45

How It Works

When you decorate a function with @Containerize:
1

Container detection

Buildr checks if it’s already running inside a container using /proc/1/cgroup and the METAPARTICLE_IN_CONTAINER environment variable.
2

Dockerfile generation

If not in a container, Buildr calls write_dockerfile() to generate the Dockerfile with your configured Python version and script name.
3

Image build

The DockerBuilder uses the Docker API to build the image from the generated Dockerfile.
4

Optional publish

If publish=True, the image is pushed to the container registry.
5

Container execution

The runner starts a container from the built image.

Limitations of Auto-Generation

The default Dockerfile works well for simple applications but has limitations:
  • Uses Alpine Linux (small but may have compatibility issues with some Python packages)
  • Installs all dependencies from requirements.txt without layer caching optimization
  • No support for system packages (apt, apk)
  • No multi-stage builds
  • No custom build arguments or environment variables
  • No custom base images
  • Requires requirements.txt at project root

Custom Dockerfile Support

Current Limitation: Custom Dockerfiles are not currently supported in Buildr. While the PackageOptions constructor signature includes a dockerfile parameter, this field is not included in the PackageOptions namedtuple definition (line 42 of option.py), so the value is lost immediately after being passed.The write_dockerfile function attempts to check for a custom dockerfile using hasattr(package, 'dockerfile'), but since the attribute was never stored, this check always fails and the default Dockerfile is always generated.
Source reference: option.py:42-47, containerize.py:33-36

Workarounds

If you need features beyond the auto-generated Dockerfile, consider these alternatives:

Option 1: Pre-build Your Image

Build your Docker image separately, then reference it directly:
# Build with your custom Dockerfile
docker build -t myusername/my-app:latest -f Dockerfile.custom .

# Push to registry
docker push myusername/my-app:latest

# Run without Buildr's build step
docker run -p 8080:8080 myusername/my-app:latest

Option 2: Modify Generated Dockerfile

You could manually edit the generated Dockerfile after Buildr creates it, but this is not recommended as it will be overwritten on the next run.

Option 3: Fork and Modify Buildr

To properly support custom Dockerfiles, you would need to:
  1. Add dockerfile to the PackageOptions namedtuple fields
  2. Modify write_dockerfile to properly handle the custom dockerfile path
  3. Test the changes
Example fix needed in option.py:
# Current (broken)
class PackageOptions(namedtuple('Package', 'repository name builder publish verbose quiet py_version')):
    def __new__(cls, repository, name, builder='docker', publish=False, verbose=True, quiet=False, py_version=3, dockerfile=None):
        # dockerfile parameter is accepted but not stored!
        return super(PackageOptions, cls).__new__(cls, repository, name, builder, publish, verbose, quiet, py_version)

# Fixed version (not in current source)
class PackageOptions(namedtuple('Package', 'repository name builder publish verbose quiet py_version dockerfile')):
    def __new__(cls, repository, name=None, builder='docker', publish=False, verbose=True, quiet=False, py_version=3, dockerfile=None):
        return super(PackageOptions, cls).__new__(cls, repository, name, builder, publish, verbose, quiet, py_version, dockerfile)

Optimizing the Auto-Generated Dockerfile

While you can’t replace the Dockerfile, you can optimize your project structure to work better with the auto-generated one:

Use requirements.txt Efficiently

The auto-generated Dockerfile copies all files first, then installs requirements. To minimize build time:
# requirements.txt - pin your versions for reproducible builds
flask==2.3.2
requests==2.31.0
gunicorn==21.2.0

Use .dockerignore

Create a .dockerignore file to exclude unnecessary files from the build context:
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.git
.gitignore
.vscode/
.idea/
*.md
tests/
.pytest_cache/
.coverage
*.log
node_modules/
This reduces build context size and speeds up builds.

Choose the Right Python Version

Configure py_version based on your needs:
from metaparticle_pkg import Containerize

@Containerize(
    package={
        'repository': 'myusername',
        'name': 'my-app',
        'py_version': 3  # Uses python:3-alpine base image
    }
)
def main():
    print("Running on Python 3.x Alpine")

if __name__ == '__main__':
    main()
Alpine-based images are smaller but may have compatibility issues with some Python packages that require compilation. If you encounter issues, you may need to pre-build with a custom Dockerfile outside of Buildr.

When Buildr’s Auto-Generation Works Well

Buildr’s auto-generated Dockerfiles are suitable for:
  • Simple Python applications with pure-Python dependencies
  • Microservices without system-level dependencies
  • Quick prototyping and development
  • Applications that work well on Alpine Linux
  • Projects with straightforward requirements.txt dependencies

When You Need More

If your application needs any of these, you’ll need to build images outside of Buildr:
  • System packages (postgresql-client, build tools, etc.)
  • Multi-stage builds for optimization
  • Non-Python base images
  • Custom build arguments
  • Compiled extensions with specific build requirements
  • Security hardening (non-root users, minimal base images)
  • Complex layer caching strategies

Example: Working Within Limitations

Here’s a complete example that maximizes what Buildr can do:
from metaparticle_pkg import Containerize

@Containerize(
    package={
        'repository': 'docker.io/myusername',
        'name': 'simple-api',
        'py_version': 3,
        'publish': True,
        'verbose': True  # See build output
    },
    runtime={
        'executor': 'docker',
        'ports': [8080]
    }
)
def main():
    from flask import Flask, jsonify
    
    app = Flask(__name__)
    
    @app.route('/health')
    def health():
        return jsonify({'status': 'healthy'})
    
    @app.route('/')
    def hello():
        return jsonify({'message': 'Hello from Buildr!'})
    
    app.run(host='0.0.0.0', port=8080)

if __name__ == '__main__':
    main()
requirements.txt:
flask==2.3.2
.dockerignore:
__pycache__
*.pyc
.git
.vscode
tests/
This setup works well within Buildr’s constraints and is suitable for simple containerized applications.

Next Steps

Basic Usage

Learn the fundamentals of the @Containerize decorator

Advanced Configuration

Explore PackageOptions and RuntimeOptions

Build docs developers (and LLMs) love