Skip to Content

Production Guide: Deploy Scrutiny Disk Health Monitor with Docker Compose + Caddy on Ubuntu

Self-host S.M.A.R.T. drive monitoring with TLS-secured dashboard, collector sidecars, SMTP alerting, and production-ready Docker Compose on Ubuntu

Hard drives fail silently. By the time you notice a filesystem error or a kernel panic, your data may already be gone. Scrutiny is a lightweight, self-hosted S.M.A.R.T. monitoring tool that polls your drives continuously, surfaces failure predictions early, and aggregates health data into a clean web dashboard. Paired with Docker Compose and Caddy as a TLS-terminating reverse proxy, you get a production-ready disk health monitor running on your own Ubuntu server in under thirty minutes — with automatic HTTPS and zero manual certificate management.

This guide walks through every step: architecture, prerequisites, Docker Compose configuration for both the Scrutiny web/API container and the collector container, Caddy reverse proxy setup, SMTP alerting, volume persistence, and a verification checklist you can run after deployment.

Architecture and flow overview

Scrutiny is split into two components that communicate over an internal Docker network:

  • scrutiny-web — the web UI and REST API, backed by an InfluxDB time-series database embedded in the same image. It listens on port 8080 internally.
  • scrutiny-collector — a sidecar that runs on each host you want to monitor. It calls smartctl against your block devices and ships the results to the web container over HTTP. Because it needs raw access to block devices, this container must run with elevated privileges and device pass-through.

Caddy sits in front of the web container, terminates HTTPS on port 443, and reverse-proxies to scrutiny-web:8080. All traffic between your browser and Scrutiny is encrypted. The collector talks to the web API directly on the internal Docker network — no TLS required for that hop.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with root or sudo access
  • Docker 24+ and Docker Compose v2 installed (docker compose version)
  • A domain name (e.g., scrutiny.example.com) pointed at your server's public IP
  • Ports 80 and 443 open in your firewall (UFW: ufw allow 80,443/tcp)
  • smartmontools installed on the host: apt install smartmontools -y
  • SMTP credentials if you want email alerting (optional)

Step-by-step deployment

1. Create the project directory

mkdir -p /opt/scrutiny/{config,influxdb}
cd /opt/scrutiny

2. Discover your block devices

Before writing the Compose file, identify the drives you want Scrutiny to monitor. The collector passes these as device mounts.

lsblk -d -o NAME,SIZE,TYPE,MODEL | grep disk

Note the device paths (e.g., /dev/sda, /dev/sdb, /dev/nvme0n1). You will mount them into the collector container.

3. Write the Docker Compose file

version: "3.9"

networks:
  scrutiny_net:
    driver: bridge
  caddy_net:
    external: true

volumes:
  scrutiny_config:
  scrutiny_influx:

services:
  scrutiny-web:
    image: ghcr.io/analogj/scrutiny:master-web
    container_name: scrutiny-web
    restart: unless-stopped
    environment:
      SCRUTINY_WEB_INFLUXDB_HOST: "localhost"
    volumes:
      - scrutiny_config:/opt/scrutiny/config
      - scrutiny_influx:/opt/scrutiny/influxdb
    networks:
      - scrutiny_net
      - caddy_net
    expose:
      - "8080"

  scrutiny-collector:
    image: ghcr.io/analogj/scrutiny:master-collector
    container_name: scrutiny-collector
    restart: unless-stopped
    privileged: true
    cap_add:
      - SYS_RAWIO
      - SYS_ADMIN
    devices:
      - /dev/sda:/dev/sda
      # Add more drives as needed:
      # - /dev/sdb:/dev/sdb
      # - /dev/nvme0n1:/dev/nvme0n1
    environment:
      COLLECTOR_API_ENDPOINT: "http://scrutiny-web:8080"
      COLLECTOR_CRON_SCHEDULE: "0 */6 * * *"  # collect every 6 hours
    volumes:
      - /run/udev:/run/udev:ro
    networks:
      - scrutiny_net
    depends_on:
      - scrutiny-web

Tip: If you have NVMe drives add them to the devices block. Virtual machines may expose disks as /dev/vda; adjust accordingly.

4. Create the shared Caddy network (if it does not exist yet)

docker network create caddy_net 2>/dev/null || true

5. Write the Caddyfile

Create /opt/scrutiny/Caddyfile:

scrutiny.example.com {
    reverse_proxy scrutiny-web:8080
    tls {
        protocols tls1.2 tls1.3
    }
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
    }
}

6. Add Caddy to the Compose file

If you are running Caddy as a standalone container in a separate Compose project, ensure it is connected to the caddy_net network and has access to your Caddyfile. A minimal standalone Caddy service looks like this:

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /opt/scrutiny/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy_net

7. Start everything

cd /opt/scrutiny
docker compose up -d
docker compose logs -f --tail=50

Watch for Starting web server on :8080 from the web container and collector scan complete from the collector. The first scan runs at startup and then on the configured cron schedule.

Configuration and secrets handling

Scrutiny's configuration lives in a YAML file at /opt/scrutiny/config/scrutiny.yaml (mounted as a Docker volume). Create it before the first start if you want SMTP alerting or custom thresholds:

version: 1

web:
  listen:
    port: 8080
    host: 0.0.0.0
  database:
    location: /opt/scrutiny/influxdb

