Skip to content
bravadoPublic

About

Isolated development environments with podman/docker

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

12 Commits

Folders and files

Repository files navigation

dev.sh

dev.sh creates and reuses development containers without requiring a Compose file in every project. Point it at a project directory to open an isolated workspace, or point it at a catalog service or image to run a supporting container.

The important idea is that containers are persistent:

  1. The first invocation creates and starts a managed container.
  2. Later invocations reuse that container and preserve its filesystem and tmux session.
  3. Changes to mounts, ports, environment variables, or other creation settings require --rebuild.

Managed containers use the devsh_ name prefix by default and join the dev_default network, so they can reach one another by name or alias.

Requirements

On the host:

  • Bash
  • Docker or Podman
  • jq
  • either mikefarah/yq v4+ or python-yq

Workspace images must also contain bash and tmux. The default bravado/dev.sh:master image includes both, along with common development tools and mise.

Quick start

Clone this repository, add its script to your PATH, and use dev.sh to open the repository's own development environment:

git clone https://github.com/bravado/dev.sh.git
cd dev.sh

mkdir -p "$HOME/.local/bin"
ln -s "$PWD/bin/dev.sh" "$HOME/.local/bin/dev.sh"
export PATH="$HOME/.local/bin:$PATH"

dev.sh .

The final command uses dev.sh to create and enter a workspace for this same repository. Its committed .dev.sh.yml supplies the development mounts and tmux layout, making the repository itself a working example of the tool.

The script resolves the symlink before locating its catalog/ directory, so catalogs continue to work when dev.sh is invoked from another directory. Add $HOME/.local/bin to your shell configuration if it is not already on your PATH.

Open another project workspace

Once installed, change to any project directory and run:

dev.sh .

On the first run, dev.sh:

  • creates a container named from the directory, such as devsh_my-project;
  • bind-mounts the project at the same absolute path inside the container;
  • uses that path as the container working directory;
  • starts the container and creates the configured tmux layout; and
  • attaches the terminal to the tmux session.

Detach from tmux with Ctrl-b d. Run dev.sh . again to return to the same container and session.

Run a one-off command instead of attaching to tmux:

dev.sh . -- npm test
dev.sh . --user root -- apt-get update

The -- separator is recommended because it makes the start of the command unambiguous. --user applies to the command executed in the running container; the container itself is created with the current host UID and GID.

Use a different image for this workspace:

dev.sh . --image registry.example.com/team/php-dev:8.4

Use --name when two projects have the same directory name:

dev.sh . --name billing-api

Configure a workspace

Add .dev.sh.yml to the project. A typical configuration looks like this:

image: registry.example.com/team/php-dev:8.4

ports:
  - "8080:8080"

environment:
  APP_ENV: development

volumes:
  - ${HOME}/.ssh:/home/app/.ssh:ro
  - app-cache:/home/app/.cache:chown

x-dev:
  requires:
    - docker
  tmux:
    session: app
    windows:
      - name: editor
        panes:
          - cmd: nvim
          - split: h
            size_percent: 35
            cmd: bash
      - name: server
        panes:
          - cmd: make serve

Preview container creation without changing anything:

dev.sh . --dry-run

After editing creation settings, recreate the container:

dev.sh . --rebuild

--rebuild removes the managed container and creates it again. Bind-mounted files and named volumes remain, but changes made only in the old container's writable layer are lost.

How configuration is resolved

Workspace configuration starts with catalog/default.yml. The script then looks for .dev.sh.yml files from the filesystem root down to the workspace:

/.dev.sh.yml
/home/.dev.sh.yml
/home/me/.dev.sh.yml
/home/me/project/.dev.sh.yml

Parent configuration is merged first and the closest file wins. Objects are merged recursively; arrays are replaced, not appended. Environment variables in string values are expanded, so ${HOME} in a volume declaration refers to the host environment.

