Skip to content

Instantly share code, notes, and snippets.

@frandieguez
Last active February 26, 2026 13:18
Show Gist options
  • Select an option

  • Save frandieguez/47416be1772ab32540e255b4ebf82f03 to your computer and use it in GitHub Desktop.

Select an option

Save frandieguez/47416be1772ab32540e255b4ebf82f03 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# =============================================================================
# opencode-container.sh
# Ejecuta OpenCode dentro de un container Docker o Podman con:
# - Config de ~/.config/opencode del host compartida (agents, skills, themes...)
# - Auth de OpenCode persistida (~/.local/share/opencode)
# - Directorio actual montado como workspace del proyecto
# - SSH keys montadas (para git)
# - Mismo UID/GID que el usuario host (sin problemas de permisos)
#
# Uso:
# ./opencode-container.sh # lanza opencode en el directorio actual
# ./opencode-container.sh --build # fuerza rebuild de la imagen
# ./opencode-container.sh --shell # abre bash en el container (debug)
# ./opencode-container.sh --podman # fuerza uso de Podman
# ./opencode-container.sh --docker # fuerza uso de Docker
# ./opencode-container.sh -- --help # pasa --help a opencode
# ./opencode-container.sh -m gpt-5 "tu prompt" # pasa args extra a opencode
# ./opencode-container.sh --help # muestra ayuda
#
# Instalación rápida (disponible como comando global):
# chmod +x opencode-container.sh
# sudo ln -s "$(pwd)/opencode-container.sh" /usr/local/bin/oc
# # Ahora puedes usar: oc (desde cualquier directorio)
# =============================================================================
set -euo pipefail
# --- Configuración -----------------------------------------------------------
IMAGE_NAME="opencode-local"
IMAGE_TAG="latest"
CONTAINER_USER="dev"
# Colores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# --- Helpers -----------------------------------------------------------------
log() { echo -e "${GREEN}[opencode]${NC} $*"; }
warn() { echo -e "${YELLOW}[opencode]${NC} $*"; }
err() { echo -e "${RED}[opencode]${NC} $*" >&2; }
usage() {
cat <<EOF
Uso: $(basename "$0") [opciones-script] [args-opencode...]
Opciones:
--build Fuerza rebuild de la imagen
--shell Abre bash en el container en vez de opencode (debug)
--podman Fuerza uso de Podman (si tienes los dos instalados)
--docker Fuerza uso de Docker (si tienes los dos instalados)
--help Muestra esta ayuda
Ejemplos:
$(basename "$0") # Lanza opencode en el directorio actual
$(basename "$0") --build # Reconstruye la imagen y lanza
$(basename "$0") --shell # Abre shell para debug
$(basename "$0") --podman # Fuerza Podman
$(basename "$0") -m gpt-5 "hola" # Pasa argumentos a opencode
$(basename "$0") -- --help # Pasa --help a opencode
EOF
}
# --- Parseo de argumentos ----------------------------------------------------
FORCE_BUILD=false
OPEN_SHELL=false
FORCE_RUNTIME=""
PASSTHROUGH_ARGS=()
WEB_MODE=false
GENERATED_SERVER_PASSWORD=""
WEB_SERVER_USERNAME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--build)
FORCE_BUILD=true
shift
;;
--shell)
OPEN_SHELL=true
shift
;;
--podman)
FORCE_RUNTIME="podman"
shift
;;
--docker)
FORCE_RUNTIME="docker"
shift
;;
--help | -h)
usage
exit 0
;;
--)
shift
PASSTHROUGH_ARGS+=("$@")
break
;;
*)
PASSTHROUGH_ARGS+=("$1")
shift
;;
esac
done
# Para `oc web` dentro de container, forzamos listen en 0.0.0.0 por defecto
# (a menos que el usuario ya haya pasado --hostname/--mdns explícitamente).
if [[ ${#PASSTHROUGH_ARGS[@]} -gt 0 && "${PASSTHROUGH_ARGS[0]}" == "web" ]]; then
WEB_MODE=true
HAS_HOSTNAME=false
HAS_MDNS=false
for ((i = 0; i < ${#PASSTHROUGH_ARGS[@]}; i++)); do
arg="${PASSTHROUGH_ARGS[i]}"
if [[ "$arg" == "--hostname" || "$arg" == --hostname=* ]]; then
HAS_HOSTNAME=true
fi
if [[ "$arg" == "--mdns" ]]; then
HAS_MDNS=true
fi
done
if [[ "$HAS_HOSTNAME" == false && "$HAS_MDNS" == false ]]; then
PASSTHROUGH_ARGS+=(--hostname 0.0.0.0)
fi
fi
# Para `oc web`, generar password aleatoria si no existe en el entorno.
if [[ "$WEB_MODE" == true ]]; then
WEB_SERVER_USERNAME=$(id -un)
if [[ -z "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
if command -v openssl &>/dev/null; then
GENERATED_SERVER_PASSWORD=$(openssl rand -base64 24 | tr -d '\n' | tr -d '=+/')
else
GENERATED_SERVER_PASSWORD=$(hexdump -n 16 -ve '1/1 "%02x"' /dev/urandom)
fi
fi
fi
# --- Detectar runtime (Docker o Podman) --------------------------------------
detect_runtime() {
if [[ -n "$FORCE_RUNTIME" ]]; then
if ! command -v "$FORCE_RUNTIME" &>/dev/null; then
err "${FORCE_RUNTIME} no está instalado"
exit 1
fi
echo "$FORCE_RUNTIME"
return
fi
# Preferir Podman si está disponible (más seguro por rootless)
if command -v podman &>/dev/null; then
echo "podman"
elif command -v docker &>/dev/null; then
echo "docker"
else
err "No se encontró Docker ni Podman instalado"
exit 1
fi
}
RUNTIME=$(detect_runtime)
log "Runtime: ${RUNTIME}"
# Verificar que el runtime está operativo
if [[ "$RUNTIME" == "docker" ]]; then
if ! docker info &>/dev/null; then
err "Docker daemon no está corriendo. Prueba: sudo systemctl start docker"
exit 1
fi
else
if ! podman info &>/dev/null; then
warn "Podman no responde correctamente"
if command -v docker &>/dev/null && docker info &>/dev/null; then
warn "Fallback automático a Docker"
RUNTIME="docker"
log "Runtime: ${RUNTIME}"
else
err "Podman no responde y Docker no está disponible"
exit 1
fi
fi
fi
IS_PODMAN=false
[[ "$RUNTIME" == "podman" ]] && IS_PODMAN=true
# --- Verificar auth de opencode ----------------------------------------------
# Rutas de opencode — iguales en macOS y Linux según la doc oficial
# ~/.local/share/opencode → auth.json, sesiones, logs
# ~/.config/opencode → config, agents, skills, plugins
OPENCODE_DATA_DIR="${HOME}/.local/share/opencode"
OPENCODE_CONFIG_DIR="${HOME}/.config/opencode"
OPENCODE_AUTH_FILE="${OPENCODE_DATA_DIR}/auth.json"
if [[ ! -f "$OPENCODE_AUTH_FILE" ]]; then
warn "No se encontró auth.json en ${OPENCODE_AUTH_FILE}"
warn "Ejecuta primero: opencode auth login"
warn "Continuando de todos modos..."
fi
# --- Verificar que no estamos lanzando desde el directorio de datos de opencode ---
if [[ "$PWD" == "${OPENCODE_DATA_DIR}"* || "$PWD" == "${OPENCODE_CONFIG_DIR}"* ]]; then
err "Estás ejecutando el script desde un directorio interno de opencode: ${PWD}"
err "Navega primero al directorio de tu proyecto: cd ~/mi-proyecto"
exit 1
fi
# --- UID/GID y rutas del container -------------------------------------------
HOST_UID=$(id -u)
HOST_GID=$(id -g)
# Con Podman rootless, --userns=keep-id mapea el UID del host automáticamente.
# El container corre como root del user namespace pero se ve como el usuario
# del host en los ficheros montados. Usamos /root como HOME.
# Con Docker creamos un usuario no-root con el UID/GID exacto del host.
if [[ "$IS_PODMAN" == true ]]; then
CONTAINER_HOME="/root"
else
CONTAINER_HOME="/home/${CONTAINER_USER}"
fi
WORKSPACE_PATH="${CONTAINER_HOME}/workspace"
# --- Dockerfile inline -------------------------------------------------------
TMPDIR_BUILD=$(mktemp -d)
trap 'rm -rf "$TMPDIR_BUILD"' EXIT
if [[ "$IS_PODMAN" == true ]]; then
cat >"${TMPDIR_BUILD}/Dockerfile" <<DOCKERFILE
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
git \
openssh-client \
bash \
jq \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /root/.config/opencode \
/root/.local/share/opencode \
/root/.ssh \
/root/workspace
WORKDIR /root
ENV HOME=/root
RUN curl -fsSL https://opencode.ai/install | bash
ENV PATH="/root/.opencode/bin:\${PATH}"
WORKDIR /root/workspace
CMD ["opencode"]
DOCKERFILE
else
cat >"${TMPDIR_BUILD}/Dockerfile" <<DOCKERFILE
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
git \
openssh-client \
bash \
jq \
&& rm -rf /var/lib/apt/lists/*
# Crear usuario con el mismo UID/GID que el host para evitar problemas de permisos
RUN groupadd -g ${HOST_GID} ${CONTAINER_USER} 2>/dev/null || true && \
useradd -m -u ${HOST_UID} -g ${HOST_GID} -s /bin/bash ${CONTAINER_USER} 2>/dev/null || \
usermod -u ${HOST_UID} -g ${HOST_GID} ${CONTAINER_USER} 2>/dev/null || true && \
mkdir -p ${CONTAINER_HOME}/.config/opencode \
${CONTAINER_HOME}/.local/share/opencode \
${CONTAINER_HOME}/.ssh \
${WORKSPACE_PATH} \
&& chown -R ${HOST_UID}:${HOST_GID} ${CONTAINER_HOME}
USER ${CONTAINER_USER}
WORKDIR ${CONTAINER_HOME}
ENV HOME=${CONTAINER_HOME}
RUN curl -fsSL https://opencode.ai/install | bash
ENV PATH="${CONTAINER_HOME}/.opencode/bin:\${PATH}"
WORKDIR ${WORKSPACE_PATH}
CMD ["opencode"]
DOCKERFILE
fi
# --- Build de imagen ---------------------------------------------------------
# Imagen separada por runtime para evitar conflictos de UID entre builds
IMAGE_FULL="${IMAGE_NAME}-${RUNTIME}:${IMAGE_TAG}"
needs_build=false
if [[ "$FORCE_BUILD" == true ]]; then
log "Forzando rebuild de imagen ${IMAGE_FULL}..."
needs_build=true
elif ! $RUNTIME image inspect "$IMAGE_FULL" &>/dev/null; then
log "Imagen ${IMAGE_FULL} no encontrada, construyendo..."
needs_build=true
fi
if [[ "$needs_build" == true ]]; then
log "Construyendo imagen (esto puede tardar un momento)..."
$RUNTIME build \
--tag "$IMAGE_FULL" \
--file "${TMPDIR_BUILD}/Dockerfile" \
"${TMPDIR_BUILD}"
log "Imagen construida: ${IMAGE_FULL}"
fi
# --- Montar SSH key ----------------------------------------------------------
SSH_MOUNT=()
SSH_KEY="${HOME}/.ssh/id_ed25519"
SSH_KEY_RSA="${HOME}/.ssh/id_rsa"
if [[ -f "$SSH_KEY" ]]; then
SSH_MOUNT+=(-v "${SSH_KEY}:${CONTAINER_HOME}/.ssh/id_ed25519:ro")
log "SSH key montada: id_ed25519"
elif [[ -f "$SSH_KEY_RSA" ]]; then
SSH_MOUNT+=(-v "${SSH_KEY_RSA}:${CONTAINER_HOME}/.ssh/id_rsa:ro")
log "SSH key montada: id_rsa"
else
warn "No se encontró SSH key en ~/.ssh — git por SSH no funcionará"
fi
if [[ -f "${HOME}/.ssh/known_hosts" ]]; then
SSH_MOUNT+=(-v "${HOME}/.ssh/known_hosts:${CONTAINER_HOME}/.ssh/known_hosts:ro")
fi
# --- Propagar API keys del entorno -------------------------------------------
ENV_VARS=()
for key in ANTHROPIC_API_KEY OPENAI_API_KEY GOOGLE_API_KEY GITHUB_TOKEN GH_TOKEN; do
if [[ -n "${!key:-}" ]]; then
ENV_VARS+=(-e "${key}=${!key}")
log "Propagando: ${key}"
fi
done
if [[ "$WEB_MODE" == true ]]; then
ENV_VARS+=(-e "OPENCODE_SERVER_USERNAME=${WEB_SERVER_USERNAME}")
log "Web username: ${WEB_SERVER_USERNAME}"
if [[ -n "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
ENV_VARS+=(-e "OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD}")
log "Usando OPENCODE_SERVER_PASSWORD del entorno"
log "Web password: ${OPENCODE_SERVER_PASSWORD}"
elif [[ -n "$GENERATED_SERVER_PASSWORD" ]]; then
ENV_VARS+=(-e "OPENCODE_SERVER_PASSWORD=${GENERATED_SERVER_PASSWORD}")
log "OPENCODE_SERVER_PASSWORD generado automáticamente"
log "Web password: ${GENERATED_SERVER_PASSWORD}"
fi
fi
# --- Opciones específicas por runtime ----------------------------------------
RUNTIME_OPTS=()
if [[ "$IS_PODMAN" == true ]]; then
# --userns=keep-id: clave en Podman rootless.
# Mapea el UID del usuario host al UID dentro del namespace del container,
# por lo que los ficheros creados en volúmenes montados tienen el propietario
# correcto sin necesidad de crear usuarios en el Dockerfile.
RUNTIME_OPTS+=(--userns=keep-id)
RUNTIME_OPTS+=(--cap-drop ALL)
else
# Docker: caps mínimas necesarias para que opencode funcione
RUNTIME_OPTS+=(
--security-opt no-new-privileges
--cap-drop ALL
--cap-add CHOWN
--cap-add SETUID
--cap-add SETGID
)
fi
# --- Puertos OAuth/Web (para autenticación y acceso externo) -----------------
# OpenCode redirige el callback OAuth a localhost en estos puertos.
# Publicamos en 0.0.0.0 para permitir acceso desde otras interfaces de red.
# Puerto por defecto de opencode auth: 1455
# Añadimos rango por si el puerto está ocupado y opencode busca el siguiente.
PORTS=()
for port in 4096 1455 1456 1457 1458 1459; do
PORTS+=(-p "0.0.0.0:${port}:${port}")
done
# --- Nombre del container (basado en directorio del proyecto) ----------------
PROJECT_NAME=$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g')
CONTAINER_NAME="opencode-${PROJECT_NAME}"
# Limpiar container previo del mismo proyecto (corriendo o parado)
if $RUNTIME container inspect "$CONTAINER_NAME" &>/dev/null; then
STATUS=$($RUNTIME inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "unknown")
if [[ "$STATUS" == "running" ]]; then
warn "Container existente en ejecución: ${CONTAINER_NAME}. Deteniéndolo antes de arrancar..."
$RUNTIME stop "$CONTAINER_NAME" &>/dev/null || true
fi
$RUNTIME rm "$CONTAINER_NAME" &>/dev/null || true
fi
# --- Determinar comando a ejecutar -------------------------------------------
if [[ "$OPEN_SHELL" == true ]]; then
CMD=("bash")
log "Abriendo shell en el container..."
else
CMD=("opencode")
log "Lanzando opencode en: ${PWD}"
# Compatibilidad con bash + set -u cuando el array está vacío.
if [[ -n "${PASSTHROUGH_ARGS[*]-}" ]]; then
CMD+=("${PASSTHROUGH_ARGS[@]}")
log "Args para opencode: ${PASSTHROUGH_ARGS[*]}"
fi
fi
# --- Ejecutar container ------------------------------------------------------
log "Iniciando container: ${CONTAINER_NAME}"
cleanup_on_interrupt() {
warn "Ctrl+C detectado: deteniendo container ${CONTAINER_NAME}..."
$RUNTIME stop "$CONTAINER_NAME" &>/dev/null || true
}
trap cleanup_on_interrupt INT TERM
$RUNTIME run \
--rm \
--detach \
--interactive \
--tty \
--name "$CONTAINER_NAME" \
-v "${PWD}:${WORKSPACE_PATH}" \
-w "${WORKSPACE_PATH}" \
-v "${OPENCODE_CONFIG_DIR}:${CONTAINER_HOME}/.config/opencode" \
-v "${OPENCODE_DATA_DIR}:${CONTAINER_HOME}/.local/share/opencode" \
-v "${HOME}/.gitconfig:${CONTAINER_HOME}/.gitconfig:ro" \
"${SSH_MOUNT[@]+"${SSH_MOUNT[@]+"${SSH_MOUNT[@]}"}"}" \
"${ENV_VARS[@]+"${ENV_VARS[@]}"}" \
"${RUNTIME_OPTS[@]+"${RUNTIME_OPTS[@]}"}" \
"${PORTS[@]+"${PORTS[@]}"}" \
"$IMAGE_FULL" \
"${CMD[@]}" \
>/dev/null
set +e
$RUNTIME attach --sig-proxy=false "$CONTAINER_NAME"
ATTACH_EXIT=$?
set -e
trap - INT TERM
# Si la sesión se interrumpe y el container sigue vivo, lo detenemos.
if $RUNTIME container inspect "$CONTAINER_NAME" &>/dev/null; then
STATUS=$($RUNTIME inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "unknown")
if [[ "$STATUS" == "running" ]]; then
warn "La sesión terminó pero el container sigue corriendo. Deteniéndolo..."
$RUNTIME stop "$CONTAINER_NAME" &>/dev/null || true
fi
fi
exit "$ATTACH_EXIT"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment