Last active
February 26, 2026 13:18
-
-
Save frandieguez/47416be1772ab32540e255b4ebf82f03 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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