The dip is a CLI dev–tool that provides native-like interaction with a Dockerized application. It gives the feeling that you are working without using mind-blowing commands to run containers.
- Local development with Docker containers
- Dockerized Ruby on Rails application
- Dockerized Node.js application: one, two
- Dockerized Ruby gem
- Dockerizing Ruby and Rails development
- Reusable development containers with Docker Compose and Dip
dip-skill helps you quickly configure a dip environment for your project. It detects the tech stack and bootstraps dip.yml, compose files, and Dockerfiles.
npx skills add kalashnikovisme/dip-skill
gem install dip
Dip can be injected into the current shell so that interaction commands (and compose, up, stop, down, build, provision) become available without the dip prefix. Bash, ZSH, and Fish are supported.
Add the matching line to your shell startup file so the integration is loaded in every session:
# Bash — ~/.bashrc or ~/.bash_profile eval "$(dip console)"
# ZSH — ~/.zshrc eval "$(dip console)"
# Fish — ~/.config/fish/config.fish dip console | source
The target shell is autodetected from the $SHELL environment variable, so the snippets above work as-is. If autodetection is wrong (for example, you run Fish but $SHELL still points to Bash), force the dialect explicitly:
eval "$(dip console --shell zsh)" # Bash / ZSH dip console --shell fish | source # Fish
--shell accepts bash, zsh, or fish (bash and zsh produce the same POSIX output).
IMPORTANT: Beware of possible collisions with local tools. One particular example is supporting both local and Docker frontend build tools, such as Yarn. If you want some developer to run yarn locally and other to use Docker for that, you should either avoid adding the yarn command to the dip.yml or avoid using the shell integration for hybrid development.
After that we can type commands without dip prefix. For example:
<run-command> *any-args compose *any-compose-arg up <service> ktl *any-kubectl-arg provision
When we change the current directory, all shell aliases are automatically removed. When we enter a directory that has a dip.yml file (in it or in a parent), the aliases are renewed. This is wired through the shell's directory-change hook — chpwd_functions on ZSH, a cd/pushd/popd wrapper on Bash, and a --on-variable PWD handler on Fish. The hook resolves the applicable dip.yml itself (no dip process involved) and only actually reloads when that path changes, so cding around inside the same project doesn't re-run dip on every prompt.
Also, in shell mode Dip tries to determine manually passed environment variables. For example:
VERSION=20180515103400 rails db:migrate:down
Once the startup snippet is in place, the integration is applied automatically every time you open your terminal.
dip console prints a bootstrap script that defines three helpers:
dip_inject— evaluatesdip console inject, which emits one shell function per interaction command plus the built-incompose/up/stop/down/build/provisionwrappers. An interaction command whose name collides with a shell builtin/keyword (jobs,test,read,set, …) is skipped, with a warning on stderr — run it asdip <name>instead, or rename it.dip_clear— removes the functions previously injected (regenerated on every inject so it always matches the currentdip.yml).dip_reload— runsdip_clearthendip_inject; also bound to the directory-change hook, which calls it only when the resolveddip.ymlpath actually changed.
The bootstrap also exports DIP_SHELL=1, DIP_EARLY_ENVS (the list of variables present at load time, used to detect manually passed env vars), and DIP_PROMPT_TEXT (ⅆ). On ZSH with the agnoster theme, DIP_PROMPT_TEXT is added as a prompt segment; other shells and themes are left untouched.
You can force a reload at any time by running dip_reload (useful after editing dip.yml).
dip --help dip SUBCOMMAND --help
The configuration is loaded from dip.yml file. It may be located in a working directory, or it will be found in the nearest parent directory up to the file system root. If a dip.override.yml file exists nearby, it will be merged into the main config.
Also, in some cases, you may want to change the default config path by providing an environment variable DIP_FILE.
Below is an example of a real config.
The schema is defined in schema.json and can be validated with dip validate.
Also, you can check out examples at the top.
# Required minimum dip version version: '8.0' environment: COMPOSE_EXT: development STAGE: "staging" compose: files: - docker/docker-compose.yml - docker/docker-compose.$COMPOSE_EXT.yml - docker/docker-compose.$DIP_OS.yml project_name: bear kubectl: namespace: rocket-$STAGE interaction: shell: description: Open the Bash shell in app's container service: app command: bash compose: run_options: [no-deps] bundle: description: Run Bundler commands service: app command: bundle rake: description: Run Rake commands service: app command: bundle exec rake rspec: description: Run Rspec commands service: app environment: RAILS_ENV: test command: bundle exec rspec rails: description: Run Rails commands service: app command: bundle exec rails subcommands: s: description: Run Rails server at http://localhost:3000 service: web compose: run_options: [service-ports, use-aliases] stack: description: Run full stack (server, workers, etc.) runner: docker_compose compose: profiles: [web, workers] sidekiq: description: Run sidekiq in background service: worker compose: method: up run_options: [detach] psql: description: Run Postgres psql console service: app default_args: db_dev command: psql -h pg -U postgres k: description: Run commands in Kubernetes cluster pod: svc/rocket-app:app-container entrypoint: /env-entrypoint subcommands: bash: description: Get a shell to the running container command: /bin/bash rails: description: Run Rails commands command: bundle exec rails kafka-topics: description: Manage Kafka topics pod: svc/rocket-kafka command: kafka-topics.sh --zookeeper zookeeper:2181 setup_key: description: Copy key service: app command: cp `pwd`/config/key.pem /root/keys/ shell: false # you can disable shell interpolations on the host machine and send the command as is clean_cache: description: Delete cache files on the host machine command: rm -rf $(pwd)/tmp/cache/* provision: - dip compose down --volumes - dip clean_cache - dip compose up -d pg redis - dip bash -c ./bin/setup preflight: - ./bin/check-credentials - test -f .env
preflight is an array of shell commands that dip runs before any command that touches a running container or cluster: dip compose ... (and its aliases dip up, dip build, dip stop, dip down), dip ktl ..., and dip run ... (including the interaction shorthand dip <name>). If any command exits non-zero, dip aborts before the underlying operation runs. Useful for verifying credentials are present, required files exist, env vars are set, etc.
Not triggered by dip down --all (cross-project teardown), dip ssh, dip infra, or dip provision.
Current OS name (e.g. linux, darwin, freebsd, and so on). Sometimes it may be useful to have one common docker-compose.yml and OS-dependent Compose configs.
Relative path from the current directory to the nearest directory where a Dip's config is found. It is useful when you need to mount a specific local directory to a container along with ability to change its working dir. For example:
- project_root
|- dip.yml (1)
|- docker-compose.yml (2)
|- sub-project-dir
|- your current directory is here <<<
# dip.yml (1) environment: WORK_DIR: /app/${DIP_WORK_DIR_REL_PATH}
# docker-compose.yml (2) services: app: working_dir: ${WORK_DIR:-/app}
cd sub-project-dir dip run bash -c pwd
returned is /app/sub-project-dir.
Exposes the current user ID (UID). It is useful when you need to run a container with the same user as the host machine. For example:
# dip.yml (1) environment: UID: ${DIP_CURRENT_USER}
# docker-compose.yml (2) services: app: image: ruby user: ${UID:-1000}
The container will run using the same user ID as your host machine.
Modules are defined as an array in the modules section of dip.yml. Module files are stored in the .dip subdirectory next to dip.yml.
The main purpose of modules is to improve maintainability for a group of projects. Imagine having multiple gems which are managed with dip, each of them has the same commands, so to change one command in dip you need to update all gems individualy.
With modules you can define a group of modules for dip.
For example having setup as this:
# ./dip.yml modules: - sasts - rails ...
# ./.dip/sasts.yml interaction: brakeman: description: Check brakeman sast command: docker run ...
# ./.dip/rails.yml interaction: annotate: description: Run annotate command service: backend command: bundle exec annotate
Will be expanded to:
# resultant configuration interaction: brakeman: description: Check brakeman sast command: docker run ... annotate: description: Run annotate command service: backend command: bundle exec annotate
Imagine .dip to be a submodule so it can be managed only in one place.
If you want to override module command, you can redefine it in dip.yml
# ./dip.yml modules: - sasts interaction: brakeman: description: Check brakeman sast command: docker run another-image ...
# ./.dip/sasts.yml interaction: brakeman: description: Check brakeman sast command: docker run some-image ...
Will be expanded to:
# resultant configuration interaction: brakeman: description: Check brakeman sast command: docker run another-image ...
Nested modules are not supported.
Run commands defined within the interaction section of dip.yml
A command will be executed by the specified runner. You can set the runner explicitly via the runner key (e.g. runner: docker_compose). Otherwise dip picks the runner automatically:
docker composerunner — used when theserviceoption is defined.kubectlrunner — used when thepodoption is defined.localrunner — used when the previous ones are not defined.
dip run rails c dip run rake db:migrate
Also, run argument can be omitted
dip rake db:migrate
You can pass in a custom environment variable into a container:
dip VERSION=12352452 rake db:rollback
Use options -p, --publish=[] if you need to additionally publish a container's port(s) to the host unless this behaviour is not configured at dip.yml:
dip run -p 3000:3000 bundle exec rackup config.ruYou can also override docker compose command by passing DIP_COMPOSE_COMMAND if you wish. For example if you want to use mutagen-compose run DIP_COMPOSE_COMMAND=mutagen-compose dip run.
If you want to persist that change you can specify command in compose section of dip.yml :
compose: command: mutagen-compose
List all available run commands.
dip ls bash # Open the Bash shell in app's container rails # Run Rails command rails s # Run Rails server at http://localhost:3000
Run commands each by each from provision section of dip.yml
Run Docker Compose commands that are configured according to the application's dip.yml:
dip compose COMMAND [OPTIONS] dip compose up -d redis
Runs shared Docker Compose services that are used by the current application. Useful for microservices.
There are several official infrastructure services available:
# dip.yml infra: foo: git: https://github.com/owner/foo.git ref: latest # default, optional bar: path: ~/path/to/bar
Repositories will be pulled to a ~/.dip/infra folder. For example, for the foo service it would be like this: ~/.dip/infra/foo/latest and clonned with the following command: git clone -b <ref> --single-branch <git> --depth 1.
Available CLI commands:
dip infra updatepulls updates from sourcesdip infra upstarts all infra servicesdip infra up -n kafkastarts a specific infra servicedip infra downstops all infra servicesdip infra down -n kafkastops a specific infra service
Run kubectl commands that are configured according to the application's dip.yml:
dip ktl COMMAND [OPTIONS] STAGE=some dip ktl get pods
Runs ssh-agent container based on https://github.com/whilp/ssh-agent with your ~/.ssh/id_rsa.
It creates a named volume ssh_data with ssh socket.
An application's docker-compose.yml should contain the environment variable SSH_AUTH_SOCK=/ssh/auth/sock and connect the external volume ssh_data.
dip ssh up
docker-compose.yml
services: web: environment: - SSH_AUTH_SOCK=/ssh/auth/sock volumes: - ssh-data:/ssh:ro volumes: ssh-data: external: name: ssh_data
if you want to use non-root user you can specify UID like so:
dip ssh up -u 1000
This especially helpful if you have something like this in your docker-compose.yml:
services: web: user: "1000:1000"
Validates your dip.yml configuration against the JSON schema. The schema validation helps ensure your configuration is correct and follows the expected format.
dip validate
The validator will check:
- Required properties are present
- Property types are correct
- Values match expected patterns
- No unknown properties are used
If validation fails, you'll get detailed error messages indicating what needs to be fixed.
You can skip validation by setting DIP_SKIP_VALIDATION environment variable.
Add # yaml-language-server: $schema=https://raw.githubusercontent.com/bibendi/dip/refs/heads/master/schema.json to the top of your dip.yml to get schema validation in VSCode. Read more about YAML Language Server.
Prints the shell integration script for the current shell. See Integration with shell for the full setup.
dip console [--shell bash|zsh|fish] # bootstrap script (default subcommand) dip console inject [--shell bash|zsh|fish] # just the command aliases
--shell(-s) selects the dialect:bash,zsh, orfish. When omitted, it is autodetected from$SHELL, falling back to POSIX (Bash/ZSH) output.dip consoleis meant to be evaluated by your shell:eval "$(dip console)"for Bash/ZSH,dip console | sourcefor Fish.dip console injectis called internally by the bootstrap script (viadip_reload); you normally don't run it by hand. It only needs the interaction command names, so it skips fulldip.ymlschema validation — rundip validate(or any otherdipcommand) to check the file against the schema.
See CHANGELOG.md.
