Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions .github/scripts/check-quickstart-compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Exercise the root Compose, optional UI and memory lab on a disposable runner."""

import base64
import json
import os
from pathlib import Path
import secrets
import ssl
import subprocess
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


ROOT = Path(__file__).resolve().parents[2]
BASE_FILES = ['docker-compose.yml']
LAB_FILES = BASE_FILES + ['docker-compose.ui.yml', 'examples/tutorial-memory/compose.yaml']
WORKER = 'monit-docker-tutorial-memory'
AUDIT_FILE = '/var/lib/monit-docker/audit/events.jsonl'
ENV = dict(os.environ, COMPOSE_PROJECT_NAME='monit-quickstart-ci',
DEMO_SCENARIO='demo-observe', MONIT_DOCKER_PORT='19808', UI_PORT='18443')


def compose(files, *args):
command = ['docker', 'compose']
for filename in files:
command.extend(['-f', filename])
return subprocess.check_output(command + list(args), cwd=ROOT, env=ENV, text=True)


def wait_for(check, description, timeout=150):
deadline = time.monotonic() + timeout
last_error = None
while time.monotonic() < deadline:
try:
result = check()
if result:
print('Verified: ' + description, flush=True)
return result
except (URLError, OSError, KeyError) as error:
last_error = error
time.sleep(2)
raise AssertionError('Timed out: %s; last error: %r' % (description, last_error))


def main():
secret_dir = ROOT / 'examples/ui/secrets.local'
if secret_dir.exists():
raise RuntimeError('Use a clean disposable checkout; secrets.local already exists')
password = secrets.token_hex(24)
subprocess.run(['python3', 'examples/ui/prepare.py', '--self-signed'],
input='compose-ci\n' + password + '\n' + password + '\n',
text=True, cwd=ROOT, check=True)
tls = ssl.create_default_context(cafile=str(secret_dir / 'tls.crt'))
auth = 'Basic ' + base64.b64encode(('compose-ci:' + password).encode()).decode()

def request(path='/v1/status', browser=False, authenticated=True):
base = 'https://localhost:18443' if browser else 'http://127.0.0.1:19808'
headers = {'Authorization': auth} if browser and authenticated else {}
with urlopen(Request(base + path, headers=headers),
context=tls if browser else None, timeout=5) as response:
return json.load(response)

def started():
return subprocess.check_output(['docker', 'inspect', '--format',
'{{.State.StartedAt}}', WORKER], text=True).strip()

try:
compose(BASE_FILES, 'up', '-d', '--wait', '--wait-timeout', '180')
wait_for(lambda: request()['ready'], 'minimal agent collects actual containers')
assert not request()['manual_actions']['enabled']
compose(BASE_FILES, 'down')

compose(LAB_FILES, 'up', '-d', '--wait', '--wait-timeout', '180')
try:
request(browser=True, authenticated=False)
except HTTPError as error:
assert error.code == 401
else:
raise AssertionError('UI allowed anonymous access')
status = wait_for(lambda: request(browser=True), 'authenticated UI reaches the agent')
assert [c['name'] for c in status['containers']] == [WORKER]
assert not status['manual_actions']['enabled']
original_start = started()
compose(LAB_FILES, 'kill', '--signal', 'SIGUSR1', 'demo-worker')
wait_for(lambda: request()['containers'][0]['mem_percent'] > 60,
'bounded worker exceeds the configured threshold')
preview = compose(LAB_FILES, 'exec', '-T', 'monit-docker', 'monit-docker',
'--name', WORKER, 'monit', '--dry-run', '--cmd-if',
'mem_percent > 60 ? restart')
assert 'dry-run' in preview
assert started() == original_start, 'Observation or preview restarted the worker'

ENV['DEMO_SCENARIO'] = 'demo-guard'
compose(LAB_FILES, 'up', '-d', '--no-deps', '--wait', '--wait-timeout', '180', 'monit-docker')
compose(LAB_FILES, 'restart', 'ui')
wait_for(lambda: started() != original_start, 'guard restarts the selected worker')
wait_for(lambda: request(browser=True)['containers'][0]['mem_percent'] < 60,
'UI observes recovery after agent recreation')
journal = compose(LAB_FILES, 'exec', '-T', 'monit-docker', 'monit-docker',
'--audit-file', AUDIT_FILE, 'audit-export')
events = [json.loads(line) for line in journal.splitlines() if line.strip()]
assert any(e.get('source') == 'automatic' and e.get('result') == 'succeeded'
for e in events), 'Missing successful automatic action event'
# Consume another incident and wait out the cooldown: the persisted budget
# must prevent a second automatic restart for this same container ID.
recovered_start = started()
compose(LAB_FILES, 'kill', '--signal', 'SIGUSR1', 'demo-worker')
wait_for(lambda: request()['actions']['restart-limit'] > 0,
'second incident is blocked by the restart budget', timeout=180)
assert started() == recovered_start

ENV['DEMO_SCENARIO'] = 'demo-observe'
compose(LAB_FILES, 'up', '-d', '--no-deps', '--wait', '--wait-timeout', '180', 'monit-docker')
compose(LAB_FILES, 'restart', 'ui')
compose(LAB_FILES, 'kill', '--signal', 'SIGUSR2', 'demo-worker')
wait_for(lambda: request()['containers'][0]['mem_percent'] < 60,
'worker releases memory on command')
before = compose(LAB_FILES, 'exec', '-T', 'monit-docker', 'monit-docker',
'--audit-file', AUDIT_FILE, 'audit-export')
compose(LAB_FILES, 'down')
compose(LAB_FILES, 'up', '-d', '--wait', '--wait-timeout', '180')
after = compose(LAB_FILES, 'exec', '-T', 'monit-docker', 'monit-docker',
'--audit-file', AUDIT_FILE, 'audit-export')
assert before == after, 'Observation restart changed or lost the retained journal'
print('Verified: retained journal survives down/up', flush=True)
finally:
print(compose(LAB_FILES, 'logs', '--tail=30'), flush=True)
compose(LAB_FILES, 'down', '--volumes', '--remove-orphans')


if __name__ == '__main__':
main()
11 changes: 11 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ permissions:
contents: read

jobs:
compose-quickstart:
name: Minimal Compose and tutorial acceptance
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Exercise agent, authenticated UI, memory rule and persistence
run: python3 .github/scripts/check-quickstart-compose.py

textual:
name: Optional Textual interface
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ examples/monitoring/notifications.local/
examples/ui/secrets.local/
examples/ui/.env
ui/test-results/

tutorial-actions.csv
tutorial-events.jsonl
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# monit-docker project

**Current stable release: [1.1.0](https://github.com/decryptus/monit-docker/releases/tag/v1.1.0)**
— [Release notes and upgrade guidance](docs/release-1.1.0.md).
**Current package and image version: [1.1.1](https://github.com/decryptus/monit-docker/tree/v1.1.1)**
— [Release notes and upgrade guidance](docs/release-1.1.1.md).

<p align="center">
<img src="https://raw.githubusercontent.com/decryptus/monit-docker/v0.0.63/ui/branding/monit-docker-logo.png" alt="monit-docker logo" width="640">
Expand All @@ -17,7 +17,7 @@
[Documentation (EN)](https://www.monit-docker.com/docs/en/) ·
[Live demo](https://demo.monit-docker.com/) ·
[Release roadmap](docs/roadmap.md) ·
[1.1.0 release notes](docs/release-1.1.0.md) ·
[1.1.1 release notes](docs/release-1.1.1.md) ·
[Logos and icons](ui/branding/README.md)

The [configuration and CLI compatibility baseline](docs/config-cli-contract.md)
Expand Down Expand Up @@ -117,6 +117,30 @@ the release workflow does not update `latest`.

## <a name="quickstart"></a>Quickstart

### Docker Compose installation

| Installation | Files | What runs |
| --- | --- | --- |
| Minimal | `docker-compose.yml` | Agent only, observation by default |
| With web UI | Add `docker-compose.ui.yml` | Same agent plus authenticated HTTPS UI |
| Prometheus and Grafana | `examples/monitoring/compose.yaml` | Separate optional history and dashboards stack |

```sh
git clone https://github.com/decryptus/monit-docker.git
cd monit-docker
docker compose up -d --wait
```

Read the [Compose quickstart](docs/compose-quickstart.md) ([français](docs/compose-quickstart-fr.md))
for prerequisites, the UI, upgrades and stopping the stack. The
[reproducible memory tutorial](docs/tutorial-memory.md) adds a bounded test worker,
then explicitly enables one automatic restart. The default installation has no action rules.

**Existing root Compose users:** the old cron/PHP-FPM example is replaced by
observation-only `serve`. Preserve and review your old configuration before
upgrading; the old `MONIT_DOCKER_CRONS` jobs do not run in this new entry point.


Before running configured rules, use [`check-config`](docs/check-config.md) to validate
the YAML, imports, selectors and aliases without connecting to Docker.

Expand Down
14 changes: 14 additions & 0 deletions docker-compose.ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Add to docker-compose.yml. Reuse the existing UI and its secret preparation.
# python3 examples/ui/prepare.py --self-signed
# docker compose -f docker-compose.yml -f docker-compose.ui.yml up -d --wait
services:
ui:
extends:
file: ./examples/ui/compose.yaml
service: ui
depends_on:
monit-docker:
condition: service_healthy

networks:
frontend:
41 changes: 32 additions & 9 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
version: '3.2'
# Default installation: observe containers, without automatic actions.
# See docs/compose-quickstart.md before replacing the old root cron example.
services:
monit-docker:
image: decryptus/monit-docker:latest
container_name: monit-docker
environment:
MONIT_DOCKER_CRONS: |
*/2 * * * * monit-docker monit --cmd-if 'mem_usage > 200 MiB ? (kill -USR2 1)'
*/2 * * * * monit-docker monit --cmd-if 'status not in (paused,running) ? restart'
*/2 * * * * monit-docker monit --rsc pid
image: decryptus/monit-docker:1.1.1
command: [monit-docker, --audit-file, /var/lib/monit-docker/audit/events.jsonl, serve, --bind, 0.0.0.0, --interval, '30']
restart: unless-stopped
stop_grace_period: 60s
ports:
- '127.0.0.1:${MONIT_DOCKER_PORT:-9808}:9808'
networks: [agent]
volumes:
- /var/run:/var/run:rw
# A read-only socket mount still grants Docker API control.
- type: bind
source: ${DOCKER_SOCKET_PATH:-/var/run/docker.sock}
target: /var/run/docker.sock
read_only: true
bind:
create_host_path: false
- state:/var/lib/monit-docker
healthcheck:
test: [CMD, python, -c, 'import urllib.request; urllib.request.urlopen("http://127.0.0.1:9808/readyz", timeout=3).read()']
interval: 10s
timeout: 5s
retries: 12
start_period: 30s

networks:
agent:
# Use a normal bridge for the loopback-published API port. The standalone
# UI example instead uses an internal network and publishes no agent port.
driver: bridge

volumes:
state:
112 changes: 112 additions & 0 deletions docs/compose-quickstart-fr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Installer avec Docker Compose

[English version](compose-quickstart.md)

Trois installations sont proposées. Toutes démarrent sans règle de redémarrage
automatique des conteneurs surveillés.

| Besoin | Fichiers | Accès local |
| --- | --- | --- |
| Agent seul et API | `docker-compose.yml` | `http://127.0.0.1:9808` |
| Agent et interface web | Base et `docker-compose.ui.yml` | `https://localhost:8443` |
| Historique et graphiques | `examples/monitoring/compose.yaml` | Grafana sur le port 3000 |

## Préparer la machine

Utiliser Docker Engine sous Linux, son socket Unix local et une version récente
du plugin Compose avec `up --wait`. Le compte doit pouvoir accéder à Docker. Les
images publiées ciblent Linux amd64 ; une autre architecture demande une compilation
ou une émulation vérifiée séparément. Docker Desktop doit utiliser les conteneurs
Linux. Sous Windows, exécuter les commandes dans WSL avec l’intégration Docker.
La préparation de l’UI demande aussi Python 3 et OpenSSL.

```sh
git clone https://github.com/decryptus/monit-docker.git
cd monit-docker
docker compose version
docker info
```

Pour Docker rootless, définir `DOCKER_SOCKET_PATH` avec le chemin absolu du socket
local. Les contextes Docker distants ne sont pas couverts par cette procédure.

## Lancer seulement l’agent

```sh
docker compose config --quiet
docker compose up -d --wait --wait-timeout 180
curl -f http://127.0.0.1:9808/readyz
curl -f http://127.0.0.1:9808/v1/status
```

La collecte tourne toutes les 30 secondes après la fin du cycle précédent.
`readyz` renvoie HTTP 200 lorsque les mesures sont fraîches et la collecte réussie.
En cas d’erreur, consulter `docker compose ps` et
`docker compose logs --tail=100 monit-docker`.

L’API reste accessible uniquement en local. L’agent reçoit seulement le socket
Docker, mais ce socket permet de contrôler Docker même si le montage est marqué
`read_only`. L’observation seule vient de l’absence de règles, pas du montage.

## Ajouter l’interface web

```sh
python3 examples/ui/prepare.py --self-signed
docker compose -f docker-compose.yml -f docker-compose.ui.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.ui.yml up -d --wait --wait-timeout 180
```

Saisir un identifiant et un mot de passe d’au moins 12 caractères. Ouvrir
**https://localhost:8443**. Le certificat local est autosigné et valable sept
jours ; le navigateur signale qu’il n’est pas reconnu. Pour un vrai déploiement,
utiliser un certificat de confiance selon le [guide UI](ui.md).

La préparation se fait une seule fois et refuse d’écraser les secrets existants.
Réutiliser `examples/ui/secrets.local` aux démarrages suivants. L’UI utilise le
même agent, sans Prometheus ni Grafana, et n’active aucun bouton de contrôle des
conteneurs. L’API de l’agent reste accessible sur la boucle locale.

Conserver la même liste de fichiers pour arrêter cette installation :

```sh
docker compose -f docker-compose.yml -f docker-compose.ui.yml down
```

Cette commande conserve le volume d’état et les secrets. `--volumes` supprimerait
les données du volume : ne pas l’ajouter pour un arrêt normal. Pour revenir à
l’agent seul, arrêter avec les deux fichiers, puis relancer le fichier de base.
Les surcharges avancées d’actions et de journal du dossier `examples/ui` ont leur
propre procédure ; ne pas les mélanger avec ces fichiers racine.

## Régler les ports et mettre à jour

Définir `MONIT_DOCKER_PORT`, `UI_PORT` ou `DOCKER_SOCKET_PATH` dans le shell avant
Compose si nécessaire. `UI_BIND` vaut `127.0.0.1` par défaut. Garder le même nom
de projet et le même répertoire pour réutiliser le volume.

Les images de l’agent et de l’UI sont fixées à **1.1.1**. Pour une évolution,
choisir des versions correspondantes, préserver la configuration et les données,
puis utiliser `pull` et `up -d --wait` avec la même liste de fichiers. Un simple
`pull` ne remplace pas les conteneurs en cours d’exécution.

## Remplacer l’ancien exemple cron

L’ancien fichier racine utilisait `latest`, montait tout `/var/run` et exécutait
des règles cron, dont un signal PHP-FPM. Le nouveau lance uniquement `serve` en
observation. **Les anciens jobs `MONIT_DOCKER_CRONS` ne seront plus exécutés.**

Avant la mise à jour, sauvegarder hors du dépôt l’ancien Compose, la configuration,
les états et les journaux. Arrêter l’ancienne installation avec ce fichier et son
nom de projet d’origine. Pour conserver des jobs, reprendre explicitement leurs
cibles et règles selon le [guide cron](cron.md), avec un état persistant et une
simulation préalable. Aucune conversion automatique des règles n’est effectuée.

## Continuer avec une démonstration

Le [tutoriel mémoire](tutorial-memory.md) ajoute un conteneur jetable et permet
d’activer explicitement un seul redémarrage automatique. La limite mémoire du
conteneur et la sélection exacte évitent de prendre un service existant comme cible.

La [pile Prometheus et Grafana](compose.md) reste une installation distincte pour
l’historique. Arrêter la pile minimale ou modifier ses ports avant de la lancer.
Les notifications demandent une [configuration supplémentaire](notifications.md).
Loading
Loading