Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Public container images for cloud work, built multi-arch and rootless with secur
|---|---|
| `ghcr.io/cloudsnacks/actions-runner` | Rootless GitHub Actions runner for [Actions Runner Controller](https://github.com/actions/actions-runner-controller) |
| `ghcr.io/cloudsnacks/claude-code` | Headless Claude Code agent with git-repo and agent-profile bootstrap |
| `ghcr.io/cloudsnacks/dev-desktop` | Arch Linux dev workstation with Hyprland streamed over [Sunshine](https://github.com/LizardByte/Sunshine)/Moonlight, cloud tooling, and AI coding agents (`linux/amd64` only, needs `/dev/dri`) |
| `ghcr.io/cloudsnacks/infisical-mcp` | Infisical MCP server (secrets management over MCP) |
| `ghcr.io/cloudsnacks/kubectl` | Rootless kubectl CLI |
| `ghcr.io/cloudsnacks/sandbox-agent` | Rootless base image for sandboxed coding agents (Node, Python, uv, git, gh, ripgrep) |
Expand Down
178 changes: 178 additions & 0 deletions images/dev-desktop/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
FROM archlinux:base@sha256:592e11bd99ab579f933a0cb77a8f66e2f3ae57f5eafacf13aea44a6e98ef21ae

# Sunshine ships an official Arch package as a release asset, so no AUR/makepkg is needed.
# renovate: datasource=github-releases depName=LizardByte/Sunshine extractVersion=^v(?<version>.+)$
ARG SUNSHINE_VERSION=2026.516.143833
# renovate: datasource=npm depName=@anthropic-ai/claude-code
ARG CLAUDE_CODE_VERSION=2.1.220
# renovate: datasource=npm depName=@openai/codex
ARG CODEX_VERSION=0.145.0
# renovate: datasource=npm depName=@google/gemini-cli
ARG GEMINI_CLI_VERSION=0.52.0
# renovate: datasource=npm depName=opencode-ai
ARG OPENCODE_VERSION=1.18.5

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

ENV LANG=C.UTF-8

# Trim docs/locales before any package lands so every later install stays lean.
# These are [options] directives, so they are inserted into that section rather
# than appended (appending would land them in the last repo section).
# DisableSandbox: pacman 7 drops to the 'alpm' user behind seccomp/Landlock to
# download, which fails under emulated and nested builds. Build-time only.
RUN printf '%s\n' \
'NoExtract = usr/share/doc/*' \
'NoExtract = usr/share/man/* !usr/share/man/man1/*' \
'NoExtract = usr/share/locale/* !usr/share/locale/en*/*' \
'NoExtract = usr/share/i18n/*' \
'DisableSandbox' \
> /tmp/pacman-opts \
&& sed -i '/^\[options\]/r /tmp/pacman-opts' /etc/pacman.conf \
&& rm -f /tmp/pacman-opts

# archlinux:base is rebuilt weekly; refresh the keyring first or signatures fail
# on any package newer than the snapshot.
RUN pacman -Sy --noconfirm archlinux-keyring \
&& pacman -Su --noconfirm \
&& pacman -S --noconfirm --needed \
gcc make pkgconf \
git git-lfs github-cli openssh \
curl wget jq less tree rsync unzip zip which man-db \
ripgrep fd eza bat fzf zoxide starship tmux zsh neovim htop btop \
python python-pip uv nodejs npm go rust \
kubectl helm k9s kubectx stern opentofu terraform aws-cli-v2 azure-cli \
chezmoi \
hyprland xdg-desktop-portal xdg-desktop-portal-hyprland xorg-xwayland \
foot waybar wofi mako \
pipewire pipewire-pulse wireplumber \
mesa vulkan-swrast intel-media-driver libva-utils \
dbus polkit seatd avahi \
ttf-jetbrains-mono-nerd noto-fonts \
&& pacman -Scc --noconfirm \
&& rm -rf /var/cache/pacman/pkg/* /var/lib/pacman/sync/* \
&& XDG_RUNTIME_DIR=/tmp Hyprland --version \
&& kubectl version --client \
&& terraform version \
&& node --version

# pacman -U pulls the package's dependencies from the repos, so the sync DB that
# the previous layer removed has to be refreshed first.
RUN curl -fsSL -o /tmp/sunshine.pkg.tar.zst \
"https://github.com/LizardByte/Sunshine/releases/download/v${SUNSHINE_VERSION}/sunshine-${SUNSHINE_VERSION}-1-x86_64.pkg.tar.zst" \
&& pacman -Sy --noconfirm \
&& pacman -U --noconfirm /tmp/sunshine.pkg.tar.zst \
&& rm -f /tmp/sunshine.pkg.tar.zst \
&& pacman -Scc --noconfirm \
&& rm -rf /var/cache/pacman/pkg/* /var/lib/pacman/sync/* \
&& sunshine --version

# claude-code and opencode fetch their platform-native binary in postinstall, which
# recent npm blocks by default; they are opted in explicitly rather than globally.
RUN npm install -g --no-fund --no-audit \
--allow-scripts=@anthropic-ai/claude-code,opencode-ai \
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
"@openai/codex@${CODEX_VERSION}" \
"@google/gemini-cli@${GEMINI_CLI_VERSION}" \
"opencode-ai@${OPENCODE_VERSION}" \
&& npm cache clean --force \
&& claude --version \
&& codex --version \
&& gemini --version \
&& opencode --version

# video/render gate /dev/dri, input gates /dev/uinput for Moonlight input injection.
RUN groupadd --gid 1001 dev \
&& useradd --uid 1001 --gid dev --create-home --shell /usr/bin/zsh dev \
&& for g in video render input audio seat; do \
getent group "${g}" > /dev/null || groupadd --system "${g}"; \
done \
&& usermod -aG video,render,input,audio,seat dev \
&& mkdir -p /workspace /run/user/1001 \
&& chown dev:dev /workspace /run/user/1001 \
&& chmod 700 /run/user/1001

# Hyprland is PID 1 and starts the session via exec-once; no supervisor is used.
# Written to the user's primary config path rather than relying on an /etc/xdg fallback.
# The directory is dev-owned so Hyprland can write state; the config itself only
# needs to be readable.
RUN install -d -o dev -g dev /home/dev/.config /home/dev/.config/hypr \
&& cat > /home/dev/.config/hypr/hyprland.conf <<'EOF'
# Headless session for remote streaming; the output is created at runtime
# because the headless backend starts with zero outputs.
monitor = HEADLESS-1, 1920x1080@60, 0x0, 1

exec-once = hyprctl output create headless HEADLESS-1
exec-once = /usr/bin/wireplumber
exec-once = /usr/bin/sunshine
exec-once = /usr/bin/waybar
exec-once = /usr/bin/mako

input {
kb_layout = us
follow_mouse = 1
}

general {
gaps_in = 4
gaps_out = 8
border_size = 2
}

decoration {
rounding = 6
}

animations {
enabled = false
}

misc {
disable_hyprland_logo = true
disable_splash_rendering = true
}

$mod = SUPER
bind = $mod, Return, exec, /usr/bin/foot
bind = $mod, Q, killactive,
bind = $mod, E, exec, /usr/bin/wofi --show drun
bind = $mod, F, fullscreen,
EOF

# Fail with a readable message instead of aquamarine's "no allocator available":
# Hyprland's headless backend reports drmFD() == -1, so a real render node must
# be present for a GBM allocator to exist.
RUN cat > /usr/local/bin/start-desktop <<'START' && chmod 0755 /usr/local/bin/start-desktop
#!/usr/bin/env bash
set -euo pipefail

export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
mkdir -p "${XDG_RUNTIME_DIR}"
chmod 700 "${XDG_RUNTIME_DIR}"

if [[ ! -e /dev/dri/renderD128 ]]; then
echo "FATAL: /dev/dri/renderD128 is missing." >&2
echo "Hyprland cannot start without a DRM render node, even headless." >&2
echo "Mount /dev/dri into the pod and set the matching supplementalGroups." >&2
exit 1
fi

exec Hyprland "$@"
START

ENV DOTFILES_REPO=https://github.com/swibrow/dotfiles.git \
XDG_RUNTIME_DIR=/run/user/1001 \
XDG_SESSION_TYPE=wayland \
XDG_CURRENT_DESKTOP=Hyprland \
NPM_CONFIG_PREFIX=/home/dev/.npm \
PATH=/home/dev/.npm/bin:/home/dev/.local/bin:/home/dev/go/bin:/home/dev/.cargo/bin:/usr/local/bin:/usr/bin:/bin

USER dev
WORKDIR /workspace

EXPOSE 47984/tcp 47989/tcp 47990/tcp 48010/tcp
EXPOSE 47998/udp 47999/udp 48000/udp 48002/udp

LABEL org.opencontainers.image.description="Arch Linux dev workstation with Hyprland streamed over Sunshine/Moonlight, cloud tooling, and AI coding agents"

CMD ["/usr/local/bin/start-desktop"]
8 changes: 8 additions & 0 deletions images/dev-desktop/metadata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: dev-desktop
description: Arch Linux dev workstation with Hyprland streamed over Sunshine/Moonlight
version: 1.0.0
platforms:
# archlinux has no official arm64 image, so this image is amd64-only.
- linux/amd64
test: Hyprland --version && sunshine --version && claude --version && codex --version && gemini --version && opencode --version && kubectl version --client && helm version --short && terraform version && tofu version && node --version && python3 --version && go version && uv --version && rg --version && nvim --version && chezmoi --version && test "$(whoami)" = dev