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
Dockerfiletailored for ARM64, using best practices for efficiency and size. - Native Build: Leveraging
nerdctlwithin 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 withcontainerd. 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 forarm64v8/aarch64. This ensures that whennerdctlpulls 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:
- Prepare Application Code: Create the Flask API and its dependency list.
- Define Image Blueprint: Write a
Dockerfilespecifying the build process. - Build the Image: Use
nerdctlwithin the container machine to construct the ARM64 OCI image. - Run and Expose Container: Launch a container from the new image and map ports for host access.
- 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.
Explanation:
- macOS Project Directory: Your application code resides here.
- Volume Mount to VM: The project directory is shared with the Linux container machine (VM) using
VirtioFS(as set up in Chapter 3). - Container Machine VM: This is where
nerdctlandcontainerdoperate, providing the ARM64 Linux environment. - Dockerfile: The instruction set for building the image, located in your project directory.
nerdctl build: Executes theDockerfilesteps within the VM.- ARM64 OCI Image: The resulting native ARM64 image, stored in the VM’s
containerdimage store. nerdctl run: Launches a container from the built image.- Running ARM64 Container: The Flask application is now executing natively within its isolated container.
- 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 sshOnce 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-appNow, 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/healthfor 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. Theslim-bookwormtag provides a minimal Debian-based environment, reducing image size. Official Python images are multi-architecture, sonerdctlon an ARM64 host (our Colima VM) will automatically select thearm64v8variant.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 ifrequirements.txtdoesn’t change,nerdctlcan 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.,psycopg2for 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 upaptcaches 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.nerdctlwill look for theDockerfileand any files referenced byCOPYcommands 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.0Explanation:
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 port5000on the container machine (the Colima/Lima VM) to port5000inside the container. Because Colima/Lima typically expose the VM’s network to your macOS host, this means you can accesslocalhost:5000directly 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 imagesYou 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/arm64Next, verify that your flask-app-dev container is active:
nerdctl psYou 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-dev2. 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/healthExpected 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: Whileslimbase 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
slimPython 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.0orlatesttags. Implement semantic versioning (v1.2.3), incorporate Git commit SHAs, or use build numbers for better traceability and easier rollbacks. .dockerignoreBest Practice: For any real-world application, a.dockerignorefile is essential. This file prevents unnecessary local development artifacts (like.gitdirectories, virtual environments, editor configuration files, ornode_modulesif 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-bookwormnot 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.comfrom inside the VM).
exec /usr/local/bin/python: no such file or directoryor similar duringCMD:- Cause: The command specified in
CMDorENTRYPOINTin yourDockerfileis incorrect, or the executable isn’t in the container’sPATH. - Solution: Carefully review your
CMDinstruction. Ensureflaskis correctly installed and its executable path is known to the container (typically/usr/local/binis in thePATHfor Python images). You can debug by runningnerdctl run -it --rm my-flask-api:1.0.0 bashand then trying to execute the command manually.
- Cause: The command specified in
curl: (7) Failed to connect to localhost port 5000: Connection refusedon 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:
- Verify container status: Inside your VM, run
nerdctl psto ensureflask-app-devisUp. - Check port mapping: Confirm
nerdctl run -p 5000:5000was used correctly. - App listening address: Ensure
app.run(host='0.0.0.0', port=5000)is in yourapp.py. - Firewall: Temporarily disable any macOS host firewall or ensure port 5000 is explicitly allowed.
- 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.
- Verify container status: Inside your VM, run
- 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
- Slow build times or unexpected
x86_64architecture:- Cause: This typically happens if the base image selected is
x86_64only, or if you’re explicitly building for a different platform. - Solution: Ensure your base image is multi-architecture, allowing
nerdctlon an ARM64 host to automatically pull the correctarm64v8variant. If you intended to build foramd64(e.g., for deployment to an x86 cloud), you would add--platform=linux/amd64tonerdctl build, but for local ARM64 development, this is generally undesirable due to emulation overhead.
- Cause: This typically happens if the base image selected is
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
Dockerfilespecifically designed for building efficient ARM64 OCI images. - Utilized
nerdctlwithin 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.