In the previous chapters, we established a solid foundation for container development on Apple Silicon: setting up Colima to leverage Virtualization.framework for a lightweight Linux VM, configuring efficient volume mounts, and building ARM64 OCI images. Now, we’ll integrate these components to run a practical, multi-service application locally.
This chapter focuses on orchestrating a common development pattern: a Python Flask API service interacting with a PostgreSQL database. You will learn to define these services, manage their lifecycle, ensure secure communication, and expose them to your macOS host. By the end, you’ll have a fully functional, containerized local development environment, making it ready for active development, testing, and even demonstrating your application.
Project Overview: A Local Multi-Service Stack
Building real-world applications often involves multiple interconnected services. Managing these services individually can quickly become cumbersome. This chapter tackles that complexity by guiding you through setting up a cohesive local development stack.
Our goal is to run a simple, yet representative, web application locally:
- A Python Flask API: This service will handle HTTP requests and interact with a database.
- A PostgreSQL Database: This service will store application data.
We will use podman-compose to define and orchestrate these services, allowing them to run, communicate, and persist data efficiently within our Colima-managed Linux VM. This setup mirrors a production deployment more closely than running services directly on macOS, offering better isolation and reproducibility.
Tech Stack Deep Dive
To achieve our multi-service orchestration, we’re leveraging a specific set of tools and technologies, each chosen for its performance and compatibility with Apple Silicon.
- Colima (v0.8.x): Our lightweight Linux VM manager, abstracting Apple’s
Virtualization.framework. It provides the ARM64 Linux environment where our containers will run. (Checked 2026-06-22) - Podman (v5.x): The OCI-compatible container engine running inside the Colima VM. It manages individual containers and networks. (Checked 2026-06-22)
podman-compose(v1.2.x): A Python-based tool that provides adocker-compose-like experience for Podman, allowing us to define and manage multi-container applications using acompose.yamlfile. (Checked 2026-06-22)- Python (v3.12): The programming language for our API service. We’ll use the latest stable version. (Checked 2026-06-22)
- Flask (v3.0.3): A micro web framework for Python, used to build our API. (Checked 2026-06-22)
- PostgreSQL (v16): The relational database for our application. We’ll use an official ARM64 Alpine Linux image for efficiency. (Checked 2026-06-22)
This combination ensures that all components run natively on your Apple Silicon Mac’s ARM64 architecture, avoiding performance penalties associated with x86-64 emulation.
Milestones and Build Plan
To build our multi-service local development environment, we’ll follow these incremental steps:
- Project Structure: Set up the basic directory layout for our Flask API and
compose.yaml. - API
Dockerfile: Define how to build our Python Flask application into an ARM64 OCI image. - API Application Code: Write the Flask application, including database connectivity logic.
compose.yamlDefinition: Create the orchestration file that describes our API and PostgreSQL services, their networks, volumes, and dependencies.podman-composeInstallation: Install the necessary tool to run ourcompose.yamlfile.- Service Startup: Launch the entire application stack within the Colima VM.
- Verification: Test connectivity and functionality of both the API and database services.
Each milestone builds upon the last, allowing you to verify functionality at every stage.
Service Architecture Overview
Our application consists of two main services, api and db, running within the Colima VM. podman-compose handles the internal networking, allowing services to communicate by their names. External access is provided by mapping ports from the VM to the macOS host.
Why this architecture matters:
- Encapsulation: Each service runs in its own isolated container, minimizing dependency conflicts and simplifying upgrades.
- Service Discovery:
podman-composecreates an internal network where services can find each other by their defined names (e.g.,apiconnects todb). - Data Persistence: A named volume ensures that our database’s data is not lost when containers are stopped or recreated.
- Development Workflow: Volume mounting (
./api:/app) allows for real-time code changes on your macOS host to reflect instantly in the running container, accelerating the development cycle. - Native Performance: By leveraging Colima on Apple Silicon, all containers run on ARM64 Linux, ensuring optimal CPU and memory usage without emulation overhead.
Step-by-Step Implementation
We’ll now create the necessary files and configure our environment to bring this multi-service stack to life.
1. Initialize Project Structure
Begin by creating a new root directory for your project and a subdirectory for the API service.
mkdir my_flask_app
cd my_flask_app
mkdir apiThis sets up my_flask_app/api for our Flask application code and my_flask_app/compose.yaml for orchestration.
2. Define the API Dockerfile
This Dockerfile specifies how to build an ARM64 OCI image for our Python Flask API. It’s designed for efficiency and native performance.
Create the file my_flask_app/api/Dockerfile:
# Use a lightweight ARM64 base image for Python 3.12
# As of 2026-06-22, Python 3.12 is the current stable release.
FROM python:3.12-alpine3.19-arm64v8
# Set the working directory inside the container
WORKDIR /app
# Copy the requirements file first to leverage Docker's build cache
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the entire application code
COPY . .
# Expose the port the Flask app will listen on
EXPOSE 5000
# Command to run the Flask application when the container starts
CMD ["python", "app.py"]Explanation of Decisions:
FROM python:3.12-alpine3.19-arm64v8: We choosealpinefor its small image size,3.12for the latest stable Python version, andarm64v8to ensure native execution on Apple Silicon. This avoids Rosetta 2 emulation.WORKDIR /app: Establishes/appas the base directory for subsequent commands within the container.COPY requirements.txt .andRUN pip install ...: By copying and installing dependencies before the rest of the application code, Docker can cache this layer. If only application code changes, the dependency installation step won’t need to rerun, speeding up subsequent builds.--no-cache-dir: Thispipflag preventspipfrom storing downloaded packages in a cache directory, which significantly reduces the final image size.COPY . .: Copies all files from the current directory on the host (which ismy_flask_app/api) to/appin the container.EXPOSE 5000: Declares that the container listens on port 5000. This is primarily documentation; port mapping happens incompose.yaml.CMD ["python", "app.py"]: Defines the default command to run the Flask application when the container starts.
3. Create API Requirements and Application Code
Next, we define the Python dependencies and the Flask application itself.
Create my_flask_app/api/requirements.txt:
Flask==3.0.3
psycopg2-binary==2.9.9Explanation:
Flask==3.0.3: Specifies the Flask web framework. Version3.0.3is stable as of 2026-06-22. Pinning versions ensures reproducible builds.psycopg2-binary==2.9.9: This is a pre-compiled binary distribution ofpsycopg2, a PostgreSQL adapter for Python. Using the binary version simplifies installation, especially in container environments, by avoiding the need forlibpqdevelopment headers. Version2.9.9is stable as of 2026-06-22.
Create my_flask_app/api/app.py:
import os
import time
import psycopg2
from flask import Flask, jsonify
app = Flask(__name__)
# Database connection details are loaded from environment variables.
# 'db' is the service name defined in compose.yaml for the PostgreSQL container.
DB_HOST = os.getenv('DB_HOST', 'db')
DB_NAME = os.getenv('DB_NAME', 'mydatabase')
DB_USER = os.getenv('DB_USER', 'myuser')
DB_PASSWORD = os.getenv('DB_PASSWORD', 'mypassword')
def get_db_connection():
"""Attempts to establish a PostgreSQL connection with retries."""
conn = None
retries = 10 # Increase retries for robustness during startup
while retries > 0:
try:
conn = psycopg2.connect(
host=DB_HOST,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD
)
print("Successfully connected to the database.")
return conn
except psycopg2.OperationalError as e:
print(f"Database connection failed: {e}. Retrying in 3 seconds ({retries-1} attempts left)...")
time.sleep(3)
retries -= 1
raise ConnectionError("Could not connect to the database after multiple retries. Exiting.")
@app.route('/')
def hello():
"""Simple health check endpoint."""
return jsonify({"message": "Hello from Flask API!", "status": "running"})
@app.route('/data')
def get_data():
"""Endpoint to interact with the database (create table, insert, fetch)."""
conn = None
cursor = None
try:
conn = get_db_connection()
cursor = conn.cursor()
# Create table if it doesn't exist
cursor.execute("CREATE TABLE IF NOT EXISTS messages (id SERIAL PRIMARY KEY, text VARCHAR(255));")
# Insert a new message
insert_text = f"Message from API at {time.strftime('%H:%M:%S')}"
cursor.execute("INSERT INTO messages (text) VALUES (%s);", (insert_text,))
conn.commit()
# Fetch the latest message
cursor.execute("SELECT text FROM messages ORDER BY id DESC LIMIT 1;")
message = cursor.fetchone()[0]
return jsonify({"database_message": message, "retrieved_at": time.strftime('%H:%M:%S')})
except ConnectionError as ce:
print(f"Application failed to connect to DB: {ce}")
return jsonify({"error": f"Database connection error: {ce}"}), 500
except Exception as e:
print(f"An unexpected error occurred: {e}")
return jsonify({"error": f"An API error occurred: {e}"}), 500
finally:
if cursor:
cursor.close()
if conn:
conn.close()
if __name__ == '__main__':
# Run Flask app, accessible from any IP (0.0.0.0) on port 5000.
# debug=True enables auto-reloading on code changes (useful with volume mounts).
app.run(host='0.0.0.0', port=5000, debug=True)Explanation of Decisions:
- Environment Variables: Database credentials and host are loaded from environment variables (
os.getenv). This is a best practice for containerized applications, allowing configuration to be externalized from the code. - Service Name Resolution:
DB_HOSTdefaults to'db'. Within a Podman network created bypodman-compose, service names (likedbfor our PostgreSQL container) are automatically resolved to their internal IP addresses, enabling seamless inter-container communication. - Database Connection Retry Logic: The
get_db_connectionfunction includes a retry loop. This is critical because the API container might start slightly before the database container is fully initialized and ready to accept connections. This pattern makes our application more resilient during startup. - API Endpoints:
/: A simple endpoint to confirm the API is running./data: This endpoint demonstrates database interaction: it creates a table (if not exists), inserts a dynamic message, and then retrieves the latest message. This verifies the full stack from API to database.
app.run(host='0.0.0.0', port=5000, debug=True):host='0.0.0.0'makes the Flask server accessible from outside the container’s localhost, allowing the port mapping incompose.yamlto work.debug=Trueenables Flask’s debugger and auto-reloader, which is highly beneficial with volume mounts, as code changes on your macOS host will trigger an automatic restart of the Flask development server inside the container.
4. Create the compose.yaml File
This file is the heart of our orchestration, defining both the db and api services, their configurations, and how they interact.
Create the file my_flask_app/compose.yaml:
# Use a specific version for podman-compose compatibility.
# As of 2026-06-22, '3.9' is a widely supported and robust version for Compose files.
version: '3.9'
services:
db:
image: postgres:16-alpine-arm64 # Official ARM64 PostgreSQL 16 image
environment:
POSTGRES_DB: mydatabase
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
volumes:
- db_data:/var/lib/postgresql/data # Persist DB data using a named volume
ports:
- "5432:5432" # Map host port 5432 to container port 5432 for direct access
healthcheck: # Define a health check for the database
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s # Give the DB time to start before checking health
restart: unless-stopped # Always restart unless explicitly stopped
api:
build: ./api # Build the image from the Dockerfile in the 'api' directory
ports:
- "5000:5000" # Map host port 5000 to container port 5000 for API access
volumes:
- ./api:/app # Mount the local 'api' directory into the container for live code changes
environment:
DB_HOST: db # Use the service name for database connectivity
DB_NAME: mydatabase
DB_USER: myuser
DB_PASSWORD: mypassword
depends_on: # Ensure DB is healthy before starting API
db:
condition: service_healthy
restart: unless-stopped # Always restart unless explicitly stopped
volumes:
db_data: # Declare the named volume for database persistenceExplanation of Decisions:
version: '3.9': Specifies the Compose file format. This version offers a good balance of features and compatibility.dbservice:image: postgres:16-alpine-arm64: We explicitly use the ARM64 variant of the PostgreSQL 16 image. This is crucial for native performance on Apple Silicon.environment: Sets standard PostgreSQL environment variables for initial setup. These values must match what the API expects.volumes: - db_data:/var/lib/postgresql/data: This creates a named volume calleddb_data. Named volumes are managed by Podman and persist data independently of container lifecycles. This ensures your database data remains even if you stop and remove thedbcontainer, preventing data loss between development sessions.ports: - "5432:5432": Maps port 5432 on your macOS host to port 5432 inside thedbcontainer. This allows you to connect to the database directly from your macOS machine using tools likepsqlor a GUI client.healthcheck: Defines how Podman should determine if thedbcontainer is healthy. Thepg_isreadycommand checks if the PostgreSQL server is accepting connections.start_periodgives the container time to initialize before health checks begin. The API service uses this health status.restart: unless-stopped: Ensures the database container automatically restarts if it crashes or if the Colima VM is restarted, unless you explicitly stop it.
apiservice:build: ./api: Instead of pulling a pre-existing image,podman-composewill build the image for this service using theDockerfilelocated in the./apidirectory relative tocompose.yaml.ports: - "5000:5000": Maps port 5000 on your macOS host to port 5000 inside theapicontainer. This is how you access your Flask API from your browser orcurl.volumes: - ./api:/app: This is a bind mount. It maps your localmy_flask_app/apidirectory directly into the/appdirectory inside theapicontainer. This is invaluable for development: any code changes you save on your macOS machine will instantly be available within the container, and with Flask’s debug mode, the server will often auto-reload.environment: Passes the database connection details to the API container.DB_HOST: dbis crucial for internal service discovery.depends_on: db: condition: service_healthy: This ensures that theapicontainer will only start once thedbcontainer has reported itself ashealthyvia itshealthcheck. This prevents the API from trying to connect to a database that isn’t ready, reducing startup errors.restart: unless-stopped: Similar to thedbservice.
volumes: This top-level key declares the named volumes used by our services, in this case,db_data.
5. Install podman-compose
If you haven’t already, install podman-compose on your macOS host. This tool is responsible for parsing your compose.yaml and issuing the corresponding podman commands to your Colima VM.
First, ensure Homebrew is up-to-date:
brew updateThen, install podman-compose. As of 2026-06-22, podman-compose version 1.2.x is stable and recommended.
brew install podman-composeVerification:
Confirm podman-compose is installed and accessible:
podman-compose --versionYou should see output similar to podman-compose version 1.2.0 (or a later stable version). If you encounter command not found, ensure Homebrew’s binary path (/opt/homebrew/bin) is in your shell’s PATH.
6. Start the Services
Now, with our compose.yaml in place, it’s time to bring our application stack online. Ensure your Colima instance is running before proceeding.
# 🧠 Important: Ensure Colima is running. If not, start it.
colima start
# Navigate to your project's root directory where compose.yaml is located
cd my_flask_app
# Build images and start containers in detached mode
podman-compose up -dExplanation:
colima start: Verifies or starts your Colima VM.podman-composerelies onpodmanbeing connected to this VM.cd my_flask_app: It’s essential to runpodman-composefrom the directory containing yourcompose.yamlfile.podman-compose up -d: This command orchestrates the entire process:- It reads
compose.yaml. - For the
apiservice, it builds the image using the specifiedDockerfile. - It pulls the
postgres:16-alpine-arm64image if not already present. - It creates a dedicated Podman network for the services.
- It starts the
dbcontainer, waiting for itshealthcheckto pass. - It then starts the
apicontainer, linking it to thedbservice. - The
-dflag runs all containers in “detached” mode, meaning they run in the background, freeing up your terminal.
- It reads
Testing & Verification
After starting the services, it’s crucial to verify that everything is running correctly and that the services can communicate.
1. Check Container Status
Use podman ps to see the status of your running containers.
podman psExpected Output:
You should see output similar to this (IDs and names will vary slightly), showing both db and api containers running and healthy:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a1b2c3d4e5f6 docker.io/library/postgres:16-alpine-arm64 postgres -c max_co... About a minute ago Up About a minute (healthy) 0.0.0.0:5432->5432/tcp my_flask_app-db-1
f6e5d4c3b2a1 localhost/my_flask_app-api:latest python app.py About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp my_flask_app-api-1STATUS: For thedbcontainer, look forUp ... (healthy). This confirms the PostgreSQL health check passed. Theapicontainer should simply showUp ....PORTS: Confirm that ports5000(for API) and5432(for DB) are mapped correctly from your macOS host.
2. Inspect Container Logs
Check the logs for each service to ensure they started without errors and are performing expected operations.
podman-compose logs db
podman-compose logs apiExpected Output (for db):
You should see PostgreSQL startup messages, eventually ending with something like database system is ready to accept connections. Health check messages might also appear.
Expected Output (for api):
You should see Flask startup messages, indicating the server is running on http://0.0.0.0:5000/. If the database connection was successful, you should see Successfully connected to the database. and no repeated “Database connection failed” messages.
3. Test the API from macOS Host
Now, use curl from your macOS terminal to hit the API endpoints exposed on localhost.
First, test the root endpoint:
curl http://localhost:5000/Expected Output:
{"message": "Hello from Flask API!", "status": "running"}Next, test the endpoint that interacts with the database:
curl http://localhost:5000/dataExpected Output:
{"database_message": "Message from API at HH:MM:SS", "retrieved_at": "HH:MM:SS"}(The exact time will vary). If you see this, it means your Flask API successfully connected to the PostgreSQL database, created a table, inserted data, and retrieved it. This confirms full stack functionality.
4. Connect to PostgreSQL Directly (Optional)
Since we mapped port 5432, you can also connect to the database directly from your macOS host using a psql client (if installed, e.g., brew install libpq for client tools).
psql -h localhost -p 5432 -U myuser -d mydatabaseEnter mypassword when prompted. Once connected, you can verify the messages table and its content:
SELECT * FROM messages;
\qThis confirms direct host access to the PostgreSQL database running inside Colima.
5. Stop and Clean Up Services
When you’re finished with your development session, you can stop and remove the services.
# Stop and remove containers and networks created by podman-compose
podman-compose downExplanation of Decisions (podman-compose down):
- This command gracefully stops the running containers and then removes them, along with any networks created by
podman-compose. - By default,
podman-compose downdoes not remove named volumes (likedb_data). This is a deliberate design choice: it allows you to preserve your database state between development sessions. When you runpodman-compose up -dagain, thedbservice will reuse the existingdb_datavolume, and your data will still be there.
If you want to explicitly remove named volumes (effectively resetting your database and all its data):
# Stop and remove containers, networks, and named volumes
podman-compose down --volumes⚠️ What can go wrong: Using --volumes will permanently delete all data stored in the db_data volume. Only use this if you intend to start with a fresh database.
Production Considerations
While this chapter focuses on local development, the patterns we’ve used are highly applicable to production environments.
- Image Optimization: Our
Dockerfilealready uses an Alpine base and--no-cache-dirforpipto create smaller images. In production, consider multi-stage builds for even smaller, more secure final images by separating build dependencies from runtime dependencies. - Secrets Management: Hardcoding
POSTGRES_PASSWORDincompose.yamlis acceptable for local development but a major security risk in production. Production systems use dedicated secret management solutions (e.g., Kubernetes Secrets, AWS Secrets Manager, HashiCorp Vault) to inject sensitive credentials securely at runtime. - Resource Limits: In production, define CPU and memory limits for your containers to prevent a single service from monopolizing resources and impacting other services.
podman-composesupportsresourcesanddeploykeys for this, which translate to underlying container engine limits. - Centralized Logging: For local development,
podman-compose logsis sufficient. In production, aggregate logs from all containers into a centralized logging system (e.g., ELK stack, Splunk, cloud-provider logging services) for monitoring, troubleshooting, and auditing. - Robust Health Checks: The
healthcheckfor PostgreSQL is a good start. For production APIs, implement comprehensive readiness and liveness probes that check internal dependencies (like database connectivity) to ensure your application is truly ready to serve traffic.
Common Issues & Solutions
Even with a well-defined setup, you might encounter issues. Here are common pitfalls and how to address them.
“Cannot connect to the Docker daemon” /
podmannot working:- Issue:
podmanis not configured to use the Colima instance, or Colima itself is not running. - Solution: Ensure
colima starthas been run. Verifypodmanis connected to Colima by checkingpodman system connection list. Ifcolimais not the default, set it withpodman system connection default colima. - Verification: Run
podman ps. It should list containers or show no errors.
- Issue:
Database Connection Failed (API Container Startup):
- Issue: The API service attempts to connect to PostgreSQL before the database is fully initialized and ready to accept connections.
- Solution: We’ve addressed this with
depends_on: db: condition: service_healthyincompose.yamland a retry loop inapp.py. Ensure these are correctly implemented. Also, checkpodman-compose logs dbto confirm PostgreSQL is starting without errors and its health checks are passing. Increasestart_periodfor the DB healthcheck if needed. - Verification:
podman-compose logs apishould show successful database connection attempts after a few retries, or no retry messages at all if the database starts quickly.
Port Conflicts:
- Issue: A port mapped in
compose.yaml(e.g.,5000or5432) is already in use by another application on your macOS host. - Solution: Change the host port mapping in your
compose.yaml(e.g.,"5001:5000"for the API) or identify and stop the conflicting process on your macOS machine usinglsof -i :<PORT>. - Verification:
podman psshould show the correct port mappings without errors.curlto the new host port should work.
- Issue: A port mapped in
podman-composecommand not found:- Issue:
podman-composewas not installed correctly or its executable path is not in your shell’sPATHenvironment variable. - Solution: Re-run
brew install podman-compose. If it’s installed but not found, ensure your shell’s configuration (.zshrc,.bashrc) includes Homebrew’s binary path (typically/opt/homebrew/binon Apple Silicon). - Verification:
which podman-composeshould return a path like/opt/homebrew/bin/podman-compose.
- Issue:
Volume Mounting Permissions:
- Issue: Inside the container, the application cannot read or write to files within a mounted volume due to permission issues (e.g., the container user doesn’t have access to your macOS user’s files).
- Solution: This is less common with Colima and
podman-composeas they often handle user ID mapping. If encountered, you might need to ensure the user inside the container matches your host user’s ID, or configure permissions more broadly on the host directory (e.g.,chmod -R 777 my_flask_app/apifor development, but use with caution). - Verification: Check
podman-compose logs apifor any “Permission denied” errors when the application tries to access files in/app.
Summary & Next Step
You’ve successfully established a robust, multi-service local development environment on your Apple Silicon Mac! You now have a working setup where:
- Your Python Flask API and PostgreSQL database run in isolated, native ARM64 containers within Colima.
- Services communicate seamlessly over an internal network.
- Project code changes on your macOS host are instantly reflected in the running API via efficient volume mounts.
- Database data persists across container restarts, ensuring a consistent development state.
- You can access and test your application directly from your macOS host.
This milestone provides a powerful and reproducible foundation for developing complex applications, offering performance advantages and a workflow that closely mirrors production deployments.
In the next chapter, we will delve into advanced techniques for testing and debugging services running within this containerized environment, covering strategies to troubleshoot issues effectively and ensure your application behaves as expected.
References
- Colima GitHub Repository
- Podman Official Documentation
- Podman Compose GitHub Repository
- PostgreSQL Official Docker Images
- Flask Documentation
- Psycopg2 Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.