Running OpenCode in a containerized environment
In this article, I will explain how to use OpenCode isolated from the host system using Podman. I will also mention the Bash script that I wrote to access this isolated environment from the command line!
LLM-based chatbots are everywhere on the internet these days, under a different name each time.
One of the places where these chatbots can be very useful—where software engineers, sysadmins, and computer geeks spend long hours—is the terminal (you can read it as terminal emulators).
Even though the idea of using only terminal-based tools in my workspace doesn’t really sound practical to me, the ability to access LLM-based chatbots can be very convenient:
- Easily forgettable and weird commands
- Remembering the syntax of programming and scripting languages
- The speed and quickness the terminal brings (compared to a web browser)
- Random questions that come into mind
- and much more….
Dockerfile and image creation
Dockerfile:
FROM docker.io/node:22-bookworm
# Persistent npm-global directory inside /root
RUN mkdir --parents /root/.npm-global \
&& npm config set prefix /root/.npm-global \
&& npm install --global opencode-ai --loglevel verbose
ENTRYPOINT ["/root/.npm-global/bin/opencode"]
Create an image as defined in the Dockerfile with the opencode tag:
$ podman image build --tag opencode --file Dockerfile
Bash script for easy access from the command line
opencode:
#!/bin/bash
set -e
DATA_VOLUME='opencode-data'
IMAGE_TAG='localhost/opencode-cli'
MOUNT_DIR_CONTAINER='/workspace'
OPENCODE_PATH='/root/.npm-global/bin/opencode'
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
-d|--directory)
MOUNT_DIR_HOST=$(realpath "$2")
shift # Pass argument
shift # Pass value
;;
-o|--mount-options)
MOUNT_OPTS="$2"
shift # Pass argument
shift # Pass value
;;
*)
POSITIONAL_ARGS+=("$1") # Save arguments which will be passed to copilot
shift # Pass argument
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}" # Replace the arguments
if [ ! -z "${MOUNT_DIR_HOST}" ]; then
WORKSPACE_VOLUME_ARG="--volume ${MOUNT_DIR_HOST}:${MOUNT_DIR_CONTAINER}"
WORKDIR="${MOUNT_DIR_CONTAINER}"
else
WORKDIR="/root"
fi
if [ ! -z "${MOUNT_OPTS}" ]; then
WORKSPACE_VOLUME_ARG="${WORKSPACE_VOLUME_ARG}:${MOUNT_OPTS}"
fi
if ! podman volume exists "${DATA_VOLUME}"; then
podman volume create "${DATA_VOLUME}" # If volume does not exist, create
fi
if ! podman image exists "${IMAGE_TAG}"; then
printf "Error: Image is not available: %s\n" "${IMAGE_TAG}" # If image does not exist, print error message and exit
exit 1
fi
podman run \
--rm \
--interactive \
--tty \
--entrypoint "${OPENCODE_PATH}" \
${WORKSPACE_VOLUME_ARG} \
--volume "${DATA_VOLUME}:/root" \
--workdir "${WORKDIR}" \
"${IMAGE_TAG}" \
"$@"
General purpose of the Bash script:
-d|--directory: Sets a directory from the host filesystem to mount into the container.-o|--mount-options: Sets mount options.- Passes the remaining arguments to the
opencodeexecutable inside the container. - Mounts the
/rootdirectory (NPM files and Copilot CLI configuration) from persistent storage (a volume), creating it if it does not already exist. - Sets the working directory inside the container (if a directory from the host is mounted, it becomes OpenCode’s working directory).
- Executes the
podman runcommand:- Deletes the container after it stops.
- Runs interactively (makes stdin available to the containerized process).
To have a similar experience to the command-line-assistant available in RHEL, you can add the following function to your .bashrc configuration:
# opencode
c() {
opencode run "$@" | glow
}
You can install the
glowpackage - which used to format the Markdown output - from your distro’s repositories.
How to use
After granting the necessary execution permissions to the Bash script and moving it into a directory listed in the PATH environment variable ($HOME/.local/bin/ in my case), you can use it as in the examples below:
$ opencode: Opens the OpenCode terminal interface (TUI). Operations such as; agent selection, LLM provider connection,AGENTS.mdconfiguration etc. get done here.$ opencode --directory ./test-files: Mounts thetest-filesdirectory from the host into the container, and this directory becomes OpenCode’s working directory.$ opencode --directory ./test-files --mount-options ro: Mounts thetest-filesdirectory into the container in read-only mode.$ c 'how to export podman images into a tar archive?': Copilot prints the answer to standard output (stdout) instead of opening the TUI.
dogumer@fedora:~ 🚢🐳 $ c "what is podman and how does it differ from docker?"
│ build · deepseek-v4-flash
│
│ Podman is a daemonless container engine. Key differences from Docker:
• Daemon vs daemonless: Docker uses a central dockerd daemon; Podman runs
containers as child processes (rootless by default, no daemon).
• Compatibility: Podman is largely CLI-compatible with Docker ( podman
build/run/pull ), and supports Dockerfile builds and OCI images.
• Security: Podman's rootless model gives better isolation; Docker supports
rootless too but it's less default.
• Pod support: Podman natively supports pods (groups of containers, like
Kubernetes); Docker only added basic compose-based grouping.
• Images: Both use OCI images; Podman can also build via buildah and can
use Docker Hub images.
• Docker Compose: Podman can run compose files via podman compose (with
the podman-compose or docker-compose backend).
In short: Podman aims for Docker compatibility without the daemon, which is
attractive for systemd-integrated, rootless, production/Kubernetes-aligned
workflows.
dogumer@fedora:~/Experiments 🚢🐳 $ ls asciidoc-experiments/
./ ../ aside.adoc counter.out counter.py demo.adoc demo.pdf sample.csv sample.json
dogumer@fedora:~/Experiments 🚢🐳 $ opencode -d asciidoc-experiments/ -o ro
Setting OpenCode permissions
OpenCode can be configured to take a specific action in the following ways:
"allow": Take action directly without asking"ask": Ask the user before taking action"deny": Block the action
Configuration settings are written in JSON format to the .config/opencode/opencode.jsonc file.
Let’s find the location of the volume created for persistent storage on the host filesystem:
dogumer@fedora:~ 🚢🐳 $ podman volume inspect opencode-data
[
{
"Name": "opencode-data",
"Driver": "local",
"Mountpoint": "/home/dogumer/.local/share/containers/storage/volumes/opencode-data/_data",
"CreatedAt": "2026-08-01T22:47:17.960474962+03:00",
"Labels": {},
"Scope": "local",
"Options": {},
"MountCount": 0,
"LockNumber": 8
}
]
Then let’s write our configuration settings to the file:
dogumer@fedora:~/.local/share/containers/storage/volumes/opencode-data/_data 🚢🐳 $ vim .config/opencode/opencode.jsonc
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"*": "ask",
"read": {
"/workspace/*": "allow",
".env*": "deny"
}
}
}
In the configuration I use, OpenCode asks by default before taking any action; only files in the
/workspace/directory (the mounted directory) are allowed to be read without asking, and reading.envfiles is blocked. You can loosen or tighten the permissions according to your own environment and personal preferences.For more detailed information about configurations
For more detailed information about permissions


