README
Poseup
If you find it useful, consider starring the project 💪 and/or following its author ❤️ -there's more on the way!
Install
Requires docker
to be installed on your system.
To install poseup globally, run: npm install -g poseup
Motivation
Poseup leverages docker-compose
with the goal of making having a containerized development and test workflow trivial. It is governed by a poseup.config
file that can be used as a single source of truth, as poseup is also able to generate traditional docker-compose
files from it.
Ultimately, it is an effort to fix the most common issues with similar solutions, which tend to be:
- Unable to properly clean up ephemeral services when errors occur or the process is terminated by the user.
- Lacking in compatibility with
docker-compose
, making it difficult to have a single source of truth. - Lacking in configuration and CLI options, as well as clarity as to what goes on under the hood.
In contrast with other solutions, poseup integrates seamlessly with docker-compose
and its configuration format. Mostly as a product of it, poseup is able to:
- Properly clean up ephemeral services in all events but a forced kill to the process.
- Run tasks either with a set of persisted services or in completely ephemeral sandboxes -see
poseup run
. - Run automated setup commands for services before tasks are run -see
exec
. - Seek setups for different environments in a single source of truth -see
environments
. - Produce
docker-compose
configuration files from its ownposeup.config
-seeposeup compose
. - Inherit all
docker-compose
commands, hence allowing for a high degree of flexibility -seeposeup compose
.
Usage
- CLI
poseup
options runsdocker-compose
as per yourposeup.config
file.poseup run
is a task runner for docker.poseup clean
cleans all non persisted containers.poseup purge
purges volumes, networks, and images from your system.- Common options: a description of common options taken by CLI commands.
- Configuration: how to get the most out of poseup through your
poseup.config
file.- Extensions: the allowed file extensions for a config file.
- Path: how poseup resolves the path for your config file by default.
- Structure: what you should put where in the configuration file.
- Environments: how to define different configurations for several environments.
- Example: a complete example configuration.
- Programmatic usage: how to use poseup programmatically.
CLI
options poseup
Usage:
$ poseup [option]
$ poseup [command] [options]
Options:
-d, --dir <dir> Project directory
-f, --file <path> Path for config file [js,json,yml,yaml]
-e, --env <env> Node environment
--log <level> Logging level
-h, --help Show help
-v, --version Show version number
Commands:
compose Runs docker-compose
run Runs tasks
clean Cleans not persisted containers and networks -optionally, also volumes
purge Purges dangling containers, networks, and volumes system-wide
Examples:
$ poseup --log debug compose -- up
$ poseup -d ./foo -e development clean
log
Sets logging level. Can be one of silent
, trace
, debug
, info
, warn
, error
.
Example: poseup --log debug clean
file
Sets the poseup configuration file as an absolute or relative path to cwd. By default, poseup will attempt to find it in the project directory -if passed- or cwd, and up.
Example: poseup --file ../poseup.development.js compose -- up
dir
Sets the project directory for docker as an absolute or relative path to cwd. By default, it is the directory where the poseup configuration file is found.
Example: poseup --dir ../ compose -- dowm
env
Assigns an arbitrary value to process.env.NODE_ENV
.
Example: poseup --env development compose -- up
The above would be equivalent to NODE_ENV=development poseup compose -- up
poseup compose
Runs docker-compose
as per the services defined in your poseup.config
or, alternatively, produces a docker compose file from the compose
object of your poseup configuration.
Example: poseup compose -- up
Usage:
$ poseup compose [options] -- [dockerArgs]
Runs docker-compose
Options:
-w, --write <path> Produce a docker compose file and save it to path
-s, --stop Stop all services on exit
-c, --clean Run clean on exit
--dry Dry run -write docker compose file only
-h, --help Show help
docker-compose
file
Generating a To generate a docker-compose
compatible file, you can just run: poseup compose --dry --write docker-compose.yml
.
If your poseup.config
configuration depends on NODE_ENV
, you could just run poseup compose -e development --dry --write docker-compose.development.yml
to get the docker-compose
configuration for the development
environment.
poseup run
A task runner. Runs a task or a series of tasks -defined in your poseup.config
- in a one off primary container while starting its dependent services, or in a single use sandbox -which creates new containers for the task runner and all its services, and removes them after the task has finished execution.
Usage:
$ poseup run [options] [tasks]
Runs tasks
Options:
-l, --list List tasks
-s, --sandbox Create new containers for all services, remove all on exit
-t, --timeout <seconds> Timeout for waiting time after starting services before running commands [${RUN_WAIT_TIMEOUT} by default]
--no-detect Prevent service initialization auto detection and wait until timeout instead
-h, --help Show help
poseup clean
Cleans all services absent from the persist
array of your poseup.config
file.
Usage:
$ poseup clean [options]
Cleans not persisted containers and networks -optionally, also volumes
Options:
-v, --volumes Clean volumes not associated with persisted containers
-h, --help Show help
poseup purge
Shorthand for a serial run of docker volume prune
, docker network prune
, docker image prune --all
, and docker container ls --all
.
Usage:
$ poseup purge [options]
Purges dangling containers, networks, and volumes system-wide
Options:
-f, --force Skip confirmation
-h, --help Show help
Configuration
A poseup configuration file is the single most important thing for you to make your poseup usage worthwhile. It contains the project name, the persisted containers, any number of tasks, and a docker-compose
configuration object containing services, volumes, and networks definition. Before you jump in, you can check out an example here.
Extensions
Valid configuration files are .js
, .json
, or .yml
files, though you'll have to use a .js
file in order to be able to define different configurations depending on environment on the same file. See environments.
Path
All poseup commands but poseup purge
(since it's system-wide) will need a poseup.config
file. You can pass the file path via the --file
flag, but otherwise, poseup will look for a poseup.config.{js,json,yml}
in the current working directory and up. Remember you can also specify a different working directory for poseup than the current on console with the --dir
flag.
Structure
Root properties are:
log
: string, optional, logging level. One of:'silent'
,'trace'
,'debug'
,'info'
,'warn'
,'error'
.project
: string, required, the name of the project. Note that you should never have different environment-dependent configurations for a same project name (see environments).persist
: strings array, optional, the name of the services to not clean when runningposeup clean
andposeup run
. This is useful if you have services you don't want to be ephemeral, like a database, which data you'd like to keep between runs.compose
: object, required, should have the same structure as a traditionaldocker-compose
file. Here is where you define your services and their configuration. Having yourdocker-compose
configuration inside the poseup config will allow you, if desired, to have all environmental differences in a single file, while you can always generate an actualdocker-compose
yaml file for any environment from it by dry runningposeup compose
.tasks
: object, optional, any number of tasks to be executed viaposeup run
. The keys of thetask
object will be the names of your task, and each have an object as value with optional keys:description
: string, a description for the task -it'll be used when runningposeup run --list
.primary
: string, the name of the service for which a new ephemeral container will be created in order to runcmd
. If noprimary
is specified,cmd
will be run in your local environment -your system.services
: strings array, the names of the associated services required to run this task. If not present and aprimary
service has been specified, poseup will use the services defined on thedepends_on
key of the service in thecompose
definition by default. If it exists, it will not merge withdepends_on
, this way you can restrict or extend the required services for this task with no regard for thecompose
definition. If not present and noprimary
has been defined, no services will be started for the task, though that would mean you'd essentially be just running a local command, which would make little sense.cmd
: strings array, the command to execute onprimary
or locally that represents the task itself. Example:['npm', 'install']
.exec
: array | object any number of commands to execute on any of the services that are required to start when running the task -either viaservices
or thecompose
depends_on
- beforecmd
is run. Each item of the array must be an object. The keys of the object will signal the service to run the command on, meanwhile their value should be a strings array for the command. All the commands in each item of theexec
array will be executed with no guaranteed execution order, while each array item will be guaranteed to run serially:
// Unordered execution example
({
tasks: {
myTask: {
primary: 'myPrimaryService',
cmd: ['echo', 'foo'],
exec: [
{
// These will execute in parallel
myDbService: ['psql', '-U', 'postgres', '-c', 'CREATE DATABASE testdb;'],
myNodeService: ['npm', 'install']
}
]
}
}
});
// Unordered execution example (equivalent to the previous)
({
tasks: {
myTask: {
primary: 'myPrimaryService',
cmd: ['echo', 'foo'],
exec: {
// These will execute in parallel
myDbService: ['psql', '-U', 'postgres', '-c', 'CREATE DATABASE testdb;'],
myNodeService: ['npm', 'install']
}
}
}
});
// Series example: guaranteed execution order
({
tasks: {
myTask: {
primary: 'myPrimaryService',
cmd: ['echo', 'foo'],
exec: [
// These will execute in series
{ myDbService: ['psql', '-U', 'postgres', '-c', 'CREATE DATABASE testdb;'] },
{ myNodeService: ['npm', 'install'] }
]
}
}
});
Environments
There are two strategies you can follow in order to define different configurations for several environments.
It's important to keep in mind, regardless of the strategy you use, poseup will treat any configuration with the same project name as if they were the same. What that means is you need to make sure that, for different configuration values, the project name in your poseup.config
file is distinct, otherwise things could get quite messy. That being said:
- you can have several
poseup.config
files and pass them to poseup depending on the one you desire to apply via the--file
flag, - or you can use a JavaScript file for your
poseup.config
that defines a different configuration as per any arbitrary number of environment variables.
Focusing on the single JavaScript file strategy, a simple solution would be to do something like:
poseup.config.js
:
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
project: 'my-project-name' + (isProduction ? 'production' : 'development'),
persist: [ /* ...my persisted services */ ],
tasks: isProduction
? { /* ...my production tasks */ }
: { /* ...my development tasks */ },
compose: {
version: '3.4',
services: { /* ...my services */ }
}
};
Solutions like slimconf
might assist you in organizing your environment-dependent configuration.
As an example:
const { default: slim, fallback } = require('slimconf');
module.exports = slim(
{ env: [process.env.NODE_ENV, fallback('development', ['test']) },
(on, { env }) => ({
project: `my-project-${env}`,
persist: on.env({
default: [
// ...my default persisted services
],
development: [
// ...my persisted services in development
],
test: [] // all services will be ephemeral on test
}),
tasks: on.env({
default: {
// ...my default tasks
},
development: {
// ...my development tasks
},
test: {
// ...my test tasks
}
}),
compose: {
version: '3.4',
services: {
// ...my services
}
}
})
);
Example
This example uses slimconf
to manage environment-dependent configuration.
poseup.config.js
:
const { default: slim, fallback, merge } = require('slimconf');
module.exports = slim(
{ env: [process.env.NODE_ENV, fallback('development', ['test']) },
// We're merging the defaults with the environment-dependent configuration
(on, { env }) => on.env(merge, {
/* Defaults */
defaults: {
log: 'info',
project: `my-project-${env}`,
compose: {
version: '3.4',
services: {
app: {
image: 'node:8-alpine',
depends_on: ['db'],
networks: ['backend'],
environment: {
NODE_ENV: env,
DB_URL: `postgres://postgres:pass@db:5432/testdb`
},
volumes: [{ type: 'bind', source: './', target: '/usr/src/app' }]
},
db: {
image: 'postgres:11-alpine',
networks: ['backend'],
environment: { POSTGRES_PASSWORD: 'pass' }
}
},
networks: { backend: {} },
volumes: {}
}
},
/* Development environment */
development: {
// We'll persist the database on development (won't be removed on poseup clean)
persist: ['db'],
tasks: {
// This will run "node index.js" locally after "db" is up.
// We'd run it with: poseup run local
local: {
services: ['db'],
cmd: ['/bin/sh', '-c'].concat('cd /usr/src/app && node index.js')
},
// This will run "node index.js" in a docker container ("app").
// Since app already depends on "db", we don't need to define it.
// We'd run it with: poseup run docker
docker: {
primary: 'app',
cmd: ['/bin/sh', '-c'].concat('cd /usr/src/app && node index.js')
},
// This will just setup the database for usage on development.
// As we are persisting the container, it's a one-off task.
bootstrap: {
services: ['db'],
exec: {
db: 'psql -U postgres -c'
.split(' ')
.concat('CREATE DATABASE testdb;')
}
}
},
compose: {
// We'll also expose ports
services: {
app: { ports: ['3000:3000'] },
db: { ports: ['5432:5432'] }
}
}
},
/* Test environment */
test: {
tasks: {
// We'd run it with: poseup run -e test jest
jest: {
primary: 'app',
cmd: ['/bin/sh', '-c'].concat('cd /usr/src/app && npx jest'),
// Create database before running tests
exec: {
db: 'psql -U postgres -c'
.split(' ')
.concat('CREATE DATABASE testdb;')
}
}
}
}
})
);
Programmatic Usage
poseup exports compose
, run
, clean
, and purge
, which are called by the equally named CLI commands.
However, when running poseup on the CLI, the program will also listen to termination events through exits
and run cleanup tasks either at end of execution or termination signals. In order to handle these cleanup tasks, you have two options: