← Start of tutorial

WIUNIX ACADEMY

Prometheus and Grafana with Docker Compose: private monitoring and recovery

Page 2 of 6. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Debian 12Reviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment11 editable values
03

LET’S BUILD

Install Engine and the Compose plugin

Install the official packages, not the retired standalone docker-compose Python command. Compose 5.6.0 is the current reviewed stable release on 20 October 2026; vendor APT versions can differ by OS. These commands deliberately install the currently supported repository candidate and record its exact version, rather than promise an unavailable cross-distro version pin. Require maintained Engine ≥28 and modern Compose with the Compose Specification. Do not add users to the docker group casually: it grants root-equivalent control. All following daemon operations use sudo.

Bash

Dedicated Linux host; Bash shell

sudo apt-get install --no-install-recommends docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl is-active docker
sudo docker version
sudo docker compose version
dpkg-query -W docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo docker network ls
sudo docker network inspect $(sudo docker network ls -q)
How do I know it worked?

Docker active, supported versions recorded, chosen subnet absent from existing routes/networks.

04

LET’S BUILD

Create only new dedicated directories

The guard rejects existing paths and nesting; it does not prove that a symlinked parent is safe. Review parents first. Configs are root-managed, Prometheus data belongs to UID/GID 65534, and Grafana data to UID 472/GID 0, matching the explicit users in Compose. Never fix a permission problem with mode 777 or by making the service root. Local POSIX storage is required; NFS is unsuitable for Prometheus TSDB.

Bash

Dedicated Linux host; Bash shell

