Hooks & Lifecycle
Complete reference for container lifecycle, hooks, dotfiles, repository cloning, extra commands, and environment variables.
Container lifecycle
Every podspawn container follows this sequence. Understanding it is key to writing hooks that work correctly.
New container (first connection)
1. Image built from Podfile (extra_commands run here, as root)
2. Container created and started (sleep infinity)
3. Non-root user created (UID 1000, with passwordless sudo)
4. Bridge network created
5. Service containers started (postgres, redis, etc.)
6. Dotfiles cloned and install script run
7. Repositories cloned
8. on_create hook runs
9. on_start hook runs
10. Shell attached (or command executed)Existing container (reattach)
1. Podfile re-parsed (so on_start is always available)
2. on_start hook runs
3. Shell attached (or command executed)Steps 6-8 only run once, when the container is first created. The on_start hook runs on every connection, including the first.
on_create
Runs once per container, after dotfiles and repos are cloned, before on_start. Use it for one-time setup: installing project dependencies, running database migrations, compiling assets, seeding data.
on_create: |
cd /workspace/backend && pip install -r requirements.txt
createdb myapp_dev
python manage.py migrateIf the container already exists when you connect, on_create does not run again. To re-run it, destroy and recreate the container.
on_start
Runs on every connection, including the first. Use it for tasks that should happen each time: starting background services, printing a status banner, refreshing tokens.
on_start: |
redis-server --daemonize yes
echo "dev environment ready"On the first connection, on_start runs immediately after on_create. On subsequent connections (reattach), it runs before the shell opens.
Execution context
Both hooks run with these properties:
- User: The non-root container user (UID 1000), not root.
- Working directory:
/home/<username>(the user's home directory). - Shell:
sh -c "<script>". The script is passed as a single string. - Sudo: Available without a password. Podfile-built images always include sudo and a NOPASSWD sudoers entry for the container user.
- Exit code: A non-zero exit is logged as a warning. It does not stop the remaining setup or prevent the shell from opening. The container stays usable.
Environment variables
Hooks have access to:
| Variable | Value | Available in |
|---|---|---|
$HOME | /home/<username> | Always |
$USER | The container username | Always |
$SHELL | The configured shell (e.g., /bin/bash) | Always |
Podfile env vars | Static values baked into the image at build time | Always |
${PODSPAWN_USER} | Expanded at container creation (in env values only) | Runtime env vars |
${PODSPAWN_PROJECT} | Expanded at container creation (in env values only) | Runtime env vars |
Static env vars (those without ${PODSPAWN_ references) are set as ENV directives in the generated Dockerfile, so they are available everywhere including hooks. Dynamic env vars containing ${PODSPAWN_USER} or ${PODSPAWN_PROJECT} are expanded at container creation and passed as runtime environment variables.
env:
EDITOR: vim # static, baked into image
DATABASE_URL: postgres://localhost/myapp # static, baked into image
WORKSPACE: /home/${PODSPAWN_USER}/projects # dynamic, expanded at runtimeDotfiles
The dotfiles field clones a git repository into the container and runs an optional install script. This is the standard way to bring your shell configuration, editor settings, and tool configs into every container.
dotfiles:
repo: https://github.com/youruser/dotfiles.git
install: ./install.sh| Field | Type | Required | Description |
|---|---|---|---|
repo | string | yes | Git URL to clone. |
install | string | no | Command to run after cloning. Executed with sh -c from the dotfiles directory. |
How it works
- The repo is cloned to
/home/<username>/dotfilesas the non-root user. - If
installis set, it runs from within that directory:cd ~/dotfiles && <install command>. - The install script runs as the non-root user. Use
sudoinside the script for operations that need root.
Error handling
- If the clone fails (network error, bad URL, auth failure), the error is logged as a warning. Setup continues.
- If the install script exits non-zero, it is logged as a warning. Setup continues.
Neither failure prevents the container from starting.
Common install approaches
GNU Stow (symlink farm manager):
dotfiles:
repo: https://github.com/youruser/dotfiles.git
install: stow -t $HOME bash vim git tmuxEach directory in your dotfiles repo maps to a program. Stow creates symlinks from ~/dotfiles/bash/.bashrc to ~/.bashrc, etc.
rcm (rc file management):
dotfiles:
repo: https://github.com/youruser/dotfiles.git
install: RCRC=$HOME/dotfiles/rcrc rcup -fCustom install script:
dotfiles:
repo: https://github.com/youruser/dotfiles.git
install: |
ln -sf ~/dotfiles/.bashrc ~/.bashrc
ln -sf ~/dotfiles/.vimrc ~/.vimrc
ln -sf ~/dotfiles/.gitconfig ~/.gitconfigRepository cloning
The repos field clones one or more git repositories into the container. Repos are cloned after dotfiles and before on_create, so your hooks can operate on the cloned code.
repos:
- url: https://github.com/yourorg/backend.git
path: /workspace/backend
branch: main
- url: https://github.com/yourorg/frontend.git
path: /workspace/frontend| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | Git URL of the repository. |
path | string | no | Clone destination inside the container. If omitted, git picks the directory name from the URL. |
branch | string | no | Branch to check out. Cloned with --single-branch for faster downloads. If omitted, the default branch is used. |
Repos are cloned as the non-root container user. If a clone fails, the error is logged as a warning and the remaining repos still clone.
Repository cloning requires git inside the container. If your base image does not include git, add it to the packages list.
extra_commands
The extra_commands field adds raw RUN directives to the generated Dockerfile. Unlike hooks, these run at image build time as root, not at container start.
extra_commands:
- curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
- apt-get install -y libssl-dev libffi-dev
- ldconfigEach entry becomes a separate RUN line in the Dockerfile, after package installation and env vars. Use this for setup that belongs in the image layer: installing system libraries, compiling native extensions, adding package repositories.
The distinction matters:
- When: Image build time
- Runs as: root
- Cached: Yes, in Docker layer cache
- Use for: System packages, native libraries, build tools
- When: Container start (first connection)
- Runs as: Non-root user (sudo available)
- Cached: No, runs every time a new container is created
- Use for: Project dependencies, database setup, migrations
Error handling
Podspawn is intentionally lenient with hook failures. The philosophy is that a partially configured container is more useful than no container.
| Step | On failure |
|---|---|
| Dotfiles clone | Warning logged, setup continues |
| Dotfiles install script | Warning logged, setup continues |
| Repo clone | Warning logged, remaining repos still clone |
on_create | Warning logged, on_start still runs, shell opens |
on_start | Warning logged, shell opens |
A non-zero exit code from any hook or script is never fatal. The container always starts.
User overrides (server mode)
In server mode, individual users can override the project's dotfiles configuration through per-user override files at /etc/podspawn/users/<username>.yaml. This lets each developer bring their own dotfiles to any shared project.
# /etc/podspawn/users/alice.yaml
dotfiles:
repo: https://github.com/alice/dotfiles.git
install: make install
env:
EDITOR: nvimUser overrides replace the project's dotfiles config entirely (no merging). If a user override specifies dotfiles, the project-level dotfiles are ignored for that user.
User overrides can also add or override environment variables. These are merged with the project's env, with user values taking precedence.
See User Overrides for the full reference.
Troubleshooting
Hook fails silently
Hooks log warnings on failure, but those logs may not be visible in your terminal. To debug:
- Open a shell into the container:
podspawn shell <machine> - Run the failing command manually from
/home/<username> - Check the exit code and output
Since hooks run with sh -c, syntax errors or missing commands produce errors that are logged server-side but not shown to the connecting user.
Dotfiles clone fails
Common causes:
- The container does not have
gitinstalled. Addgittopackages. - The repo URL requires authentication but no credentials are available inside the container.
- Network issues from inside the container (DNS, proxy).
on_create ran but setup is incomplete
Remember that on_create runs as the non-root user from $HOME. If your script assumes a different working directory, cd explicitly:
on_create: |
cd /workspace/backend && npm install
cd /workspace/frontend && npm installNeed to re-run on_create
Destroy the container and create a new one. There is no way to re-trigger on_create on an existing container, since it is defined as "runs once on creation."
podspawn stop <machine>
podspawn create <machine>Hook needs root access
Hooks run as the non-root user, but sudo is available without a password:
on_create: |
sudo apt-get update && sudo apt-get install -y ripgrep
npm installHow is this guide?