Generic Deploy — a Bash orchestrator that turns a parent Docker-stack project's templates and per-environment variable files into a generated, validated, and deployed Docker Compose stack. It is intended to be embedded as a git submodule inside a parent project that owns the templates and secrets.
./gpd/gpd.sh -b /srv/docker -e prod -t full -g -p -d
# | | | | | |
# | | | | | └── deploy
# | | | | └───── push to target
# | | | └──────── generate config
# | | └────────────── deploy type (see deployments file)
# | └────────────────────── environment (see environments file)
# └───────────────────────────────── base directory on targetgpd.sh walks five workflow stages, gated by command-line flags:
| Flag | Stage | Effect |
|---|---|---|
-c |
clean | Remove the previously generated docker/final/<env>/ tree |
-g |
generate | Render templates: copy → variable substitution (base/env/CI) → password hashing → optional GeoIP allow-list → .env |
-p |
push | Rsync the generated tree to the target host |
-d |
deploy | Pull images, dry-run, side-stack ordering, docker compose up -d |
-u |
unused | Prune dangling and old images on the target |
local* environments skip the SSH/SCP/rsync transport and run docker compose
directly on the host where gpd.sh runs. Any other environment ships over SSH
to the host defined in its variables file.
Embed this repo at any path you like (the harness uses gpd/) and lay out the
parent project so the script can find it via ${SCRIPT_DIR}/../docker/:
parent/
├── gpd/ # this repo, as a submodule
│ └── gpd.sh
└── docker/
├── variables/ # how to assemble the stack
│ ├── name # one line, the stack name (used in COMPOSE_PROJECT_NAME)
│ ├── environments # one env per line: local, stage, prod, …
│ ├── deployments # one deploy-type per line: full=svc1 svc2 …
│ ├── mandatory # variable names required for every deploy
│ └── acme # (optional) ACME issue-command driver
├── template/
│ ├── compose/ # service definitions, *.yml
│ │ ├── nginx.yml # placeholders use __VAR__ tokens
│ │ ├── nginx_extend.yml # *_extend.yml overrides the base service
│ │ └── …
│ ├── config/ # service config files (nginx.conf, …)
│ ├── asset/ # binary artifacts shipped with the stack
│ └── variables/
│ ├── nginx_template # mandatory variable names *for this service*
│ └── opensearch_template # appended to the global mandatory list
└── config/ # per-environment, populated by CI
├── LOCAL_STACK_ENVIRONMENT_VARIABLES
├── LOCAL_STACK_SECRETS
├── LOCAL_STACK_DEPLOY_SSH_KEY
├── LOCAL_STACK_DEPLOY_SSH_KNOWN_HOSTS
├── LOCAL_STACK_CI_VARIABLES # optional — falls back to CI env
└── PROD_STACK_… # one set per environment
docker/final/<env>/ is generated by GPD at run time and is not committed.
-
variables/name— exactly one line, the stack name. -
variables/environments— one environment per line. Names that start withlocal(e.g.local,local1,localdev) skip the SSH transport and run docker compose against the local Docker host. -
variables/deployments— one deploy-type per line, format<deploy-type>=<service> <service> …. The<deploy-type>is what-texpects. Each service name corresponds totemplate/compose/<service>.yml, and optionallytemplate/variables/<service>_template.full=acme nginx postgres opensearch portainer minimal=acme nginx -
variables/mandatory— variable names (without env prefix) required for every deploy. Per-service files intemplate/variables/<service>_templateare appended to this list whenever that service is part of the selected deploy-type.STACK_ADMIN_MANAGEMENT_PASSWORD STACK_DEPLOY_HOST STACK_DEPLOY_USER STACK_NETWORK_IPV4_SUBNET -
config/<ENV>_STACK_ENVIRONMENT_VARIABLESandconfig/<ENV>_STACK_SECRETS— shell-source-able files. Variables are env-prefixed:LOCAL_STACK_ACME_DOMAIN=local.example.com LOCAL_STACK_ADMIN_MANAGEMENT_PASSWORD=…
GPD reads
<ENV>_STACK_Xfor everySTACK_Xlisted inmandatory(or in a service template) and substitutes the value into every__STACK_X__placeholder intemplate/compose/*andtemplate/config/*. -
config/<ENV>_STACK_DEPLOY_SSH_KEY/<ENV>_STACK_DEPLOY_SSH_KNOWN_HOSTS— required for non-local*environments. The script does not enforce file permissions; CI runners are expected to drop these withchmod 600. -
variables/acme(optional) — variable names that contribute domain arguments to the ACME issue command. Only consulted when<ENV>_STACK_ACME_VAR1matchesACME_EMPTY.
Templates contain __TOKEN__ placeholders. Three substitution passes run in order:
- Base —
__STACK_ENV__,__STACK_ENV_LOWER__,__STACK_PATH__,__STACK_ASSET_PATH__,__STACK_COMPOSE_PATH__,__STACK_CONFIG_PATH__,__STACK_DATA_PATH__. - Env — every name in the assembled mandatory list, sourced from the
<ENV>_STACK_*files. - CI —
__CI_REGISTRY__,__CI_REGISTRY_USER__,__CI_REGISTRY_PASSWORD__,__CI_PROJECT_PATH__,__CI_COMMIT_REF_NAME__,__CI_COMMIT_SHORT_SHA__.
Substitution treats values as opaque strings — passwords containing ;, &,
\, $1, or $2y$10$… are inserted verbatim. If a value is destined for a
docker-compose command:/environment: line, GPD doubles the literal $
characters ($$) so compose does not treat them as variable references.
When template/compose/nginx.yml is part of the deploy:
| Token | Hash | Used by |
|---|---|---|
__STACK_ADMIN_PASSWORD__ |
bcrypt cost 8 | (config files) |
__STACK_ADMIN_10_PASSWORD__ |
bcrypt cost 10 | (config files) |
__PORTAINER_PASSWORD__ |
bcrypt cost 5 | Portainer --admin-password |
__WUD_PASSWORD__ |
apr1 / MD5 | What's-up-Docker WUD_AUTH_BASIC_ADMIN_HASH |
(file) nginx_nginxpasswd |
apr1 / MD5 | nginx basic auth |
When template/compose/opensearch.yml is part of the deploy, every
__OPENSEARCH_<USER>_PASSWORD__ placeholder gets a bcrypt-12 hash sourced
from <ENV>_STACK_OPENSEARCH_<USER>_PASSWORD.
| Short | Long | Argument |
|---|---|---|
-h |
--help |
print usage and exit |
-b |
--basedir |
base directory on the target host (e.g. /srv/docker); no trailing slash |
-e |
--environment |
environment name; must appear in variables/environments |
-g |
--generate |
generate docker/final/<env>/ |
-p |
--push |
rsync docker/final/<env>/ to the target host |
-d |
--deploy |
run docker compose up -d on the target |
-t |
--deploy-type |
deploy type; must appear as a key in variables/deployments |
-f |
--force |
bypass deploy safety checks (running services, dry-run) |
-u |
--unused |
prune dangling/old images on the target |
-c |
--clean |
remove the generated tree before re-generating |
-o |
--geoip-disable |
skip the GeoIP database download and nginx allow-list |
-r |
--retries=<N> |
attempts for flaky-network ops (rsync push, registry login, image pull, registry logout, GeoIP download); default 3, must be ≥ 1. Backoff between attempts is exponential (1s, 2s, 4s, …). |
Combined invocations are common:
# generate locally for inspection
./gpd/gpd.sh -b /srv/docker -e local -t full -g
# CI: clean, generate, push, deploy
./gpd/gpd.sh -b /srv/docker -e prod -t full -c -g -p -d
# local stack on the dev machine (no SSH)
./gpd/gpd.sh -b ~/Docker/stack -e local1 -t full -c -g -d- Bash 4 or newer. The script asserts this on startup; on macOS the
default
/bin/bashis 3.2 — install Homebrew bash withbrew install bash. - GNU
getopt._usage.shrelies on--long-optionsyntax. macOS ships a BSDgetoptthat does not support it; installbrew install gnu-getoptand prepend itsbin/to PATH (echo 'export PATH="$(brew --prefix gnu-getopt)/bin:$PATH"' >> ~/.zshrc). - Required at runtime:
diff find grep perl rsync sha256sum sort ssh zstd, plus OpenSSL ≥ 1.1.1. - Preferred but optional:
htpasswd(apache-utils): used for password hashing when present; GPD falls back topython3 -m bcryptfor bcrypt and toopenssl passwd -apr1for apr1 hashes.curl: used for downloads when present; GPD falls back towget.- GNU
readlink -e/realpath: used for symlink resolution; GPD falls back to BSDreadlink -fand ultimately toperl Cwd::realpath.
- Platforms tested: Linux, macOS (with
brew install bash gnu-getopt), Windows Git-Bash and WSL.
GPL v3 — see LICENSE.