Creare un progetto Docker Compose su OMV: dalla cartella al container gestito
Guida passo-passo per portare un'applicazione esistente (che gira a host, con
cron/systemd) in un container gestito dall'app Docker di OMV
(openmediavault-compose), la stessa usata per dmnotes, assistente, mailcow.
Esempio concreto usato in tutta la guida: il dashboard DBSoft
(busce.it/dashboard/dbsoft) migrato a container su NAS-MAIN il 2026-09-15.
Panoramica dell'architettura OMV
L'app OMV compose gestisce progetti così:
- Il config vive in
conf.service.compose.file: ogni voce haname,description,body(il YAML dei services),env(variabili opzionali in.env),override(opzionalecompose.override.yml). - Lo stato di deploy (
omv.deploy.compose) renderizza in/export/docker_config/<name>/:<name>.yml— il compose completo (header auto-generato + body)compose.yml— symlink su<name>.yml<name>.env(+ symlink.env) ecompose.override.ymlse configurati
- I comandi dell'app:
omv-compose-start/stop/update/build/backup <name>(bottoni nel WebUI sotto Docker, stessa cosa da CLI). - I jobs (
conf.service.compose.job) generano cron in/etc/cron.d/omv-compose-*per auto-start al boot, backup, update periodici, ecc.
Regola d'oro: i file renderizzati non si modificano a mano — si cambia il progetto nel WebUI (o il conf) e si rilancia lo stato di deploy.
Passo 1 — Preparare la cartella del progetto
Tutto ciò che gira a host deve essere portabile in un filesystem isolato.
Crea una cartella sotto /export/docker_config/<nome>/ con:
/export/docker_config/dashboard/
├── Dockerfile
├── entrypoint.sh
├── <scripti dell'app> # .py, .sh, …
└── config/ # tutto ciò che l'app legge (config, log storici, chiavi se serve)
Per il dashboard:
status-collector.py+dashboard-admin.py(i due programmi)config/dbsoft.json(config dell'app),config/admin.pw,config/dbsoft/logs/(storico log già centralizzato),config/verify-reports/
Principio: l'app dentro il container non deve vedere il filesystem della macchina, solo ciò che monta. Perciò i path vanno resi configurabili:
# esempio: path da env con default = valore storico (compatibile con il deploy host)
CONFIG_DIR = os.environ.get("DASHBOARD_CONFIG_DIR", "/etc/dashboard")
e i comandi local root-aware:
SUDO = "" if os.geteuid() == 0 else "sudo -n " # nel container siamo root, senza sudo
Il codice resta eseguibile a host invariato: i default non cambiano.
Passo 2 — Dockerfile
Base minimale + solo i pacchetti che servono (nessun pip install se l'app usa
solo stdlib, come il dashboard):
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
openssh-client smartmontools mdadm procps \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY status-collector.py dashboard-admin.py entrypoint.sh ./
COPY config/ ./config/
RUN chmod +x /app/entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/app/entrypoint.sh"]
Pacchetti del dashboard: openssh-client (SSH verso gli altri host),
smartmontools (smartctl), mdadm (RAID), procps (uptime).
Passo 3 — entrypoint.sh (i processi del container)
Un container = un processo principale + eventuali background. Pattern:
#!/bin/bash
set -e
export DASHBOARD_CONFIG_DIR=/app/config
export ADMIN_BIND=0.0.0.0
export ADMIN_PORT=8080
python3 /app/dashboard-admin.py & # pannello admin
ADMIN_PID=$!
while true; do
python3 /app/status-collector.py dbsoft # job ciclico: 1 ciclo subito
sleep 300 # poi ogni 5 minuti (come il vecchio cron)
if ! kill -0 $ADMIN_PID 2>/dev/null; then
echo "[entrypoint] admin morto: exit per far riavviare docker"
exit 1
fi
done
Con restart: unless-stopped nel compose, se l'entrypoint esce docker
riavvia tutto. I job ciclici "tipo cron" vivono qui, non più in cron del host.
Passo 4 — Creare il progetto in OMV
Due vie:
A. WebUI (consigliata): OMV → Docker → Aggiungi, inserisci nome e il body del compose (sotto). OMV crea la voce in conf e lo stato di deploy.
B. Da CLI/root (senza password OMV, come fatto per il dashboard): script
Python con openmediavault.config.Database — serve solo OMV_CONFIG_FILE
(/etc/openmediavault/config.xml, da /etc/default/openmediavault) e il
trucco OMV_CONFIGOBJECT_NEW_UUID per la creazione:
import os, uuid as u
os.environ["OMV_CONFIG_FILE"] = "/etc/openmediavault/config.xml"
import openmediavault
openmediavault.getenv = lambda k, d=None, return_type="str": os.environ.get(k, d)
from openmediavault.config import Database, Object
new = str(u.uuid4())
os.environ["OMV_CONFIGOBJECT_NEW_UUID"] = new # serve che db.set() lo veda come "nuovo"
f = Object("conf.service.compose.file")
f.set("uuid", new)
f.set("name", "dashboard")
f.set("description", "Dashboard DBSoft: collector stato host + pannello amministrazione")
f.set("body", """
services:
dashboard:
...
""".strip())
f.set("showenv", False); f.set("env", "")
f.set("showoverride", False); f.set("override", "")
Database().set(f)
Il body va così com'è nel file renderizzato (il template lo stampa
verbum tergo dopo l'header auto-generato), quindi deve contenere
services: e tutto il resto del compose.
Passo 5 — Rilanciare lo stato di deploy
omv-salt deploy run compose
Dopo: in /export/docker_config/dashboard/ compaiono dashboard.yml,
compose.yml (symlink), dashboard.env (+ .env), compose.override.yml.
Verificare subito che il YAML renderizzato sia quello atteso (specie
healthcheck e quote).
Passo 6 — Primo avvio
L'app usa questo pattern di comandi (stesso ordine dei bottoni WebUI):
cd /export/docker_config/dashboard
docker compose -f dashboard.yml -f compose.override.yml \
--env-file /export/docker_config/global.env --env-file dashboard.env up -d
omv-compose-build dashboard→docker compose build(immagine locale)omv-compose-update dashboard→ pull + ricreazione dei container running- al primo avvio
up -d(crea il container con le label dell'app:com.docker.compose.project.config_files→dashboard.yml)
Nota: se l'immagine è solo di build locale (es.
dashboard:1.0.0, non presente in alcun registry),docker compose pullfallisce: per gli update usarla solo dopo unabuild, oppureup -d --build.
Passo 7 — Auto-avvio al boot
Aggiungere un job (WebUI: Docker → job, oppure da conf come al Passo 4):
j = Object("conf.service.compose.job")
j.set("uuid", str(u.uuid4()))
j.set("enable", True)
j.set("filter", "dashboard") # solo questo progetto
j.set("filestart", True) # start al boot
j.set("execution", "reboot") # @reboot (o schedulazioni: minute/hour/…)
# gli altri flag (backup/update/prune/filestop/…) = False
Database().set(j)
poi omv-salt deploy run compose → genera
/etc/cron.d/omv-compose-start con
@reboot root omv-compose-start-multi -f 'dashboard' ....
Passo 8 — Rendere il servizio raggiungibile
Dipende dal caso:
Serve sulla LAN/Internet: rete esterna del reverse proxy (es.
web_proxydi dmnotes,buscesite_defaultper il busce) + Caddy:handle_path /dmnotes/* { reverse_proxy dmnotes:8000 }Solo da host (es. pannello admin del dashboard): pubblica sulla loopback del host:
ports: ["127.0.0.1:8080:8080"].Serve file statici (il caso dashboard): nessuna route Caddy — il container scrive i file in una dir già servita dal
file_server:volumes: - /export/docker_config/buscesite/site/dashboard:/site # rw
con
site_root: /sitenella config dell'app.
Attenzione ai nomi host: dentro il container non esistono le voci di
/etc/hosts del NAS. Se l'app usa nomi (es. PC-DESKTOP), metterli in
config con IP espliciti, altrimenti la risoluzione fallisce.
Passo 9 — Healthcheck e verifica
Sempre uno healthcheck (i bottoni OMV e il dashboard lo usano):
healthcheck:
test:
- CMD
- python3
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/', timeout=5).read()"
interval: 30s
timeout: 7s
retries: 3
start_period: 15s
Verifiche post-deploy (checklist):
docker ps --filter name=dashboard # Up (healthy)
docker inspect dashboard --format '{{index .Config.Labels "com.docker.compose.project.config_files"}}'
curl -sk https://busce.it/dashboard/dbsoft/ # pagina pubblica 200
curl -s http://127.0.0.1:8080/ # admin (solo host)
docker logs dashboard --tail 20
e testare davvero i comandi dell'app: omv-compose-stop dashboard →
omv-compose-start dashboard.
Passo 10 — Gestione quotidiana
| Azione | WebUI | CLI |
|---|---|---|
| start/stop | bottoni su Docker → dashboard |
omv-compose-start/stop dashboard |
| update (pull+recreate) | bottone Update | omv-compose-update dashboard |
| build immagine | bottone Build | omv-compose-build dashboard |
| cambiare config/volumi/porte | modificare il progetto nel WebUI | modificare conf.service.compose.file + omv-salt deploy run compose + omv-compose-update dashboard |
| log | Docker → dashboard → log |
docker logs dashboard |
Il compose finale del dashboard (body del progetto OMV)
services:
dashboard:
build:
context: /export/docker_config/dashboard
image: dashboard:1.0.0
container_name: dashboard
restart: unless-stopped
environment:
- TZ=Europe/Rome
volumes:
- /export/docker_config/dashboard/config:/app/config
- /export/docker_config/buscesite/site/dashboard:/site
- /var/log/omv-backup.log:/var/log/omv-backup.log:ro
- /var/log/NAS-BACKUP-orchestrator:/var/log/NAS-BACKUP-orchestrator:ro
- /var/lib/NAS-BACKUP-orchestrator:/var/lib/NAS-BACKUP-orchestrator:ro
- /export/DRD_Backup:/export/DRD_Backup:ro
- /root/.ssh:/root/.ssh
networks:
- buscesite_default
ports:
- "127.0.0.1:8080:8080"
healthcheck:
test:
- CMD
- python3
- -c
- "import urllib.request; urllib.request.urlopen(\"http://127.0.0.1:8080/\", timeout=5).read()"
interval: 30s
timeout: 7s
retries: 3
start_period: 15s
networks:
buscesite_default:
external: true
Errori tipiche (viste sul campo)
- Healthcheck con URL malformato: attenzione alle quote nel body del
compose (YAML scalar con quote dentro). Verificare sempre la riga
renderizzata in
<name>.ymle chedocker inspectmostri health OK. is_newdiDatabase.set: senzaOMV_CONFIGOBJECT_NEW_UUIDimpostato e letto, la creazione di un oggetto conf viene trattata come update e fallisce.- SSH self-connect: se l'app deve SSH su se stessa (es. host locale
diventato
root@IPnel container) la key di root va inauthorized_keys(il vecchio collector usavatype: locale non ne aveva bisogno). - Nomi host non risolti nel container → usare IP espliciti in config.
/etc/hostsdel host non esiste nel container → idem, IP espliciti.- File renderizzati editati a mano → persi al prossimo deploy; passare sempre dal WebUI/conf.
omv-compose-startnon crea i container mancanti (fa solodocker compose start): il primo avvio richiedeup -d/update.- Pull di immagini solo locali fallisce negli update: fare prima
buildo usareup -d --build.
Pulizia del vecchio deployment host
Dopo il cutover, verificare che non restino riferimenti e rimuovere:
- disabilitare prima (per sicurezza):
systemctl disable <svc>, cron →.disabled - testare il container (pagina, log, cicli)
- archiviare in un tarball di sicurezza (es.
/root/backup-<nome>-host-YYYYMMDD.tgz) - rimuovere: unit systemd (+
daemon-reload), cron, config dir (es./etc/<app>), script in/usr/local/sbin/(e.bak,__pycache__), log dedicati (es./var/log/<app>/), file compose manuali (docker-compose.yml→ rimosso, al suo posto il<name>.ymldell'app) - verifica finale:
grep -rdei path in/etc/systemd,/etc/cron*,/usr/local/sbin→ nessun riferimento; container healthy; servizi 200.