Setting Up Starship Prompt in a DevContainer
Starship gives every shell the same fast, informative prompt from a single config — ideal for a devcontainer where the team may use different shells. This page installs Starship at image time, initializes it in each shell, and shares one starship.toml.
This matters because prompt configuration is one of the most personal and most fragmented parts of a shell setup. Left to individual dotfiles, one developer ends up with a git-aware Powerlevel10k prompt in zsh, another with a plain $PS1 in bash, and a third with a Tide theme in fish — and when they screenshot a terminal in a bug report, none of the prompts agree on what branch, exit code, or Python version they were looking at. Starship collapses that into a single Rust binary that reads one starship.toml and renders an identical prompt regardless of which shell invoked it. In a devcontainer, where the whole point is that everyone gets the same environment, letting the prompt drift per person undercuts that guarantee for the one surface every developer stares at all day.
Reach for this setup when your devcontainer needs to support more than one shell, when you want prompt behavior (git status, command duration, language versions) to be reproducible across the team, or when you are tired of porting the same prompt logic between shell dialects. The mental model is a clean separation of three concerns: the binary is installed once at image build time so it is baked into the layer cache and never fetched at container-create time; the init line is a tiny per-shell hook that hands prompt rendering off to Starship; and the starship.toml holds every visual and behavioral decision in one shell-agnostic file. Get those three layers right and the prompt stops being a per-developer accident and becomes part of the reproducible environment.
Prerequisites
You need a devcontainer and a shell to initialize Starship in.
- Starship installed in the image.
- An init line in the shell rc (zsh/fish/bash).
- A shared
starship.tomlconfig.
These prerequisites are less about installing extra software and more about deciding where each piece lives. The Starship binary should be present in the image, not installed by a personal dotfiles script, because anything that runs only at container-create time or only for one user reintroduces the drift you are trying to eliminate. The init line has to exist in the rc file that the shell actually sources for an interactive session — ~/.zshrc for zsh, ~/.config/fish/config.fish for fish, and ~/.bashrc for bash — and it has to be reachable by whatever user the devcontainer runs as, which is frequently a non-root vscode user rather than root.
The one detail people get wrong is font support. Starship's default preset uses Nerd Font glyphs — the branch symbol, the language icons, the powerline separators — and those render as empty boxes or question marks unless the terminal that displays the container is using a patched Nerd Font. That terminal lives on the host, not inside the container, so no amount of image tweaking fixes it; you either install a Nerd Font in your host terminal or choose a plain-text preset in starship.toml. Sorting this out before you start saves you from chasing a "broken prompt" that is actually a perfectly working prompt drawn with the wrong font.
Step-by-Step Implementation
- Install Starship in the image.
RUN curl -fsSL https://starship.rs/install.sh | sh -s -- -y
Running the install script as a RUN step bakes the binary into an image layer, so the download happens once at build time and is reused from the layer cache on every subsequent container create — not re-fetched each time someone rebuilds. The -fsSL flags on curl are deliberate: -f makes curl fail on an HTTP error instead of piping an error page into sh, -s and -S keep it quiet but still surface real errors, and -L follows the redirect the starship.rs URL issues. The trailing -s -- -y passes -y through to the install script so it installs non-interactively; without it the script would block waiting for a confirmation prompt that a Docker build can never answer, and the build would hang or fail. Installing at image time also means starship is on PATH before any shell rc runs, which is what the next step depends on.
- Initialize it in the shell (zsh shown).
grep -q 'starship init zsh' ~/.zshrc || echo 'eval "$(starship init zsh)"' >> ~/.zshrc
The eval "$(starship init zsh)" line is what actually turns Starship on: starship init zsh prints the shell hooks Starship needs, and eval executes that output in the current shell so the prompt is redrawn by Starship on every command. The grep -q ... || echo ... >> guard is the load-bearing part in a devcontainer. Bootstrap logic in a postCreateCommand or a dotfiles script tends to re-run every time the container is created, and an unguarded echo ... >> ~/.zshrc would append a duplicate init line on each run — leaving you with the same eval stacked five times and a measurably slower shell startup. The grep -q check makes the append idempotent: it only writes the line if it is not already present, so re-running the bootstrap is safe. Match the shell you are configuring — swap zsh for fish or bash and target the matching rc file — because starship init emits different hook code per shell dialect.
- Share one config.
# starship.toml (mounted or copied to ~/.config/starship.toml)
add_newline = false
[git_branch]
symbol = " "
Starship looks for its config at ~/.config/starship.toml by default, so placing the shared file there — whether copied in the Dockerfile or bind-mounted from the repo — is what makes every shell read the same settings. add_newline = false removes the blank line Starship prints above the prompt by default, keeping the terminal compact, which matters more in a devcontainer terminal panel than in a full-screen window. The [git_branch] section overrides the branch symbol; the exact glyph shown here is a Nerd Font icon, and this is precisely the line that renders as a blank box if the host terminal lacks the font. Keeping this config in the repo rather than in personal dotfiles is what turns "my prompt" into "our prompt": a change to starship.toml propagates to the whole team on the next rebuild instead of living only on the machine of whoever tweaked it. If you need a per-machine location, set STARSHIP_CONFIG to point elsewhere, but the default path keeps the setup simple.
- Verify the prompt renders in a new terminal.
After a rebuild, open a fresh terminal rather than reusing an existing one: the init line runs when the rc file is sourced, so a shell that was already open before the change will not show the new prompt until it is restarted. A quick starship --version confirms the binary is on PATH, and echo $STARSHIP_CONFIG (or checking that ~/.config/starship.toml exists) confirms the config is where Starship expects it. If the prompt renders with the branch, directory, and exit-status segments you configured, all three layers — binary, init hook, and config — are wired together correctly.
Common Pitfalls
Starship issues are a missing init line or an unshared config.
When the config is bind-mounted rather than copied, ownership and permissions become the quiet failure mode. A starship.toml mounted from the host arrives owned by whatever UID wrote it there, and if the devcontainer runs as a non-root vscode user whose UID does not match, the file may be read-only or unreadable to the running shell. Starship fails softly here — it does not error loudly, it just falls back to defaults and ignores your config — so the symptom looks like "my settings do nothing" rather than a permission denial. If your carefully edited starship.toml seems to have no effect, check the file's ownership and mode inside the container before assuming the config is wrong; a chown in the Dockerfile or a matching remoteUser/UID often clears it up.
The second common trap is shell-specific: the init line has to live in the rc the shell actually sources for interactive use, and that file differs by shell. Bash on many base images sources ~/.bashrc for interactive non-login shells but ~/.bash_profile or ~/.profile for login shells, so an init line placed in the wrong one silently never runs. Fish ignores ~/.zshrc entirely and expects ~/.config/fish/config.fish. If you added the init line and the prompt still does not appear, confirm you edited the rc that matches both the shell and the way the devcontainer launches it — that mismatch, not a bad Starship install, is usually the culprit.
| Symptom | Root Cause | Remediation |
|---|---|---|
| Prompt not showing | No init line in the rc | Add eval "$(starship init |
| Different prompt per developer | Config in personal dotfiles only | Share starship.toml in the config |
| Init line duplicated | Unguarded append | Guard with grep before appending |
| starship not found | Not installed in the image | Install it in the Dockerfile |
Conclusion
Starship gives one prompt for every shell from a single starship.toml. Install it at image time, add a guarded init line to the shell rc, and share the config so the whole team sees the same prompt — fast, informative, and shell-agnostic.
The strategic payoff is that the prompt stops being a variable in your debugging. When every developer's terminal shows the same branch indicator, the same command-duration readout, and the same exit-status glyph, a screenshot in a bug report is unambiguous and a pairing session does not start with "why does your prompt look different from mine." The prompt joins the rest of the reproducible environment instead of being the one surface each person hand-tunes in secret.
This ties directly into the pin-and-cache theme. Installing the binary in a RUN layer pins Starship into the image and caches it, so container creation stays fast and offline-friendly; committing starship.toml to the repo makes prompt behavior a versioned, reviewable artifact rather than tribal knowledge; and the guarded init line keeps the setup idempotent under repeated bootstraps. Change the prompt the way you change any other config — edit the file, rebuild, and the whole team picks it up on the next create. The prompt you configure once is the prompt everyone gets, every rebuild, in every shell.
FAQ
Why Starship over a shell-specific prompt framework? Starship is cross-shell and compiled, so one config gives zsh, fish, and bash an identical, fast prompt. In a devcontainer where teammates may use different shells, that consistency and speed beats maintaining separate prompt frameworks per shell. A framework like Powerlevel10k or Oh My Posh ties you to one shell's syntax and lifecycle, so supporting a second shell means porting or duplicating the whole prompt definition. Starship reads a single TOML file that neither shell can disagree with, and because it is a native binary rather than interpreted shell functions, prompt render time stays low even with git status and language-version segments enabled — the kind of latency you feel on every keystroke otherwise.
Where should the starship.toml live?
Share it through the config (copied or mounted to ~/.config/starship.toml) so everyone gets the same prompt, rather than leaving it in individual dotfiles. Personal tweaks can still layer on, but the shared config sets the team baseline. Copying it in the Dockerfile bakes it into the image and is the most reproducible option; bind-mounting it from the repo lets you edit and reload without a rebuild but adds the ownership caveat covered above. Whichever you choose, keeping the file under version control means prompt changes go through the same review as any other config, and a STARSHIP_CONFIG override is available if one machine genuinely needs a different path.
How do I keep the init line from duplicating?
Guard the append (grep -q 'starship init' ~/.zshrc || echo … >> ~/.zshrc) since the bootstrap re-runs on each create. Or add the init line in the image's shell rc so it's baked in and never appended at runtime. The baked-in approach is the cleaner of the two because it removes runtime mutation of the rc entirely — the line is present from the first boot and there is nothing to guard against. If you must append at create time, the grep -q guard is what keeps repeated runs idempotent; without it, a stack of identical eval lines accumulates and each one slightly slows shell startup.
Does Starship slow down shell startup?
In practice the impact is small because Starship is a compiled binary and its init step only registers a hook rather than doing heavy work at load time. The prompt is computed per command, and Starship parallelizes and time-limits the modules that gather git or language information so a slow repo cannot stall the prompt. If you do notice lag, the usual cause is a module scanning a large working tree; tune command_timeout in starship.toml or disable the offending module rather than abandoning Starship.
Do I need root to install and configure Starship?
The image build usually runs as root, so the RUN curl … | sh install step writes the binary to a system path without trouble. The init line and config, however, are per-user: they belong to whatever user the devcontainer runs as at runtime, commonly a non-root vscode user. Make sure that user owns its own ~/.zshrc (or equivalent) and its ~/.config/starship.toml, since a file owned by root but read by vscode is exactly the permission mismatch that makes the prompt silently fall back to defaults.
Related
- Up to Shell Environment Customization: zsh, fish, bash — the shell overview.
- Persisting zsh History Across Container Rebuilds — durable shell state alongside the prompt.
- Using GNU Stow for Dotfiles in a DevContainer — managing the config files.