notify:
  urls:
    - "smtp://username:[email protected]:587/[email protected]&[email protected]"
    # Supports Slack, Discord, Telegram, PagerDuty via Shoutrrr URL format

log:
  level: INFO

Secrets management best practice: Do not hardcode SMTP credentials in the YAML file in version control. Instead, use a Docker secret or an environment variable substitution file:

# Create a .env file (never commit this)
cat > /opt/scrutiny/.env <<EOF
SMTP_USER=myuser
SMTP_PASS=s3cr3t
SMTP_HOST=smtp.example.com
EOF
chmod 600 /opt/scrutiny/.env

Reference ${SMTP_USER} and similar variables inside your YAML using the envsubst pattern or Docker Compose env_file interpolation.

Verification

After the stack is running, confirm each layer:

# All containers running
docker compose ps

# Web UI reachable internally
curl -s http://localhost:8080/api/health | python3 -m json.tool

# TLS endpoint via Caddy
curl -I https://scrutiny.example.com

# Collector successfully posted data
docker compose logs scrutiny-collector | grep -i "scan complete\|posted"

# At least one device record in the API
curl -s https://scrutiny.example.com/api/summary | python3 -m json.tool | head -20

Open https://scrutiny.example.com in your browser. You should see a dashboard with one or more drives listed, each showing S.M.A.R.T. attribute values and a pass/warn/fail badge. A green Passed badge means all monitored attributes are within safe thresholds.

Common issues and fixes

Collector exits with "no devices found": The /dev/sdX path in the devices: block does not exist on the host. Run lsblk again and correct the path. NVMe drives use /dev/nvme0n1 not /dev/sda.

Collector exits with "permission denied on smartctl": Ensure privileged: true and the SYS_RAWIO cap are set on the collector service. Some cloud VMs restrict raw I/O; check your provider's documentation.

Caddy returns 502 Bad Gateway: The scrutiny-web container is not yet on the caddy_net network, or has not finished initializing. Check docker network inspect caddy_net to verify membership and wait 10–15 seconds after startup for the embedded InfluxDB to initialize.

Dashboard shows no data after the first scan: The collector container's COLLECTOR_API_ENDPOINT must resolve to the web container. On a single-host deployment, use the container name (http://scrutiny-web:8080) not localhost.

Notification emails not arriving: Test the Shoutrrr URL format independently with the shoutrrr CLI (docker run --rm containrrr/shoutrrr send --url "smtp://...") before embedding it in the config. Common issues are port 587 requiring STARTTLS vs port 465 requiring SSL — check your mail provider's documentation.

InfluxDB data lost after restart: Ensure the scrutiny_influx volume is a named Docker volume, not a local bind mount with incorrect permissions. Named volumes persist across container recreates automatically.

FAQ

Does Scrutiny support NVMe drives?

Yes. Add /dev/nvme0n1:/dev/nvme0n1 to the devices section of the collector service. Scrutiny uses nvme-cli under the hood for NVMe attributes in addition to smartctl. Make sure nvme-cli is installed on the host if the image does not bundle it.

Can I monitor drives on multiple servers from one Scrutiny instance?

Yes. Deploy only the collector container on each remote host, and set COLLECTOR_API_ENDPOINT to point at your central Scrutiny web URL (e.g., https://scrutiny.example.com). Each host will ship its drive data to the same dashboard. Make sure the web API endpoint is reachable from every collector host.

How do I set custom S.M.A.R.T. thresholds?

Scrutiny ships with the Backblaze drive reliability dataset as default thresholds. You can override per-attribute thresholds in scrutiny.yaml under the thresholds key. See the Scrutiny docs for attribute IDs and acceptable value ranges. In most production setups the defaults are appropriate.

Is it safe to run the collector as privileged?

The privileged: true flag is required for smartctl to access raw block devices. This is an acceptable risk when the Docker host is dedicated to this workload or the container is isolated from untrusted code. For multi-tenant or shared hosts, consider running the collector directly as a systemd service with smartctl access rather than in a privileged container.

How often does Scrutiny collect S.M.A.R.T. data?

The default cron schedule in this guide is 0 */6 * * * — every 6 hours. You can change COLLECTOR_CRON_SCHEDULE to any valid cron expression. 0 */1 * * * for hourly polling is safe for most drives but adds marginal overhead. The initial scan always runs immediately on container start.

What notification channels does Scrutiny support?

Scrutiny uses the Shoutrrr notification library, which supports SMTP email, Slack, Discord, Telegram, PagerDuty, Gotify, ntfy, and more. Configure any supported URL in the notify.urls list in scrutiny.yaml. You can list multiple URLs to send alerts to several channels simultaneously.

How do I back up Scrutiny data?

Scrutiny's time-series data is stored in the embedded InfluxDB instance, which writes to the scrutiny_influx Docker volume. Back up with: docker run --rm -v scrutiny_influx:/data -v /backup:/backup alpine tar czf /backup/scrutiny_influx_$(date +%Y%m%d).tar.gz /data. Schedule this with a nightly cron job alongside your other volume backups.

Internal links

Talk to us

If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.

Contact Us

Production Guide: Deploy Directus with Docker Compose + Caddy + PostgreSQL on Ubuntu
A complete production walkthrough: Directus headless CMS, PostgreSQL, and Caddy with automatic TLS on Ubuntu.