diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fb28e63..2bd60a0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -235,7 +235,7 @@ jobs:
- name: Verify Bionic linkage
run: |
set -euo pipefail
- for f in dist/microinit-android-* dist/shutdown-android-*; do
+ for f in dist/libmicroinit.so dist/libshutdown.so dist/microinit-android-* dist/shutdown-android-*; do
[ -f "$f" ] || continue
file "$f"
if ! file "$f" | grep -qi 'android\|linker64\|linker'; then
@@ -288,7 +288,13 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish microinit-android-arm64
- run: ./scripts/publish-oci-microinit-android.sh dist/microinit-android-arm64 dist/shutdown-android-arm64
+ run: |
+ set -euo pipefail
+ microinit=dist/libmicroinit.so
+ shutdown=dist/libshutdown.so
+ [[ -f "${microinit}" ]] || microinit=dist/microinit-android-arm64
+ [[ -f "${shutdown}" ]] || shutdown=dist/shutdown-android-arm64
+ ./scripts/publish-oci-microinit-android.sh "${microinit}" "${shutdown}"
- name: Job summary
run: |
diff --git a/README.md b/README.md
index d9b8a7a..9646842 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-Lightweight PID 1 init system and service supervisor designed for BigFred OS.
+Lightweight PID 1 init system and service supervisor for embedded Linux and containers.
Works in embedded systems based on Linux as well as in containers.
Lightning fast and solid-rock reliable. Inspired by supervisord and Kubernetes, handles dependencies, able to self-heal.
@@ -131,6 +131,9 @@ MIT
See **[docs/README.md](docs/README.md)** for the full index.
-- [Operator guide](docs/operator.md) — manage services, write config, dependencies, boot
+- [Operator guide](docs/operator.md) — everyday CLI and boot
+- [Configuration](docs/configuration.md) — JSON, drop-ins, hot reload
+- [Service lifecycle](docs/service-lifecycle.md) — states and dependency behaviour at boot
+- [Using as supervisord](docs/using-as-supervisord.md) — PHP-FPM + NGINX example
- [Control socket API](docs/api.md) — Unix socket protocol for integrations
- [Architecture](docs/architecture.md) — design overview (`init` / `supervise`, reload, OTel)
diff --git a/docs/README.md b/docs/README.md
index acf14b9..3c8a08a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -4,7 +4,10 @@ Guides and references for running and integrating **microinit**.
| Document | Audience | Contents |
|----------|----------|----------|
-| [Operator guide](operator.md) | Linux admins / device operators | Managing services from the shell, writing JSON config, dependencies, boot sequence |
+| [Operator guide](operator.md) | Linux admins / device operators | Everyday CLI, boot, quick config overview |
+| [Configuration](configuration.md) | Administrators | JSON files, drop-ins in subfolders, hot reload, service fields |
+| [Service lifecycle](service-lifecycle.md) | Administrators | States over time; dependency when a service restarts at boot |
+| [Using as supervisord](using-as-supervisord.md) | Container / VM admins | `supervise` mode; PHP-FPM + NGINX with drop-ins and hot reload |
| [Control socket API](api.md) | Integrators / UI / scripts | Unix socket framing, request/response JSON |
| [Architecture](architecture.md) | Developers | Design overview: `init` vs `supervise`, reload, OTel, distribution |
| [Developer index](developer.md) | Contributors / embedders | Doc map + Go SDK pointer |
diff --git a/docs/api.md b/docs/api.md
index 4193a14..f0736d2 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -333,6 +333,7 @@ For `logs` with `follow: true`, keep reading frames in a loop (each frame has it
## Further reading
-- [Operator guide](operator.md) — day-to-day CLI and config
+- [Operator guide](operator.md) — day-to-day CLI
+- [Configuration](configuration.md) — JSON files and hot reload
- [Architecture](architecture.md) — internals
- [Documentation index](README.md)
diff --git a/docs/architecture.md b/docs/architecture.md
index aa25792..1e8c9eb 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -8,7 +8,7 @@ This document explains **why** microinit exists, **how** it is designed, and **w
**microinit** is a small program that on a device (or in a container) **starts and supervises services** — networking, databases, applications — according to a list stored in JSON files. It can also act as a classic system init (Linux process number 1).
-It is not a full systemd. It aims to be **lightweight, predictable, and easy to embed** in a system image (for example BigFred OS) and in containers.
+It is not a full systemd. It aims to be **lightweight, predictable, and easy to embed** in custom Linux system images and in containers.
---
@@ -18,7 +18,7 @@ The same supervisor logic runs under two thin wrappers:
| | `microinit init` | `microinit supervise` |
|---|---|---|
-| Typical use | Embedded / hub system (PID 1) | Container, distroless |
+| Typical use | Embedded / custom Linux (PID 1) | Container, distroless |
| Early-boot (mounts, etc.) | Yes | No |
| Console login (getty) | Yes, when PID 1 | No |
| Logs on TTYs (tty2 / tty3) | Yes | No (in-memory ring + IPC / optional files) |
@@ -175,11 +175,10 @@ With `panic = abort` in release, metrics code must avoid panics — a failure in
Two independent Linux artifacts, plus an Android binary bundle:
-1. **ORAS (linux)** — static arm64 binaries (`microinit` + `shutdown`) plus early-boot and unmount scripts for the hub system image, e.g. `ghcr.io/dcc-bigfred/microinit-linux-arm64`.
+1. **ORAS (linux)** — static arm64 binaries (`microinit` + `shutdown`) plus early-boot and unmount scripts for system images (see project README for registry URLs).
2. **ORAS (android)** — supervise-only Bionic binaries (`microinit` + `shutdown`, built
- with `--no-default-features`), e.g. `ghcr.io/dcc-bigfred/microinit-android-arm64`.
- GitHub Releases also ship `armv7` and `x86_64`.
-3. **Distroless container image** (amd64 + arm64) — `ghcr.io/dcc-bigfred/microinit`, default `ENTRYPOINT /microinit` + `CMD supervise`.
+ with `--no-default-features`). GitHub Releases also ship `armv7` and `x86_64`.
+3. **Distroless container image** (amd64 + arm64) — default `ENTRYPOINT /microinit` + `CMD supervise` (see README for the current image name).
Tag lifecycle: `main` / `sha-<7>` on push, `v*` / `latest-release` on release.
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000..2bfb31a
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,230 @@
+# Configuration
+
+This page explains **where** microinit reads settings, **how** to describe a service in JSON, and **what happens when you save a file** (hot reload). No Rust required — only a text editor.
+
+---
+
+## Where files live
+
+By default durable state lives under **`/data`**. Override with **`DATA_DIR`** (absolute path, e.g. `/data`).
+
+| File or folder | Purpose |
+|----------------|---------|
+| `$DATA_DIR/etc/microinit.json` | Main file — service list and global options |
+| `$DATA_DIR/etc/microinit.services.enabled-override.json` | Written by `microinit enable` / `disable` |
+| `$DATA_DIR/etc/microinit.d/services/**/*.json` | **Drop-ins** — extra or overriding services |
+| `$DATA_DIR/etc/microinit.json.example` | Example (created if missing) |
+
+A system image may **seed** `/data/etc/microinit.json` from `/etc/microinit/microinit.json` during early-boot — only if `/data` does not already have its own copy.
+
+With **`microinit supervise --config /path/microinit.json`**, drop-ins sit next to that file:
+
+```text
+/etc/microinit/microinit.json
+/etc/microinit/microinit.d/services/
+/etc/microinit/microinit.services.enabled-override.json
+```
+
+---
+
+## Drop-ins (files in subfolders)
+
+You do not have to put every service in one big `microinit.json`. Extra JSON under **`microinit.d/services/`** is loaded automatically.
+
+**Rules:**
+
+1. Any `**/*.json` in that tree (subfolders are fine).
+2. Files sorted **alphabetically** (full path).
+3. Same service **`name`** — the **later** file wins.
+4. Add a new service or replace fields of an existing one.
+
+**Example layout:**
+
+```text
+/data/etc/
+ microinit.json
+ microinit.d/
+ services/
+ base/network.json
+ web/
+ php-fpm.json
+ nginx.json
+```
+
+`web/nginx.json` can set `"dependsOn": ["php-fpm"]` while `php-fpm` lives in `web/php-fpm.json`. Folder order matters only when the **same** service name appears twice.
+
+**Tip:** folder names like `10-base/`, `20-web/` control override order.
+
+---
+
+## Main file (skeleton)
+
+```json
+{
+ "version": 1,
+ "socket": "/run/microinit.sock",
+ "console": "/dev/tty1",
+ "logs": {
+ "tty": "/dev/tty2",
+ "initTty": "/dev/tty3",
+ "lines": 300,
+ "logToFiles": false,
+ "dir": "/data/logs"
+ },
+ "services": []
+}
+```
+
+| Field | Meaning |
+|-------|---------|
+| `version` | Schema version (use `1`) |
+| `socket` | Control socket for `microinit list`, UI, scripts |
+| `console` | Boot console and getty (init mode) |
+| `logs.tty` | Service logs (init mode) |
+| `logs.initTty` | microinit’s own messages |
+| `logs.logToFiles` | If `true`, also files under `$DATA_DIR/logs/` |
+| `openTelemetry` | Optional metrics (see README) |
+
+Most operators only edit **`services`**.
+
+---
+
+## One service entry
+
+### Long-running service (daemon)
+
+```json
+{
+ "name": "myapp",
+ "enabled": true,
+ "daemon": true,
+ "restart": true,
+ "restartBackoff": 2,
+ "startWaitSecs": 1,
+ "shutdownWaitSecs": 5,
+ "dependsOn": ["network"],
+ "cmd": "/etc/init.d/myapp",
+ "cwd": "/"
+}
+```
+
+With `cmd`, microinit runs `cmd start`, `cmd stop`, `cmd restart`.
+
+Or explicit commands:
+
+```json
+"startCmd": "/usr/sbin/myapp --config /data/etc/myapp.conf",
+"stopCmd": "killall myapp"
+```
+
+**Important:** start should **`exec`** the program in the **foreground** so microinit can track the process and restart on crash. A script that backgrounds a daemon and exits makes microinit think everything is fine with no PID tracked.
+
+### One-shot job (at boot)
+
+```json
+{
+ "name": "network",
+ "daemon": false,
+ "restart": false,
+ "cmd": "/etc/init.d/network"
+}
+```
+
+Success → `succeeded`. Failure → `failed`.
+
+### Fields — short reference
+
+| Field | Role |
+|-------|------|
+| `name` | Unique name (CLI, `dependsOn`) |
+| `enabled` | `false` = do not start at boot |
+| `daemon` | `true` = long-lived; `false` = one-shot |
+| `restart` | Retry after crash (daemon only) |
+| `restartBackoff` | Seconds before each retry |
+| `startWaitSecs` | After start, wait; if process dies in window → `failed` |
+| `shutdownWaitSecs` | After stop, wait then `SIGKILL` |
+| `background` | Parallel start at boot |
+| `dependsOn` | These must be `running` or `succeeded` first |
+| `livenessProbe` | Optional health check; failure triggers restart |
+
+### Liveness probe
+
+Exactly **one** of `cmd`, `httpUrl`, or `tcpAddr`:
+
+```json
+"livenessProbe": {
+ "httpUrl": "http://127.0.0.1:8080/health",
+ "httpAcceptedCodes": [200],
+ "interval": 30,
+ "timeout": 5
+}
+```
+
+Defaults: `interval` 60 s, `timeout` 5 s.
+
+---
+
+## Enable / disable without editing JSON
+
+```bash
+microinit disable grafana
+microinit enable grafana
+```
+
+The override file wins over `"enabled"` in JSON.
+
+---
+
+## Hot reload (save file → apply)
+
+microinit watches JSON via **inotify** (no periodic disk scanning). On save:
+
+1. Short pause (~300 ms) — one save often emits several events.
+2. Load and merge (main + drop-ins + override).
+3. **Invalid JSON** → old config kept; warning in logs.
+4. **Valid JSON** → diff services:
+ - new → start (with `dependsOn`)
+ - removed → stop
+ - definition changed → restart
+ - `enabled` toggled → start or stop
+
+**Reboot is usually not needed.** After saving:
+
+```bash
+microinit list
+```
+
+### What does **not** hot-reload
+
+Requires **microinit restart** (on PID 1 hosts: reboot):
+
+- `socket` path
+- `logs.*` (TTYs, `logToFiles`, buffer size)
+- `console`
+
+---
+
+## Dependencies
+
+```json
+"dependsOn": ["network", "redis"]
+```
+
+A service starts when every listed name is **`running`** or **`succeeded`**. Until then: **`waiting_for_dependency`** — it starts **on its own** when ready.
+
+`microinit stop` while waiting **cancels** the wait — fixing the dependency does **not** start the service without `microinit start`.
+
+```bash
+microinit start --force myapp # debugging only
+```
+
+Boot example with restarts: [Service lifecycle](service-lifecycle.md).
+
+---
+
+## Further reading
+
+- [Operator guide](operator.md) — everyday commands
+- [Using as supervisord](using-as-supervisord.md) — PHP-FPM + NGINX
+- [Service lifecycle](service-lifecycle.md)
+- `man/man5/microinit.json.5.mdoc` — full field list
diff --git a/docs/operator.md b/docs/operator.md
index 200525d..f07be36 100644
--- a/docs/operator.md
+++ b/docs/operator.md
@@ -1,20 +1,24 @@
# Operator guide
-This guide is for people who already know a Linux shell and need to **run and maintain services** under microinit on a device (or in a container). You do not need to know Rust.
+For someone who knows basic Linux (SSH, editing files, systemd-style thinking) but **does not need to know** microinit or Rust. Day-to-day work: status, start/stop, boot.
+
+**More detail:**
+
+- [Configuration](configuration.md) — JSON files, drop-ins, hot reload
+- [Service lifecycle](service-lifecycle.md) — states over time, dependencies at boot
+- [Using as supervisord](using-as-supervisord.md) — PHP-FPM + NGINX in a container
---
## What microinit does
-microinit is the program that:
-
-1. Starts a list of services from a JSON file
-2. Keeps long-running services alive (optional restart on crash)
-3. Lets you start/stop/enable/disable services and read their logs
+1. Starts services from JSON config
+2. Keeps daemons alive (optional restart after crash)
+3. Lets you start/stop/enable/disable and read logs from the shell
-On BigFred OS it is usually **PID 1** (`/sbin/init`). In a container you often run `microinit supervise` instead.
+On embedded or custom Linux systems it often runs as **PID 1** (`/sbin/init`). In a container: **`microinit supervise`** (supervisord-like).
-Default durable data lives under **`/data`**. You can point that elsewhere with the environment variable **`DATA_DIR`** (must be an absolute path).
+Settings and logs usually live under **`/data`**. Another root: **`DATA_DIR`** (absolute path only).
---
@@ -26,15 +30,14 @@ The control socket defaults to `$DATA_DIR/run/microinit.sock` (hub: `/data/run/m
microinit list # name, state, pid, restarts, enabled, live_fail
microinit list --show-labels # same + LABELS column
microinit list -l created-by=bigfred # filter (AND if -l repeated)
-microinit describe redis # deps, events, labels
microinit describe redis # deps, reverse deps, graph, recent events
microinit start redis
-microinit start --force alloy # start even if dependsOn are not ready
+microinit start --force alloy # start even if dependsOn are not ready
microinit stop redis
microinit restart redis
microinit enable dropbear
microinit disable dropbear
-microinit logs # mixed recent lines
+microinit logs
microinit logs redis --follow
microinit logs redis --lines 100
```
@@ -45,43 +48,29 @@ microinit logs redis --lines 100
| State | Meaning |
|-------|---------|
-| `running` | Daemon process is up (PID tracked) |
-| `succeeded` | One-shot job finished successfully |
-| `failed` | Start failed or process exited badly |
+| `running` | Daemon is up (PID shown) |
+| `succeeded` | One-shot finished OK |
+| `failed` | Start or exit error |
| `stopped` | Stopped on purpose |
-| `disabled` | Not allowed to start (`enabled: false`) |
-| `waiting_for_dependency` | Start requested; waiting for `dependsOn` |
-| `starting` / `restarting` | Transition in progress |
-
----
-
-## Configuration files
-
-### Main file
-
-**`$DATA_DIR/etc/microinit.json`** (usually `/data/etc/microinit.json`)
-
-If the file is missing at first boot, microinit can create an empty one and an example. On BigFred OS the image often **seeds** this file from `/etc/microinit/microinit.json` during early-boot (only if `/data` does not already have a copy).
-
-Editing `/data/etc/microinit.json` is the normal way to change what runs on a given device.
+| `disabled` | Not allowed to start |
+| `waiting_for_dependency` | Waiting for `dependsOn` |
+| `starting` / `restarting` | In progress |
-### Enable/disable override
+Full example with restarts: [Service lifecycle](service-lifecycle.md).
-**`$DATA_DIR/etc/microinit.services.enabled-override.json`**
-
-Written when you run `microinit enable` / `disable`. It only stores `true`/`false` per service name and wins over the `enabled` field in the main JSON. You rarely edit this by hand.
-
-### Drop-ins
-
-**`$DATA_DIR/etc/microinit.d/services/**/*.json`**
+---
-Extra or overriding service definitions. Files are merged in **path sort order**; for the same service `name`, a **later** file wins. Useful for site-specific add-ons without rewriting the whole base config.
+## Configuration (short)
-### Hot-reload
+| What | Where |
+|------|--------|
+| Main list | `$DATA_DIR/etc/microinit.json` |
+| Enable/disable override | `$DATA_DIR/etc/microinit.services.enabled-override.json` |
+| Extra services | `$DATA_DIR/etc/microinit.d/services/**/*.json` |
-Saving any of those JSON files is picked up automatically (inotify). Invalid JSON keeps the previous config and logs a warning.
+Edit JSON, **save** — hot reload (no reboot in most cases). Invalid JSON is ignored.
-Changes to **socket path**, **log TTYs**, and **log-to-files** options need a **full microinit restart** (reboot on a hub).
+Details: [Configuration](configuration.md).
---
@@ -175,37 +164,22 @@ Example: Redis needs the network service first.
"dependsOn": ["network"]
```
-What happens:
-
-1. You (or boot) request start of `redis`.
-2. If `network` is not yet `running`/`succeeded`, redis goes to **`waiting_for_dependency`**.
-3. When `network` becomes ready, redis **starts by itself**.
-4. If you **`stop`** redis while it is waiting, that wait is cancelled. Later, when network is up, redis will **not** auto-start until you `start` it again.
-
-To start anyway (debugging):
+If `network` is not `running` or `succeeded`, the service stays in **`waiting_for_dependency`** and starts **on its own** when ready. Manual **`stop`** cancels the wait.
```bash
-microinit start --force redis
+microinit start --force redis # debugging only
```
-You will see a short message on stdout, for example:
-
-- `redis: waiting for dependencies (network)`
-- `redis: starting with --force (unmet dependencies: network)`
-- `redis: starting`
-
---
-## Boot sequence (init mode)
-
-Typical hub boot:
+## Boot sequence (init mode as PID 1)
1. Kernel starts `/sbin/init` (microinit).
-2. **Early-boot** script runs (mount `/data`, seed configs, remount root RO, …).
-3. Config is loaded from disk (**after** early-boot).
-4. Enabled services start (order respects `dependsOn`; `background: true` services start in parallel).
-5. Console shows `[ OK ]` / `[ FAIL ]` style status; getty on the console TTY.
-6. Control socket listens; config files are watched for reload.
+2. **Early-boot** (mount `/data`, seed config, …).
+3. Config loaded from disk.
+4. Enabled services start (`dependsOn` order; `background: true` in parallel).
+5. Console `[ OK ]` / `[ FAIL ]`; getty.
+6. IPC socket; JSON files watched for reload.
On shutdown in **`init`** mode (`shutdown -r`, IPC `shutdown`, SIGTERM, …): services stop in reverse dependency order, then the **unmount** script runs (unbind mounts / umount `/data`), then reboot or power-off.
@@ -215,32 +189,31 @@ In **`supervise`** mode there is no early-boot, getty, late unmount, or machine
| Where | What |
|-------|------|
-| `/dev/tty2` (default) | Service stdout/stderr |
-| `/dev/tty3` (default) | microinit’s own messages |
-| `microinit logs …` | Same rings over the socket |
-| `$DATA_DIR/logs/` | Optional files if `logs.logToFiles` is true |
+| `/dev/tty2` | Service stdout/stderr |
+| `/dev/tty3` | microinit messages |
+| `microinit logs …` | Same via socket |
+| `$DATA_DIR/logs/` | Files when `logs.logToFiles: true` |
---
-## Common operator tasks
+## Common tasks
-**See why something did not start**
+**Why did it not start?**
```bash
microinit list
microinit logs nameofservice --lines 50
-# also check /dev/tty3
```
-**Temporarily disable a service across reboots**
+**Disable across reboots**
```bash
microinit disable grafana
```
-**Change config and apply**
+**Apply a config change**
-Edit `/data/etc/microinit.json` (or a drop-in), save — wait a moment for reload — then `microinit list`.
+Edit a file under `/data/etc/`, save, wait a moment, `microinit list`.
**Service dies and stays dead**
@@ -250,6 +223,9 @@ Check `restartPolicy` and that microinit is tracking a real PID (`list` shows a
## Further reading
-- [Control socket API](api.md) — for scripts and UIs that talk to the socket directly
-- [Architecture](architecture.md) — design background
-- [Documentation index](README.md)
+- [Configuration](configuration.md)
+- [Service lifecycle](service-lifecycle.md)
+- [Using as supervisord](using-as-supervisord.md)
+- [Control socket API](api.md) — scripts and UI
+- [Architecture](architecture.md) — for developers
+- [Documentation index](README.md)
diff --git a/docs/service-lifecycle.md b/docs/service-lifecycle.md
new file mode 100644
index 0000000..a401887
--- /dev/null
+++ b/docs/service-lifecycle.md
@@ -0,0 +1,186 @@
+# Service lifecycle
+
+This page shows **what happens over time** — not just a list of states, but a realistic boot story: one service keeps crashing and restarting, and another waits until the first one is finally healthy.
+
+---
+
+## States you will see
+
+| State | In plain words |
+|-------|----------------|
+| `starting` | Start command is running |
+| `running` | Daemon is up; microinit tracks a PID |
+| `succeeded` | One-shot job finished OK |
+| `failed` | Start failed or bad exit |
+| `stopped` | Stopped on purpose |
+| `disabled` | `enabled: false` |
+| `waiting_for_dependency` | Should start, but `dependsOn` is not ready |
+| `restarting` | Crashed; waiting `restartBackoff` before retry |
+| `stopping` | Stop in progress |
+
+Quick check:
+
+```bash
+microinit list
+```
+
+Columns **`restarts`** and **`live_fail`** help spot unstable services.
+
+---
+
+## Example: dependency + restart loop at boot
+
+Scenario:
+
+- **`database`** — a daemon that sometimes fails on cold boot (disk, port). It has `restart: true` and `restartBackoff: 3`.
+- **`webapp`** — needs the database. It has `dependsOn: ["database"]`.
+
+Short config:
+
+```json
+{
+ "services": [
+ {
+ "name": "database",
+ "daemon": true,
+ "restart": true,
+ "restartBackoff": 3,
+ "startWaitSecs": 2,
+ "startCmd": "/usr/sbin/mydatabase --foreground"
+ },
+ {
+ "name": "webapp",
+ "daemon": true,
+ "restart": true,
+ "restartBackoff": 2,
+ "startWaitSecs": 1,
+ "dependsOn": ["database"],
+ "startCmd": "/usr/sbin/webapp"
+ }
+ ]
+}
+```
+
+### Timeline (what the admin sees)
+
+```text
+Boot
+ │
+ ├─ database: starting
+ │ └─ quick crash → failed, then restarting (restarts=1)
+ │
+ ├─ webapp: waiting_for_dependency (database not running yet)
+ │
+ ├─ … 3 seconds (restartBackoff) …
+ │
+ ├─ database: starting (2nd attempt)
+ │ └─ crash again → restarting (restarts=2)
+ │
+ ├─ webapp: still waiting_for_dependency
+ │
+ ├─ … 3 seconds …
+ │
+ ├─ database: starting (3rd attempt)
+ │ └─ stays up → running ✓
+ │
+ └─ webapp: dependency ready → starting → running ✓
+```
+
+After the **second restart** (third start attempt) the database finally runs — then **webapp starts on its own**, without `microinit start webapp`.
+
+microinit retries `waiting_for_dependency` about every 200 ms in the background. No manual step needed.
+
+### `microinit list` mid-boot
+
+After the first database crash:
+
+```text
+database failed - 1 true 0
+webapp waiting_for_dependency - 0 true 0
+```
+
+When the database is up:
+
+```text
+database running 1234 2 true 0
+webapp running 1240 0 true 0
+```
+
+(`restarts=2` on database = two crashes before the successful run.)
+
+---
+
+## Diagram
+
+```mermaid
+sequenceDiagram
+ participant Boot
+ participant DB as database
+ participant WA as webapp
+
+ Boot->>DB: start (attempt 1)
+ Boot->>WA: start requested
+ WA-->>WA: waiting_for_dependency
+ DB-->>DB: crash → restarting
+ Note over DB: wait 3s
+ DB->>DB: start (attempt 2)
+ DB-->>DB: crash → restarting
+ Note over DB: wait 3s
+ DB->>DB: start (attempt 3)
+ DB-->>DB: running
+ WA->>WA: deps ready → starting
+ WA-->>WA: running
+```
+
+---
+
+## One-shot dependency
+
+If **`network`** is one-shot (`daemon: false`) and exits 0 → state **`succeeded`**. That counts as “ready” for `dependsOn`.
+
+If the one-shot **fails** (`failed`), dependents keep waiting until you fix and restart the dependency (or use `--force`).
+
+---
+
+## When waiting does **not** resume on its own
+
+| You did | Result |
+|---------|--------|
+| Boot / `microinit start webapp` | Waits, then starts |
+| `microinit stop webapp` while waiting | Stays **stopped** — fixing database does **not** start webapp |
+| `microinit disable webapp` | **disabled** until `enable` |
+
+---
+
+## Crash after a successful start
+
+If **`webapp`** is already **`running`** and **`database`** crashes:
+
+- database goes through restarting / running (if `restart: true`);
+- **webapp is not stopped automatically** — microinit does not cascade-stop dependents when a dependency dies.
+
+If webapp must die with the database, that belongs in the app or a `livenessProbe` on webapp.
+
+---
+
+## Debugging
+
+```bash
+microinit list
+microinit logs database --lines 80
+microinit logs webapp --lines 80
+# on a full system, also /dev/tty3
+```
+
+Force start (debugging only):
+
+```bash
+microinit start --force webapp
+```
+
+---
+
+## Further reading
+
+- [Configuration](configuration.md) — `dependsOn`, `restart`, hot reload
+- [Operator guide](operator.md)
diff --git a/docs/using-as-supervisord.md b/docs/using-as-supervisord.md
new file mode 100644
index 0000000..56758c1
--- /dev/null
+++ b/docs/using-as-supervisord.md
@@ -0,0 +1,195 @@
+# Using as supervisord
+
+**`microinit supervise`** is for **containers** and **servers** without full PID-1 init (no early-boot, no getty). It does what **supervisord** does: keep processes alive, start/stop from the CLI, read logs.
+
+When **`microinit init`** runs as PID 1 (`/sbin/init`) on a host system. In Docker or on a VM:
+
+```bash
+microinit supervise --config /etc/microinit/microinit.json
+```
+
+Build locally or pull a published image (see project README). Example:
+
+```bash
+docker run --rm -v "$PWD/config:/etc/microinit" microinit:main
+```
+
+---
+
+## Example: PHP-FPM + NGINX
+
+Classic stack: **PHP-FPM** on port 9000, **NGINX** serves HTTP and talks to PHP-FPM. NGINX must start **after** PHP-FPM.
+
+Config split into:
+
+- a small **main** file (socket, logs)
+- **drop-ins** in subfolders (easy to add more services)
+- **hot reload** — edit JSON, save, no container restart
+
+### File layout
+
+```text
+/etc/microinit/
+ microinit.json
+ microinit.d/
+ services/
+ web/
+ php-fpm.json
+ nginx.json
+```
+
+### Main file — `microinit.json`
+
+```json
+{
+ "version": 1,
+ "socket": "/run/microinit.sock",
+ "logs": {
+ "lines": 500,
+ "logToFiles": true,
+ "dir": "/var/log/microinit"
+ },
+ "services": []
+}
+```
+
+Services live only in drop-ins — the main file stays short.
+
+### PHP-FPM — `microinit.d/services/web/php-fpm.json`
+
+```json
+{
+ "services": [
+ {
+ "name": "php-fpm",
+ "enabled": true,
+ "daemon": true,
+ "restart": true,
+ "restartBackoff": 2,
+ "startWaitSecs": 1,
+ "shutdownWaitSecs": 10,
+ "dependsOn": [],
+ "startCmd": "exec /usr/sbin/php-fpm8.2 --nodaemonize --fpm-config /etc/php8/php-fpm.conf",
+ "stopCmd": "killall php-fpm8.2",
+ "cwd": "/",
+ "livenessProbe": {
+ "tcpAddr": "127.0.0.1:9000",
+ "interval": 30,
+ "timeout": 3
+ }
+ }
+ ]
+}
+```
+
+- **`exec`** and **`--nodaemonize`** — PHP-FPM in the foreground; microinit tracks the PID.
+- **`livenessProbe`** — checks port 9000.
+
+### NGINX — `microinit.d/services/web/nginx.json`
+
+```json
+{
+ "services": [
+ {
+ "name": "nginx",
+ "enabled": true,
+ "daemon": true,
+ "restart": true,
+ "restartBackoff": 2,
+ "startWaitSecs": 1,
+ "shutdownWaitSecs": 10,
+ "dependsOn": ["php-fpm"],
+ "startCmd": "exec /usr/sbin/nginx -g 'daemon off;'",
+ "stopCmd": "nginx -s quit",
+ "cwd": "/",
+ "livenessProbe": {
+ "httpUrl": "http://127.0.0.1:8080/health",
+ "httpAcceptedCodes": [200],
+ "interval": 30,
+ "timeout": 5
+ }
+ }
+ ]
+}
+```
+
+- **`dependsOn: ["php-fpm"]`** — NGINX stays in `waiting_for_dependency` until PHP-FPM is `running`.
+- **`daemon off`** — NGINX in the foreground (supervisord-style).
+
+---
+
+## Hot reload in practice
+
+microinit watches the main file and everything under `microinit.d/services/**/*.json`.
+
+### Add a service without restarting the container
+
+e.g. `microinit.d/services/web/extra-app.json`:
+
+```json
+{
+ "services": [
+ {
+ "name": "extra-app",
+ "daemon": true,
+ "restart": true,
+ "startWaitSecs": 1,
+ "dependsOn": ["php-fpm"],
+ "startCmd": "exec /usr/sbin/my-extra-daemon"
+ }
+ ]
+}
+```
+
+Save the file. Within about a second:
+
+```bash
+microinit list
+```
+
+`extra-app` should start (after `php-fpm` is ready).
+
+### Change PHP-FPM settings
+
+Edit `php-fpm.json` — microinit detects a definition change and **restarts** `php-fpm`. NGINX keeps running unless you change its JSON too or run `microinit restart nginx`.
+
+### Invalid JSON
+
+Broken JSON keeps the **previous** config. Check:
+
+```bash
+microinit logs --lines 20
+```
+
+---
+
+## Everyday commands
+
+```bash
+microinit list
+microinit restart nginx
+microinit stop php-fpm
+microinit logs nginx --follow
+```
+
+Socket defaults to `/run/microinit.sock`; use `--socket` on the daemon and clients if needed.
+
+---
+
+## supervise vs init
+
+| | `supervise` | `init` (host PID 1) |
+|---|-------------|---------------------|
+| Early-boot, `/data` | No | Yes |
+| Getty | No | Yes |
+| Logs on tty2/tty3 | No (ring + files + IPC) | Yes |
+| Supervision + hot reload | Yes | Yes |
+| `shutdown -r` | Stop services, exit | Stop, umount, reboot |
+
+---
+
+## Further reading
+
+- [Configuration](configuration.md) — drop-ins, JSON fields
+- [Service lifecycle](service-lifecycle.md) — waiting on dependencies
+- [Operator guide](operator.md)
diff --git a/scripts/build-android.sh b/scripts/build-android.sh
index 6007f52..840d811 100755
--- a/scripts/build-android.sh
+++ b/scripts/build-android.sh
@@ -124,8 +124,22 @@ build_one() {
cp -f "target/${rust_target}/release/microinit" "dist/${dist_micro}"
cp -f "target/${rust_target}/release/shutdown" "dist/${dist_shut}"
chmod 755 "dist/${dist_micro}" "dist/${dist_shut}"
+
+ # Android jniLibs require native libraries to use the lib*.so convention.
+ # Keep arch-specific release artifacts for GitHub Release compatibility.
+ if [[ "${rust_target}" == "aarch64-linux-android" ]]; then
+ cp -f "dist/${dist_micro}" dist/libmicroinit.so
+ cp -f "dist/${dist_shut}" dist/libshutdown.so
+ chmod 755 dist/libmicroinit.so dist/libshutdown.so
+ fi
+
file "dist/${dist_micro}" "dist/${dist_shut}" || true
- echo "wrote dist/${dist_micro} dist/${dist_shut}"
+ if [[ "${rust_target}" == "aarch64-linux-android" ]]; then
+ file dist/libmicroinit.so dist/libshutdown.so || true
+ echo "wrote dist/${dist_micro} dist/${dist_shut} dist/libmicroinit.so dist/libshutdown.so"
+ else
+ echo "wrote dist/${dist_micro} dist/${dist_shut}"
+ fi
}
for a in "${ARCHES[@]}"; do
diff --git a/scripts/publish-oci-microinit-android.sh b/scripts/publish-oci-microinit-android.sh
index 45cd0fb..0beb623 100755
--- a/scripts/publish-oci-microinit-android.sh
+++ b/scripts/publish-oci-microinit-android.sh
@@ -1,12 +1,12 @@
#!/usr/bin/env bash
# Publish microinit Android/arm64 OCI artifact to GHCR (ORAS).
# Intended for CI on push to main/master only.
-# Usage: publish-oci-microinit-android.sh [shutdown-android-arm64]
+# Usage: publish-oci-microinit-android.sh [libshutdown.so|shutdown-android-arm64]
#
# Tags: main, sha-<7>
set -euo pipefail
-BIN="${1:?usage: $0 [shutdown-android-arm64]}"
+BIN="${1:?usage: $0 [libshutdown.so|shutdown-android-arm64]}"
SHUTDOWN="${2:-}"
IMAGE="${MICROINIT_OCI_ANDROID_IMAGE:-ghcr.io/dcc-bigfred/microinit-android-arm64}"
BIN_MEDIA_TYPE="application/vnd.dcc-bigfred.microinit.android.arm64.v1"
@@ -29,11 +29,12 @@ tmpdir="$(mktemp -d)"
cleanup() { rm -rf "${tmpdir}"; }
trap cleanup EXIT
-cp -f "${BIN}" "${tmpdir}/microinit-android-arm64"
-chmod 755 "${tmpdir}/microinit-android-arm64"
+# Publish jniLibs-compatible layer names even when a legacy artifact is passed.
+cp -f "${BIN}" "${tmpdir}/libmicroinit.so"
+chmod 755 "${tmpdir}/libmicroinit.so"
layers=(
- "microinit-android-arm64:${BIN_MEDIA_TYPE}"
+ "libmicroinit.so:${BIN_MEDIA_TYPE}"
)
if [[ -n "${SHUTDOWN}" ]]; then
@@ -41,9 +42,9 @@ if [[ -n "${SHUTDOWN}" ]]; then
echo "error: shutdown binary not found: ${SHUTDOWN}" >&2
exit 1
fi
- cp -f "${SHUTDOWN}" "${tmpdir}/shutdown-android-arm64"
- chmod 755 "${tmpdir}/shutdown-android-arm64"
- layers+=("shutdown-android-arm64:${SHUTDOWN_MEDIA_TYPE}")
+ cp -f "${SHUTDOWN}" "${tmpdir}/libshutdown.so"
+ chmod 755 "${tmpdir}/libshutdown.so"
+ layers+=("libshutdown.so:${SHUTDOWN_MEDIA_TYPE}")
fi
annotate=(
@@ -54,9 +55,9 @@ annotate=(
)
echo "Publishing ${IMAGE}:main and :${SHA_TAG}"
-echo " microinit: $(wc -c < "${tmpdir}/microinit-android-arm64") bytes"
-if [[ -f "${tmpdir}/shutdown-android-arm64" ]]; then
- echo " shutdown: $(wc -c < "${tmpdir}/shutdown-android-arm64") bytes"
+echo " libmicroinit.so: $(wc -c < "${tmpdir}/libmicroinit.so") bytes"
+if [[ -f "${tmpdir}/libshutdown.so" ]]; then
+ echo " libshutdown.so: $(wc -c < "${tmpdir}/libshutdown.so") bytes"
fi
(
cd "${tmpdir}"
diff --git a/scripts/retag-oci-microinit-android.sh b/scripts/retag-oci-microinit-android.sh
index 741bf31..a9a4f21 100755
--- a/scripts/retag-oci-microinit-android.sh
+++ b/scripts/retag-oci-microinit-android.sh
@@ -33,20 +33,29 @@ find_layer() {
return 1
}
-BIN_NAME="$(find_layer microinit-android-arm64)" || true
+BIN_NAME="$(find_layer libmicroinit.so || find_layer microinit-android-arm64)" || true
if [[ -z "${BIN_NAME}" ]]; then
- echo "error: expected microinit-android-arm64 in OCI artifact, found:" >&2
+ echo "error: expected libmicroinit.so (or legacy microinit-android-arm64) in OCI artifact, found:" >&2
find "${tmpdir}" -type f >&2
exit 1
fi
+# Normalize legacy layers to the jniLibs-compatible OCI layer names.
+if [[ "${BIN_NAME}" != "libmicroinit.so" ]]; then
+ cp -f "${tmpdir}/${BIN_NAME}" "${tmpdir}/libmicroinit.so"
+ chmod 755 "${tmpdir}/libmicroinit.so"
+fi
push_args=(
- "${BIN_NAME}:${BIN_MEDIA_TYPE}"
+ "libmicroinit.so:${BIN_MEDIA_TYPE}"
)
-SHUTDOWN_NAME="$(find_layer shutdown-android-arm64)" || true
+SHUTDOWN_NAME="$(find_layer libshutdown.so || find_layer shutdown-android-arm64)" || true
if [[ -n "${SHUTDOWN_NAME}" ]]; then
- push_args+=("${SHUTDOWN_NAME}:${SHUTDOWN_MEDIA_TYPE}")
+ if [[ "${SHUTDOWN_NAME}" != "libshutdown.so" ]]; then
+ cp -f "${tmpdir}/${SHUTDOWN_NAME}" "${tmpdir}/libshutdown.so"
+ chmod 755 "${tmpdir}/libshutdown.so"
+ fi
+ push_args+=("libshutdown.so:${SHUTDOWN_MEDIA_TYPE}")
fi
annotate=(
@@ -57,9 +66,9 @@ annotate=(
)
echo "Publishing ${IMAGE}:${RELEASE_TAG} and :latest-release"
-echo " microinit: $(wc -c < "${tmpdir}/${BIN_NAME}") bytes"
+echo " libmicroinit.so: $(wc -c < "${tmpdir}/libmicroinit.so") bytes"
if [[ -n "${SHUTDOWN_NAME}" ]]; then
- echo " shutdown: $(wc -c < "${tmpdir}/${SHUTDOWN_NAME}") bytes"
+ echo " libshutdown.so: $(wc -c < "${tmpdir}/libshutdown.so") bytes"
fi
(
cd "${tmpdir}"