Building container images that run natively on your Apple Silicon Mac is a critical step for achieving optimal performance in your local development environment. When you target the ARM64 architecture, you bypass the overhead of Rosetta 2 emulation, leading to faster build times, quicker container startup, and more responsive applications. This chapter guides you through creating a simple Python Flask API, defining its Dockerfile, and building an optimized ARM64 OCI (Open Container Initiative) image using nerdctl within your dedicated container machine.

By the end of this chapter, you will have a fully functional ARM64 container image of your sample application. This image serves as the performant foundation for orchestrating more complex multi-service applications in subsequent chapters, ensuring your development environment truly leverages the power of Apple Silicon.

Project Overview: Packaging for Native Performance

The core objective of this chapter is to take a basic web application and package it into an OCI-compliant container image that runs natively on ARM64 architecture. This is more than just putting code in a container; it’s about optimizing for the specific hardware of your Apple Silicon Mac.

We will focus on:

  • Application Development: A simple Python Flask REST API.
  • Image Definition: A Dockerfile tailored for ARM64, using best practices for efficiency and size.
  • Native Build: Leveraging nerdctl within your Colima/Lima VM to produce a pure ARM64 image.
  • Verification: Confirming the image is ARM64 and the container runs correctly and is accessible.

Tech Stack: Choices for an ARM64-Native Workflow

To achieve our goal of a natively performing container, specific technology choices are made:

  • Python Flask: A lightweight and widely used Python web framework, ideal for a simple API example. It’s easy to containerize and demonstrates common web application patterns.
  • Dockerfile: The industry standard for defining how to build a container image. Its declarative nature ensures reproducibility and version control for our image builds.
  • nerdctl: An OCI-compatible CLI that works with containerd. Chosen for its lightweight nature and direct integration into our Colima/Lima-based container machine. It provides a familiar Docker-like experience for building and running images.
  • ARM64 Linux Base Images: Crucially, we select base images (like python:3.11-slim-bookworm) that are multi-architecture or explicitly tagged for arm64v8/aarch64. This ensures that when nerdctl pulls the image on our ARM64 VM, it gets the correct native variant, avoiding any performance penalties from emulation.

Milestones: Building Your First Native Image

This chapter is structured around a clear set of milestones to progressively build and verify our ARM64 container image:

  1. Prepare Application Code: Create the Flask API and its dependency list.
  2. Define Image Blueprint: Write a Dockerfile specifying the build process.
  3. Build the Image: Use nerdctl within the container machine to construct the ARM64 OCI image.
  4. Run and Expose Container: Launch a container from the new image and map ports for host access.
  5. Validate Functionality: Test the running API from your macOS host.

Architecture: The Image Build Flow

The Dockerfile acts as our architectural blueprint for the container image. It outlines the sequence of operations that transform our application code into a runnable container.

flowchart TD A[macOS Project] --> B[VM Environment] B --> C[Dockerfile] C --> D[Build OCI Image] D --> E[Run Container] E --> F[Exposed Port]

Explanation:

  1. macOS Project Directory: Your application code resides here.
  2. Volume Mount to VM: The project directory is shared with the Linux container machine (VM) using VirtioFS (as set up in Chapter 3).
  3. Container Machine VM: This is where nerdctl and containerd operate, providing the ARM64 Linux environment.
  4. Dockerfile: The instruction set for building the image, located in your project directory.
  5. nerdctl build: Executes the Dockerfile steps within the VM.
  6. ARM64 OCI Image: The resulting native ARM64 image, stored in the VM’s containerd image store.
  7. nerdctl run: Launches a container from the built image.
  8. Running ARM64 Container: The Flask application is now executing natively within its isolated container.
  9. Exposed Port to macOS: The container’s port (e.g., 5000) is mapped through the VM’s network to your macOS host, allowing direct access.

Step-by-Step Implementation

We’ll begin by setting up our application files and then proceed to build the container image.

First, ensure your container machine (e.g., Colima) is running and you have an SSH session into it.

colima start --cpu 4 --memory 8 --disk 100 # Adjust resources as needed, ensure it's running.
colima ssh

