• Tools used with DevContainers such as, Mise, Chezmoi, and Neovim - including all the troubleshooting, mistakes, and lessons learned along the way.

  • This article documents my journey setting up a portable development environment. Personal references (usernames, repo names, paths) have been generalized to make the content applicable to any reader.

Table of Contents

  1. Introduction
  2. The Initial Goal
  3. The Technology Stack
  4. The Setup Process
  5. Troubleshooting Chronicles
  6. Integration with Obsidian
  7. Making It Portable
  8. Final Architecture
  9. Lessons Learned
  10. Resources

Introduction

I wanted to build a portable development environment that I could replicate across any project or machine. What seemed like a simple container setup turned into a deep dive into dotfiles management, tool installation strategies, and the intricacies of containerized development environments.

This post documents the entire journey - not just the final working solution, but all the problems encountered, and how I eventually arrived at a truly portable, reproducible development environment.

The Initial Goal

What I wanted:

  • A consistent, modern terminal setup
  • Portable configuration that works across any project
  • Integration with my existing Obsidian note-taking system
  • Neovim with LazyVim for editing
  • ZSH with a modern prompt (Starship)
  • Ability to run Docker within the dev container

The Challenge: Starting with minimal DevContainer experience, I needed to understand:

  • How DevContainers work
  • How to manage dotfiles across containers
  • How tool installation should be orchestrated
  • How to separate project-level and personal configurations

The Technology Stack

Core Components

DevPod - A lightweight alternative to VS Code dev containers

  • Simpler than VS Code’s implementation
  • Works from the command line
  • No IDE lock-in
  • Manages multiple workspace environments
  • Provider-agnostic (Docker, Kubernetes, SSH, etc.)

Mise - Modern development tool version manager

  • Replaces asdf, nvm, rbenv, etc.
  • Supports multiple registries (aqua, core, cargo)
  • Can run tasks like Make
  • Two-level configuration strategy

Chezmoi - Dotfiles manager

  • Templates and external sources
  • Git-based synchronization
  • Handles file permissions correctly
  • Can run installation scripts

LazyVim - Neovim distribution

  • Pre-configured plugins
  • Sensible defaults
  • Extensible plugin system

Supporting Tools

  • Pure - ZSH prompt theme
  • Telescope - Fuzzy finder for Neovim
  • Tmux - Terminal multiplexer
  • Ripgrep - Fast text search
  • FZF - Command-line fuzzy finder
  • Bat - Better cat with syntax highlighting

The Setup Process

Phase 0: Installing and Configuring DevPod

I needed to install DevPod and understand how it manages development environments.

Installation:

DevPod supports multiple platforms. On Linux:

1
2
3
4
5
6
7
# Download and install
curl -L -o devpod "https://github.com/loft-sh/devpod/releases/latest/download/devpod-linux-amd64"
sudo install -c -m 0755 devpod /usr/local/bin
rm -f devpod

# Verify installation
devpod version

Understanding DevPod Workspace Management:

DevPod treats each project directory as a potential “workspace”. When you run devpod up in a directory, it:

  1. Reads the .devcontainer/devcontainer.json configuration
  2. Builds a container based on that configuration
  3. Names the workspace after the directory (by default)
  4. Manages the lifecycle of that container

Key DevPod Commands:

 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
# List all workspaces
devpod list

# Create/start a workspace from current directory
devpod up .

# Create/start a workspace from remote repo
devpod up github.com/username/repo

# SSH into a workspace
devpod ssh workspace-name
# Or use the convenient shorthand:
ssh workspace-name.devpod

# Stop a workspace (keeps it for later)
devpod stop workspace-name

# Delete a workspace completely
devpod delete workspace-name

# View workspace logs
devpod logs workspace-name

# Get workspace status
devpod status workspace-name

Example workspace listing:

1
2
3
4
5
6
$ devpod list

     NAME          |         SOURCE          | PROVIDER | IDE  | AGE
  -----------------+-------------------------+----------+------+---------
    my-project     | local:/home/user/project| docker   | none | 2h30m
    k8s-lab        | local:/home/user/lab    | docker   | none | 45m

