Docker images for consistent, persistent SSH sessions
Devcontainer-ish
20 Jun, 2025 | 2 min
I’ve finally got around to setting up a personal Docker image for my personal development environment. Interestingly, I thought it’d be substantially more effort than it ended up being. I’ve briefly tried to do this several years ago and did not love the experience. The largest breakthrough was integrating proper dotfiles management via YADM and correctly setting configurations up in different OS environments.
The Dockerfile is surprisingly short:
FROM archlinux:multilib-devel AS unfetched
RUN pacman -Syu --noconfirm git cargo inetutils zsh
ARG username
RUN useradd -m -G wheel -s /bin/zsh $username \
&& echo "$username ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
USER $username
WORKDIR /home/$username
RUN cd /tmp \
&& git clone https://aur.archlinux.org/paru.git \
&& cd paru \
&& makepkg -si --noconfirm
RUN paru -S --noconfirm \
neovim \
yadm \
fzf \
ripgrep \
fd \
bat \
htop \
uv \
jujutsu \
neovim-remote \
tailscale \
openssh \
luarocks \
nvidia-open \
gosu
RUN uv tool install llm
RUN mkdir -p -m 700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \
"" --unattended
RUN git clone https://github.com/zsh-users/zsh-autosuggestions \
~/.oh-my-zsh/custom/plugins/zsh-autosuggestions
COPY <<EOF ./entrypoint.sh
#!/bin/bash
yadm clone git@github.com:<dotfiles-repo>.git && yadm alt && yadm checkout -- ~/
exec zsh
EOF
ENTRYPOINT ["/bin/bash", "./entrypoint.sh"]
FROM unfetched AS prefetched
RUN --mount=type=ssh,uid=1000 yadm clone git@github.com:<dotfiles-repo>.git \
&& yadm alt \
&& yadm checkout -- ~/
ENTRYPOINT ["/bin/zsh"]
Based of off Arch Linux, this replicates my Linux desktop experience, with all my development tools and configurations. This approach uses SSH agent forwarding to handle authentication for both updating dotfiles when starting the container and for interacting with repos and the like during development. Sometimes, I’ve had issues with cloud providers having weird virtualization that affects SSH agent forwarding into the container. For those settings (or more generally, ones where I can’t or don’t need SSH), I have a prefetched image that bakes in the dotfiles.
I tend to use a command like this to mount my server’s working home directory in the container (NVIDIA runtime optional).
docker run --rm -it \
--platform linux/amd64 \
# --runtime=nvidia \
--mount type=bind,src=$HOME/.ssh/agent.sock,dst=/agent.sock \
-e SSH_AUTH_SOCK=/agent.sock \
-e TERM=$TERM \
-e SHELL=/bin/zsh \
--mount type=bind,src=$HOME/,dst=/home/$USER/host-dir \
$IMAGE
A great workflow has been to detach from the container once I’ve launched a long-lived data job or training run and follow logs via docker logs <container id>
. I can
safely handle disconnecting from the instance while also having all my preferred configuration and tooling.
Note that $SSH_AUTH_SOCK
can change every time you login. This means if you have a long-running container, and SSH in several times, the container’s mount path will
break. In order to avoid this, I create a symlink from the actual path to a fixed path on login on the host, and then mount that to the container.