Once inside the container machine, navigate to your project directory. This should be the same path as on your macOS host if you configured volume mounting in Chapter 3. For example, if your project on macOS is at ~/projects/my-flask-app and mounted to /Users/youruser/projects/my-flask-app inside the VM:

cd /Users/youruser/projects/my-flask-app

Now, let’s create the application files.

1. Create the Sample Flask Application

We’ll create a minimal Flask application.

File: my-flask-app/app.py

Create this file in your my-flask-app directory.

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def hello_world():
    """Returns a simple greeting message."""
    return jsonify(message="Hello from ARM64 Flask Container!")

@app.route('/health')
def health_check():
    """Returns a health status for the service."""
    return jsonify(status="ok")

if __name__ == '__main__':
    # Listen on all available network interfaces on port 5000
    # This is crucial for accessibility from outside the container.
    app.run(host='0.0.0.0', port=5000)

Explanation:

  • This standard Flask application defines two API endpoints: / for a general greeting and /health for basic service status.
  • app.run(host='0.0.0.0', port=5000) configures the Flask development server to listen on all network interfaces within the container, making it reachable when we map ports.

File: my-flask-app/requirements.txt

Create this file in the same directory.

Flask==3.0.3 # As of 2026-06-22, this is a recent stable version.

Explanation:

  • This file lists the Python packages required by our application. Pinning the version (Flask==3.0.3) ensures reproducible builds.
    • ⚡ Quick Note: While Flask 3.0.3 is current as of 2026-06-22, always check the official Flask documentation for the absolute latest stable release if building a new production application.

2. Create the Dockerfile

Next, we define how to build our container image. Create Dockerfile in the root of your my-flask-app directory.

File: my-flask-app/Dockerfile

# Stage 1: Build the application
# Use a multi-arch Python base image. 'python:3.11-slim-bookworm' will automatically
# pull the ARM64 variant when built on an ARM64 host like our Colima VM.
# Python 3.11 is a stable, widely used version. Newer versions like 3.12+ are also available.
FROM python:3.11-slim-bookworm AS builder

# Set the working directory inside the container. All subsequent commands will run here.
WORKDIR /app

# Copy the requirements file first. This optimizes Docker's layer caching.
# If requirements.txt doesn't change, this layer and the 'pip install' step can be cached,
# significantly speeding up subsequent builds.
COPY requirements.txt .

# Install Python dependencies and clean up build artifacts.
# 'gcc' and 'build-essential' are installed temporarily to compile any Python packages
# that include C extensions. They are then purged to keep the final image small.
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc build-essential && \
    pip install --no-cache-dir -r requirements.txt && \
    apt-get purge -y gcc build-essential && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Copy the application code into the working directory.
COPY app.py .

# Expose port 5000. This is documentation for users of the image; actual port mapping
# happens during 'nerdctl run'.
EXPOSE 5000

# Set the default command to run the application when the container starts.
# '0.0.0.0' ensures the Flask app is accessible from external connections to the container.
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"]