Provider Configuration:

DevPod uses “providers” to create containers. The Docker provider is simplest for local development:

1
2
3
4
5
6
7
8
# Add Docker provider (usually automatic)
devpod provider add docker

# Set as default
devpod provider use docker

# List available providers
devpod provider list

DevPod Configuration Files:

DevPod stores its configuration in ~/.devpod/:

~/.devpod/
├── contexts/          # Different DevPod contexts
├── provider/          # Provider configurations
└── workspaces/        # Workspace metadata

The .devcontainer Directory:

Every project with DevPod needs a .devcontainer/ directory:

project/
├── .devcontainer/
│   ├── devcontainer.json    # Main configuration
│   └── Dockerfile            # Container definition (optional)
├── [your project files]

At minimum, devcontainer.json needs:

1
2
3
4
{
  "name": "My Dev Container",
  "image": "ubuntu:22.04"
}

But as you’ll see, we’ll use a custom Dockerfile for more control.

Phase 1: Basic Container Structure

Started with a minimal Dockerfile:

1
2
3
4
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
COPY --from=jdxcode/mise /usr/local/bin/mise /usr/local/bin/
RUN echo 'eval "$(mise activate bash)"' >> /home/vscode/.bashrc && \
    echo 'eval "$(mise activate zsh)"' >> /home/vscode/.zshrc

Why this base?

  • Ubuntu 24.04 LTS for stability
  • Pre-installed with common dev tools
  • Uses vscode user (non-root by default)
  • Mise activation happens automatically on shell startup

Phase 2: Project-Level Configuration

Created mise.toml in the project root:

1
2
3
4
5
[tools]
chezmoi = "latest"

[tasks.setup]
run = "/workspaces/my-project/setup"

The Strategy:

  • Minimal project requirements (just chezmoi)
  • Setup task orchestrates the rest
  • Chezmoi pulls personal configuration

Phase 3: Setup Script

The setup script handles system packages and initialization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
set -euo pipefail

# Install system packages that can't come from mise
sudo apt update && sudo apt install tmux -y

# Initialize chezmoi with dotfiles
if [ ! -d "$HOME/.local/share/chezmoi" ]; then
    chezmoi init --apply [email protected]:username/dotfiles.git
fi

# Change default shell to zsh
if command -v zsh >/dev/null; then
    sudo chsh -s $(command -v zsh) $USER
fi

# Setup pure prompt
if [ ! -d "$HOME/.zsh/pure" ]; then
    mkdir -p "$HOME/.zsh"
    git clone https://github.com/sindresorhus/pure.git "$HOME/.zsh/pure"
fi

exit 0

Phase 4: Dotfiles Repository

Created a separate dotfiles repository with:

dotfiles/
├── .chezmoiexternals/
│   └── neovim.toml          # LazyVim starter configuration
├── .chezmoiscripts/
│   └── install_packages.sh   # Install additional tools via mise
├── dot_bashrc
├── dot_zshrc
├── dot_vimrc
├── dot_config/
│   ├── mise/
│   │   └── config.toml      # Personal tool definitions
│   ├── nvim/
│   │   └── lua/
│   │       └── plugins/     # Neovim plugin overrides
│   └── starship/
└── dot_local/
    └── bin/                 # Personal scripts

Key Innovation: Two-Level Mise Configuration

Project level defines baseline:

1
2
3
4
# Project mise.toml
[tools]
chezmoi = "latest"
starship = "latest"

Personal level defines everything else:

1
2
3
4
5
6
7
8
# ~/.config/mise/config.toml (managed by chezmoi)
[tools]
bat = "latest"
kubectl = "latest"
fzf = "latest"
neovim = "latest"
ripgrep = "latest"
node = "latest"

Phase 5: DevContainer Configuration

