Skip to content

Commit a98402f

Browse files
authored
feat(devcontainer): overhaul dev environment for chatwoot-br fork (#12)
## Summary Complete overhaul of the devcontainer configuration for the chatwoot-br fork, introducing a three-layer Docker build chain (base → devx → app), multi-architecture support (amd64 + arm64), rich developer tooling, and automated CI/CD for image publishing. - **Three-layer Docker build**: `Dockerfile.base` (Ruby, Node, Overmind, GH CLI) → `Dockerfile.devx` (Fish, Neovim, lazygit, delta, yazi, Playwright, Claude Code, Codex) → `Dockerfile` (thin app layer with deps + source) - **Multi-arch support**: All binary downloads handle `TARGETARCH` for both amd64 and arm64 (Mac M-series) - **Security hardening**: API key handling uses file-based storage with `chmod 600`, shell injection fix in key helper script - **Persistent mounts**: Claude config, Neovim config, shell history, and shared code volume survive container rebuilds - **CI/CD pipeline**: GitHub Actions workflow builds and pushes both base and devx images to GHCR on `.devcontainer/` changes - **CLAUDE.md architecture docs**: Added high-level architecture overview for faster onboarding ## How to test 1. Clone the repo and open in VS Code with Dev Containers extension 2. Verify container builds and starts successfully 3. Confirm Fish shell, Neovim, lazygit, and other devx tools are available 4. Run `overmind start -f Procfile.dev` to verify Rails + Vite start correctly 5. On Mac M-series: verify the container runs natively on ARM64
2 parents 899fce1 + 9b05d35 commit a98402f

23 files changed

Lines changed: 823 additions & 135 deletions

.devcontainer/Dockerfile

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# The below image is created out of the Dockerfile.base
22
# It has the dependencies already installed so that codespace will boot up fast
3-
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
3+
ARG DEVX_IMAGE=ghcr.io/chatwoot-br/chatwoot_codespace_devx:latest
4+
FROM ${DEVX_IMAGE}
45

56
# Do the set up required for chatwoot app
6-
WORKDIR /workspace
7+
WORKDIR /workspace/chatwoot
78

89
# Copy dependency files first for better caching
9-
COPY package.json pnpm-lock.yaml ./
10-
COPY Gemfile Gemfile.lock ./
10+
COPY --chown=vscode:vscode package.json pnpm-lock.yaml ./
11+
COPY --chown=vscode:vscode Gemfile Gemfile.lock ./
1112

1213
# Install dependencies (will be cached if files don't change)
1314
RUN pnpm install --frozen-lockfile && \
1415
gem install bundler && \
1516
bundle install --jobs=$(nproc)
1617

1718
# Copy source code after dependencies are installed
18-
COPY . /workspace
19+
COPY --chown=vscode:vscode . /workspace/chatwoot

.devcontainer/Dockerfile.base

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
44

55
ENV DEBIAN_FRONTEND=noninteractive
66

7-
ARG NODE_VERSION
8-
ARG RUBY_VERSION
9-
ARG USER_UID
10-
ARG USER_GID
7+
ARG NODE_VERSION="24.14.1"
8+
ARG RUBY_VERSION="3.4.4"
9+
ARG USER_UID="1000"
10+
ARG USER_GID="1000"
11+
ARG TARGETARCH
1112
ARG PNPM_VERSION="10.2.0"
12-
ENV PNPM_VERSION ${PNPM_VERSION}
13+
ENV PNPM_VERSION=${PNPM_VERSION}
1314
ENV RUBY_CONFIGURE_OPTS=--disable-install-doc
1415

1516
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
@@ -38,14 +39,15 @@ RUN NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1) \
3839
ca-certificates \
3940
tmux \
4041
nodejs \
42+
unzip \
4143
&& apt-get clean \
4244
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
4345

4446
# Install rbenv and ruby for root user first
4547
RUN git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv \
4648
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
4749
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
48-
ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH"
50+
ENV PATH="/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH"
4951
RUN git clone --depth 1 https://github.com/rbenv/ruby-build.git && \
5052
PREFIX=/usr/local ./ruby-build/install.sh
5153

@@ -60,25 +62,26 @@ RUN su - vscode -c "git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rb
6062
&& su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv install $RUBY_VERSION" \
6163
&& su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv global $RUBY_VERSION"
6264

63-
# Install overmind and gh in single layer
64-
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
65+
# Overmind (process manager)
66+
ARG OVERMIND_VERSION=2.5.1
67+
RUN OVERMIND_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") && \
68+
curl -L "https://github.com/DarthSim/overmind/releases/download/v${OVERMIND_VERSION}/overmind-v${OVERMIND_VERSION}-linux-${OVERMIND_ARCH}.gz" > overmind.gz \
6569
&& gunzip overmind.gz \
6670
&& mv overmind /usr/local/bin \
67-
&& chmod +x /usr/local/bin/overmind \
68-
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
69-
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
70-
&& apt-get update \
71-
&& apt-get install -y --no-install-recommends gh \
72-
&& apt-get clean \
73-
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
71+
&& chmod +x /usr/local/bin/overmind
7472

