A lightweight, multi-tool Docker image for:
- β
SSL automation (
mkcert+certify) - β
Interactive vhost generation (
mkhost) + templates - β
Cleanup vhosts (
rmhost) - β
SOPS/Age encrypted env workflow (
senv) - β
Host notifications pipeline (
notifierd+notify+ hostdocknotify) - β
Docker ops + TUI (
docker-cli+ compose +lazydocker) - β
Network diagnostics (
netx,dig,mtr,traceroute,nmap, etc.) - β
Daily dev/ops utilities (
git,jq,yq,rg,fd,sqlite,shellcheck,nano, etc.)
| Registry | Image Name |
|---|---|
| Docker Hub | docker.io/infocyph/tools |
| GitHub Container | ghcr.io/infocyph/tools |
mkcertbundledcertifyscans vhosts under/etc/share/vhosts/**and generates:- Apache server/client certs
- Nginx server/proxy/client certs (includes
.p12for Nginx client)
- Wildcards are auto-added from filenames (
example.com.confβexample.com+*.example.com) - Always includes:
localhost,127.0.0.1,::1 - Stable CA root via
CAROOT=/etc/share/rootCA
mkhostgenerates Nginx/Apache vhost configs using predefined templates- Uses runtime-versions DB baked during build:
/etc/share/runtime-versions.json(override viaRUNTIME_VERSIONS_DB)
- Stores runtime state in
env-store(JSON), including helper query/reset flags (APACHE_ACTIVE)
age+sopsinstalledsenvprovides a clean workflow around.envβ.env.enc- Supports:
- repo-local config
./.sops.yaml(highest priority) - global fallback config/key under
/etc/share/sops/global(mountable; back-compat:/etc/share/sops/*) - multi-project keys (per-repo) + βshared encrypted env repoβ input mount
- repo-local config
notifierdlistens on TCP (default9901) and emits a stable single-line event to stdout using a prefix (default__HOST_NOTIFY__)notifysends events intonotifierd(inside container)- Host can watch formatted events and show popups
- Optional host-side sender
docknotifycan push events to the container from the host
docker-cli+ composelazydockerbundled (mount the docker socket)
netx(Toolset wrapper)curl,wget,ping,ncdig/nslookup(bind-tools)iproute2,traceroute,mtrnmap
git+gitx(Toolset)jq,yqripgrep (rg),fdsqlite+sqlitex(Toolset)shellcheckzip,unzip,tree,ncdu- Default editor UX:
nanois defaultEDITORandVISUAL/etc/nanorcis configured to load syntax rules when available
chromacat,figlet,show-bannershell hook
| Command | Purpose |
|---|---|
mkcert |
Local CA + trusted TLS certificates |
certify |
Scan vhosts and generate server/client certs |
mkhost |
Generate vhost configs (Nginx/Apache) + optional Node compose |
rmhost |
Remove vhost configs for domain(s) (Nginx/Apache/Node yaml) |
senv |
SOPS/Age workflow for .env + .env.enc |
lazydocker |
Docker TUI (requires docker socket) |
notify |
Send notification to notifierd |
notifierd |
TCP β stdout bridge (for host watchers) |
status |
Docker compose project status and diagnostics (--json supported) |
env-store |
JSON-backed key/value store for runtime state (jq managed) |
profile-chooser |
Interactive profile+env collector for host-side compose flush |
domain-which |
Resolve app/container/profile/docroot for a domain (supports --json) |
es-policy |
Bootstrap/update Elasticsearch ILM + templates + Kibana data views |
gitx |
Git helper CLI |
chromacat |
Colorized output |
sqlitex |
SQLite helper CLI |
netx |
Networking helper wrapper |
composer |
PHP dependency manager |
This repo is designed so you can keep all generated + persistent artifacts in a single configuration/ folder, and mount them into the container.
Rule of thumb:
- Mount RW if the container should generate/update files there (
certify,mkhost,senv init/keygen).- Mount RO if you want βconsume onlyβ behavior (good for shared secrets repo).
.
ββ configuration/
β ββ apache/ # Generated/managed Apache vhosts (*.conf)
β ββ nginx/ # Generated/managed Nginx vhosts (*.conf)
β ββ node/ # Node vhost/profile metadata (*.yaml)
β ββ fpm/ # FPM pool config dirs/files (phpXX/*)
β ββ ssl/ # Generated certificates (.pem, .p12, keys)
β ββ certs/ # Exported cert copies from `certify`
β ββ rootCA/ # mkcert CA store (persist across rebuilds)
β ββ sops/ # Global SOPS (Model B; persisted)
β ββ global/ # Global fallback key + config (preferred)
β β ββ age.keys
β β ββ .sops.yaml
β ββ keys/ # Per-project keys (recommended)
β β ββ projectA.age.keys
β β ββ projectB.age.keys
β ββ config/ # Optional per-project configs
β ββ projectA.sops.yaml
β ββ projectB.sops.yaml
β
ββ secrets-repo/ # Optional shared encrypted env store (usually RO mount)
β ββ projectA/
β β ββ .env.enc
β ββ projectB/
β ββ prod/.env.enc
β
ββ logs/ # Optional host logs for status/logviewer (/global/log)
β
ββ docker-compose.yml
Back-compat: if your
configuration/sopsalready containsage.keysand/or.sops.yamlat the top level,senvwill still detect/use them. New defaults are created underconfiguration/sops/global/.
| Host path | Container path | Used by |
|---|---|---|
./configuration/apache |
/etc/share/vhosts/apache |
mkhost, certify |
./configuration/nginx |
/etc/share/vhosts/nginx |
mkhost, certify |
./configuration/docker-compose |
/etc/share/vhosts/docker-compose |
mkhost, rmhost, status checks |
./configuration/fpm |
/etc/share/vhosts/fpm |
mkhost, init-fpm-pool-dirs, status checks |
./configuration/ssl |
/etc/mkcert |
certify, mkcert |
./configuration/certs |
/etc/share/certs |
certify export dir, status checks |
./configuration/rootCA |
/etc/share/rootCA |
mkcert (CA store) |
./configuration/sops |
/etc/share/sops |
senv init, senv keygen, senv enc/dec/edit |
./secrets-repo |
/etc/share/vhosts/sops |
senv dec --in=... (alias input source) |
./logs |
/global/log |
status checks, LogViewer |
/var/run/docker.sock |
/var/run/docker.sock |
docker, lazydocker |
services:
tools:
image: infocyph/tools:latest
container_name: docker-tools
volumes:
- ./configuration/apache:/etc/share/vhosts/apache
- ./configuration/nginx:/etc/share/vhosts/nginx
- ./configuration/docker-compose:/etc/share/vhosts/docker-compose
- ./configuration/fpm:/etc/share/vhosts/fpm
- ./configuration/ssl:/etc/mkcert
- ./configuration/certs:/etc/share/certs
- ./configuration/rootCA:/etc/share/rootCA
- ./configuration/sops:/etc/share/sops
- ./secrets-repo:/etc/share/vhosts/sops:ro
- ./logs:/global/log:ro
- /var/run/docker.sock:/var/run/docker.sock
environment:
- TZ=Asia/Dhaka
# - NOTIFY_TCP_PORT=9901
# - NOTIFY_PREFIX=__HOST_NOTIFY__
# - NOTIFY_TOKEN=Use as:
- one-shot cert generator:
docker run --rm ... infocyph/tools certify - long-lived utility box: default CMD runs
notifierd
docker run --rm -it \
-v "$(pwd)/configuration/apache:/etc/share/vhosts/apache" \
-v "$(pwd)/configuration/nginx:/etc/share/vhosts/nginx" \
-v "$(pwd)/configuration/docker-compose:/etc/share/vhosts/docker-compose" \
-v "$(pwd)/configuration/fpm:/etc/share/vhosts/fpm" \
-v "$(pwd)/configuration/ssl:/etc/mkcert" \
-v "$(pwd)/configuration/certs:/etc/share/certs" \
-v "$(pwd)/configuration/rootCA:/etc/share/rootCA" \
-v "$(pwd)/configuration/sops:/etc/share/sops" \
-v "$(pwd)/logs:/global/log:ro" \
-v /var/run/docker.sock:/var/run/docker.sock \
infocyph/tools:latestOn container startup, the entrypoint runs certify (best-effort). It:
- Scans all
*.confunder/etc/share/vhosts/** - Extracts domains from filenames (basename without
.conf) - Adds wildcard variants automatically (
*.domain) - Always includes:
localhost,127.0.0.1,::1 - Generates server and client certificates using
mkcert
| File name | Domains generated |
|---|---|
test.local.conf |
test.local, *.test.local |
example.com.conf |
example.com, *.example.com |
internal.dev.site.conf |
internal.dev.site, *.internal.dev.site |
All certs are written to /etc/mkcert.
| Certificate Type | Files Generated |
|---|---|
| LDS (Server) | lds-server.pem, lds-server-key.pem |
| LDS (Client Internal) | lds-client-internal.pem, lds-client-internal-key.pem |
| LDS (Client User) | lds-client-user.pem, lds-client-user-key.pem, lds-client-user.p12 |
Use certs by TLS role, not by service name:
| Traffic / Role | Certificate to use |
|---|---|
| Client -> Nginx (TLS termination, including localhost router) | lds-server.pem, lds-server-key.pem |
| Nginx -> Apache (mTLS upstream client auth) | lds-client-internal.pem, lds-client-internal-key.pem |
| Apache as TLS server for Nginx | lds-server.pem, lds-server-key.pem |
| Human/browser/API client cert bundle | lds-client-user.p12 (from lds-client-user.pem, lds-client-user-key.pem) |
For locals.conf-style Nginx HTTPS routers, use:
ssl_certificate /etc/mkcert/lds-server.pem;
ssl_certificate_key /etc/mkcert/lds-server-key.pem;mkhost is your βdomain setup wizardβ. It generates:
- Nginx vhost:
/etc/share/vhosts/nginx/<domain>.conf - Apache vhost (only if you choose Apache):
/etc/share/vhosts/apache/<domain>.conf - Node service yaml (only if you choose Node):
/etc/share/vhosts/docker-compose/<token>.yaml - PHP service yaml (only if you choose PHP):
/etc/share/vhosts/docker-compose/phpXX.yaml
Run it:
docker exec -it docker-tools mkhostIt runs a guided 9-step flow (slightly different for PHP vs Node):
-
Domain name (validated)
-
App type: PHP or Node
-
Server type (PHP only): Nginx or Apache
- Node always uses Nginx proxy mode
-
HTTP / HTTPS mode (keep HTTP, redirect, or HTTPS)
-
Document root (
/app/<path>) -
Client body size
-
Runtime version selection:
- PHP: choose PHP version
- Node: choose Node version + optional run command
-
If HTTPS: optional client certificate verification (mutual TLS)
If you enable HTTPS, mkhost triggers certify automatically so the required certs exist.
mkhost stores state in env-store.
You can query/reset these values:
mkhost --RESET
mkhost --APACHE_ACTIVE
mkhost --JSON--RESETclears mkhost state.--APACHE_ACTIVEprintsapachewhen Apache mode was selected.--JSONprints structured state from keyMKHOST_STATE.
rmhost deletes the generated files for a domain:
/etc/share/vhosts/nginx/<domain>.conf/etc/share/vhosts/apache/<domain>.conf/etc/share/vhosts/docker-compose/<token>.yaml(Node token is a safe slug of the domain)
Run it:
docker exec -it docker-tools rmhost example.comMultiple domains (batch plan + single confirmation):
docker exec -it docker-tools rmhost a.localhost b.localhost api.example.comInteractive mode (no args):
docker exec -it docker-tools rmhostBehavior:
- Validates the domain format before deleting
- Shows exactly what files it will remove
- Requires confirmation (
y/N) β in multi-domain mode it asks once for the full plan - If nothing exists for that domain, it exits with code
2(useful for scripts)
State/query flags:
rmhost --RESET
rmhost --APACHE_DELETE
rmhost --JSONstatus reports compose-project health and runtime diagnostics in both human and machine-readable forms.
Usage:
status [--json] [--quiet] [service]Examples:
status
status php84
status --json | jq .Human output sections:
- Core:
Project,Profiles,Containers,Ports,URLs - Diagnostics:
Problems,Container runtime(Top consumers+Stats),Disk,Volumes,Networks,Probes,Recent errors,Drift Checks:System test: internet reachability, egress IP, memory, docker runtimeProject containers: container health summaryProject artifacts: artifact and log countsProject mounts: mount readiness and emptiness checks
--json shape:
- Top-level:
generated_at,full,core,sections core: project metadata, running summary, port summaries, URLssections:problems,containers(mergedcore+top_consumers+stats),disk,volumes,networks,probes,recent_errors,drift,checks
Helpful env overrides:
STATUS_PROJECT(force project name)STATUS_PROBE=0|1(disable/enable URL probing)STATUS_FORCE_COLOR=1(force ANSI colors)STATUS_MOUNT_DEEP_COUNT=1(opt-in deep recursive mount file counts; default is fast shallow mode)STATUS_LOG_SCAN_MAX_DEPTH=3(depth limit for/global/logchecks; useallor-1for full recursion)WORKING_DIR/LDS_WORKDIR(workdir hint)ENV_DOCKER(custom docker env file path)VHOST_NGINX_DIR(domain source dir)
domain-which resolves runtime metadata for a domain by reading LDS headers from Nginx vhost files.
domain-which --list-domains
domain-which example.com
domain-which --json example.com
domain-which --app example.comprofile-chooser lets you interactively select service profiles and their required env values, then stores state in env-store (JSON by default; SQLite optional).
Host-side tooling can fetch newline-separated outputs and decide how/when to flush into compose env/profiles.
profile-chooser # interactive selection
profile-chooser --json # full saved state
profile-chooser --profiles # newline list
profile-chooser --services # newline list
profile-chooser --envs # newline KEY=VALUE pairs
profile-chooser --resetStored state key in env-store:
PROFILE_CHOOSER_STATE(structured JSON object)
env-store is a small JSON-backed key/value store for container runtime state.
It is used by profile/mkhost/rmhost flows as the single state backend.
Default file:
/etc/share/state/env-store.json(override withENV_STORE_JSON)- Optional SQLite backend: set
ENV_STORE_BACKEND=sqlite(DB path:ENV_STORE_DB)
Common structured keys used by bundled scripts:
PROFILE_CHOOSER_STATEMKHOST_STATERMHOST_STATE
Examples:
env-store set-json STACK_META '{"name":"LocalDevStack","ports":[80,443],"flags":{"probe":true}}'
env-store get-json STACK_META
env-store list
env-store json | jq .es-policy ensures ILM policies/templates for log data streams and can provision Kibana data views.
es-policy
es-policy --forceCommon env overrides:
ES_URL(defaulthttp://elasticsearch:9200)KIBANA_URL(defaulthttp://kibana:5601)REPLICAS(default0)
senv wraps SOPS + Age for a predictable .env β .env.enc workflow, with:
- Repo-local config:
./.sops.yaml(highest priority) - Global defaults:
/etc/share/sops/global/{age.keys,.sops.yaml}(preferred) - Model B multi-project keys: per-project keys under
/etc/share/sops/keys/ - Shared encrypted env repo mount:
/etc/share/vhosts/sopsfor sourcing/storing encrypted envs
senv chooses the Age key in this order:
--key <path>orSOPS_AGE_KEY_FILE=<path>--project <id>β/etc/share/sops/keys/<id>.age.keys- Global fallback (preferred) β
/etc/share/sops/global/age.keys - Back-compat fallback (if present) β
/etc/share/sops/age.keys
senv chooses the SOPS config in this order:
- Repo-local β
./.sops.yaml - Project config (optional) β
/etc/share/sops/config/<id>.sops.yaml - Global fallback (preferred) β
/etc/share/sops/global/.sops.yaml - Back-compat fallback (if present) β
/etc/share/sops/.sops.yaml - Override:
SOPS_CONFIG_FILE=/path/to/.sops.yaml
senv init / senv keygen will only create files under /etc/share/sops/** when:
- the container user is root, and
- the target path is writable (not a read-only mount).
If you mount /etc/share/sops read-only, senv will operate in consume-only mode.
Initialize (ensures missing global defaults + optional project config + key when writable):
senv initInitialize and also create repo-local config in the current directory:
senv init --localLocal-only init (creates ./.sops.yaml only; never touches /etc/share/sops):
senv init --local-onlyStatus / info:
senv infoGenerate a per-project key (refuses to overwrite a real key):
senv keygen --project projectAOpen the effective config in nano:
senv configEncrypt / decrypt (defaults):
senv enc # .env -> .env.enc
senv dec # .env.enc -> .env
senv edit # edit .env.enc using sops editor modeExplicit key / project selection:
senv enc --project projectA
senv dec --project projectA
senv enc --key ./keys/projectA.age.keysIf --in / --out is not absolute (/β¦) and not ./β¦ / ../β¦, it is treated as an alias under:
SOPS_REPO_DIR(default/etc/share/vhosts/sops)
Examples:
# reads: /etc/share/vhosts/sops/projectA/prod/.env.enc
# writes: ./.env
senv dec --in projectA/prod/.env.enc --out ./.env
# if --out is omitted, it writes to current directory by default
senv dec --in projectA/.env.encPush/Pull sugar (shared encrypted repo):
# pull /etc/share/vhosts/sops/<project>/.env.enc -> ./.env
senv pull --project projectA
# push ./.env -> /etc/share/vhosts/sops/<project>/.env.enc
senv push --project projectABy default senv restricts input/output paths to stay inside:
- current working directory
/etc/share/vhosts/sops/etc/share/sops
To bypass (not recommended unless you know what youβre doing):
senv dec --unsafe --in /somewhere/file.env.enc --out /somewhere/file.envnotifierd listens on TCP (default 9901) and emits a single-line event to stdout with a fixed prefix (default __HOST_NOTIFY__).
notify "Build done" "All services are healthy β
"A host-side companion that sends notifications to the tools notifierd service using a stable one-line TCP protocol.
Protocol (tab-separated): token timeout urgency source title body
sudo curl -fsSL \
"https://raw.githubusercontent.com/infocyph/Scriptomatic/refs/heads/main/bash/docknotify.sh" \
-o /usr/local/bin/docknotify \
&& sudo chmod +x /usr/local/bin/docknotifydocknotify "Build done" "All services are healthy β
"docker logs -f docker-tools 2>/dev/null | awk -v p="__HOST_NOTIFY__" '
index($0, p) == 1 {
line = $0
sub("^" p "[ \t]*", "", line)
n = split(line, a, "\t")
if (n >= 6) {
urgency = a[3]
source = a[4]
title = a[5]
body = a[6]
for (i = 7; i <= n; i++) body = body "\t" a[i]
printf("[%-8s][%s] %s β %s\n", urgency, source, title, body)
} else {
print line
}
fflush()
}
'| Variable | Default | Description |
|---|---|---|
TZ |
(empty) | Timezone |
CAROOT |
/etc/share/rootCA |
mkcert CA root directory |
RUNTIME_VERSIONS_DB |
/etc/share/runtime-versions.json |
runtime versions DB used by mkhost |
EDITOR / VISUAL |
nano |
default editor |
NOTIFY_TCP_PORT |
9901 |
notifier TCP port |
NOTIFY_FIFO |
/run/notify.fifo |
internal FIFO path |
NOTIFY_PREFIX |
__HOST_NOTIFY__ |
stdout prefix |
NOTIFY_TOKEN |
(empty) | optional token auth |
LOGVIEW_AUTOSTART |
1 |
start built-in LogViewer on container start |
LOGVIEW_BIND |
0.0.0.0 |
bind address for LogViewer PHP server |
LOGVIEW_PORT |
9911 |
listen port for LogViewer |
LOGVIEW_ROOTS |
/global/log |
colon-separated log roots for LogViewer |
LOGVIEW_MAX_TAIL_LINES |
25000 |
maximum tail lines returned by LogViewer APIs |
LOGVIEW_CACHE_TTL |
2 |
LogViewer cache ttl (seconds) |
SOPS_BASE_DIR |
/etc/share/sops |
global SOPS base directory |
SOPS_KEYS_DIR |
/etc/share/sops/keys |
per-project keys directory |
SOPS_CFG_DIR |
/etc/share/sops/config |
per-project config directory |
SOPS_GLOBAL_DIR |
/etc/share/sops/global |
global fallback key/config directory |
SOPS_CONFIG_FILE |
(empty) | override global fallback .sops.yaml |
SOPS_AGE_KEY_FILE |
(empty) | override age key file path |
SENV_PROJECT |
(auto) | project id (auto-detected from git) |
SOPS_REPO_DIR |
/etc/share/vhosts/sops |
shared encrypted env repo mount |
STATUS_PROJECT |
(auto) | force project name for status |
STATUS_PROBE |
1 |
enable URL probes in status |
STATUS_FORCE_COLOR |
0 |
force color output in status |
STATUS_MOUNT_DEEP_COUNT |
0 |
deep recursive mount file counts in status checks (slow on bind mounts) |
STATUS_LOG_SCAN_MAX_DEPTH |
3 |
max depth for /global/log file counts in status checks (all/-1 = full recursion) |
WORKING_DIR / LDS_WORKDIR |
current dir | stack root hint for status |
ENV_DOCKER |
$WORKING_DIR/docker/.env |
compose env file path used by status |
VHOST_NGINX_DIR |
auto | vhost dir used by status URL discovery |
ENV_STORE_BACKEND |
json |
backend for env-store (json or sqlite) |
ENV_STORE_JSON |
/etc/share/state/env-store.json |
JSON state file used by env-store and stateful shell tools |
ENV_STORE_DB |
/etc/share/state/env-store.db |
SQLite state DB used when ENV_STORE_BACKEND=sqlite |
ENV_STORE_SQLITE_BIN |
sqlite3 |
sqlite client binary used by env-store |
docker exec -it docker-tools lazydockerMake sure /var/run/docker.sock is mounted.
Licensed under the MIT License Β© infocyph