Project overrides are only merged when the default catalog is selected. A named service catalog such as catalog/docker.yml is self-contained and ignores .dev.sh.yml.

The default catalog also receives a persistent mise mount at /home/app/.local/share/mise unless configuration already mounts that destination.

Use mise for project runtimes

The default image includes mise but does not install a global Node.js, Python, or other language version. Commit a mise.toml in the project instead:

[tools]
node = "24"

When a workspace containing mise.toml starts, dev.sh runs mise install in the container. Runtime installations and caches persist through the host mount, while each project selects its own versions.

For example:

dev.sh . -- node --version
dev.sh . -- npm install

Run a catalog service or image

If the first argument is not a directory or an existing managed container, dev.sh treats it as an image/service target:

dev.sh docker

For a simple target name, the script first looks for catalog/<target>.yml. In this repository, docker selects catalog/docker.yml, which starts a privileged Docker-in-Docker service and prints its managed container name. A target tag overrides the catalog image's tag:

dev.sh docker:27

If no matching service catalog exists, the target is treated as an image and catalog/default.yml supplies the remaining defaults:

dev.sh bravado/dev.sh:master -- bash

The image must stay running long enough for dev.sh to execute an interactive command in it. Also note that default-catalog behavior, including its x-dev.default_command, still applies to image targets without their own catalog. For reusable services, a dedicated catalog file is usually clearer.

Service dependencies

A configuration can start other catalog services before its own container:

x-dev:
  requires:
    - docker

Required services are started in order. A service can expose environment values to its dependents:

x-dev:
  environment:
    DOCKER_HOST: tcp://{{name}}:2375
    DOCKER_TLS_CERTDIR: ""

Placeholders use the service context; {{name}} is the managed container name. Values in the dependent container's regular environment: section take precedence over injected values.

Work with existing containers

You can use either a managed container's name or one of its aliases:

dev.sh ps
dev.sh billing-api
dev.sh billing-api -- php -v

If the container is stopped, dev.sh starts it. A workspace target reopens its tmux session; a service target runs its configured default command or prints its container name.

Generated names replace /, :, and . with _. The default devsh_ prefix can be changed for all newly created containers:

DEVSH_PREFIX=myteam_ dev.sh .

Every managed container receives both its full container name and its short unprefixed name as network aliases.

Command reference

Start, enter, or rebuild

dev.sh <folder> [options] [-- command...]
dev.sh <image-or-service> [options] [-- command...]
dev.sh <existing-name-or-alias> [--user USER] [-- command...]

Creation options:

Option Purpose
--name NAME Set the managed container name.
--network NETWORK Join a network other than dev_default.
--alias ALIAS Add a network alias when creating the container.
--image IMAGE Override the image for a workspace target only.
--rebuild Recreate the container with current configuration.
--dry-run Print the engine command without changing state.
--tmux-reset Recreate the configured workspace tmux session.
--key VALUE Supply a value to exposed-environment templates.

Execution option:

Option Purpose
--user USER Run the explicit command as this container user.

Unknown --key value or --key=value pairs are stored as template parameters and are available while rendering x-dev.environment. They are not passed directly to Docker or Podman. Parameters are preserved by --rebuild, with newly supplied values taking precedence.

Inspect and manage

dev.sh ps                         # list managed containers; * means running
dev.sh ls                         # alias for ps
dev.sh info <name-or-alias>       # status, published ports, connection string
dev.sh logs <name-or-alias> -f    # host-side container logs
dev.sh stop <name-or-alias>
dev.sh rm <name-or-alias-or-dir>
dev.sh rm -f <name-or-alias-or-dir>
dev.sh cd <name-or-alias>         # print a running workspace's host path

rm refuses to remove a running container unless -f or --force is used. Passing a directory to rm resolves the managed container associated with that workspace.

inspect is intentionally different from info:

dev.sh inspect <name-or-alias> [docker-inspect-arguments...]

