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.
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
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.
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)
Docker active, supported versions recorded, chosen subnet absent from existing routes/networks.
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.
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
New directories have deliberate ownership and no unrelated content.
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.
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
Digest and native Linux platform match the reviewed manifests.
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.
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'
Nonempty protected secret, with no password printed to output.
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.
Dedicated Linux host; Bash shell
sudo nano '/srv/academy-monitor/compose.yaml'
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.
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