Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
194 lines (175 loc) · 8.33 KB
/
Copy pathDockerfile
File metadata and controls
194 lines (175 loc) · 8.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# Remote dev environment for OpenCode + Openchamber.
# Route Openchamber 3000 externally. OpenCode 4096 binds 0.0.0.0 by default --
# protect with trusted network or front it with auth. Shell access is sshd on
# 2222, key authentication only.
FROM ubuntu:26.04
ENV DEBIAN_FRONTEND=noninteractive \
TZ=UTC \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
# bash with pipefail: a failed `curl` in a `curl ... | sh` pipeline must fail
# the build rather than feeding an empty script to the shell.
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl gnupg tzdata locales sudo \
rsync less tini \
git openssh-client openssh-server \
ripgrep fd-find jq \
python3 python3-venv python3-pip python3-requests \
python-is-python3 \
tmux nano neovim mosh \
direnv \
&& locale-gen en_US.UTF-8 \
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
&& mkdir -p /run/sshd /etc/ssh/host-keys \
&& printf '%s\n' \
'Port 2222' \
'PermitRootLogin no' \
'PasswordAuthentication no' \
'KbdInteractiveAuthentication no' \
'PubkeyAuthentication yes' \
'AllowUsers dev' \
'HostKey /etc/ssh/host-keys/ssh_host_ed25519_key' \
'AuthorizedKeysFile /etc/ssh/authorized_keys/%u .ssh/authorized_keys' \
'ClientAliveInterval 30' \
'ClientAliveCountMax 3' \
'LoginGraceTime 30' \
'MaxAuthTries 3' \
> /etc/ssh/sshd_config.d/00-opencode.conf \
&& chmod 0644 /etc/ssh/sshd_config.d/00-opencode.conf \
&& printf '%s\n' \
'if [ -n "${SSH_TTY:-}" ] && [ -z "${TMUX:-}" ] && command -v tmux >/dev/null 2>&1; then' \
' exec tmux new -A -s main -c /workspace' \
'fi' \
> /etc/profile.d/tmux-attach.sh \
&& chmod 0644 /etc/profile.d/tmux-attach.sh \
&& printf '%s\n' \
'export SSH_AUTH_SOCK=/home/dev/.ssh-agent.sock' \
> /etc/profile.d/ssh-agent.sh \
&& chmod 0644 /etc/profile.d/ssh-agent.sh \
&& printf '%s\n' \
'Host *' \
' UserKnownHostsFile /home/dev/.ssh-state/known_hosts' \
> /etc/ssh/ssh_config.d/00-known-hosts.conf \
&& chmod 0644 /etc/ssh/ssh_config.d/00-known-hosts.conf \
&& rm -rf /var/lib/apt/lists/*
# System-wide tmux defaults; a user's ~/.tmux.conf still overrides these.
# Mouse support gives touch scrolling in mobile SSH clients (Blink, Termius).
RUN printf '%s\n' \
'set -g mouse on' \
'set -g history-limit 50000' \
> /etc/tmux.conf \
&& chmod 0644 /etc/tmux.conf
RUN arch="$(dpkg --print-architecture)" \
&& curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}" \
-o /usr/local/bin/yq \
&& chmod 0755 /usr/local/bin/yq
# Node 26 via NodeSource's distro-agnostic `nodistro` repo. The version check
# fails the build if the repo setup silently no-ops, which would otherwise leave
# the distro's Node 22 installed and only surface at runtime.
RUN set -eux; \
curl -fsSL https://deb.nodesource.com/setup_26.x | bash -; \
apt-get install -y --no-install-recommends nodejs; \
node_version="$(node --version)"; \
case "${node_version}" in \
v26.*) echo "installed node ${node_version}" ;; \
*) echo "ERROR: expected Node 26.x, got ${node_version}" >&2; exit 1 ;; \
esac; \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p -m 755 /etc/apt/keyrings \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update && apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/*
# PHP (Ubuntu 26.04's default, 8.5) for CLI work. date, filter, hash, json,
# pcre, PDO, session, SPL and tokenizer are compiled into php-cli; php-xml
# adds dom, SimpleXML and xml; php-gd adds gd. mbstring, curl and zip keep
# Composer happy; unzip is what Composer uses to unpack dist archives. The
# extension loop fails the build if distro packaging ever drops one.
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
php-cli php-xml php-gd php-mbstring php-curl php-zip unzip; \
php_missing=""; \
for ext in date dom filter gd hash json pcre pdo session simplexml spl tokenizer xml mbstring curl zip; do \
php -r "extension_loaded('${ext}') or exit(1);" || php_missing="${php_missing} ${ext}"; \
done; \
if [ -n "${php_missing}" ]; then \
echo "ERROR: missing PHP extensions:${php_missing}" >&2; \
exit 1; \
fi; \
rm -rf /var/lib/apt/lists/*
# Latest Composer via the official installer. Unpinned on purpose: the weekly
# no-cache rebuild picks up new releases. The version check fails the build if
# upstream ever drifts -- same idea as the Node 26 check above.
RUN set -eux; \
curl -fsSL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \
composer_version="$(composer --version)"; \
case "${composer_version}" in \
"Composer version 2."*) echo "installed ${composer_version}" ;; \
*) echo "ERROR: expected Composer 2.x, got: ${composer_version}" >&2; exit 1 ;; \
esac
RUN curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh
# `npm cache clean` matters here: `npm install -g` leaves the downloaded
# tarballs in /root/.npm, and without the clean they are committed into this
# layer for the life of the image. OpenCode 2.x ships as `@opencode/cli`
# (`opencode-ai` is the 1.x line, which OpenChamber 2.x refuses to use). Its
# postinstall hardlinks or copies the matching platform binary into its own
# bin/, so the per-platform packages can be dropped afterwards.
RUN npm install -g --omit=dev \
backlog.md \
@opencode/cli \
@openchamber/web \
chrome-devtools-mcp \
&& npm cache clean --force \
&& rm -rf /usr/lib/node_modules/@opencode/cli/node_modules/@opencode/cli-* \
&& opencode_version="$(opencode --version)" \
&& case "${opencode_version}" in \
"opencode v2."*) echo "installed ${opencode_version}" ;; \
*) echo "ERROR: expected OpenCode 2.x, got ${opencode_version}" >&2; exit 1 ;; \
esac
ARG DEV_UID=1000
ARG DEV_GID=1000
RUN set -eux; \
existing_group="$(getent group "${DEV_GID}" | cut -d: -f1 || true)"; \
if [ -n "${existing_group}" ]; then \
if [ "${existing_group}" != dev ]; then groupmod -n dev "${existing_group}"; fi; \
else \
groupadd -g "${DEV_GID}" dev; \
fi; \
existing_user="$(getent passwd "${DEV_UID}" | cut -d: -f1 || true)"; \
if [ -n "${existing_user}" ]; then \
if [ "${existing_user}" != dev ]; then usermod -l dev "${existing_user}"; fi; \
usermod -u "${DEV_UID}" -g "${DEV_GID}" -d /home/dev -m -s /bin/bash dev; \
else \
useradd -m -u "${DEV_UID}" -g "${DEV_GID}" -s /bin/bash dev; \
fi; \
usermod -aG sudo dev; \
echo 'dev ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/dev; \
chmod 0440 /etc/sudoers.d/dev; \
mkdir -p \
/workspace \
/home/dev/.ssh-state \
/home/dev/.config/opencode \
/home/dev/.config/openchamber \
/home/dev/.local/share/opencode \
/home/dev/.local/state/opencode; \
chown -R dev:dev /workspace /home/dev/.ssh-state /home/dev/.config /home/dev/.local; \
install -d -m 0755 /etc/opencode
# /etc/opencode must exist before the COPY below: BuildKit applies --chmod to
# any parent directory the COPY creates, and a 0644 directory cannot be
# traversed, which hides environment.md and the mounted opencode.json from dev.
COPY --chmod=0755 start.sh /usr/local/bin/start.sh
COPY --chmod=0644 config/environment.md /etc/opencode/environment.md
USER dev
WORKDIR /workspace
ENV HOME=/home/dev \
SHELL=/bin/bash \
PATH=/home/dev/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
EXPOSE 3000 4096 2222 60000/udp
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsS http://127.0.0.1:3000/ >/dev/null || exit 1
ENTRYPOINT ["tini", "--", "/usr/local/bin/start.sh"]