The devcontainer.json ties everything together:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "build": {
    "dockerfile": "Dockerfile",
    "context": "."
  },
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "postCreateCommand": "mise trust && mise install && mise run setup",
  "mounts": [
    "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
    "source=${localEnv:HOME}/obsidian/YourVault,target=/home/vscode/notes,type=bind,consistency=cached"
  ]
}

Mount Strategy:

  • SSH keys for git authentication
  • Obsidian vault for integrated note-taking
  • Both use bind mounts for instant sync

Troubleshooting Chronicles

Issue 1: Terminal Getting Stuck

Problem: After enabling vi-mode in ZSH, the terminal appeared to hang after pressing Enter.

Diagnosis: Vi-mode defaults to command mode. The cursor was waiting for input but I was in the wrong mode.

Solution: Press i to enter insert mode. Added this to my muscle memory.This sounds trivial but its essential to understaning Vim

Learning: Shell modes are powerful but require understanding the mental model. Document mode transitions.

Issue 2: Tool Activation vs Installation

Problem: Shell environment not reflecting installed tools.

Investigation:

1
2
which tool    # Found: installed
echo $SHELL   # Confirmed: /usr/bin/zsh

Root Cause: Tools weren’t being initialized in .zshrc.

Solution: Added proper initialization to dotfiles:

1
2
# ~/.zshrc
eval "$(tool init zsh)"

Learning: Tool installation ≠ tool activation. Many CLI tools require explicit initialization in your shell configuration.

Issue 3: Neovim Wasn’t Actually Neovim

Problem: Typing v opened basic vim, not LazyVim.

The Confusion: I thought I had Neovim installed.

Reality Check:

1
2
which nvim   # not found
which vim    # /usr/bin/vim (basic vim)

Root Cause: I had created Neovim configuration files but never actually installed Neovim itself via mise.

Solution: Added neovim = "latest" to personal mise config.

Learning: Configuration without installation accomplishes nothing. Always verify tools are actually installed before debugging configuration.

Issue 4: GitHub API Rate Limiting

Problem: Repeated container rebuilds failed with 503 errors:

HTTP status server error (503 Service Unavailable)
aqua:neovim/neovim@latest: failed to download

Root Cause: GitHub rate limits API requests. Rebuilding containers 10+ times in an hour exhausted the free tier quota.

Workaround: Wait for rate limit to reset (shown in error message).This can be anything from 10 minutes to 30 minutes depending.

Better Solution:

  • Fix multiple issues before rebuilding
  • Use ssh into existing container for testing
  • Make changes incrementally
  • Cache is your friend - DevPod reuses layers

Learning: Cloud services have limits. Rapid iteration has costs. Plan your rebuilds strategically.

Issue 5: ZSH Parse Errors After Merge

Problem:

