podspawnpodspawn

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 migrate

If 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:

VariableValueAvailable in
$HOME/home/<username>Always
$USERThe container usernameAlways
$SHELLThe configured shell (e.g., /bin/bash)Always
Podfile env varsStatic values baked into the image at build timeAlways
${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 runtime

Dotfiles

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
FieldTypeRequiredDescription
repostringyesGit URL to clone.
installstringnoCommand to run after cloning. Executed with sh -c from the dotfiles directory.

How it works

  1. The repo is cloned to /home/<username>/dotfiles as the non-root user.
  2. If install is set, it runs from within that directory: cd ~/dotfiles && <install command>.
  3. The install script runs as the non-root user. Use sudo inside 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 tmux

Each 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 -f

Custom 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 ~/.gitconfig

Repository 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
FieldTypeRequiredDescription
urlstringyesGit URL of the repository.
pathstringnoClone destination inside the container. If omitted, git picks the directory name from the URL.
branchstringnoBranch 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
  - ldconfig

Each 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.

StepOn failure
Dotfiles cloneWarning logged, setup continues
Dotfiles install scriptWarning logged, setup continues
Repo cloneWarning logged, remaining repos still clone
on_createWarning logged, on_start still runs, shell opens
on_startWarning 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: nvim

User 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:

  1. Open a shell into the container: podspawn shell <machine>
  2. Run the failing command manually from /home/<username>
  3. 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 git installed. Add git to packages.
  • 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 install

Need 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 install

How is this guide?

On this page