Run Codex AI locally on macOS, powered by Ollama
This repository combines multiple open-source tools to run an agentic AI safely and privately on a Mac.
It runs OpenAI's Codex CLI on your Mac using Apple's Containerization tool, connecting to the locally running Ollama instance.
All tools are available as Open Source on GitHub:
container: https://github.com/apple/container/- Codex CLI: https://github.com/openai/codex
- Ollama: https://github.com/ollama/ollama
You need a Mac with Apple Silicon and at least 32 GB RAM. The setup is currently only tested on macOS 26 but might also work on macOS 15 (possible restrictions apply to the container tool).
I suppose you do not want to install everything from source (which would be doable). Therefore here are links to install the official releases of Ollama and Apple's container tool:
containerGitHub releases: https://github.com/apple/container/releases- Ollama download page: https://ollama.com/download
After installing both tools, open Terminal app and run the following commands:
# Pull gpt-oss:20b model (requires 13 GB on disk!)
ollama pull gpt-oss:20b
# start the container API server (required for building container images)
container system startcodexctl is the entry point for running Codex containers. It wraps the Apple container CLI, handles naming, and automates OpenAI device-auth when requested.
The basic idea is to start a container in the directory where your sources and/or documents are located. The container ensures that codex can only access files in the current directory tree (this directory and its subdirectories), but still the container contains many development tools codex needs to be useful/efficient in its work.
By installing codexctl somewhere in your system PATH (e.g. /usr/local/bin/ or better privately in ~/bin/), you can run a codex container in any directory you currently work, as easy as running codexctl run.
chmod 700 codexctl # restrict it to your user
sudo ln -s "$PWD/codexctl" /usr/local/bin/ # link it to system directory# Build images once
codexctl build
# Run Codex for the current directory (persistent by default)
codexctl run
# Run Codex for a specific directory
codexctl run --workdir /path/to/project
# Run with a specific image
codexctl run --image codex-office
# Read-only workdir mount
codexctl run --read-only
# Throwaway session (container is deleted when codex is quit)
codexctl run --temp
# OpenAI mode (auto-auth if needed)
codexctl run --openai
# Start a shell inside the container
codexctl run --shell
# Run a custom command (must be last)
codexctl run --cmd bashNote: codexctl build produces the codex, codex-python, codex-swift, and codex-office images by default.
Note: --cmd consumes the remaining arguments and cannot be combined with --shell, so place it last. If you pass a single quoted string containing spaces, it runs via $CODEX_SHELL -lc. This same behavior applies to codexctl exec.
Note: CODEX_SHELL is an environment variable to override the default shell used by run --shell and exec (default is bash). You can also set DEFAULT_SHELL in codexctl for a static default.
Note: The --rebuild, --refresh-base, and --pull-base options are for occasional refreshes when you want to pick up newer Codex or base image updates. See the build cache section below for guidance.
Note: codexctl was authored by Codex itself, running inside an Apple container in --openai mode.
Use --image when you need a specific toolchain, or set DEFAULT_IMAGE in codexctl for a permanent default.
When to use which image:
codex: general-purpose CLI work or small scripts without a heavy runtime.codex-python: Python-heavy tasks, data wrangling, and libraries not in the base image.codex-swift: Swift projects, SwiftPM builds, and Swift tooling.codex-office: document-centric workflows (docx/xlsx/pdf parsing, report generation).
codexctl exposes a few top-level constants (in codexctl) that you can edit to adjust default behavior:
DEFAULT_IMAGE(default image forrunandauth, e.g.codex,codex-python,codex-swift,codex-office)DEFAULT_NAME_PREFIX(default local container prefix)AUTH_NAME_PREFIX(default auth container prefix)DEFAULT_SHELL(default shell forrun --shellandexec)DEFAULT_CODEX_PROFILE(default profile passed to codex in local mode)DEFAULT_CODEX_CMD(base codex command/flags used when launching codex)
codexctl auth # run device-auth and store in Keychain
codexctl exec # shell into running container
codexctl ls # list containers
codexctl rm # remove the default container for this directoryNotes:
--authonly works together with--openai.--tempcreates a disposable container that is removed after the command exits.--read-onlymounts the workdir as read-only; codex can use its home directory or/tmpfor scratch data. But cannot modify the workdir.- The mount mode is fixed on first creation for a given container name. To switch between read-only and read-write, remove the container (e.g.
codexctl rm) or use--temp/--nameto create a fresh one. --openai --tempstill injects Keychain auth before running the command.- Keychain auth is the source of truth for
--openai; it is synced into running containers before each run. - After a run, if the container refresh time is newer (and present), the updated auth is saved back into Keychain.
- After
codexctl auth, exit any running--openaisessions and re-runcodexctl run --openaito pick up the new token.
Security notes:
- Containers run as a non-root
coderuser with Linux capabilities dropped. config.tomlsetssandbox_mode = "danger-full-access"to maximize capabilities. Codex can read/write anything in the mounted directory tree and run tools inside the container. Use only with trusted workspaces or change it toworkspace-writefor tighter guardrails.- Containers have full outbound network access by default.
--openaineeds outbound access; if you want to constrain networking, use host firewall rules or a restricted container network. - If you need root for maintenance tasks, use
codexctl su-exec(orcontainer exec -u 0 ...). - Some scripts that assume root access to system paths may fail; run them via
su-execor update them.
The rest of this README explains what codexctl does behind the scenes and how to run the underlying container commands directly.
To build the codex container images for later use, I have written four DockerFiles which are installing codex, git and other basic tools (bash, npm, file, curl):
DockerFilefor a plain Alpine Linux (~191 MB)DockerFile.pythonfor a Alpine based Python installation (~203 MB, built on top ofcodex)DockerFile.swiftfor an Ubuntu based Swift installation (~1.41 GB)DockerFile.officefor Alpine + Python + Office tooling (~417 MB, built on top ofcodex-python)
The Alpine-based images are layered for incremental builds: codex -> codex-python -> codex-office.
The image build process uses npm to install the latest openai/codex package, and configures git to use "Codex CLI" and codex@localhost as the container user's identity when interacting with git and to use main as the default branch when initializing a new repository.
Further the build process is going to copy the config.toml file into the container at /home/coder/.codex/ so that codex will properly connect to the locally running Ollama instance on the default network's host IP address 192.168.64.1.
Use the following container commands to build the codex images codex, codex-python, codex-swift, and codex-office from the corresponding DockerFile (build the Alpine images in order so the bases exist):
container build -t codex -f DockerFile
container build -t codex-python -f DockerFile.python
container build -t codex-office -f DockerFile.office
container build -t codex-swift -f DockerFile.swift
Notes:
- The Swift image includes `format` and `lint` wrappers for `swift-format` and initializes `swiftly` for toolchain management.
- The Office image sets up a writable venv at `/opt/venv` and puts it on `PATH` by default.--rebuilddisables Dockerfile layer cache (--no-cache). Use when Codex warns you about an outdated version and you want all new containers to start from a fresh image.--pull-basepulls the latest base image tag before building. Use when you want to update base images without deleting them first (preferred).--refresh-basedeletes the base image first, forcing a re-fetch on build. Use when you need a brute-force refresh; this may fail if the base image is still referenced by containers.
By default, Ollama is only listening on localhost connections, i.e. on http://localhost:11434 or http://127.0.0.1:11434. Containers have outbound network access enabled (required for --openai), so be deliberate about exposing host services. To be able to connect from a container (through a virtual network) to the Ollama service running on localhost, we have two options:
Expose the Ollama service to ALL network connections, by activating the setting "Expose Ollama to the network" in the Ollama GUI.
The problem of this approach: when connected to a "untrusted" public network, the Ollama service is visible to other computers and risks to be abused or attacked.
You should consider stopping the service on such networks!
Instead of using the GUI setting within Ollama you can launch a second Ollama service listening only on the virtual network interface using the following command:
OLLAMA_HOST=192.168.64.1 ollama serve
Otherwise you will get the following error:
Error: listen tcp 192.168.64.1:11434: bind: can't assign requested address
The following sub‑options provide ways to forward traffic from the container's network interface to the Ollama service running on the host.
In this option we are running a tool which listens on the virtual network interface 192.168.64.1 and forwards to the service running on localhost.
This can be done using socat (install with brew install socat):
socat TCP-LISTEN:11434,fork,bind=192.168.64.1 TCP:127.0.0.1:11434socat[12345] E bind(5, {LEN=16 AF=2 192.168.64.1:11434}, 16): Can't assign requested addressAlternatively if you are interested in also reading what the container is talking with Ollama, you can use my OllamaProxy Swift app.
This tool should be run in a separate Terminal window, as it will log all the "chat" running proxied to/from Ollama.
swift build
HOST=192.168.64.1 PORT=11434 swift runThe following command runs a "throwaway" codex container with the current directory as 'workdir'
container run --rm -it --mount type=bind,src="$(pwd)",dst=/workdir codexYou can name the container using the '--name' argument:
container run --rm -it --name "my-codex" --mount type=bind,src="$(pwd)",dst=/workdir codexTo start a bash within the same (running!) container, you can use
container exec -it "my-codex" bashIf you want to keep your session/chat history over multiple container restarts/runs, do not remove the container after termination (=omit --rm argument and make sure you give it a unique name – here derived from the current directory):
container run -it --name "codex-`basename $PWD`" --mount type=bind,src="$(pwd)",dst=/workdir codexThis is basically a shortcut for two commands below: create a new named container and start it (interactively)
container create -t --name "codex-`basename $PWD`" --mount type=bind,src="$(pwd)",dst=/workdir codex
container start -i "codex-`basename $PWD`"After quitting the current session in the container using CTRL+D, you can start the container again:
container start -i "codex-`basename $PWD`"To remove the container later after you finished working with it, use the following command to remove it:
container rm "codex-`basename $PWD`"To check what containers (even stopped ones) are lingering around use:
container ls -aIf you want to run more CPU and memory hungry builds within the codex container, you can specify CPU and memory when starting:
container run -it -c 6 -m 8G --name "codex-`basename $PWD`" --mount type=bind,src="$(pwd)",dst=/workdir codexIf you really want to connect to an OpenAI model, you have to connect codex within the container to OpenAI using either an API key or a device key. This means, you have to preserve the configuration within the container.
Basically we will create a container keeping the configuration around (auth.json generated by codex login), then always restart the same container as long as we need it.
-
Create the desired container, launching bash upon start:
container run -it --name "codex-`basename $PWD`" --mount type=bind,src="$(pwd)",dst=/workdir codex-swift bash
-
Within the container's shell, log in to codex using device auth:
codex login --device-auth
-
After successful login you can launch codex using the OpenAI models:
codex
To restart the container later, start the container:
container start -i "codex-`basename $PWD`" and launch codex again:
codexRemove the container to destroy the device configuration:
container rm "codex-`basename $PWD`"After you complete the OpenAI device-auth login flow, you may want to keep a copy of the device authorization tokens in your macOS Keychain. This is only relevant when auth.json exists (device-auth creates it). The file lives at /home/coder/.codex/auth.json inside the container and contains sensitive tokens. Use codex-auth-keychain.sh to move it into the Keychain:
codex-auth-keychain.sh store-from-container "codex-`basename $PWD`"If you created a fresh container and want to restore the authorization from the Keychain, use:
codex-auth-keychain.sh load-to-container "codex-`basename $PWD`"Notes:
- The container must be running for both commands.
- The default path is
/home/coder/.codex/auth.jsonunless you pass an explicit path. codexctl run --openaiandcodexctl authautomatically sync the Keychain auth into running containers when it differs.codexctl run --openaisaves refreshed auth back to Keychain when the container reports a newer refresh time (and that field is present).