Debugging and testing are fundamental to developing robust applications. When your services run in an isolated container machine on your Apple Silicon Mac, understanding how to interact with them, inspect their state, and step through code becomes a critical skill. This chapter guides you through establishing seamless testing and remote debugging workflows from your macOS host into your Linux container environment.
By the end of this milestone, you will have a fully functional local development setup where you can:
- Access containerized API and database services from your macOS host.
- Efficiently inspect real-time logs from individual containers.
- Configure and execute remote debugging sessions from your macOS IDE (e.g., VS Code) into a Python application running inside a container.
Project Overview for This Chapter
In this chapter, we are extending our local container development environment to include essential testing and debugging capabilities. The goal is to bridge the isolation boundary between your macOS host and the Linux container machine, allowing for direct interaction and deep inspection of your running services. This is crucial for iterating quickly on features and resolving issues without deploying to a separate test environment.
Relevant Tech Stack Choices
For this chapter, our primary tools and configurations include:
- Colima/Lima: Continues to manage the lightweight Linux virtual machine on Apple Virtualization.framework.
- Podman (or Docker CLI via Colima): Used for managing containers, inspecting logs, and rebuilding images.
- Python
debugpy: A popular, open-source Python debugger that enables remote debugging. We’ll integrate this into our Python API container. - VS Code: Our chosen IDE on macOS, configured to attach to the remote debugger running inside the container.
- Network Configuration: We’ll leverage the port forwarding capabilities of Colima and Podman to expose container ports to the macOS host.
Milestones for Testing and Debugging
To achieve our goal, we’ll follow these incremental milestones:
- Verify External Service Access: Confirm the API and database are reachable from macOS.
- Stream Container Logs: Learn to access real-time output from running containers.
- Prepare Container for Debugging: Modify the API’s
Dockerfileto include a debugger agent. - Rebuild and Redeploy: Update the container image and restart services with new port mappings.
- Configure IDE for Remote Debugging: Set up VS Code to attach to the containerized application.
- Execute Debugging Session: Hit breakpoints and step through code.
Architecture for Debugging Across Boundaries
Our debugging strategy involves extending network access and integrating a debugger agent within the application container. The macOS host will connect to specific ports on the container machine’s IP address, which are then forwarded to the respective services and the debugger agent inside the containers.
The Container Machine acts as the intermediary, forwarding requests and debug connections from your macOS Host to the specific ports exposed by the API Container and DB Container. Logs are streamed from the containers and can be inspected directly from the macOS Host.
Step-by-Step Implementation
We’ll pick up from Chapter 5, assuming you have your sample API and PostgreSQL containers running. If not, please ensure they are up and running before proceeding.
Step 1: Verify Service Accessibility from macOS
The first step in debugging is ensuring your services are even reachable. We’ll use simple network tools to confirm connectivity.
Identify the Container Machine’s IP Address: Colima or Lima assigns an IP address to the virtual machine. This is the gateway from your macOS host to your containerized services.
colima statusLook for the
VM AddressorHost Addressin the output. For many default Colima setups, it’s configured to bind to127.0.0.1(localhost) on the macOS host, making it straightforward to access. If it’s a192.168.x.xaddress, use that instead. For this guide, we’ll assume127.0.0.1.When you use
podman run -p 8000:8000 ...or define ports indocker-compose.yaml(e.g.,ports: - "8000:8000"), you are mapping the container’s internal port (e.g.,8000) to a port on the container machine’s network interface (also8000in this example). Colima then ensures this port on the VM is accessible from your macOS host.Test the API Endpoint from macOS: Open a new terminal on your macOS host and send a request to your API’s health check endpoint.
curl http://127.0.0.1:8000/healthExpected Output:
{"status": "ok"}If you receive a connection refused error, re-check your
colima status, ensure yourmy-apicontainer is running (podman ps), and verify the port mappings in yourdocker-compose.yaml.⚡ Quick Note: You can also test database connectivity from your macOS host using a tool like
psqlorDBeaver, connecting to127.0.0.1:5432with the credentials configured in Chapter 5.
Step 2: Inspecting Container Logs
Logs are invaluable for understanding application behavior, diagnosing issues, and observing real-time events.
Stream Logs for the API Service: Assuming your API container is named
my-api(as defined indocker-compose.yaml), you can view its logs in real-time using thepodman logs -fcommand.podman logs -f my-apiIf you’re using a Docker-compatible CLI via Colima, it would be
docker logs -f my-api.Now, in a separate terminal, send another request to your API:
curl http://127.0.0.1:8000/healthExpected Output (in log terminal): You should see new log entries appear, indicating your API processed the request, confirming your application is logging correctly and that you can observe its output.
Stream Logs for the Database Service: Similarly, you can monitor the PostgreSQL database logs. This is essential for debugging database connection issues, slow queries, or startup failures.
podman logs -f my-postgresExpected Output: You’ll see PostgreSQL’s internal logs, confirming its health and operations.
Step 3: Setting Up Remote Debugging (Python API with VS Code)
Remote debugging allows you to attach your IDE to a running process inside a container, enabling you to set breakpoints, step through code, and inspect variables. We’ll use debugpy for our Python API.
3.1 Modify the API’s Dockerfile for Debugging
To enable remote debugging, we need to install the debugger agent (debugpy) within the container and instruct our application to start with it, listening on a specific port.
Edit
api/Dockerfile: Locate yourapi/Dockerfileand add thedebugpyinstallation. Then, modify theCMDinstruction to launch your application usingdebugpy.# api/Dockerfile FROM python:3.11-slim-bookworm AS builder WORKDIR /app # 🧠 Important: Install debugpy for remote debugging. # We specify a stable, recent version as of 2026-06-22. RUN pip install --no-cache-dir debugpy==1.8.0 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # Expose both the application port and the debugger port. # EXPOSE is declarative, it doesn't actually open the port. EXPOSE 8000 EXPOSE 5678 # Debugger port # Command to run the application with debugpy. # --listen 0.0.0.0:5678: Tells debugpy to listen on all interfaces. # --wait-for-client: Pauses app startup until a debugger connects. Remove if you want to attach later. # -m uvicorn main:app: Your actual application startup command. CMD python -m debugpy --listen 0.0.0.0:5678 --wait-for-client -m uvicorn main:app --host 0.0.0.0 --port 8000Explanation of Changes:
RUN pip install --no-cache-dir debugpy==1.8.0: This installs thedebugpypackage into your container.1.8.0is a stable release as of late 2023, and remains a solid choice for a current example in 2026. Always prefer specific versions for reproducibility.EXPOSE 5678: This line documents that port 5678 will be used by the debugger. WhileEXPOSEitself doesn’t publish the port, it’s good practice for image documentation. The actual port mapping happens indocker-compose.yaml.CMD python -m debugpy --listen 0.0.0.0:5678 --wait-for-client -m uvicorn main:app --host 0.0.0.0 --port 8000: This modifies your application’s entrypoint.python -m debugpy: Invokes thedebugpymodule.--listen 0.0.0.0:5678: Crucially, this tellsdebugpyto listen for incoming debugger connections on all available network interfaces within the container on port 5678. Using0.0.0.0ensures it’s accessible from the container machine’s network.--wait-for-client: This flag instructs the application to pause its execution right after the debugger starts, and wait until a debugger client (like VS Code) connects. This is extremely useful for debugging application startup logic. If you prefer the application to start immediately and attach the debugger later, you can omit this flag.-m uvicorn main:app --host 0.0.0.0 --port 8000: This is your original command to start the API, now run under the debugger.
3.2 Rebuild and Restart the API Container
After modifying the Dockerfile, you need to rebuild the image and restart your services to apply the changes.
Navigate to your project root: This is where your
docker-compose.yaml(or equivalent Podman compose file) is located.Rebuild the API image: This command builds a new image named
my-api-debugfrom your updatedDockerfile.podman build -t my-api-debug api/If you’re using
docker-compose.yamlwith thedocker-composeCLI (via Colima):docker-compose build apiUpdate your container definition: Now, modify your
docker-compose.yamlto use this new debug-enabled image and expose the debugger port.# docker-compose.yaml version: '3.8' services: api: image: my-api-debug:latest # 🧠 Important: Use the new debug image build: context: ./api dockerfile: Dockerfile ports: - "8000:8000" - "5678:5678" # Map debugger port from container to host environment: DATABASE_URL: postgresql://user:password@db:5432/mydatabase depends_on: - db db: image: postgres:16.3-alpine # Latest stable as of 2026-06-22 environment: POSTGRES_DB: mydatabase POSTGRES_USER: user POSTGRES_PASSWORD: password ports: - "5432:5432" volumes: - db_data:/var/lib/postgresql/data volumes: db_data:Explanation of Changes:
image: my-api-debug:latest: We explicitly tellpodman compose(ordocker-compose) to use the image we just built, which containsdebugpy.ports: - "5678:5678": This is critical for network accessibility. It maps the debugger port 5678 inside the container to port 5678 on the container machine’s IP address. This makes the debugger accessible from your macOS host.
Restart your services: Bring down your old services and start the new ones. If you made changes to
docker-compose.yaml,podman compose up -dwill detect them.podman compose up -d --build # --build ensures the api image is rebuilt if changes are detectedor
docker-compose up -d --buildIf your API container was configured with
--wait-for-client, it will start but remain in a paused state, waiting for your VS Code debugger to connect. You can verify this withpodman ps(it will show(health: starting)or similar, and logs will showdebugpywaiting).
3.3 Configure VS Code for Remote Debugging
Now, let’s set up your IDE to connect to the debugger running in the container.
Install the Python Extension for VS Code: If you haven’t already, install the “Python” extension by Microsoft from the VS Code Extensions view. This provides the necessary debugging capabilities for Python.
Create a Debug Configuration: Open your
apiproject folder in VS Code. Go to the “Run and Debug” view (accessible via the sidebar icon, orCmd+Shift+D/Ctrl+Shift+D). Click on “create a launch.json file” (if you don’t have one) and select “Python”. This will create a.vscode/launch.jsonfile in your project root.Add a new configuration for remote attaching to this file:
// .vscode/launch.json { "version": "0.2.0", "configurations": [ { "name": "Python: Remote Attach (Container)", "type": "python", "request": "attach", "port": 5678, "host": "127.0.0.1", // Or your Colima VM IP, e.g., "192.168.106.1" "pathMappings": [ { "localRoot": "${workspaceFolder}/api", // Path to your API code on macOS "remoteRoot": "/app" // Path to your API code inside the container } ], "justMyCode": false // Set to true if you only want to debug your own code, not library code } ] }Explanation of Configuration:
name: A descriptive name for this debug configuration.type: "python": Specifies that this is a Python debugging session.request: "attach": Indicates we want to attach to an already running process (thedebugpyagent in the container).port: 5678: This must match the debugger port exposed by your container (5678in ourdocker-compose.yaml).host: "127.0.0.1": This is the IP address of your container machine as seen from your macOS host. Use the IP fromcolima statusif it’s not127.0.0.1.pathMappings:🧠 Important:This is the most crucial part for remote debugging. It tells VS Code how to translate file paths."localRoot": "${workspaceFolder}/api": This is the absolute path to your API’s source code on your macOS host.${workspaceFolder}is a VS Code variable pointing to your project’s root. Make sure/apiis the correct subfolder containing yourmain.py."remoteRoot": "/app": This is the absolute path to the same source code inside the container. This must match theWORKDIRdefined in yourDockerfile.
"justMyCode": false: Whenfalse, the debugger will step into library code (e.g., Uvicorn internals). Set totrueto only debug your own application code, which is often preferred.
Start Debugging:
- Open your API’s source file (e.g.,
api/main.py) in VS Code. - Set a breakpoint on a line within one of your API endpoints (e.g., inside the
/itemsroute handler). - In the “Run and Debug” view, select the “Python: Remote Attach (Container)” configuration from the dropdown.
- Click the green play button to start debugging.
VS Code will now attempt to connect to the
debugpyagent in your container. If your container was started with--wait-for-client, the container will unpause and begin executing. The VS Code debug console will show messages indicating a successful connection.- Open your API’s source file (e.g.,
Trigger the Breakpoint: Send a request to the API endpoint where you set the breakpoint from your macOS terminal:
curl http://127.0.0.1:8000/itemsExpected Behavior: VS Code should now hit your breakpoint, pausing execution at that line. You can then use the debugger controls (step over, step into, step out), inspect variables, and evaluate expressions, just as you would with local debugging. This confirms your remote debugging setup is fully functional.
Testing & Verification
To confirm your debugging environment is correctly configured and operational:
- API Connectivity: Successfully execute
curl http://127.0.0.1:8000/healthfrom your macOS terminal and receive the expected{"status": "ok"}response. - Log Inspection: Run
podman logs -f my-api(ordocker logs -f my-api) and observe new log entries appearing in real-time when you interact with your API. - Remote Debugging:
- Successfully attach VS Code using the “Python: Remote Attach (Container)” configuration.
- Set a breakpoint in your API code.
- Trigger the API endpoint that hits the breakpoint.
- Verify that VS Code pauses execution at the breakpoint and allows you to step through the code and inspect variables.
If any of these verification steps fail, consult the “Common Issues & Solutions” section below for troubleshooting guidance.
Production Considerations
While this local debugging setup is powerful for development, it’s crucial to understand how it differs from production environments:
- Debugger Overhead: Debugger agents like
debugpyintroduce performance overhead and can increase the attack surface of your application.- Best Practice: Never deploy debug-enabled images to production. Always maintain separate
Dockerfiles or build arguments to create optimized production images without debugging tools.
- Best Practice: Never deploy debug-enabled images to production. Always maintain separate
- Logging Strategy: In production, container logs are typically collected by a dedicated logging agent (e.g., Fluentd, Vector, Logstash) and shipped to a centralized logging platform (e.g., Elasticsearch, Loki, Splunk, cloud-native logging services). While
podman logsis great for local development, production logging systems offer advanced features like aggregation, filtering, alerting, and long-term retention. - Security of Debug Ports: Debugger ports (like 5678) should never be exposed publicly in a production environment due to severe security implications. Even in development, be mindful of what ports you open, especially on shared networks.
- Performance Impact: Debugging can significantly slow down application execution. Ensure you disable debugging when performing performance-critical tests or benchmarks.
Common Issues & Solutions
Local container environments can sometimes present unique challenges. Here are common issues and how to resolve them:
Issue 1: curl: (7) Failed to connect to 127.0.0.1 port 8000: Connection refused
- Cause: The container machine (
colima) is not running, the API container is not running, or the port mapping is incorrect. - Solution:
- Check Colima Status: Ensure your container machine is active:
colima status. It should showRunning. If not, start it withcolima start. - Check Container Status: Verify your
my-apicontainer isUp:podman ps(ordocker ps). - Verify Port Mappings: Double-check your
docker-compose.yamlfor correct port mapping (e.g.,8000:8000). Ensure no other process on your macOS host is already using port 8000. - Inspect Container Logs: Look for startup errors in your API container:
podman logs my-api.
- Check Colima Status: Ensure your container machine is active:
Issue 2: VS Code debugger fails to attach or hangs
- Cause: This is often due to incorrect host/port configuration,
debugpynot running correctly in the container, or mismatchedpathMappings. - Solution:
- Verify Debugger Process: Check the
my-apicontainer logs (podman logs my-api). You should see messages fromdebugpyindicating it’s listening on port 5678 (e.g.,debugpy: Listening for clients on 0.0.0.0:5678...). - Confirm Port Mapping: Ensure
5678:5678is correctly defined in yourdocker-compose.yamlfor theapiservice and that you’ve restarted the services (podman compose up -d). - Correct
hostinlaunch.json: Confirm thehostin your VS Codelaunch.json(127.0.0.1or the specific Colima VM IP) is accurate. pathMappingsAccuracy: This is a very common source of errors. MismatchedlocalRoot(your macOS project path) andremoteRoot(the path inside the container) will prevent the debugger from correlating your local code with the running code. Ensure they are exact.- Firewall: Temporarily disable your macOS firewall to rule it out. If it works, re-enable and configure an exception for port 5678.
- Verify Debugger Process: Check the
Issue 3: Application starts immediately without waiting for the debugger
- Cause: The
--wait-for-clientflag was omitted or incorrectly applied in yourDockerfile’sCMDinstruction. - Solution:
- Review
Dockerfile: Ensure yourCMDinapi/Dockerfileexplicitly includespython -m debugpy --listen 0.0.0.0:5678 --wait-for-client .... - Rebuild and Restart: After correcting the
Dockerfile, rebuild the image (podman build -t my-api-debug api/) and restart your services (podman compose up -d).
- Review
Summary & Next Step
You have successfully established a robust local development environment on your Apple Silicon Mac, now enhanced with essential testing and debugging capabilities. You can seamlessly interact with your containerized services, monitor their behavior through logs, and perform deep code inspection using an integrated remote debugger. This setup provides a high-fidelity development experience, closely mirroring how you would debug directly on your host, while leveraging the isolation and reproducibility benefits of containers.
Next, in Chapter 7, we will transition our focus from local development to the broader container lifecycle: pushing your built OCI images to a remote container registry. This crucial step makes your images shareable, versionable, and ready for deployment in other environments.
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.