diff --git a/README.md b/README.md new file mode 100644 index 0000000..90b4ecc --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# LocalDevStack + +LocalDevStack provides an easy-to-use Docker-based development environment for your projects. +All modules are **selective** and can be enabled via environment settings (Compose profiles). +Supports **multiple domains** and local TLS (mkcert). + +> 1) Local development only. +> 2) Your domain(s) must be resolvable on your host: +> - add entries to your hosts file: ` `, **or** +> - use a DNS that resolves to your machine, **or** +> - use `*.localhost` (no hosts entry required in many setups) + +--- + +## Prerequisites (Docker) + +Install docker on your system first. If you already have docker installed, you can skip this step. +- It is recommended to use [Docker Engine](https://docs.docker.com/engine/install/). +- If Docker Engine not supported in your OS, use [Docker Desktop](https://docs.docker.com/desktop/) (although you can also install this on linux as well). + +--- + +## Quickstart + +### 1) Clone +```bash +git clone https://github.com/infocyph/LocalDevStack.git +cd LocalDevStack +```` + +### 2) Permissions (Linux/macOS) + +```bash +chmod +x ./lds 2>/dev/null || true +sudo ./lds setup permissions +``` + +### 3) Start the stack + +```bash +lds start +``` + +### 4) Add a domain (vhost wizard) + +```bash +lds setup domain +``` + +### 5) Optional: trust HTTPS locally (Root CA) + +```bash +sudo lds certificate install +``` + +### 6) Reload HTTP (after vhost/cert changes) + +```bash +lds http reload +``` + +--- + +## Minimal configuration + +Most setups only need: + +* `PROJECT_DIR` (where your apps live) +* `COMPOSE_PROFILES` (what to run) + +Example: + +```dotenv +PROJECT_DIR=../application +COMPOSE_PROFILES=nginx,php,php84,tools,mariadb,redis +``` + +--- + +## CLI help (built-in “man”) + +```bash +lds help +lds help +lds help setup +lds help certificate +``` + +--- + +## Documentation + +This README stays intentionally short. + +* Full documentation: **Read the Docs** (Sphinx docs in `docs/`) +* Quick reference: `lds help ...` + +--- + +## License + +MIT diff --git a/README.mdx b/README.mdx deleted file mode 100644 index c4ec6b8..0000000 --- a/README.mdx +++ /dev/null @@ -1,245 +0,0 @@ -# localhost - -This project provides easy to use docker based development environment for your projects. All the modules are selective & -can be enabled based on Environment file (.env)! Supports multiple domains. - ->1. This is for local development only! ->2. Domain(s) should be available in the hosts file ` ` or DNS propageted to your host unless the TLD is localhost (*.localhost). - -## Prerequisites (Docker) -Install docker on your system first. If you already have docker installed, you can skip this step. -- It is recommended to use [Docker Engine](https://docs.docker.com/engine/install/). -- If Docker Engine not supported in your OS, use [Docker Desktop](https://docs.docker.com/desktop/) (although you can also install this on linux as well). - -## The directory structure - -For using your projects with this, by default you should arrange your projects structure as follows, -``` -| application - |- site1 - |- site2 - |- site3 - |- ..... -| localhost (this repository) -``` -But the project structure is flexible. If wanna change the directory of your projects (set other custom path instead of -same level application directory), just change/add the `PROJECT_DIR` in `.env` file, which should point to your project directory. - -```dotenv -PROJECT_DIR=../path/to/your/projects/directory # supports relative/absolute path -``` - -Now lets look into the localhost structure for where your configuration files should be. - -``` -| localhost - |- bin - |- configuration # this is where you should put your configuration files - |- apache # your apache Site configuration files (example available in directory) - |- nginx # your nginx Site configuration files (example available in directory) - |- php # your php configuration file (php.ini) - |- ssh # your ssh keys (needed specifically for git ssh authentication) - |- ssl # your ssl certificates - |- docker # this directory should stay untouched - |- compose # docker compose file(s) - |- conf # docker configuration files - |- data # docker data files for persistence - |- logs # container logs - |- .env - |- cruise - |- cruise.bat -``` - -## Run the server, the easiest way -- Simply, create `.env` file, place your settings. -- Create site configuration file in `localhost/configuration/(nginx or apache)`. Example configuration available in those directories. -- Don't forget to add host file entry for your domain(s) in your local machine. -- Run `cruise start` or `./cruise start` (on linux you must run `chmod +x cruise` first) -- Your site(s) will be available in your desired domain(s) - -## Usage -_Note: on linux you must run `chmod +x cruise` first_ -- `./cruise start/relaod` or `cruise start/reload` to start the server or reload with updated Environment variables -- If you want to enter in PHP container shell, simply run `cruise core` or `./cruise core` -- To stop the server, simply run `cruise stop` or `./cruise stop` -- To restart/reboot the server, simply run `cruise restart/reboot` or `./cruise restart/reboot` -- To rebuild the server, simply run `cruise rebuild ` or `./cruise rebuild ` -- Launch Docker CLI GUI using `cruise lzd` or `./cruise lzd` -- You can run any docker compose command using `cruise ` or `./cruise ` (except the above-mentioned ones) - -## Modules & Other main settings - -Modules (Docker Images, Linux Packages, PHP Versions & Extensions, NodeJS) are controlled based on the environment variables. -Checkout the .env.example file for example. To further understand these keep reading further. - -### 1. Sync the system user -Sync the internal docker user with the system user using the environment variable `UID`. In case of linux you can get this -using `id -u` command which is the UID of current user. In case of windows, you can get with same command if you use [cmder -terminal](https://github.com/cmderdev/cmder) or git-bash. - -```dotenv -UID=1000 -``` - -### 2. Selecting the docker image -To select the docker image, we used the environment variable `COMPOSE_PROFILES`. You will include your required modules -in CSV format (i.e. nginx,mysql). Here are the list of modules you can state here, -- `nginx` loads nginx image with php (service: web, php) -- `apache` loads apache with php (service: app) -- `mysql` or `mariadb` loads mysql/mariadb with phpmyadmin (service: mysql-server, mysql-client) -- `postgresql` loads postgresql & pgadmin (service: postgres-server, postgres-client) -- `mongodb` loads mongodb & mongo express (service: mongo-server, mongo-client) -- `elasticsearch` loads elasticsearch & kibana (service: elasticsearch-server, elasticsearch-client) -- `redis` loads redis & redis insight (service: redis-server, redis-client) -- `tools` loads server tools (check below for more info) (service: server-tools) - -_Note: don't include both `nginx` & `apache`_ - -```dotenv -COMPOSE_PROFILES=nginx,postgresql -``` -### 3. PHP version -Select the PHP version using the environment variable `PHP_VERSION`. Supports single PHP version. - -```dotenv -PHP_VERSION=8.3 -``` - -### 4. PHP extensions -List your required PHP extensions using the environment variable `PHP_EXTENSIONS`. Supports CSV formatted list. For the list -of available modules please refer to [mlocati docker extension list](https://github.com/mlocati/docker-php-extension-installer#supported-php-extensions). -Latest composer will be installed by default, no need to specify it. - -```dotenv -PHP_EXTENSIONS=bcmath,zip,gd -``` - -### 5. Linux packages -To install additional linux packages we used the environment variable `LINUX_PACKAGES`. -These extensions are additional & not related to your php extensions (as those will be auto installed by `PHP_EXTENSIONS`). -Supports CSV formatted values. - -```dotenv -LINUX_PACKAGES=git,curl -``` - -### 6. Node.js -Your project is also using node.js? To install it, we used the environment variable `NODE_VERSION`. Support major version number (i.e. 18/20/...). Also, -supports either of `lts` or `current` as well. Check [node.js debian source](https://github.com/nodesource/distributions#nodejs) for more details. -Leaving this empty, won't install node.js. - -```dotenv -NODE_VERSION=lts -``` -### 7. Environment variables - -In addition to the above, you can define the following environment variables as you see fit. - -- `TZ` Timezone _(default: Asia/Dhaka)_ - -#### nginx/apache -- `HTTP_PORT` http port _(default: 80)_ -- `HTTPS_PORT` https port _(default: 443)_ - -#### php -- `PHP_VERSION` PHP version _(default: 8.3)_ -- `UID` The uid of system user _(default: 1000)_ -- `PHP_EXTENSIONS` List of php extensions in csv format -- `LINUX_PACKAGES` List of linux packages in csv format -- `NODE_VERSION` If node.js is required, specify version - -#### mariadb/mysql -- `MYSQL_IMAGE` What you wanna use? `mariadb` or `mysql` _(default: mariadb)_ -- `MYSQL_VERSION` The version for `mariadb` or `mysql` _(default: latest)_ -- `MYSQL_PORT` DB port _(default: 3306)_ -- `MYSQL_ROOT_PASSWORD` Root user password _(default: 12345)_ -- `MYSQL_USER` DB User _(default: devuser)_ -- `MYSQL_PASSWORD` DB password _(default: 12345)_ -- `MYSQL_DATABASE` DB name _(default: localdb)_ - -#### mariadb/mysql client (PHPMyAdmin) -- `MYADMIN_PORT` The client access port _(default: 3300)_ - -#### postgres -- `POSTGRESQL_VERSION` The version for `PostgreSQL` _(default: latest)_ -- `POSTGRESQL_PORT` DB port _(default: 5432)_ -- `POSTGRES_USER` DB user _(default: postgres)_ -- `POSTGRES_PASSWORD` DB password _(default: postgres)_ -- `POSTGRES_DATABASE` DB name _(default: postgres)_ - -#### postgres client (PgAdmin 4) -- `PGADMIN_PORT` The client access port _(default: 5400)_ - -#### mongodb -- `MONGODB_VERSION` The version for `MongoDB` _(default: latest)_ -- `MONGODB_PORT` DB port _(default: 27017)_ -- `MONGODB_ROOT_USERNAME` username _(default: root)_ -- `MONGODB_ROOT_PASSWORD` password _(default: 12345)_ - -#### mongodb client (Mongo Express) -- `ME_VERSION` App version _(default: latest)_ -- `ME_BA_USERNAME` Basic Auth User _(default: root)_ -- `ME_BA_PASSWORD` Basic Auth Password _(default: 12345)_ - -#### elasticsearch -- `ELASTICSEARCH_VERSION` ElasticSearch version _(default:8.12.2)_ -- `ELASTICSEARCH_PORT` ES port _(default: 9200)_ - -#### elasticsearch client (Kibana) -- `KIBANA_PORT` The client access port _(default: 5601)_ - -#### redis -- `REDIS_VERSION` Redis version _(default: latest)_ -- `REDIS_PORT` Redis port _(default: 6379)_ - -#### redis client (Redis Insight) -- `RI_PORT` The client access port _(default: 5540)_ - -## CLI Utilities -You can add the `localhost/bin` directory, to your PATH environment variable for global usage of several commands. These -will be available depending on what you enable in `COMPOSE_PROFILES`. - -_** If you have any other docker container running with the same name as of this docker container names, it will end up in conflict!_ - -**Available commands:** -- `php` -- `mysql` -- `mysqldump` -- `mariadb` -- `mariadb-dump` -- `psql` -- `pg_dump` -- `pg_restore` -- `redis-cli` -- `lzd` # Docker CLI GUI -- `cert` # Generate ssl certificates, in case you don't have it - - Certificate(s) will be generated directly inside the `configuration/ssl` directory - - usage: `cert site1.internal site2.com *.site3.com .....` - -In windows, it is recommended to use [cmder terminal](https://github.com/cmderdev/cmder) or git-bash for better experience. - -## Server Tools -Well, you can use these tools to help you out. We have created a list of them below. These tools can help you do several things. -This container mounts your application directory as `/app` inside the container. So you can use these tools on all your projects. - -**Usable outside docker container:** -- `lzd` # Docker CLI GUI -- `cert` # Generate ssl certificates, in case you don't have it - - Certificate(s) will be generated directly inside the `configuration/ssl` directory - - usage: `cert site1.internal site2.com *.site3.com .....` - -**Usable inside docker container:** -- `git` (if you need to incorporate ssh keys with git, use the `configuration/ssh` directory) -- `curl` -- `wget` -- net-tools (available commands: `arp`, `hostname`, `ifconfig`, `netstat`,... etc.) -- `git fame` # Git contributor stats - - `git fame -e --enum` # Get Normal Counter on Surviving code (low accuracy) - - `git fame -ewMC --enum` # Deep Counter on Surviving code (intra-file & inter-file modifier detector, file system independent) more calculation time - - Check `git fame -h` command for command details -- `owners` # generate owners list (i.e for Github: `owners | tee .github/CODEOWNERS`) -- `git-story` # Animated Git story generator (check `git-story -h` for command details) - -## ToDo -- Tunnel support -- SupervisorD with PHP diff --git a/docs/concepts/architecture.rst b/docs/concepts/architecture.rst index 00a6c79..b7d15a1 100644 --- a/docs/concepts/architecture.rst +++ b/docs/concepts/architecture.rst @@ -21,7 +21,7 @@ Instead of a monolithic "one container does everything" model, LocalDevStack use How containers cooperate ------------------------ -1. You generate vhost configs (via ``server setup domain``). +1. You generate vhost configs (via ``lds setup domain``). 2. The Tools container can scan all vhosts and generate certificates. 3. Nginx loads hosts and routes requests either: diff --git a/docs/guides/node-apps.rst b/docs/guides/node-apps.rst deleted file mode 100644 index 7ce1329..0000000 --- a/docs/guides/node-apps.rst +++ /dev/null @@ -1,24 +0,0 @@ -Node Apps -========= - -LocalDevStack supports Node applications behind Nginx reverse proxy. - -Common model ------------- - -- Nginx terminates HTTP/HTTPS -- Nginx proxies to the Node container (e.g., port 3000) -- Node app source is mounted under ``/app`` - -Vhost generation ----------------- - -When you choose a Node app in the domain wizard, the Tools container can generate: - -- a domain-specific Nginx vhost -- a compose fragment describing the Node service - -Health checks -------------- - -Node services are typically checked via a simple TCP connect probe. diff --git a/docs/guides/notifications.rst b/docs/guides/notifications.rst index c0e2acc..7e8214a 100644 --- a/docs/guides/notifications.rst +++ b/docs/guides/notifications.rst @@ -1,18 +1,322 @@ Notifications ============= -LocalDevStack can optionally emit notifications from containers to the host. +LocalDevStack can optionally emit notifications from containers to the host (non-Windows). + +The idea is simple: + +- Your host runs a watcher that listens for notification events. +- Containers fire-and-forget messages using a tiny client binary (``docknotify``). +- The watcher turns those events into desktop notifications (toast / notify-send / etc.). + +Host usage (non-Windows) +------------------------ + +Start watching (recommended during development):: + + lds notify watch + +Send a one-off test notification:: + + lds notify test "T" "B" + +From inside containers (docknotify) +----------------------------------- + +Inside LocalDevStack containers, trigger a notification by calling ``docknotify``:: + + docknotify -t 2500 -u normal some_title some_body >/dev/null 2>&1 & + +Options: + +- ``-t``: timeout in milliseconds (example: ``2500``) +- ``-u``: urgency (example: ``low``, ``normal``, ``critical``) +- The final two arguments are: ``title`` and ``body`` + +The redirection + ``&`` makes it fire-and-forget so it never blocks your request or job. Common pattern -------------- -- A small TCP server (often ``notifierd``) runs in the Tools container. -- Other scripts send events (often via a ``notify`` client). -- The host can tail container logs and forward events to OS notifications (toast, notify-send, etc.). +- You keep ``lds notify watch`` running on the host. +- Your apps/services inside containers call ``docknotify`` when something noteworthy happens + (errors, deploy events, background jobs, long tasks, etc.). Message format -------------- -Implementations commonly use a single-line, tab-separated payload (token, timeout, urgency, source, title, body). +Notifications are transmitted as a single-line payload (safe for log streaming and easy parsing). +Implementations typically use a tab-separated payload like: + +- token +- timeout +- urgency +- source +- title +- body + +This is intentionally simple: it survives log streaming and is easy to parse reliably. + +PHP example: forward all PHP errors to notifications +---------------------------------------------------- + +Below is a minimal helper you can drop into any PHP project to log everything to a file and optionally +emit desktop notifications via ``docknotify`` (when running inside LocalDevStack containers). + +.. code-block:: php + + /dev/null')); + if ($bin === '') { + return; + } + + // Keep short + safe; remove newlines/tabs to keep one-line protocol stable + $title = (string)(\preg_replace('/\s+/', ' ', $title) ?? 'PHP Error'); + $body = (string)(\preg_replace('/\s+/', ' ', $body) ?? ''); + + $title = \substr($title, 0, 80); + $body = \substr($body, 0, 220); + + // Escape args (no injection) + $t = \escapeshellarg($title); + $b = \escapeshellarg($body); + + // Send as "normal" urgency, 2500ms timeout; fire-and-forget + @\shell_exec($bin . ' -t 2500 -u normal ' . $t . ' ' . $b . ' >/dev/null 2>&1 &'); + }; + + $map = [ + E_ERROR => 'E_ERROR', + E_WARNING => 'E_WARNING', + E_PARSE => 'E_PARSE', + E_NOTICE => 'E_NOTICE', + E_CORE_ERROR => 'E_CORE_ERROR', + E_CORE_WARNING => 'E_CORE_WARNING', + E_COMPILE_ERROR => 'E_COMPILE_ERROR', + E_COMPILE_WARNING => 'E_COMPILE_WARNING', + E_USER_ERROR => 'E_USER_ERROR', + E_USER_WARNING => 'E_USER_WARNING', + E_USER_NOTICE => 'E_USER_NOTICE', + E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', + E_DEPRECATED => 'E_DEPRECATED', + E_USER_DEPRECATED => 'E_USER_DEPRECATED', + ]; + + // Log non-fatal errors (warnings/notices/deprecations, etc.) + \set_error_handler( + static function (int $severity, string $message, string $file, int $line) use ($logFile, $map, $notifyFn): bool { + // Respect @ suppression + if (!(error_reporting() & $severity)) { + return true; + } + + $label = $map[$severity] ?? ('E_' . (string)$severity); + $ts = \date('Y-m-d H:i:s'); + + @\file_put_contents( + $logFile, + $ts . ' [' . $label . '] ' . $message . ' in ' . $file . ':' . $line . PHP_EOL, + FILE_APPEND | LOCK_EX + ); + + $notifyFn($label, $message . ' (' . \basename($file) . ':' . $line . ')'); + + // We handled it; do not let PHP print/log elsewhere + return true; + } + ); + + // Log uncaught exceptions / TypeErrors, etc. + \set_exception_handler( + static function (\Throwable $e) use ($logFile, $notifyFn): void { + $ts = \date('Y-m-d H:i:s'); + $type = \get_class($e); + + $msg = $ts + . ' [UNCAUGHT ' . $type . '] ' + . $e->getMessage() + . ' in ' . $e->getFile() . ':' . $e->getLine() + . PHP_EOL + . $e->getTraceAsString() + . PHP_EOL; + + @\file_put_contents($logFile, $msg . PHP_EOL, FILE_APPEND | LOCK_EX); + + $notifyFn( + 'UNCAUGHT ' . $type, + $e->getMessage() . ' (' . \basename($e->getFile()) . ':' . $e->getLine() . ')' + ); + + exit(255); + } + ); + + // Log fatal errors (E_ERROR, E_PARSE, E_COMPILE_ERROR, etc.) + \register_shutdown_function( + static function () use ($logFile, $notifyFn, $map): void { + $err = \error_get_last(); + if ($err === null) { + return; + } + + $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; + $type = (int)($err['type'] ?? 0); + + if (!\in_array($type, $fatalTypes, true)) { + return; + } + + $label = $map[$type] ?? ('E_' . (string)$type); + $ts = \date('Y-m-d H:i:s'); + + $message = (string)($err['message'] ?? ''); + $file = (string)($err['file'] ?? ''); + $line = (int)($err['line'] ?? 0); + + @\file_put_contents( + $logFile, + $ts . ' [' . $label . '] ' . $message . ' in ' . $file . ':' . $line . PHP_EOL, + FILE_APPEND | LOCK_EX + ); + + $notifyFn($label, $message . ' (' . \basename($file) . ':' . $line . ')'); + } + ); + + // Ensure nothing is printed to screen by PHP itself + \ini_set('display_errors', '0'); + \ini_set('log_errors', '0'); + } + + // Usage: + $log = __DIR__ . '/php-upg-err-' . \date('Ymd') . '.log'; + registerAllErrorsToFile($log, true); + +Node.js example: send notifications using docknotify +---------------------------------------------------- + +LocalDevStack ships ``docknotify`` inside Node containers too, so Node apps can emit host notifications +without extra dependencies. + +Below is a small helper that: + +- checks ``docknotify`` exists +- strips newlines/tabs (keeps the one-line protocol stable) +- sends a fire-and-forget notification (non-blocking) + +.. code-block:: js + + // docknotify.js + const { spawnSync, spawn } = require("node:child_process"); + + // Cache existence check so we don't run it per request + const HAS_DOCKNOTIFY = (() => { + const r = spawnSync("sh", ["-lc", "command -v docknotify >/dev/null 2>&1"], { stdio: "ignore" }); + return r.status === 0; + })(); + + function clean(s, max) { + return String(s ?? "") + .replace(/[\t\r\n]+/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, max); + } + + function notify(title, body, { timeout = 2500, urgency = "normal" } = {}) { + if (!HAS_DOCKNOTIFY) return; + + const t = clean(title, 80) || "Node"; + const b = clean(body, 220); + + // Fire-and-forget: no stdout/stderr, detached, unref + const child = spawn( + "docknotify", + ["-t", String(timeout), "-u", urgency, t, b], + { stdio: "ignore", detached: true } + ); + + child.on("error", () => {}); + child.unref(); + } + + module.exports = { notify }; + +.. code-block:: js + + // example usage (Express) + const express = require("express"); + const { notify } = require("./docknotify"); + + const app = express(); + + process.on("unhandledRejection", (err) => { + notify("Unhandled Rejection", err?.stack || String(err)); + }); + + process.on("uncaughtException", (err) => { + notify("Uncaught Exception", err?.stack || String(err), { urgency: "critical" }); + // process.exit(1); + }); + + app.get("/", (req, res) => res.json({ ok: true })); + + // test route + app.get("/boom", () => { + throw new Error("Test error from /boom"); + }); + + // eslint-disable-next-line no-unused-vars + app.use((err, req, res, next) => { + notify("Express Error", `${err.message} (${req.method} ${req.originalUrl})`); + res.status(500).json({ error: "Internal Server Error" }); + }); + + const port = process.env.PORT || 3000; + app.listen(port, () => { + notify("Node Started", `Listening on :${port}`, { timeout: 1500, urgency: "low" }); + }); + +Quick test +~~~~~~~~~~ + +1. On host, start watcher:: + + lds notify watch + +2. Trigger the test route:: + + curl -sS http://your-domain.localhost/boom >/dev/null + +Practical workflow +------------------ + +1. On your host, keep this running in a terminal:: + + lds notify watch + +2. In your PHP/Node apps (inside containers), trigger ``docknotify`` on important events + (errors, failed jobs, timeouts, etc.). -This is intentionally simple: it survives log streaming and is easy to parse. +This gives you immediate feedback without tailing logs all day. diff --git a/docs/guides/tls-and-certificates.rst b/docs/guides/tls-and-certificates.rst index 2b6d419..ffe9153 100644 --- a/docs/guides/tls-and-certificates.rst +++ b/docs/guides/tls-and-certificates.rst @@ -1,25 +1,128 @@ TLS and Certificates ==================== -LocalDevStack uses mkcert-based local TLS. +LocalDevStack uses **mkcert-based local TLS** for development. -Two components are commonly involved: +At first run, it creates a **local Root CA** and issues development certificates. +After that, it scans your vhost configs and (re)generates certificates for all detected domains. -- ``mkcert``: creates a local CA and issues dev certificates -- ``certify``: scans vhost configs and (re)generates certificates for all detected domains +Certificates are generated and persisted under the host-mounted ``configuration/`` tree so they survive rebuilds. + +Generated files (host) +---------------------- + +LocalDevStack persists TLS artifacts in this layout:: + + configuration/ + ├── rootCA + │ ├── rootCA-key.pem + │ └── rootCA.pem + └── ssl + ├── apache-client-key.pem + ├── apache-client.pem + ├── apache-server-key.pem + ├── apache-server.pem + ├── local-key.pem + ├── local.pem + ├── nginx-client-key.pem + ├── nginx-client.p12 + ├── nginx-client.pem + ├── nginx-proxy-key.pem + ├── nginx-proxy.pem + ├── nginx-server-key.pem + └── nginx-server.pem + +Notes: + +- ``configuration/rootCA/rootCA.pem`` is your local development CA certificate. +- The ``*-server*.pem`` pairs are used by Nginx/Apache for HTTPS. +- The ``*-client*.pem`` pairs are used when **mutual TLS** is enabled for a domain. +- ``nginx-client.p12`` is provided for convenient browser import when mutual TLS is enabled. Domain discovery ---------------- -``certify`` typically scans all ``*.conf`` files under a shared vhost directory (mounted from your host). -From that, it derives domain names and generates SAN certificates. +Certificate generation scans all ``*.conf`` files under the shared vhost directory (mounted from your host). +From those filenames/configs, LocalDevStack derives domain names and generates SAN certificates +covering all detected domains. + +This keeps certs aligned with your active vhost set: add/remove a domain, regenerate, done. + +Trusting the Root CA +-------------------- + +To trust your local CA on your host system, run:: + + sudo lds certificate install + +This installs ``configuration/rootCA/rootCA.pem`` into your OS trust store (where supported). + +Manual install is also possible: + +- Import ``configuration/rootCA/rootCA.pem`` into your OS trust store using your system UI/tools. +- This is useful in locked-down environments where automated install is restricted. + +Mutual TLS (Client certificates) +-------------------------------- + +If you enable **mutual TLS** for any domain: + +- You must install the client certificate in your browser. +- Recommended: import ``configuration/ssl/nginx-client.p12`` into the browser certificate store. + +After importing, the browser will present the client certificate when accessing mTLS-protected domains. + +Uninstalling the Root CA +------------------------ + +If you previously trusted the LocalDevStack Root CA and want to remove it from your system trust store, use:: + + sudo lds certificate uninstall + +This removes the installed CA file from the detected OS trust anchor location and then refreshes the system trust store +(best-effort). + +Remove from all known locations (cleanup mode) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you changed distros, moved CA paths, or installed it manually in different locations, use:: + + sudo lds certificate uninstall --all + +This additionally scans common CA anchor locations and removes any leftover ``rootCA`` entries it finds, then refreshes +the trust store. + +Notes +~~~~~ + +- Uninstall requires sudo/admin privileges. +- Trust store refresh is best-effort; on uncommon distributions you may need to refresh trust manually after removal. +- This only removes the *installed* OS trust anchor. It does not delete your generated CA files under + ``configuration/rootCA`` (those are part of your project persistence). + + +Troubleshooting +--------------- + +Browser still shows “Not Secure” +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Confirm the Root CA is trusted: + + - Run ``lds certificate install`` again, or + - Manually install ``configuration/rootCA/rootCA.pem`` into your OS trust store. + +- Restart the browser after installing the CA (some browsers cache trust decisions). -Persistence ------------ +Certificate mismatch after changing domains +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Persist these host-mounted directories: +- If you renamed/removed domains, regenerate certs so SANs match the current vhost set. +- Ensure the vhost files under ``configuration/nginx`` / ``configuration/apache`` reflect the current domains. -- ``configuration/rootCA``: the local Root CA -- ``configuration/ssl``: generated cert/key output +Mutual TLS enabled but browser doesn’t prompt / request fails +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -After persistence, you can rebuild containers without losing trust. +- Import the client cert (recommended): ``configuration/ssl/nginx-client.p12``. +- Verify the client cert is imported into the *correct* browser profile. +- If you have multiple client certs, remove old ones and retry to avoid wrong-certificate selection. diff --git a/docs/index.rst b/docs/index.rst index 84dae69..37c7cc3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ LocalDevStack Documentation LocalDevStack is a modular, Docker-based local development stack designed to replace traditional local bundles (XAMPP/MAMP/LAMP) with a reproducible, profile-driven setup. -It is built around a small orchestrator (the ``server`` CLI + Compose profiles) and a set of purpose-built images +It is built around a small orchestrator (the ``lds`` CLI + Compose profiles) and a set of purpose-built images that work together (tools, HTTP, runner). .. toctree:: @@ -27,6 +27,5 @@ that work together (tools, HTTP, runner). guides/domain-setup guides/tls-and-certificates - guides/node-apps guides/secrets-sops-age guides/notifications diff --git a/lds b/lds index a22af96..1244193 100755 --- a/lds +++ b/lds @@ -1079,7 +1079,7 @@ cmd_rebuild() { local i for i in "${!all_svcs[@]}"; do - printf " %2d) %s\n" "$((i+1))" "${all_svcs[$i]}" + printf " %2d) %s\n" "$((i + 1))" "${all_svcs[$i]}" done echo @@ -1102,15 +1102,20 @@ cmd_rebuild() { # range like 3-7 if [[ "$arg" =~ ^[0-9]+-[0-9]+$ ]]; then local a b - a="${arg%-*}"; b="${arg#*-}" - (( a >= 1 )) || continue - (( b >= 1 )) || continue - (( a <= b )) || { local t="$a"; a="$b"; b="$t"; } + a="${arg%-*}" + b="${arg#*-}" + ((a >= 1)) || continue + ((b >= 1)) || continue + ((a <= b)) || { + local t="$a" + a="$b" + b="$t" + } local n - for ((n=a; n<=b; n++)); do - (( n >= 1 && n <= ${#all_svcs[@]} )) || continue - _add_target "${all_svcs[$((n-1))]}" + for ((n = a; n <= b; n++)); do + ((n >= 1 && n <= ${#all_svcs[@]})) || continue + _add_target "${all_svcs[$((n - 1))]}" done continue fi @@ -1118,8 +1123,8 @@ cmd_rebuild() { # single index if [[ "$arg" =~ ^[0-9]+$ ]]; then local n="$arg" - (( n >= 1 && n <= ${#all_svcs[@]} )) || continue - _add_target "${all_svcs[$((n-1))]}" + ((n >= 1 && n <= ${#all_svcs[@]})) || continue + _add_target "${all_svcs[$((n - 1))]}" continue fi @@ -1559,21 +1564,65 @@ notify_watch() { local container="${1:-SERVER_TOOLS}" local prefix="__HOST_NOTIFY__" - need docker notify-send + need docker + + local _disp="${DISPLAY-}" + local _dbus="${DBUS_SESSION_BUS_ADDRESS-}" + + # Args: timeout(ms) urgency title body + _host_notify() { + local timeout="${1:-2500}" urgency="${2:-normal}" title="${3:-Notification}" body="${4:-}" + + # Linux desktop (or WSLg) + if command -v notify-send >/dev/null 2>&1; then + (env DISPLAY="${_disp-}" DBUS_SESSION_BUS_ADDRESS="${_dbus-}" \ + setsid -f notify-send -u "$urgency" -t "$timeout" "$title" "$body" \ + >/dev/null 2>&1 || true) & + return 0 + fi + + # Windows toast (Git Bash) / WSL-on-Windows + if command -v powershell.exe >/dev/null 2>&1; then + # Pass values as args to avoid quoting issues entirely. + # Note: urgency/timeout not used by toast api here; kept for parity. + powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \ + 'param([string]$t,[string]$b) + try { + [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null + [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null + + function Esc([string]$s) { + if ($null -eq $s) { return "" } + return ($s -replace "&","&" -replace "<","<" -replace ">",">" -replace "\"",""" -replace "'\''","'") + } + + $title = Esc $t + $body = Esc $b + + $xml = New-Object Windows.Data.Xml.Dom.XmlDocument + $xml.LoadXml("$title$body") + $toast = New-Object Windows.UI.Notifications.ToastNotification $xml + [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Devtainer").Show($toast) + } catch { }' \ + --% "$title" "$body" >/dev/null 2>&1 || true + + return 0 + fi + + # Fallback + printf "%s [%s] %s - %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$urgency" "$title" "$body" >&2 + return 0 + } trap - ERR set +e set +o pipefail - local _disp="${DISPLAY-}" - local _dbus="${DBUS_SESSION_BUS_ADDRESS-}" local _stop=0 _watcher_notify() { local urgency="${1:-critical}" title="${2:-Notifier}" body="${3:-Watcher event}" - (env DISPLAY="${_disp-}" DBUS_SESSION_BUS_ADDRESS="${_dbus-}" \ - setsid -f notify-send -u "$urgency" -t 2500 "$title" "$body" \ - >/dev/null 2>&1 || true) & + _host_notify 2500 "$urgency" "$title" "$body" } _watcher_int_term() { @@ -1584,7 +1633,7 @@ notify_watch() { trap _watcher_int_term INT TERM local grep_cmd=(grep -a --line-buffered -E "^${prefix}([[:space:]]|$)") - command -v stdbuf &>/dev/null && grep_cmd=(stdbuf -oL -eL "${grep_cmd[@]}") + command -v stdbuf >/dev/null 2>&1 && grep_cmd=(stdbuf -oL -eL "${grep_cmd[@]}") printf "%bNotify Watch:%b monitoring is active. Ctrl+C to stop.\n" "$GREEN" "$NC" @@ -1599,6 +1648,7 @@ notify_watch() { ("${grep_cmd[@]}" || true) | while IFS=$'\t' read -r _ f1 f2 f3 f4 rest; do local timeout urgency title body + if [[ "${f1:-}" =~ ^[0-9]{1,6}$ ]]; then timeout="$f1" urgency="${f2:-normal}" @@ -1610,10 +1660,11 @@ notify_watch() { title="${f2:-Notification}" body="${f3:-}" fi + [[ -n "${rest:-}" ]] && body+=$'\t'"${rest}" case "$urgency" in low | normal | critical) ;; *) urgency="normal" ;; esac - notify-send -t "$timeout" -u "$urgency" "$title" "$body" >/dev/null 2>&1 || true + _host_notify "$timeout" "$urgency" "$title" "$body" printf "%s [%s] %s - %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$urgency" "$title" "$body" >&2 done @@ -2009,7 +2060,7 @@ EOF ############################################################################### main() { need docker - ensure_files_exist "/docker/.env" "/configuration/php/php.ini" "/.env" + ((EUID == 0)) || ensure_files_exist "/docker/.env" "/configuration/php/php.ini" "/.env" [[ $# -gt 0 ]] || { cmd_help