73+
# GitHub CLI
74+
ARG GH_VERSION=2.88.1
75+
RUN GH_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") && \
76+
curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" \
77+
| tar xz --strip-components=2 -C /usr/local/bin "gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh"
7578

76-
# Do the set up required for chatwoot app
77-
WORKDIR /workspace
78-
RUN chown vscode:vscode /workspace
79+
# --- App setup ---
80+
WORKDIR /workspace/chatwoot
81+
RUN chown vscode:vscode /workspace/chatwoot
7982

80-
# set up node js, pnpm and claude code in single layer
81-
RUN npm install -g pnpm@${PNPM_VERSION} @anthropic-ai/claude-code \
83+
# set up pnpm
84+
RUN npm install -g pnpm@${PNPM_VERSION} \
8285
&& npm cache clean --force
8386

8487
# Switch to vscode user
@@ -95,4 +98,4 @@ RUN eval "$(rbenv init -)" \
9598
&& pnpm install --frozen-lockfile
9699

97100
# Copy source code after dependencies are installed
98-
COPY --chown=vscode:vscode . /workspace
101+
COPY --chown=vscode:vscode . /workspace/chatwoot

.devcontainer/Dockerfile.devx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# DevX tooling layer — sits between Dockerfile.base and Dockerfile
2+
# Build: docker-compose -f .devcontainer/docker-compose.base.yml build devx
3+
ARG BASE_IMAGE=ghcr.io/chatwoot-br/chatwoot_codespace:latest
4+
FROM ${BASE_IMAGE}
5+
6+
USER root
7+
8+
ARG TARGETARCH
9+
ARG USERNAME=vscode
10+
11+
# DevX system dependencies (not needed by Rails itself)
12+
RUN apt-get update && apt-get install -y --no-install-recommends \
13+
fzf \
14+
ripgrep \
15+
jq \
16+
socat \
17+
file \
18+
poppler-utils \
19+
man-db \
20+
bubblewrap \
21+
&& apt-get clean \
22+
&& rm -rf /var/lib/apt/lists/*
23+
24+
# Fish shell
25+
ARG FISH_VERSION=4.6.0
26+
RUN FISH_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") && \
27+
curl -fsSL "https://github.com/fish-shell/fish-shell/releases/download/${FISH_VERSION}/fish-${FISH_VERSION}-linux-${FISH_ARCH}.tar.xz" \
28+
| tar xJ -C /usr/local/bin && \
29+
echo /usr/local/bin/fish >> /etc/shells
30+
31+
# Neovim (LazyVim requires >= 0.11.2)
32+
ARG NVIM_VERSION=0.12.0
33+
RUN NVIM_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "x86_64") && \
34+
curl -fsSL "https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-linux-${NVIM_ARCH}.tar.gz" \
35+
| tar xz --strip-components=1 -C /usr/local && \
36+
ln -s /usr/local/bin/nvim /usr/local/bin/vim
37+
38+
# Configure vscode user: set shell to fish
39+
RUN usermod -s /usr/local/bin/fish $USERNAME
40+
41+
# lazygit
42+
ARG LAZYGIT_VERSION=0.60.0
43+
RUN LAZYGIT_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "x86_64") && \
44+
curl -fsSL "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_${LAZYGIT_ARCH}.tar.gz" \
45+
| tar xz -C /usr/local/bin lazygit
46+
47+
# git-delta
48+
ARG DELTA_VERSION=0.19.1
49+
RUN DELTA_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") && \
50+
curl -fsSL "https://github.com/dandavison/delta/releases/download/${DELTA_VERSION}/delta-${DELTA_VERSION}-${DELTA_ARCH}-unknown-linux-gnu.tar.gz" \
51+
| tar xz --strip-components=1 -C /usr/local/bin "delta-${DELTA_VERSION}-${DELTA_ARCH}-unknown-linux-gnu/delta"
52+
53+
# yazi + ya companion (terminal file manager)
54+
ARG YAZI_VERSION=26.1.22
55+
RUN YAZI_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") && \
56+
curl -fsSL "https://github.com/sxyazi/yazi/releases/download/v${YAZI_VERSION}/yazi-${YAZI_ARCH}-unknown-linux-musl.zip" -o /tmp/yazi.zip && \
57+
unzip /tmp/yazi.zip -d /tmp/yazi && \
58+
mv /tmp/yazi/yazi-${YAZI_ARCH}-unknown-linux-musl/yazi /usr/local/bin/ && \
59+
mv /tmp/yazi/yazi-${YAZI_ARCH}-unknown-linux-musl/ya /usr/local/bin/ && \
60+
rm -rf /tmp/yazi.zip /tmp/yazi
61+
62+
# glow (markdown renderer)
63+
ARG GLOW_VERSION=2.1.1
64+
RUN GLOW_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "x86_64") && \
65+
curl -fsSL "https://github.com/charmbracelet/glow/releases/download/v${GLOW_VERSION}/glow_${GLOW_VERSION}_Linux_${GLOW_ARCH}.tar.gz" \
66+
| tar xz --strip-components=1 -C /usr/local/bin "glow_${GLOW_VERSION}_Linux_${GLOW_ARCH}/glow"
67+
68+
# chafa >= 1.16 from source (yazi image preview; Debian ships 1.14)
69+
ARG CHAFA_VERSION=1.18.1
70+
RUN apt-get update && apt-get install -y --no-install-recommends \
71+
libglib2.0-dev libjpeg-dev libpng-dev libwebp-dev libfreetype6-dev && \
72+
curl -fsSL -o /tmp/chafa.tar.xz \
73+
"https://github.com/hpjansson/chafa/releases/download/${CHAFA_VERSION}/chafa-${CHAFA_VERSION}.tar.xz" && \
74+
cd /tmp && tar xf chafa.tar.xz && cd chafa-${CHAFA_VERSION} && \
75+
./configure --prefix=/usr/local --disable-static --disable-man && \
76+
make -j$(nproc) && make install && ldconfig && \
77+
cd / && rm -rf /tmp/chafa* && \
78+
apt-get purge -y --auto-remove libglib2.0-dev libjpeg-dev libpng-dev libwebp-dev libfreetype6-dev && \
79+
apt-get clean && rm -rf /var/lib/apt/lists/*
80+
81+
# Playwright + Chromium (browser automation / E2E tests)
82+
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
83+
RUN npx playwright install --with-deps chromium && \
84+
chmod -R a+rwx /opt/playwright && \
85+
CHROME_BIN="$(find /opt/playwright -name chrome -type f | head -1)" && \
86+
mkdir -p /opt/google/chrome && \
87+
ln -s "$CHROME_BIN" /opt/google/chrome/chrome && \
88+
apt-get clean && rm -rf /var/lib/apt/lists/* && \
89+
npm cache clean --force && \
90+
rm -rf /root/.cache/ms-playwright
91+
92+
# pnpm via corepack (overrides base npm-installed pnpm)
93+
ARG PNPM_VERSION="10.33.0"
94+
RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate
95+
96+
# Starship prompt (pinned version)
97+
ARG STARSHIP_VERSION=1.22.1
98+
RUN curl -fsSL "https://github.com/starship/starship/releases/download/v${STARSHIP_VERSION}/starship-$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64")-unknown-linux-musl.tar.gz" \
99+
| tar xz -C /usr/local/bin starship
100+
101+
# Ghostty terminal integration (pinned version)
102+
ARG GHOSTTY_SHELL_TAG=v1.1.3
103+
COPY .devcontainer/ghostty/xterm-ghostty.terminfo /tmp/xterm-ghostty.terminfo
104+
RUN tic -x /tmp/xterm-ghostty.terminfo && rm /tmp/xterm-ghostty.terminfo
105+
RUN mkdir -p /usr/share/ghostty/shell-integration/fish/vendor_conf.d \
106+
/usr/share/ghostty/shell-integration/bash && \
107+
curl -fsSL "https://raw.githubusercontent.com/ghostty-org/ghostty/${GHOSTTY_SHELL_TAG}/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish" \
108+
-o /usr/share/ghostty/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish && \
109+
curl -fsSL "https://raw.githubusercontent.com/ghostty-org/ghostty/${GHOSTTY_SHELL_TAG}/src/shell-integration/bash/ghostty.bash" \
110+
-o /usr/share/ghostty/shell-integration/bash/ghostty.bash
111+
112+
# Fish system configs (/etc/fish/conf.d/ is the only path not shadowed by bind mounts)
113+
RUN mkdir -p /etc/fish/conf.d
114+
COPY .devcontainer/ghostty/ghostty-shell-integration.fish /etc/fish/conf.d/ghostty-shell-integration.fish
115+
RUN echo 'starship init fish | source' > /etc/fish/conf.d/starship.fish
116+
RUN echo 'fish_add_path -gP ~/.claude/bin' > /etc/fish/conf.d/claude-path.fish
117+
RUN echo 'status is-interactive; and rbenv init - fish | source' > /etc/fish/conf.d/rbenv.fish
118+
119+
# Pre-create user-local directories so bind mounts don't leave root-owned parents
120+
RUN mkdir -p /home/$USERNAME/.local/share /home/$USERNAME/.local/state \
121+
&& chown -R $USERNAME:$USERNAME /home/$USERNAME/.local
122+
123+
# Global npm tools
124+
RUN npm install -g @anthropic-ai/claude-code \
125+
&& npm cache clean --force
126+
127+
# Switch to vscode user
128+
USER vscode
129+
ENV PATH="/home/vscode/.rbenv/bin:/home/vscode/.rbenv/shims:/home/vscode/.claude/bin:/home/vscode/.bun/bin:/home/vscode/.local/bin:$PATH"
130+
131+
# bun (JS runtime/bundler, pinned version)
132+
ARG BUN_VERSION=1.2.17
133+
RUN curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}"
134+
135+
# Git config for delta
136+
RUN git config --global core.pager delta && \
137+
git config --global interactive.diffFilter "delta --color-only" && \
138+
git config --global delta.navigate true && \
139+
git config --global delta.side-by-side true && \
140+
git config --global merge.conflictstyle zdiff3

0 commit comments

Comments
 (0)