1
/home/vscode/.zshrc:127: parse error near `<<<'

Root Cause: unresolved merge conflict markers in .zshrc.

Why It Happened: Editing dotfiles both locally and from inside containers created divergent changes.

Solution:

  1. Chose a single source of truth (host machine at ~/github-repos/dotfiles)
  2. Never edit chezmoi-managed files directly
  3. Always edit in the dotfiles repo, then chezmoi apply

Learning: Dotfiles management requires discipline. Pick one workflow and stick to it.

Issue 6: Telescope Not Found

Problem:

E492: Not an editor command: Telescope find_files

Root Cause: LazyVim was installed but Telescope plugin wasn’t explicitly configured.

Solution: Created plugin configuration:

1
2
3
4
5
6
7
-- ~/.config/nvim/lua/plugins/telescope.lua
return {
  "nvim-telescope/telescope.nvim",
  dependencies = {
    "nvim-lua/plenary.nvim",
  },
}

Then ran :Lazy sync in Neovim.

Learning: Plugin distributions like LazyVim provide a framework, but you still need to explicitly add the plugins you want. Check the plugin list carefully.

Issue 7: Aliases Not Working After Dotfiles Update

Problem: After fixing .zshrc and rebuilding, the nf and ng aliases still didn’t work.

Diagnosis:

1
2
alias nf  # not found
cat ~/.zshrc | grep nf  # Found the alias definition

Root Cause: Shell wasn’t reloaded after chezmoi applied changes.

Solutions:

1
2
3
4
5
6
7
8
9
# Option 1: Reload shell config
source ~/.zshrc

# Option 2: Start new shell
exec zsh

# Option 3: Exit and re-enter container (most thorough)
exit
ssh devcontainer-pybash.devpod

Learning: Configuration changes don’t apply automatically. The shell reads config files at startup. Reload explicitly after changes.

Issue 8: Wrong Workspace Path in New Container

Problem: Creating k8s-lab container failed:

[setup] $ /workspaces/devcontainer-pybash/setup
sh: 1: /workspaces/devcontainer-pybash/setup: not found

Root Cause: The mise.toml had hardcoded path:

1
2
[tasks.setup]
run = "/workspaces/my-first-project/setup"

But the new container workspace was /workspaces/k8s-lab/.

Solution: Update path for each project:

1
2
[tasks.setup]
run = "/workspaces/k8s-lab/setup"

Learning: When copying configuration between projects, search for hardcoded paths. Some configuration is project-specific even when most is portable.

Integration with Obsidian

One of my requirements was seamless note-taking integration. I use Obsidian with a PARA structure (Projects, Areas, Resources, Archive) for knowledge management.

The Mount Strategy

1
2
3
4
5
{
  "mounts": [
    "source=${localEnv:HOME}/obsidian/Ciarans_Vault,target=/home/vscode/notes,type=bind,consistency=cached"
  ]
}

This gives bidirectional sync:

  • Edit in Neovim inside container → Changes appear in Obsidian on host
  • Edit in Obsidian on host → Changes available in container immediately

Note-Taking Aliases

Added to .zshrc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Navigation shortcuts
alias n='cd ~/notes'
alias ni='cd ~/notes/0\ Inbox'
alias np='cd ~/notes/1\ Projects'

# Note creation
alias nn='nvim "$HOME/notes/0 Inbox/${*}.md"'

# Search with Telescope
alias nf='cd ~/notes && nvim -c "Telescope find_files"'
alias ng='cd ~/notes && nvim -c "Telescope live_grep"'

The Workflow

During development:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Working on k8s manifests
cd /workspaces/k8s-lab
vim deployment.yaml

# Need to document something
nn "kubernetes deployment patterns"
# Opens Neovim with new note in Obsidian inbox

# Search existing notes about containers
ng "docker networking"
# Opens Telescope grep search across all notes

Note structure:

~/notes/
├── 0 Inbox/              # Quick captures
├── 1 Projects/           # Project-specific notes
│   ├── k8s-lab/
│   └── my-first-project/
├── 2 Areas/              # Ongoing responsibilities
│   ├── Homelab/
│   └── TechBlog/
├── 3 Resources/          # Reference material
└── 4 Archive/            # Completed items

Helper Scripts

Created nn script for quick note creation:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# ~/.local/bin/nn
if [ -z "$*" ]; then
    title="Untitled"
else
    title=$(echo "$*" | tr ' ' '-')
fi
filename="$HOME/notes/0 Inbox/${title}.md"
nvim "$filename"

Usage:

1
2
nn kubernetes networking
# Creates and opens: ~/notes/0 Inbox/kubernetes-networking.md

Making It Portable

The final test: Can I replicate this setup in a new project?

Creating the Second Container

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# In new project directory
cd ~/github-repos/k8s-lab

# Copy configuration from template
cp -r ../my-first-project/.devcontainer .
cp ../my-first-project/mise.toml .
cp ../my-first-project/setup .

# Update workspace path in mise.toml
sed -i 's|my-first-project|k8s-lab|g' mise.toml

# Commit to project repo
git add .
git commit -m "Add devcontainer configuration"
git push

# Build container
devpod up .

What Transfers Automatically

Personal Configuration (via dotfiles repo):

  • ZSH config with aliases
  • Neovim with LazyVim
  • Starship prompt
  • Personal mise tools (kubectl, fzf, bat, ripgrep, etc.)
  • Custom scripts in ~/.local/bin/

Mounted Resources:

  • SSH keys (for git authentication)
  • Obsidian vault (at ~/notes)

Container Features:

  • Docker-in-Docker
  • ZSH as default shell
  • Tmux installed

What Needs Per-Project Adjustment

Project-Specific Paths:

  • Workspace path in mise.toml setup task
  • Any hardcoded references to the project directory

The Result

1
2
3
4
$ devpod list
NAME                  | SOURCE                         | PROVIDER | AGE
my-first-project      | ~/github-repos/my-first-project| docker   | 4h
k8s-lab              | ~/github-repos/k8s-lab          | docker   | 15m

Two completely independent containers, identical configuration, different projects. Adding a third would take less than 5 minutes.

Final Architecture

Repository Structure

GitHub Repositories:
├── dotfiles/                    # Personal configuration
│   ├── dot_zshrc
│   ├── dot_config/
│   │   ├── mise/config.toml    # Personal tools
│   │   └── nvim/               # Neovim config
│   ├── dot_local/bin/          # Personal scripts
│   └── .chezmoiexternals/      # LazyVim starter
│
├── my-first-project/           # First project
│   ├── .devcontainer/
│   │   ├── Dockerfile
│   │   └── devcontainer.json
│   ├── mise.toml              # Project tools
│   ├── setup                  # Setup script
│   └── [project files]
│
└── k8s-lab/                   # Second project
    ├── .devcontainer/
    │   ├── Dockerfile
    │   └── devcontainer.json
    ├── mise.toml
    ├── setup
    └── [project files]

Startup Sequence

When running devpod up .:

  1. Build Phase

    • DevPod reads .devcontainer/devcontainer.json
    • Builds image from Dockerfile
    • Installs mise from image
  2. Container Start

    • Mounts SSH keys from host
    • Mounts Obsidian vault from host
    • Sets up Docker-in-Docker
  3. Post-Create Command

    1
    
    mise trust && mise install && mise run setup
    
    • mise trust: Trusts the project’s mise.toml
    • mise install: Installs tools (chezmoi)
    • mise run setup: Executes setup script
  4. Setup Script

    • Installs system packages (tmux)
    • Initializes chezmoi with dotfiles repo
    • Changes default shell to zsh
    • Installs pure prompt
  5. Chezmoi Apply

    • Clones dotfiles repository
    • Applies all dotfiles templates
    • Runs .chezmoiscripts/install_packages.sh
    • Installs personal mise tools (bat, kubectl, fzf, neovim, etc.)
    • Pulls LazyVim starter configuration
  6. Ready State

    • ZSH with custom prompt active
    • Neovim with LazyVim available
    • All personal tools installed
    • Aliases and scripts ready
    • Notes accessible at ~/notes

Configuration Layers

┌─────────────────────────────────────┐
│         Project Layer               │
│  - Dockerfile                       │
│  - devcontainer.json                │
│  - mise.toml (chezmoi, starship)    │
│  - setup script                     │
└─────────────────────────────────────┘
              ↓
┌─────────────────────────────────────┐
│       Personal Layer (Dotfiles)     │
│  - .zshrc, .bashrc, .vimrc          │
│  - mise tools (kubectl, fzf, etc.)  │
│  - neovim configuration             │
│  - custom aliases & scripts         │
└─────────────────────────────────────┘
              ↓
┌─────────────────────────────────────┐
│          Host Mounts                │
│  - SSH keys                         │
│  - Obsidian vault                   │
└─────────────────────────────────────┘

Lessons Learned

1. Separate Project and Personal Concerns

The Pattern:

  • Project repos define minimum requirements
  • Personal dotfiles repo contains preferences
  • Chezmoi bridges the gap

Why It Works:

  • Project configuration is shareable
  • Personal configuration stays private
  • Changes to either don’t affect the other

2. Tool Installation vs Configuration

The Mistake: Creating configuration files without installing tools.

The Learning: Always verify:

1
2
which toolname    # Is it installed?
toolname --version # Does it work?

Configuration is worthless without the underlying tool.

3. Understanding the Container Boundary

What Happens Where:

  • Container: Tool execution, file operations, shell environment
  • Host: Font rendering, terminal emulation, file storage (via mounts)
  • Bridge: SSH keys and Obsidian vault (mounted from host)

Impact: Some problems (like font rendering) can’t be solved in the container. Know the architecture.

4. Rate Limits Are Real

The Problem: Cloud services limit API usage.

The Strategy:

  • Batch your changes
  • Test in running containers when possible
  • Use Docker layer caching
  • Wait when you hit limits (they reset)

5. Version Control Everything

What to Track:

  • Dotfiles (obviously)
  • DevContainer configuration
  • Setup scripts
  • Even experimental configurations

Why: Git history becomes your troubleshooting log. “What changed between the working version and now?”

6. Documentation Pays Off

This Blog Post Exists Because:

  • I documented problems as I hit them
  • I saved error messages
  • I noted what didn’t work and why

Future Benefit: When I hit similar issues (or help someone else), the solutions are documented.

7. Mise’s Two-Level Strategy Is Powerful

Project Level: Defines team requirements

1
2
3
[tools]
python = "3.11"
terraform = "1.6"

Personal Level: Defines your preferences

1
2
3
4
[tools]
bat = "latest"
fzf = "latest"
neovim = "latest"

Everyone gets their preferred tools without cluttering project config.

8. Chezmoi Externals Are Underrated

The Feature: Pull configuration from external sources.

Example: LazyVim starter config

1
2
3
4
[".config/nvim"]
type = "archive"
url = "https://github.com/LazyVim/starter/archive/refs/heads/main.tar.gz"
stripComponents = 1

Benefit: Maintain base configs separately, personalize via overrides.

9. Aliases Are Force Multipliers

Before:

1
cd ~/notes && nvim -c "Telescope find_files"

After:

1
nf

Investment: 5 minutes to create alias. Return: Saved 30 seconds, dozens of times per day, forever.

10. Perfect Is the Enemy of Done

The Temptation: Rebuild containers endlessly chasing perfection.

The Reality: A working 95% solution that you actually use beats a theoretical 100% solution you never finish.

Example: The font rendering issue. Could have obsessed over it for hours. Instead: 5-minute fix on host machine, moved on.

Common Pitfalls to Avoid

1. Editing Dotfiles in Multiple Places

Wrong:

  • Edit .zshrc in container
  • Edit dot_zshrc in dotfiles repo
  • Confusion when they diverge

Right:

  • Edit only in dotfiles repo
  • Run chezmoi apply to sync
  • Container gets changes automatically

2. Forgetting to Reload After Config Changes

Wrong:

1
2
3
vim ~/.zshrc
# Make changes
# Exit and expect them to work

Right:

1
2
vim ~/.zshrc
source ~/.zshrc  # or: exec zsh

3. Installing Tools Without Activation

Wrong:

1
2
mise install starship
# Shell still looks plain

Right:

1
2
3
mise install starship
echo 'eval "$(starship init zsh)"' >> ~/.zshrc
exec zsh

4. Hardcoding Paths

Wrong:

1
2
[tasks.setup]
run = "/workspaces/devcontainer-pybash/setup"

Better: Use project-specific mise.toml and remember to update paths when copying.

Best: Use environment variables where possible:

1
2
[tasks.setup]
run = "$PWD/setup"

5. Not Checking Tool Installation

Wrong:

1
2
# Assume tool installed because config exists
vim ~/.config/tool/config.yml

Right:

1
2
3
which tool  # Verify it's installed
tool --version  # Verify it works
# Then configure

Quick Reference

Essential Commands

 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
# DevPod
devpod up .              # Create/start container
devpod list              # List containers
devpod delete <name>     # Delete container
devpod ssh <name>        # SSH into container (or just ssh <name>.devpod)

# Mise
mise trust               # Trust config file
mise install             # Install all tools
mise install <tool>      # Install specific tool
mise list                # List installed tools
mise run <task>          # Run defined task

# Chezmoi
chezmoi init --apply <repo>   # Initialize with dotfiles
chezmoi apply                 # Apply changes
chezmoi cd                    # Go to dotfiles repo
chezmoi edit <file>           # Edit a dotfile
chezmoi diff                  # See what would change

# Container Operations
ssh my-workspace.devpod          # SSH into container  
exit                             # Exit container

# Note-Taking (custom aliases)
n                        # Jump to notes directory
ni                       # Jump to inbox
np                       # Jump to projects
nn "title"               # Create new note
nf                       # Fuzzy find files
ng                       # Grep search content

File Locations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Project Configuration
/workspaces/<project>/.devcontainer/
/workspaces/<project>/mise.toml
/workspaces/<project>/setup

# Personal Configuration (via Dotfiles)
~/.zshrc                          # Shell config
~/.config/mise/config.toml        # Personal tools
~/.config/nvim/                   # Neovim config
~/.local/bin/                     # Personal scripts

# Chezmoi
~/.local/share/chezmoi/           # Dotfiles repo clone

# Notes (if using Obsidian integration)
~/notes/                          # Obsidian vault mount

Dotfiles Template Structure

dotfiles/
├── .chezmoiignore                    # Files to skip
├── .chezmoiexternals/
│   └── neovim.toml                   # External configs
├── .chezmoiscripts/
│   └── install_packages.sh           # Installation script
├── dot_bashrc                        # → ~/.bashrc
├── dot_zshrc                         # → ~/.zshrc
├── dot_vimrc                         # → ~/.vimrc
├── dot_config/
│   ├── mise/
│   │   └── config.toml              # → ~/.config/mise/config.toml
│   ├── nvim/
│   │   └── lua/
│   │       ├── config/              # → ~/.config/nvim/lua/config/
│   │       └── plugins/             # → ~/.config/nvim/lua/plugins/
│   └── starship/
│       └── starship.toml            # → ~/.config/starship/starship.toml
└── dot_local/
    └── bin/
        ├── executable_nn            # → ~/.local/bin/nn (note creation)
        └── executable_notes         # → ~/.local/bin/notes (note browser)

Resources

Official Documentation

  • Nerd Fonts - Fonts with programming ligatures and icons
  • Pure Prompt - Minimal ZSH prompt
  • Ripgrep - Fast grep alternative
  • FZF - Command-line fuzzy finder
  • Bat - Cat with syntax highlighting

Conclusion

Building this portable development environment took significantly longer than expected. What I thought would be a few hours of configuration turned into days of troubleshooting, learning, and iteration.

  1. Consistency: Every project starts with the same powerful environment
  2. Portability: Can replicate on any machine in minutes
  3. Isolation: Projects don’t interfere with each other
  4. Integration: Notes and code live side-by-side
  5. Learning: Deep understanding of development tooling

This took me a few weeks to get my head around it. DevContainers, dotfiles management, and development tooling are deep topics. You’ll hit problems I didn’t encounter, and you’ll solve problems I struggled with more easily.

My Advice:

  1. Start Simple: Begin with a basic Dockerfile and devcontainer.json. Add complexity gradually.

  2. Document Everything: Keep notes on what you’re doing and why. When something breaks, you’ll want that history.

  3. Embrace Iteration: Your first attempt won’t be perfect. That’s fine. Each rebuild teaches you something.

  4. Separate Concerns: Keep project config separate from personal config. Future you will thank present you.

  5. Test Before Committing: Make sure changes work before pushing to your dotfiles repo. A broken dotfiles repo breaks all your containers.

  6. Use Version Control: Commit often. Git history is your safety net.

  7. Read Error Messages: Really read them. They usually tell you exactly what’s wrong.

  8. Know When to Stop: Perfect is the enemy of done. Ship it and be happy with the imperfections