sudo bash -eu <<'SETUP'
paths=('/srv/academy-monitor' '/srv/academy-monitor-backups')
[[ 'academy-monitor' =~ ^[a-z0-9][a-z0-9_-]*$ ]]
for a in "${paths[@]}"; do
  [[ "$a" = /* && "$a" != / && ! -e "$a" && ! -L "$a" ]]
  for b in "${paths[@]}"; do
    [[ "$a" = "$b" ]] && continue
    case "$a/" in "$b/"*) exit 1;; esac
  done
done
install -d -m 0755 '/srv/academy-monitor' '/srv/academy-monitor/config' '/srv/academy-monitor/config/targets' '/srv/academy-monitor/config/datasources' '/srv/academy-monitor/config/dashboards' '/srv/academy-monitor/dashboards'
install -d -m 0700 '/srv/academy-monitor/secrets' '/srv/academy-monitor-backups'
install -d -m 0750 -o 65534 -g 65534 '/srv/academy-monitor/prometheus-data'
install -d -m 0750 -o 472 -g 0 '/srv/academy-monitor/grafana-data'
SETUP
How do I know it worked?

New directories have deliberate ownership and no unrelated content.

05

LET’S BUILD

Inspect and lock official image digests

The reviewed multi-architecture manifest digests below pin Prometheus 3.15.0 and Grafana OSS 13.2.4. Grafana OSS now uses grafana/grafana; grafana-oss is no longer updated. Tags are readable labels; the digest controls the actual image. Inspect platform support and vendor advisories before using the pin later. HTTPS registry provenance and digest integrity are not an independent signature or vulnerability-free guarantee. The stack supports Linux amd64/arm64; never silently run emulation. Pulling images needs registry egress from the daemon, not from the internal service network.

Bash

Dedicated Linux host; Bash shell

sudo docker buildx imagetools inspect prom/prometheus:v3.15.0@sha256:efd719c99d83b060d9daefdcf00360461adf279f45ef5391f8d111892118753e
sudo docker buildx imagetools inspect grafana/grafana:13.2.4@sha256:dbbc38ea21103cefb50396340cd88afec779c7e070b0a7bf5a75b4ce46651d3f
How do I know it worked?

Digest and native Linux platform match the reviewed manifests.

06

LET’S BUILD

Enter the bootstrap password in a protected editor

Create a new strong unique Grafana bootstrap administrator password, one line, in the protected editor. Never put it in a command argument, .env, tutorial field, Git, screenshot or shared terminal log. This file is mounted only into Grafana as a Compose secret. Standalone Compose secrets are local file bind mounts, not encrypted Swarm storage; source mode and host protection matter. File-backed uid/gid/mode remapping is not implemented by Compose. Root:0 with 0440 permits explicit Grafana UID472/GID0 to read the mounted file; the root-only parent protects the source on the host. This password initializes a fresh database only: changing the file later does not rotate an existing administrator.

Bash

Dedicated Linux host; Bash shell

sudo install -m 0600 /dev/null '/srv/academy-monitor/secrets/grafana-admin'
sudo nano '/srv/academy-monitor/secrets/grafana-admin'
sudo chown root:0 '/srv/academy-monitor/secrets/grafana-admin'
sudo chmod 0440 '/srv/academy-monitor/secrets/grafana-admin'
sudo test -s '/srv/academy-monitor/secrets/grafana-admin'
How do I know it worked?

Nonempty protected secret, with no password printed to output.

07

LET’S BUILD

Open the Compose document

Open the new file and paste the complete next panel. Do not use automatic directory creation for a missing bind file: all files below must exist before launch.

Bash

Dedicated Linux host; Bash shell

sudo nano '/srv/academy-monitor/compose.yaml'
How do I know it worked?

Editor ready for the full YAML document.

Compose: identity, storage and isolation

Save the complete file. The internal bridge intentionally denies ordinary external egress: there are no runtime plugin downloads, external data sources, email contacts or OAuth in this lab. Restricting egress is not protection from host root or peers. Bind mounts make same-host backup paths explicit; named volumes are also supported upstream but require their own volume-aware recovery. Resource ceilings are starting limits, not reserved capacity. No depends_on health promise is made: each service starts independently and the next part proves readiness. A restart policy restarts exited processes; it does not by itself detect a hung or unhealthy service.

YAML · /srv/academy-monitor/compose.yaml
name: academy-monitor
services:
  prometheus:
    image: prom/prometheus:v3.15.0@sha256:efd719c99d83b060d9daefdcf00360461adf279f45ef5391f8d111892118753e
    user: "65534:65534"
    restart: unless-stopped
    read_only: true
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    mem_limit: 1536m
    cpus: 1.0
    pids_limit: 128
    tmpfs: [/tmp:size=64m,mode=1777]
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=15d
      - --storage.tsdb.retention.size=5GB
      - --web.listen-address=0.0.0.0:9090
    ports: ["127.0.0.1:19090:9090"]
    volumes:
      - type: bind
        source: /srv/academy-monitor/config/prometheus.yml
        target: /etc/prometheus/prometheus.yml
        read_only: true
        bind: {create_host_path: false}
      - type: bind
        source: /srv/academy-monitor/config/targets
        target: /etc/prometheus/targets
        read_only: true
        bind: {create_host_path: false}
      - type: bind
        source: /srv/academy-monitor/prometheus-data
        target: /prometheus
        bind: {create_host_path: false}
    networks: [monitor]
    logging:
      driver: json-file
      options: {max-size: "10m", max-file: "3"}
  grafana:
    image: grafana/grafana:13.2.4@sha256:dbbc38ea21103cefb50396340cd88afec779c7e070b0a7bf5a75b4ce46651d3f
    user: "472:0"
    restart: unless-stopped
    read_only: true
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    mem_limit: 768m
    cpus: 1.0
    pids_limit: 256
    tmpfs: [/tmp:size=64m,mode=1777]
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin
      GF_AUTH_ANONYMOUS_ENABLED: "false"
      GF_USERS_ALLOW_SIGN_UP: "false"
      GF_METRICS_ENABLED: "true"
      GF_ANALYTICS_REPORTING_ENABLED: "false"
      GF_ANALYTICS_CHECK_FOR_UPDATES: "false"
      GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES: "false"
      GF_PLUGINS_PREINSTALL_DISABLED: "true"
    ports: ["127.0.0.1:13000:3000"]
    secrets: [grafana_admin]
    volumes:
      - type: bind
        source: /srv/academy-monitor/grafana-data
        target: /var/lib/grafana
        bind: {create_host_path: false}
      - type: bind
        source: /srv/academy-monitor/config/datasources
        target: /etc/grafana/provisioning/datasources
        read_only: true
        bind: {create_host_path: false}
      - type: bind
        source: /srv/academy-monitor/config/dashboards
        target: /etc/grafana/provisioning/dashboards
        read_only: true
        bind: {create_host_path: false}
      - type: bind
        source: /srv/academy-monitor/dashboards
        target: /var/lib/grafana/dashboards
        read_only: true
        bind: {create_host_path: false}
    networks: [monitor]
    logging:
      driver: json-file
      options: {max-size: "10m", max-file: "3"}
networks:
  monitor:
    internal: true
    ipam:
      config: [{subnet: "172.30.71.0/24"}]
secrets:
  grafana_admin:
    file: /srv/academy-monitor/secrets/grafana-admin
Explore all tutorials ↗