A lightweight, multi-tool Docker image for:
- β
SSL automation (
mkcert+certify) - β
Interactive vhost generation (
mkhost) + templates - β
Cleanup vhosts (
delhost) - β
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)
- Supports helper flags to query/reset internal βactiveβ selections (
ACTIVE_PHP_PROFILE,ACTIVE_NODE_PROFILE,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 |
delhost |
Remove vhost configs for a domain (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) |
gitx |
Git helper CLI |
chromacat |
Colorized output |
sqlitex |
SQLite helper CLI |
netx |
Networking helper wrapper |
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)
β ββ ssl/ # Generated certificates (.pem, .p12, keys)
β ββ 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
β
ββ 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/ssl |
/etc/mkcert |
certify, mkcert |
./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) |
/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/ssl:/etc/mkcert
- ./configuration/rootCA:/etc/share/rootCA
- ./configuration/sops:/etc/share/sops
- ./secrets-repo:/etc/share/vhosts/sops: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/ssl:/etc/mkcert" \
-v "$(pwd)/configuration/rootCA:/etc/share/rootCA" \
-v "$(pwd)/configuration/sops:/etc/share/sops" \
-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 |
|---|---|
| Apache (Server) | apache-server.pem, apache-server-key.pem |
| Apache (Client) | apache-client.pem, apache-client-key.pem |
| Nginx (Server) | nginx-server.pem, nginx-server-key.pem |
| Nginx (Proxy) | nginx-proxy.pem, nginx-proxy-key.pem |
| Nginx (Client) | nginx-client.pem, nginx-client-key.pem, nginx-client.p12 |
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/node/<token>.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/profile
- 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 the βactive selectionsβ into env (used by your server wrapper to enable compose profiles).
You can query/reset these values:
mkhost --RESET
mkhost --ACTIVE_PHP_PROFILE
mkhost --ACTIVE_NODE_PROFILE
mkhost --APACHE_ACTIVE--RESETclears all active selections.--ACTIVE_PHP_PROFILEprints the chosen PHP profile (if PHP was selected).--ACTIVE_NODE_PROFILEprints the chosen Node profile (if Node was selected).--APACHE_ACTIVEprintsapachewhen Apache mode was selected.
delhost deletes the generated files for a domain:
/etc/share/vhosts/nginx/<domain>.conf/etc/share/vhosts/apache/<domain>.conf/etc/share/vhosts/node/<token>.yaml(token is a safe slug of the domain)
Run it:
docker exec -it docker-tools delhost example.comInteractive mode (no args):
docker exec -it docker-tools delhostBehavior:
- Validates the domain format before deleting
- Shows exactly what files it will remove
- Requires confirmation (
y/N) - If nothing exists for that domain, it exits with code
2(useful for scripts)
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 |
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 |
docker exec -it docker-tools lazydockerMake sure /var/run/docker.sock is mounted.
Licensed under the MIT License Β© infocyph