Explanation of Dockerfile Instructions:

  • FROM python:3.11-slim-bookworm AS builder: Specifies the base image. The slim-bookworm tag provides a minimal Debian-based environment, reducing image size. Official Python images are multi-architecture, so nerdctl on an ARM64 host (our Colima VM) will automatically select the arm64v8 variant.
  • WORKDIR /app: Defines the default directory for subsequent instructions within the container.
  • COPY requirements.txt .: Copies the dependency list. This step is placed early because if requirements.txt doesn’t change, nerdctl can reuse the cached layer for dependency installation, saving significant build time.
  • RUN apt-get update ...: This multi-line command handles dependency installation:
    • apt-get update: Refreshes the package lists.
    • apt-get install -y --no-install-recommends gcc build-essential: Installs essential build tools. Many Python packages with C extensions (e.g., psycopg2 for PostgreSQL, which we might use later) require these to compile.
    • pip install --no-cache-dir -r requirements.txt: Installs Python packages, disabling pip’s cache to save space.
    • apt-get purge -y gcc build-essential: Removes the build tools after use, as they are not needed at runtime.
    • apt-get clean && rm -rf /var/lib/apt/lists/*: Cleans up apt caches and temporary files, further reducing the image size.
  • COPY app.py .: Copies your application code into the container.
  • EXPOSE 5000: Declares that the container listens on port 5000. This is primarily for documentation and network configuration awareness.
  • CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"]: Sets the default command to execute when a container starts from this image. The Flask server will listen on port 5000, accessible from anywhere within the container’s network.

3. Build the ARM64 OCI Image with nerdctl

With the Dockerfile in place, we can now build the image. Ensure you are still in your container machine’s shell, inside the my-flask-app directory.

nerdctl build -t my-flask-api:1.0.0 .

Explanation:

  • nerdctl build: The command to initiate an image build.
  • -t my-flask-api:1.0.0: Assigns a name (my-flask-api) and a tag (1.0.0) to the resulting image. This is crucial for referencing the image later.
  • .: Specifies the “build context” as the current directory. nerdctl will look for the Dockerfile and any files referenced by COPY commands within this directory.

The build process will download the base image (the ARM64 variant, thanks to your VM’s architecture), execute each Dockerfile instruction, and create intermediate layers.

4. Run the Containerized Application

Once the image is successfully built, you can launch a container from it and map its internal port to a port on your container machine (VM). Colima/Lima handle the networking so that the VM’s ports are typically accessible from your macOS host.

Still in your container machine’s shell:

nerdctl run -d -p 5000:5000 --name flask-app-dev my-flask-api:1.0.0

Explanation:

  • nerdctl run: The command to create and start a new container.
  • -d: Runs the container in “detached” mode, meaning it runs in the background and doesn’t tie up your terminal.
  • -p 5000:5000: This is the crucial port mapping. It maps port 5000 on the container machine (the Colima/Lima VM) to port 5000 inside the container. Because Colima/Lima typically expose the VM’s network to your macOS host, this means you can access localhost:5000 directly from your macOS browser or terminal.
  • --name flask-app-dev: Assigns a user-friendly name to this specific container instance, making it easier to identify and manage.
  • my-flask-api:1.0.0: Specifies the image (by name and tag) from which to create the container.

Testing & Verification

After building and running your container, it’s essential to verify that everything is functioning correctly.

1. Verify Image and Container Status within the VM

First, let’s confirm your image was created and the container is running inside your Colima/Lima VM.

From your container machine’s shell:

nerdctl images

You should see your my-flask-api image listed, with linux/arm64 as its platform:

REPOSITORY         TAG       IMAGE ID        CREATED              SIZE        PLATFORMS
my-flask-api       1.0.0     <image_id>      About a minute ago   <size>      linux/arm64
python             3.11-slim-bookworm   <image_id>      2 weeks ago          <size>      linux/arm64

Next, verify that your flask-app-dev container is active:

nerdctl ps

You should see your container listed with a STATUS of Up:

CONTAINER ID    IMAGE                  COMMAND                   CREATED          STATUS         PORTS                   NAMES
<container_id>  my-flask-api:1.0.0     "flask run --host=..."    5 seconds ago    Up 4 seconds   0.0.0.0:5000->5000/tcp  flask-app-dev

2. Access the API from your macOS Host

Now, open a new terminal on your macOS host (do not use your Colima/Lima SSH session). We’ll use curl to interact with the Flask API, which should be accessible via localhost:5000.

To access the root endpoint:

curl http://localhost:5000/

Expected output:

{"message":"Hello from ARM64 Flask Container!"}

To access the health check endpoint:

curl http://localhost:5000/health

Expected output:

{"status":"ok"}

If you receive these JSON responses, congratulations! You have successfully built a native ARM64 OCI image and run it on your Apple Silicon Mac. The application is running efficiently inside your lightweight Linux container machine and is fully accessible from your macOS host.

Production Considerations

Building for local development is one thing; preparing for production requires additional rigor.

  • Image Optimization Beyond slim: While slim base images and build-time cleanup help, consider multi-stage builds more rigorously for complex applications. This separates build-time dependencies (compilers, SDKs) from runtime dependencies, resulting in even smaller and more secure final images.
  • Image Scanning: Integrate vulnerability scanning tools like Trivy or Clair into your CI/CD pipeline. Regularly scan your base images and final application images for known CVEs. The slim Python images generally have a smaller attack surface, but vulnerabilities can still exist in application dependencies.
  • Robust Tagging Strategy: For production deployments, move beyond simple 1.0.0 or latest tags. Implement semantic versioning (v1.2.3), incorporate Git commit SHAs, or use build numbers for better traceability and easier rollbacks.
  • .dockerignore Best Practice: For any real-world application, a .dockerignore file is essential. This file prevents unnecessary local development artifacts (like .git directories, virtual environments, editor configuration files, or node_modules if you were building a Node.js app) from being copied into the build context. This significantly speeds up build times and reduces the final image size.

Example: my-flask-app/.dockerignore

Create this file in your my-flask-app directory.

.git
.venv/
__pycache__/
*.pyc
*.log
.DS_Store
*.swp

⚡ Real-world insight: A well-crafted .dockerignore can prevent sensitive information from accidentally being included in your image and dramatically reduce build times by minimizing the data transferred to the Docker daemon (or nerdctl in our case).

Common Issues & Solutions

Even with careful planning, you might encounter issues. Here are some common pitfalls and their solutions:

  • docker.io/library/python:3.11-slim-bookworm not found (or similar image pull error):
    • Cause: A typo in the image name or tag, or the specific tag might have been deprecated or moved. It can also indicate a network connectivity issue within your Colima/Lima VM.
    • Solution: Double-check the exact tag on the official Python image page on Docker Hub. Verify your Colima/Lima VM has internet access (e.g., ping google.com from inside the VM).
  • exec /usr/local/bin/python: no such file or directory or similar during CMD:
    • Cause: The command specified in CMD or ENTRYPOINT in your Dockerfile is incorrect, or the executable isn’t in the container’s PATH.
    • Solution: Carefully review your CMD instruction. Ensure flask is correctly installed and its executable path is known to the container (typically /usr/local/bin is in the PATH for Python images). You can debug by running nerdctl run -it --rm my-flask-api:1.0.0 bash and then trying to execute the command manually.
  • curl: (7) Failed to connect to localhost port 5000: Connection refused on macOS:
    • Cause: This is a common networking issue. The container might not be running, the port mapping is incorrect, the Flask app isn’t listening on 0.0.0.0, or a firewall is blocking access.
    • Solution:
      1. Verify container status: Inside your VM, run nerdctl ps to ensure flask-app-dev is Up.
      2. Check port mapping: Confirm nerdctl run -p 5000:5000 was used correctly.
      3. App listening address: Ensure app.run(host='0.0.0.0', port=5000) is in your app.py.
      4. Firewall: Temporarily disable any macOS host firewall or ensure port 5000 is explicitly allowed.
      5. Colima/Lima Network: If you’ve customized Colima/Lima’s networking, double-check that the VM’s network is correctly configured to expose ports to the host.
  • Slow build times or unexpected x86_64 architecture:
    • Cause: This typically happens if the base image selected is x86_64 only, or if you’re explicitly building for a different platform.
    • Solution: Ensure your base image is multi-architecture, allowing nerdctl on an ARM64 host to automatically pull the correct arm64v8 variant. If you intended to build for amd64 (e.g., for deployment to an x86 cloud), you would add --platform=linux/amd64 to nerdctl build, but for local ARM64 development, this is generally undesirable due to emulation overhead.

Summary & Next Step

In this chapter, you’ve achieved a significant milestone in leveraging your Apple Silicon Mac for local container development. You’ve successfully:

  • Developed a simple Python Flask application.
  • Crafted an optimized Dockerfile specifically designed for building efficient ARM64 OCI images.
  • Utilized nerdctl within your Colima/Lima container machine to build and run your containerized application natively.
  • Confirmed the application’s functionality by accessing it directly from your macOS host.
  • Gained insights into production best practices for image optimization, security, and using .dockerignore.

This native ARM64 image is now a performant and verified building block. In the next chapter, we’ll expand on this by orchestrating multiple containerized services. We’ll introduce a PostgreSQL database, connect our Flask API to it, and manage these services together to simulate a more complex, real-world application environment directly on your Apple Silicon Mac.


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.

References