It runs docker inspect inside the selected running container. This is useful for a workspace with access to a Docker daemon; use info for details about the selected managed container itself.

Save a container's current filesystem as an image:

dev.sh commit <name-or-alias> [tag]

The resulting image is bravado/dev.sh:<tag>. If the tag is omitted, the full managed container name is used.

Manage network aliases

dev.sh alias add <container-or-alias> <new-alias>
dev.sh alias rm <alias>

Aliases are added to every network currently joined by the container and can be used both as CLI identifiers and as DNS names between managed containers.

Configuration reference

Top-level keys mostly map to container creation options:

Key Meaning
image Image used by a workspace or catalog service.
command Container startup command; accepts a string or array.
ports Port mappings such as 8080:80.
environment Environment map or list.
volumes Bind mounts and named volumes.
working_dir Container working directory. Workspace mode overrides this with the workspace path.
privileged Enable privileged mode with true.
security_opt One or more security options.
cap_add One or more Linux capabilities.
devices One or more device mappings.
sysctl Map, list, or single sysctl value.

x-dev contains behavior implemented by this wrapper:

Key Meaning
requires Catalog targets to start first.
environment Environment values exposed to dependent containers.
default_command Command executed after start when no command was supplied; string or array.
connection.url Template shown by dev.sh info.
user.additional_groups Supplemental group names or GIDs.
user.auto_group_from_docker_sock Automatically add the mounted Docker socket's GID; defaults to true.
tmux.session Session name; defaults to main.
tmux.windows Workspace window and pane layout.

Named-volume ownership

Append :chown to a named volume to make app:app own the mounted content when the container is first created:

volumes:
  - app-cache:/home/app/.cache:chown

:chown is rejected for host bind mounts because recursively changing host file ownership would be unsafe. It runs only on creation, including --rebuild.

Supplemental groups and the Docker socket

To access a host socket whose group differs between machines:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

x-dev:
  user:
    additional_groups:
      - docker-sock
      - sock:/path/to/another.sock
      - 1234
      - docker

docker-sock (or docker.sock) resolves the host GID of /var/run/docker.sock; sock:/path resolves the GID of another host path. Numeric GIDs and group names are passed through unchanged. Mounting the Docker socket enables its GID automatically unless disabled:

x-dev:
  user:
    auto_group_from_docker_sock: false

Tmux layouts

x-dev:
  tmux:
    session: workspace
    windows:
      - name: code
        layout: even-horizontal
        panes:
          - cmd: nvim
          - cmd: bash
      - name: services
        panes:
          - cmd: make serve
          - split: v
            size_percent: 30
            target: 0
            cmd: make logs

Pane fields:

  • cmd: command sent to the pane; defaults to bash.
  • split: h, horizontal, v, or vertical.
  • size_percent: split size from 1 to 99.
  • target: zero-based index of the pane to split; defaults to the previous pane.

If any pane uses split, size_percent, or target, the window's layout is not applied. Layout creation is idempotent for an existing session; use --tmux-reset to discard and rebuild it.

Troubleshooting

  • Configuration changes appear to do nothing: run dev.sh . --rebuild.
  • A workspace opens the wrong same-named project: give one of them a unique --name; dev.sh reports ambiguous workspace matches when it can detect them.
  • A container name conflicts: dev.sh will not replace an unmanaged Docker/Podman container. Rename or remove the conflicting container, or use --name.
  • Docker or Podman selection is wrong: set CONTAINER_ENGINE=docker or CONTAINER_ENGINE=podman. Docker is preferred when both are installed.
  • A command disappeared after --rebuild: rebuild preserves catalog parameters, aliases, image, and network when possible, but it does not preserve changes made only in the container's writable layer.

Smoke test

The integration test creates and removes temporary containers, volumes, and images:

scripts/smoke.sh [image] [name] [alias] [volume-name]

Its default image is bravado-dev-mise-test; build or provide a compatible image before running it.

About

Isolated development environments with podman/docker

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages