Running OpenCode in a containerized environment

2026-05-30 Doğukan Meral

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….

A TUI web browser

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 opencode executable inside the container.
  • Mounts the /root directory (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 run command:
    • 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 glow package - 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.md configuration etc. get done here.
  • $ opencode --directory ./test-files: Mounts the test-files directory from the host into the container, and this directory becomes OpenCode’s working directory.
  • $ opencode --directory ./test-files --mount-options ro: Mounts the test-files directory 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 .env files 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