Add the example stack payloads

The files copied to /srv/stacks/<stack>/ verbatim: compose files, vhosts,
Prometheus configuration, the webapp image source. Payloads are data, never
templated and never linted as Ansible content.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-09-16 03:58:01 +02:00
co-authored by Claude Opus 5
parent 7b52e9af9e
commit b76c76e3b0
16 changed files with 399 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
<!-- Served at / by the reverse proxy on every host, as a placeholder for
requests that match no vhost. Deployed by ansible/playbooks/banner.yml,
which drops this one file into the reverse-proxy stack's html directory on
every host rather than owning a stack of its own. -->
<!doctype html>
<meta charset="utf-8">
<title>example.com</title>
<h1>Nothing is served at this address.</h1>
<p>If you expected a service here, check the vhost configuration.</p>
+7
View File
@@ -0,0 +1,7 @@
# Copy to .env ON THE HOST, not here. The compose_stack role excludes .env from
# every sync (stack_exclude), so a .env committed to this repository would be
# ignored, and a host's .env is never overwritten by a deploy.
#
# ssh [email protected]
# cd /srv/stacks/metrics && cp .env.example .env && $EDITOR .env
GF_SECURITY_ADMIN_PASSWORD=
+56
View File
@@ -0,0 +1,56 @@
# Prometheus and Grafana, deployed to every host from this one payload.
#
# It lives under server/shared/ rather than server/<host>/ because both hosts run
# exactly the same thing; ansible/playbooks/metrics.yml points stack_src here.
# Per-host differences (which networks exist, which extra directories to create)
# belong in the playbook, not in duplicated copies of this file.
#
# Both bind mounts below need a directory that exists with the right ownership
# before the container starts, and neither image will fix it for you:
#
# grafana-data the image runs as uid 472 and writes a SQLite database here
# prometheus-data the image runs as nobody (65534)
#
# The playbook pre-creates both. See "stack_dirs" in AGENTS.md for why that is
# the role's job and not a chown in pre_tasks.
services:
prometheus:
container_name: prometheus
image: prom/prometheus:v3.1.0
restart: unless-stopped
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
# Served under a sub-path by the reverse proxy, so it has to generate
# absolute URLs that include it.
- --web.external-url=https://app.example.com/internal/metrics/
- --web.route-prefix=/
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus-data:/prometheus
networks:
- default
- proxy-net
grafana:
container_name: grafana
image: grafana/grafana-oss:11.5.1
restart: unless-stopped
networks:
- default
- proxy-net
environment:
VIRTUAL_HOST: dashboards.example.com
VIRTUAL_PORT: "3000"
LETSENCRYPT_HOST: dashboards.example.com
GF_SERVER_ROOT_URL: https://dashboards.example.com
# Compose fails the run if this is not set, rather than starting Grafana
# with a blank admin password. The value comes from the host's .env, which
# is never synced from this repository — see docs/secrets.md.
GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:?set it in .env on the host}
volumes:
- ./grafana-data:/var/lib/grafana
networks:
proxy-net:
external: true
+15
View File
@@ -0,0 +1,15 @@
# Bind-mounted read-only into the prometheus container. Prometheus re-reads it on
# SIGHUP, so metrics.yml reloads the container when this file changes rather than
# recreating it.
global:
scrape_interval: 30s
evaluation_interval: 30s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: node
static_configs:
- targets: ["node-exporter:9100"]