Warning
This project is under active development and is not yet fully functional. Features described in this README may be incomplete, unstable, or broken. Do not use in production.
Crucible
Unified interactive Bash toolkit for Linux server provisioning
Crucible automates the full lifecycle of a Linux server β from bare-metal OS installation through web application deployment and post-install hardening β across four enterprise Linux distributions. It drives every step through an interactive TUI (whiptail/dialog with plain-text fallback) and persists session state to JSON, so interrupted workflows resume exactly where they left off.
| Install | Provision | Deploy | Harden | Docker | |
|---|---|---|---|---|---|
| What | Bare-metal OS | PHP, web servers, databases | 10 app types | SSH, firewall, fail2ban | Dev environments |
| How | Disk partitioning, LUKS2, LVM, BTRFS subvolumes, debootstrap/dnf | PHP-FPM, nginx, OpenLiteSpeed, MariaDB, PostgreSQL | Idempotent 10-step pipeline with per-app state | Key auth, nftables/firewalld, jails, logrotate | Compose generation, distro mirroring, per-app dev |
| Where | Bare metal | Bare metal, VPS | Bare metal, VPS | Bare metal, VPS | Any Docker host |
| Distribution | Family | Codename |
|---|---|---|
| Ubuntu 24.04 LTS | Debian | Noble Numbat |
| Debian 13 | Debian | Trixie |
| Rocky Linux 10 | RHEL | Green Obsidian |
| AlmaLinux 10 | RHEL | Purple Lion |
All cross-distro differences (package names, service paths, config locations) are resolved through lib/detect.sh β no module ever checks the distro directly.
git clone https://github.com/fluxgear/crucible.git
cd crucible
sudo ./crucibleThis launches the interactive TUI. For CLI usage, see CLI Reference.
Crucible uses a phase-gated workflow β each major phase must complete (or be explicitly skipped) before the next unlocks:
State persistence β All progress is stored in a JSON state file (/var/lib/crucible/state.json as root, ~/.crucible/state.json otherwise) via atomic writes (mktemp + jq + mv). If a session is interrupted, re-running crucible picks up from the last completed step.
Phase badges β The main menu shows live progress for each phase:
[ ]pending Β Β[~]in progress Β Β[β]complete Β Β[-]skipped
The main menu adapts based on environment and phase progress:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Crucible v0.13.2 β
β β
β Select a phase: β
β β
β [ ] OS Installation β
β [ ] Server Provisioning β
β Web Applications β
β [ ] Hardening β
β Docker Environments β
β Update Crucible β
β Quit β
β β
β < OK > < Cancel > β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Three TUI backends are supported, auto-detected in order: whiptail (default on Debian/Ubuntu), dialog (common on RHEL), and plain-text (universal fallback requiring no dependencies).
Automates bare-metal disk setup and OS bootstrapping. Skipped automatically on WSL2 and Docker.
| Feature | Details |
|---|---|
| Boot modes | EFI (GPT + ESP) and Legacy BIOS (GPT + BIOS boot partition) β auto-detected |
| Encryption | LUKS2 with configurable iteration time, double-entry password verification |
| Filesystems | BTRFS, BTRFS-on-LVM, XFS, LVM+XFS |
| BTRFS subvolumes | @ (root), @home, @snapshots |
| Bootstrap | debootstrap for Debian-family, dnf --installroot for RHEL-family |
| Device awareness | NVMe and loop device partition naming handled automatically |
The installation wizard walks through disk selection, filesystem choice, optional LUKS encryption, partitioning, and bootstrap β with a confirmation summary before any destructive operation.
Provisions the server stack through a sub-menu:
| Component | Options |
|---|---|
| PHP | Installs PHP-FPM with per-app pool configuration, OPcache, APCu |
| Web server | nginx or OpenLiteSpeed β vhost templates with security rules per app type |
| Database | MariaDB (with tuning template) or PostgreSQL β user/database provisioning |
Each component tracks its own completion state. The provisioning sub-menu shows which components have been configured.
Deploys web applications through an idempotent 10-step pipeline. Each step is individually tracked β if any step fails, re-running resumes from exactly where it stopped.
| PHP Apps | Python Apps |
|---|---|
| WordPress | Django |
| Drupal | Flask |
| XenForo | FastAPI |
| Invision Power Board | Generic WSGI/ASGI |
| StackLuma | |
| Generic PHP |
1. Create system user 6. Create web server vhost
2. Create database 7. Set filesystem ACLs
3. Create database user 8. Generate self-signed SSL certificate
4. Configure app type 9. Set up cron jobs (app-specific)
5. Create FPM pool / venv 10. Write credentials file
- PHP apps get a dedicated FPM pool with per-app socket, OPcache, and session paths
- Python apps get a virtualenv, systemd service unit (gunicorn or uvicorn), and reverse-proxy vhost
- OpenLiteSpeed is supported for PHP apps only β Python apps require nginx
- SSL is generated by default (self-signed) and can be disabled per app
- Each app gets its own system user, home directory (from
skel-app/), and isolated log paths
Post-provisioning security hardening through four modules:
| Module | Capabilities |
|---|---|
| SSH | Key-only auth, disable root login, custom port, allowed-users list, protocol hardening |
| Firewall | nftables (Debian) / firewalld (RHEL) β opens SSH port from state, HTTP/HTTPS, custom ports |
| fail2ban | nginx-auth jail, bad-bots filter, WordPress login protection (auto-detected from state) |
| Logrotate | Per-app log rotation for nginx, PHP-FPM, and application logs |
All systemctl calls are gated on environment detection β hardening modules are skipped in Docker containers where systemd is unavailable.
Create disposable development environments that mirror your production distro. Two modes:
| Mode A β Distro Mirror | Mode B β Per-App Dev | |
|---|---|---|
| Purpose | Full server replica | Single-app development |
| Volumes | Named: apps + db |
Bind-mount: host webroot; Named: db |
| Services | nginx | PHP: php-fpm + mysqld + nginx; Python: postgresql + nginx |
| Use case | Run crucible provisioning inside a container | Edit code on host, run services in container |
Docker environments are managed through the TUI or CLI:
# Mode A β mirror an Ubuntu server
crucible docker up myserver a ubuntu2404
# Mode B β WordPress dev environment with host webroot
crucible docker up myapp b debian13 apptype=wordpress webroot=/path/to/site ports=9080:80
# Manage containers
crucible docker list
crucible docker exec myserver
crucible docker down myserver| Command | Description |
|---|---|
crucible |
Launch the interactive TUI |
crucible docker up <name> <mode> <distro> [opts] |
Create and start a Docker environment |
crucible docker down <name> |
Stop and remove a Docker environment |
crucible docker list |
List all managed Docker environments (syncs live status) |
crucible docker exec <name> [cmd] |
Execute a command in a running container (default: bash) |
crucible update |
Update crucible to the latest version |
crucible --version |
Print version |
crucible --help |
Print usage information |
Options are passed as KEY=VALUE pairs after the positional arguments:
| Option | Default | Description |
|---|---|---|
ports |
8080:80,8443:443 |
Port mappings (comma-separated host:container) |
webroot |
β | Host directory to bind-mount (required for Mode B) |
apptype |
β | App type for Mode B service selection |
Crucible detects its installation method and updates accordingly:
- Git clone β
git pull --ff-only origin developwith automatic stash of local changes - Release tarball β Downloads the latest GitHub release, verifies SHA-256 checksum (if provided), and replaces files in-place
In both cases, the state file is never touched β your provisioning progress is preserved across updates.
Architecture Overview
crucible β entrypoint (arg parsing, dispatch)
βββ lib/
β βββ detect.sh β distro/environment detection (leaf dependency)
β βββ state.sh β JSON state persistence via jq
β βββ tui.sh β TUI abstraction (whiptail/dialog/plaintext)
β βββ install/
β β βββ os-install.sh β disk setup, partitioning, filesystem creation
β β βββ bootstrap.sh β debootstrap/dnf chroot bootstrapping
β βββ provision/
β β βββ php.sh β PHP-FPM installation and pool management
β β βββ nginx.sh β nginx installation and vhost management
β β βββ ols.sh β OpenLiteSpeed installation and vhost management
β β βββ mariadb.sh β MariaDB installation, tuning, user/db management
β β βββ postgresql.sh β PostgreSQL installation, user/db management
β β βββ webapp.sh β 10-step app provisioning orchestrator
β β βββ apptypes.sh β app type dispatcher (sources all 10 modules)
β β βββ apptypes/ β per-app-type configuration
β β βββ wordpress.sh βββ django.sh
β β βββ drupal.sh βββ flask.sh
β β βββ xenforo.sh βββ fastapi.sh
β β βββ ipb.sh βββ generic_wsgi.sh
β β βββ stackluma.sh βββ generic_php.sh
β βββ harden/
β β βββ ssh.sh β SSH hardening
β β βββ firewall.sh β nftables/firewalld management
β β βββ fail2ban.sh β jail and filter management
β β βββ logrotate.sh β per-app log rotation
β βββ docker/
β βββ compose.sh β docker-compose.yml generation
β βββ env.sh β container lifecycle management
βββ templates/ β 15 config templates (FPM, vhosts, systemd, etc.)
βββ skel-app/ β skeleton home directory for app users
βββ tests/ β 203 BATS tests + 4-distro matrix runner
detect.sh β state.sh β tui.sh β everything else
(leaf)
detect.sh is the leaf dependency β every module depends on it, and it depends on nothing. All cross-distro path lookups, package names, and service names are resolved through detect.sh functions. No other module ever inspects CRUCIBLE_DISTRO_FAMILY directly.
State is a single JSON document managed by lib/state.sh:
{
"version": "0.13.2",
"phases": {
"os_install": { "status": "complete" },
"provisioning": {
"status": "complete",
"components": { "php": true, "webserver": "nginx", "database": "mariadb" }
},
"apps": [
{ "name": "example.com", "type": "wordpress", "status": "complete", "completed_steps": [...] }
],
"hardening": {
"status": "complete",
"components": { "ssh": true, "firewall": true, "fail2ban": true, "logrotate": true }
},
"docker": { "containers": [...] }
}
}All writes are atomic: write to a temp file via jq, then mv to the final path. The in-memory _CRUCIBLE_STATE global avoids repeated disk reads.
- Bash 5.0+
- jq (for state management)
- Root access (for provisioning and hardening operations)
- whiptail or dialog (optional β plain-text fallback works without either)
- curl (for self-update via release tarball)
- Docker + Docker Compose (for Docker dev environments only)
| Environment | Install | Provision | Apps | Harden | Docker |
|---|---|---|---|---|---|
| Bare metal | Yes | Yes | Yes | Yes | Yes |
| VPS | β | Yes | Yes | Yes | Yes |
| WSL2 | β | Requires systemd | Requires systemd | Requires systemd | Yes |
| Docker | β | β | β | β | β |
Crucible auto-detects the runtime environment and adjusts available phases accordingly. OS installation is only available on bare metal. Provisioning and hardening require systemd.
Testing
# Run all host-compatible tests (skips container-only tests)
bats tests/
# Run a specific test suite
bats tests/test_detect.bats
bats tests/test_webapp.bats
# Run the 4-distro matrix (requires Docker)
bash tests/matrix.sh| Suite | Tests | Scope |
|---|---|---|
test_detect.bats |
66 | Distro detection, path lookups, environment detection |
test_webapp.bats |
31 | App provisioning pipeline, user creation, state tracking |
test_state.bats |
26 | JSON state persistence, atomic writes, schema validation |
test_crossdistro.bats |
20 | Container-only: cross-distro path resolution |
test_mariadb.bats |
20 | Database/user provisioning, tuning template |
test_php.bats |
17 | PHP-FPM installation, pool creation |
test_matrix.bats |
11 | Matrix runner infrastructure |
test_postgresql.bats |
8 | Database/user provisioning |
test_tui.bats |
4 | Backend detection, dialog fallback |
| Total | 203 |
The matrix runner (tests/matrix.sh) builds lightweight containers for all four supported distros and executes the cross-distro test suite inside each:
bash tests/matrix.sh # run all distros
bash tests/matrix.sh ubuntu # run a single distro
bash tests/matrix.sh --cleanup # remove matrix images- Follow the coding standards in docs/conventions.md
- All cross-distro differences go through
lib/detect.shβ never checkCRUCIBLE_DISTRO_FAMILYin other modules - Every
.shfile needs: shebang,set -euo pipefail, file header, source guard - Functions are prefixed by module name:
nginx_create_vhost(),state_save() - Run
shellcheckandbats tests/before submitting changes
MIT β see LICENSE
Marc Mironescu β fluxgear.io