1. Web & Serve
바깥에서도 모바일로 Agent에게 작업을 시키고 싶었다.
그래서 기존에 잘 사용하던 OpenCode를 서버에 직접 호스팅해봤다.
OpenCode는 처음부터 프론트엔드와 백엔드를 분리해서 개발된 구조다.
이 구조 덕분에 서버에 백엔드만 올리고, 다양한 방식으로 접근하는 구성이 가능하다.
opencode web
Code language: Bash (bash)
OpenCode는 기본적으로 Web 인터페이스를 제공한다.
서버에 배포하면 웹 브라우저를 통해 바로 접근 가능하다.
내부망에 OpenCode를 배포하고 VPN으로 접근하는 방식도 가능하다.
opencode serve
Code language: Bash (bash)
OpenCode Serve는 일종의 헤드리스(headless) 실행 방식이다.
UI 없이 백엔드만 실행해 일종의 API 서버처럼 동작한다.
Serve 기능을 활용하면 다양한 방식으로 확장이 가능하다.
플러그인을 이용하면 디스코드나 텔레그램 같은 메신저와 연동할 수 있다.
2. 설치
가. 설치 1차 시도
OPENCODE_VERSION=latest
PORT=81
OPENCODE_USERNAME=opencode
OPENCODE_PASSWORD=verylonganddifficultpassword
chmod 600 .env를 실행해서 나만 볼 수 있게 설정하자.
services:
opencode:
image: "ghcr.io/anomalyco/opencode:${OPENCODE_VERSION:-latest}"
restart: unless-stopped
ports:
- "${PORT:-4096}:4096"
env_file:
- .env
environment:
HOME: /var/lib/opencode
OPENCODE_SERVER_USERNAME: ${OPENCODE_USERNAME}
OPENCODE_SERVER_PASSWORD: ${OPENCODE_PASSWORD}
entrypoint:
- opencode
command:
- web
- --hostname=0.0.0.0
- --port=4096
volumes:
- "./workspace:/workspace"
- "./data:/var/lib/opencode/.local/share/opencode"
- "./config:/var/lib/opencode/.config/opencode"
working_dir: /workspace
Code language: YAML (yaml)
Opencode 공식 이미지를 사용하는 docker-compose.yml 파일을 추가한다.
{
"$schema": "https://opencode.ai/config.json",
"autoupdate": true,
"share": "disabled"
}
Code language: JSON / JSON with Comments (json)
OpenCode 설정 문서에서는 share를 config에서 비활성화하는 걸 권장함.
./config/opencode.json을 만들고 내용 붙여넣음.
웹에서 터미널이 안 열리는 버그있음.
나. 설치 2차 시도
26.04.06일 기준 공식이미지에 웹에서 터미널이 열리지 않는 버그가 있다.
찾아보니 베이스 이미지를 Alphine으로 바꾸면서 pty에 필요한 glibc라는 것이 musl로 대체되었기 때문이다.
문제가 해결될 때까지 도커 이미지를 직접 만들어 써야겠다.
FROM node:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y \
ca-certificates \
curl \
bash \
unzip \
tini \
&& rm -rf /var/lib/apt/lists/*
# bun 설치
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
# opencode 설치
RUN bun add -g opencode-ai@latest
WORKDIR /workspace
EXPOSE 4096
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["opencode", "web", "--hostname", "0.0.0.0", "--port", "4096"]
Code language: Dockerfile (dockerfile)
debian 계열을 기반으로 하는 도커 이미지를 직접 만든다.
services:
opencode:
build: .
restart: unless-stopped
ports:
- "${PORT:-4096}:4096"
env_file:
- .env
environment:
HOME: /var/lib/opencode
OPENCODE_SERVER_USERNAME: ${OPENCODE_USERNAME}
OPENCODE_SERVER_PASSWORD: ${OPENCODE_PASSWORD}
volumes:
- "./workspace:/workspace"
- "./opencode-home:/var/lib/opencode"
working_dir: /workspace
Code language: YAML (yaml)
docker-compose.yml 옆에 Dockerfile을 추가한다.
PORT=81
OPENCODE_USERNAME=opencode
OPENCODE_PASSWORD=verylonganddifficultpassword
더이상 OPENCODE_VERSION은 필요없다.
chmod 600 .env를 실행해서 나만 볼 수 있게 설정하자.
{
"$schema": "https://opencode.ai/config.json",
"autoupdate": true,
"share": "disabled"
}
Code language: JSON / JSON with Comments (json)
./config/opencode.json는 1차 시도와 동일하다.
다. 설치 3차 시도
opencode 자체 로그인 기능이 문제가 있다.
HTTP Basic Auth를 사용해서 로그인 기능을 구현했는데, 어떠한 이유로 로그인 요청이 무한 반복되는 버그가 있다.
services:
opencode:
build: .
restart: unless-stopped
ports:
- "${PORT:-4096}:4096"
env_file:
- .env
environment:
HOME: /var/lib/opencode
#OPENCODE_SERVER_USERNAME: ${OPENCODE_USERNAME}
#OPENCODE_SERVER_PASSWORD: ${OPENCODE_PASSWORD}
volumes:
- "./workspace:/workspace"
- "./opencode-home:/var/lib/opencode"
working_dir: /workspace
Code language: YAML (yaml)
docker-compose.yml에서 username과 password를 비활성화하면 잘 동작한다.

하지만 극히 위험하다.
하… 귀찮네…
services:
opencode:
build: .
restart: unless-stopped
expose:
- "4096"
environment:
HOME: /var/lib/opencode
volumes:
- "./workspace:/workspace"
- "./opencode-home:/var/lib/opencode"
working_dir: /workspace
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "${PORT}:8080"
env_file:
- .env
environment:
BASIC_AUTH_USER: "${BASIC_AUTH_USER}"
BASIC_AUTH_HASH: "${BASIC_AUTH_HASH}"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
depends_on:
- opencode
Code language: YAML (yaml)
자체 인증을 포기하고 대신에 Caddy를 추가해서 대신 인증한다.
Opencode는 port 대신에 expose를 사용해서 직접적인 포트 노출을 막는다.
:8080 {
basicauth {
{$BASIC_AUTH_USER} {$BASIC_AUTH_HASH}
}
reverse_proxy opencode:4096
}
Code language: PHP (php)
docker-compose.yml 옆에 Caddyfile을 만든다.
PORT=81
# To generate a BASIC_AUTH_USER, BASIC_AUTH_HASH plz run generate_caddy_hash.sh
Code language: PHP (php)
.env도 수정한다. chmod 600 .env는 잊지말기.
HASHED_PASSWORD를 생성하기 위해서 generate_caddy_hash.sh를 실행해야 한다.
#!/usr/bin/env bash
set -euo pipefail
ENV_FILE=".env"
echo "=== Caddy Basic Auth Hash Generator ==="
# 사용자 입력
read -rp "Username [opencode]: " USERNAME
USERNAME=${USERNAME:-opencode}
# 비밀번호 입력 (숨김)
read -rsp "Password: " PASSWORD; echo
read -rsp "Confirm Password: " PASSWORD_CONFIRM; echo
if [[ "$PASSWORD" != "$PASSWORD_CONFIRM" ]]; then
echo "❌ Passwords do not match"
exit 1
fi
# 해시 생성 (히스토리 안전)
HASH=$(docker run --rm -i caddy:2 sh -c '
read -r PASSWORD
caddy hash-password --plaintext "$PASSWORD"
' <<< "$PASSWORD")
# 메모리 제거
unset PASSWORD PASSWORD_CONFIRM
# $ escape (docker-compose용)
ESCAPED_HASH=$(printf '%s' "$HASH" | sed 's/\$/\$\$/g')
# 기존 값 제거
if [[ -f "$ENV_FILE" ]]; then
grep -v -E '^(BASIC_AUTH_USER|BASIC_AUTH_HASH)=' "$ENV_FILE" > "${ENV_FILE}.tmp" || true
mv "${ENV_FILE}.tmp" "$ENV_FILE"
else
touch "$ENV_FILE"
fi
# 추가
{
echo "BASIC_AUTH_USER=$USERNAME"
echo "BASIC_AUTH_HASH=$ESCAPED_HASH"
} >> "$ENV_FILE"
echo "✅ .env updated"
echo "BASIC_AUTH_USER=$USERNAME"
echo "BASIC_AUTH_HASH saved"
Code language: Bash (bash)
generate_caddy_hash.sh는 이렇게 생겼다.
chmod 700 generate_caddy_hash.sh로 실행 권한을 부여한다.

되나?

아오 진짜.
테스트해보니 앞에 인증이 붙기라도 하면 무한 인증 루프에 빠진다.
따라서 지금으로선 어떻게 하든 안전하게 사용할 방법이 없다.
Opencode Web을 호스팅하는 것은 포기한다.
라. 설치 4차 시도
지나가던 중 우연히 나랑 비슷한 증상을 겪고 있는 이슈를 보았다.
그런데 그곳에서 인증 정보를 추가하는 UI가 있다는 새로운 사실을 알게 되었다.

우측 상단에 서버 아이콘을 클릭하고 서버 관리를 누른다.

원하는 서버를 선택하고 사용자 이름과 비밀번호를 채운다.

와… 된다.

로컬에서 테스트하고 바로 서버에 적용해보니 된다!
아니 이런거를 해야하면 안내를 해주던가. 이게 뭐하는 짓인지…
마. 최종 버전
#!/usr/bin/env bash
set -Eeuo pipefail
ENV_FILE=".env"
# 필요한 디렉토리 생성
mkdir -p workspace opencode-home
# 권한 (호스트 UID/GID 기준으로 맞춤)
chown "$(id -u)":"$(id -g)" workspace opencode-home
# .env 준비
touch "${ENV_FILE}"
# UID/GID 중복 없이 갱신
{
grep -Ev '^(UID|GID)=' "${ENV_FILE}" 2>/dev/null || true
echo "UID=$(id -u)"
echo "GID=$(id -g)"
} > "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "${ENV_FILE}"
chmod 600 "${ENV_FILE}"
# 실행
docker compose up -d
Code language: Bash (bash)
setup.sh를 만들었다.
이전의 작업물 때문에 변경사항이 잘 적용되지 않는다면 실행하기 전에 docker builder prune -f, docker rmi <이미지id>로 캐시를 지워주는 좋다.
#!/usr/bin/env bash
set -Eeuo pipefail
NEW_HOME="${HOME}"
OLD_HOME="/home/node"
mkdir -p "${NEW_HOME}"
chmod 700 "${NEW_HOME}" 2>/dev/null || true
if [ "${NEW_HOME}" != "${OLD_HOME}" ]; then
[ ! -e "${NEW_HOME}/.bashrc" ] && [ -f "${OLD_HOME}/.bashrc" ] && cp "${OLD_HOME}/.bashrc" "${NEW_HOME}/.bashrc"
[ ! -e "${NEW_HOME}/.profile" ] && [ -f "${OLD_HOME}/.profile" ] && cp "${OLD_HOME}/.profile" "${NEW_HOME}/.profile"
fi
cd /workspace
exec "$@"
Code language: Bash (bash)
docker-entrypoint.sh를 추가했다.
HOME을 옮기면서 .bashrc를 읽지 않는 문제가 있어서 추가했다.
FROM node:bookworm-slim
# UID, GID, NODE_PASSWORD는 .env와 docker-compose.yml에서 수정할 수 있음
ARG UID=1000
ARG GID=1000
ARG NODE_PASSWORD=yourpassword
ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y \
ca-certificates \
curl \
bash \
unzip \
tini \
vim \
sudo \
passwd \
&& rm -rf /var/lib/apt/lists/*
RUN groupmod -g "${GID}" node \
&& usermod -u "${UID}" -g "${GID}" node
# node 비밀번호 설정 + sudo 설정
RUN echo "node:${NODE_PASSWORD}" | chpasswd \
&& usermod -aG sudo node \
&& echo "node ALL=(ALL) PASSWD:ALL" > /etc/sudoers.d/node \
&& chmod 0440 /etc/sudoers.d/node
RUN mkdir -p /home/opencode /workspace \
&& chown -R node:node /home/opencode /workspace
USER node
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/home/node/.bun/bin:${PATH}"
RUN bun add -g opencode-ai@latest
WORKDIR /workspace
EXPOSE 4096
COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
CMD ["opencode", "web", "--hostname", "0.0.0.0", "--port", "4096"]
Code language: Dockerfile (dockerfile)
docker-entrypoint.sh를 실행하도록 수정했다.
보안을 강화하기 위해서 root보다 node를 사용하도록 수정했다.
sudo를 사용하기 위한 node의 시작 패스워드는 yourpassword다.
참고로 빌드 시점 비밀번호라서 이미지 레이어나 빌드 히스토리에 남을 수 있습니다.
자신의 비밀번호로 빌드해서 다른 사람에게 공유하지 마세요.
services:
opencode:
build:
context: .
args:
UID: "${UID:-1000}"
GID: "${GID:-1000}"
NODE_PASSWORD: "${NODE_PASSWORD:-yourpassword}"
restart: unless-stopped
ports:
- "${PORT:-4096}:4096"
env_file:
- .env
environment:
HOME: /home/opencode
OPENCODE_SERVER_USERNAME: ${OPENCODE_USERNAME}
OPENCODE_SERVER_PASSWORD: ${OPENCODE_PASSWORD}
volumes:
- "./workspace:/workspace"
- "./opencode-home:/home/opencode"
working_dir: /workspace
Code language: YAML (yaml)
HOME 위치를 수정하고 빌드시 UID/GID를 .env에서 가져올 수 있도록 했다.
PORT=8080
NODE_PASSWORD=yourpassword
OPENCODE_USERNAME=opencode
OPENCODE_PASSWORD=verylonganddifficultpassword
sudo 실행을 위한 NODE_PASSWORD를 추가했다.
.env는 chmod 600 .env를 실행하기.
{
"$schema": "https://opencode.ai/config.json",
"autoupdate": true,
"share": "disabled"
}
Code language: JSON / JSON with Comments (json)
./config/opencode.json는 동일하다.
3. 후기
고난과 역경을 뚫고 결과적으로 4번의 시도 끝에 OpenCode Web을 호스팅 할 수 있었다.
대충할 수도 있었지만 외부에서 원격으로 터미널을 사용할 수 있는거라 각별히 보안에 신경썼다.
억지로 컨테이너에 넣고, root 계정 대신 node 계정 사용하게 하고, 버그?있는 로그인 기능도 어떻게든 사용하려고 노력하고, VPN에 HTTPS까지 엄청 신경썼다.
이제 바깥에서도 일할 수 있어서 너무 좋다.
(나는 알바하면서 토큰을 벌고, AI는 일해서 나를 먹여살리는 아름다운 운명 공동체의 탄생의 순간이 아닐까.)
요즘 토큰 값이 장난이 아닌 것 같다.
OpenCode는 Zen, Go 같이 최대한 경제적으로 토큰을 공급할 수 있는 노력하고 있다.
옆동네랑 비교하면 아주 마음에 든다.
어렵게 구축한 시스템이니 만큼 마르고 닳도록 써먹어야 겠다.