Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
# NOTE!!!: Don't copy and edit this file yourself. Instead, use ./scripts/generate-env.sh to generate a new one

APP_HOSTNAME=dhis2-127-0-0-1.nip.io
LETSENCRYPT_ACME_EMAIL=<your@email.org>
# ACME CA server - use staging for testing to avoid rate limits
# Uncomment the line below to use Let's Encrypt staging
# LETSENCRYPT_ACME_CASERVER=https://acme-staging-v02.api.letsencrypt.org/directory

# Set this to the exact version you want to use. Example: 42.3.1
DHIS2_VERSION=42

DHIS2_ADMIN_USERNAME=admin
DHIS2_ADMIN_PASSWORD=<some very secure password>

# This user is automatically created during startup
# These credentials are copied from stacks/monitoring/.env by generate-env.sh.
# They must match across all instances so the shared Prometheus can scrape metrics.
DHIS2_MONITOR_USERNAME=monitor
DHIS2_MONITOR_PASSWORD=<another very secure password>

Expand All @@ -24,10 +21,4 @@ POSTGRES_DB_PASSWORD=<another very secure password>
POSTGRES_METRICS_USERNAME=metrics
POSTGRES_METRICS_PASSWORD=<another very secure password>

GRAFANA_VERSION=10.0.0
GRAFANA_ADMIN_PASSWORD=<another very secure password>
PROMETHEUS_VERSION=v2.45.0
PROMETHEUS_RETENTION_TIME=15d
LOKI_VERSION=2.9.0
LOKI_RETENTION_PERIOD=744h
POSTGRES_EXPORTER_VERSION=v0.17.1
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,14 @@ Thumbs.db

.ansible/

traefik/acme.json
backups/

# Standalone stacks — env files contain passwords
stacks/traefik/.env
stacks/monitoring/.env

# Generated route and target files (created by make launch-instance / launch-monitoring)
stacks/traefik/conf.d/*.yml
!stacks/traefik/conf.d/middlewares.yml
stacks/monitoring/targets/dhis2/*.json
stacks/monitoring/targets/postgres/*.json
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ repos:
hooks:
- id: shellcheck
exclude: .envrc
args: [ -x ]

- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.12.0-2
Expand Down
148 changes: 129 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
PRE_COMMIT_VERSION ?= 4.3.0

.PHONY: init playwright test reinit check backup-database backup-file-storage backup restore-database restore-file-storage restore docs launch clean clean-all config get-backup-timestamp
# PROJECT_NAME: unique name for this instance (e.g. dev, test, prod).
# Env file is always instances/$(PROJECT_NAME).env — generated by scripts/generate-env.sh.
PROJECT_NAME ?= $(notdir $(CURDIR))
ENV_FILE = instances/$(PROJECT_NAME).env
BACKUP_DIR ?= ./backups/$(PROJECT_NAME)

.PHONY: init playwright test reinit check backup-database backup-file-storage backup restore-database restore-file-storage restore docs generate-stack-envs create-instance list-instances start-postgres start-instance start-traefik start-monitoring ensure-networks stop-instance delete-instance clean clean-all config get-backup-timestamp

init:
@test -d .venv || python3 -m venv .venv
Expand All @@ -21,6 +27,14 @@ reinit:
rm -rf .venv
$(MAKE) init

# Generate .env files for stacks/traefik/ and stacks/monitoring/ (run once per server).
# Requires: GEN_LETSENCRYPT_ACME_EMAIL and GEN_GRAFANA_HOSTNAME to be set.
# Example: GEN_LETSENCRYPT_ACME_EMAIL=ops@example.com GEN_GRAFANA_HOSTNAME=grafana.example.com make generate-stack-envs
generate-stack-envs:
GEN_LETSENCRYPT_ACME_EMAIL=$(GEN_LETSENCRYPT_ACME_EMAIL) \
GEN_GRAFANA_HOSTNAME=$(GEN_GRAFANA_HOSTNAME) \
./scripts/generate-stack-envs.sh

install-loki-driver:
docker plugin ls --format '{{.Name}}' | grep -q 'loki:latest' || ./scripts/install-loki-driver.sh
docker plugin ls
Expand All @@ -30,44 +44,140 @@ check:

BACKUP_TIMESTAMP ?= $(shell date -u +%Y-%m-%d_%H-%M-%S_%Z)

POSTGRES_COMPOSE_CMD = docker compose \
--project-name $(PROJECT_NAME) \
--env-file $(ENV_FILE) \
-f stacks/postgres/docker-compose.yml

BACKUP_COMPOSE_CMD = BACKUP_DIR=$(BACKUP_DIR) docker compose \
--project-name $(PROJECT_NAME) \
--env-file $(ENV_FILE) \
-f docker-compose.yml \
-f stacks/backup/docker-compose.yml

get-backup-timestamp:
@echo $(BACKUP_TIMESTAMP)

backup-database:
mkdir -p ./backups
docker compose run -e BACKUP_TIMESTAMP=$(BACKUP_TIMESTAMP) --rm backup-database
mkdir -p $(BACKUP_DIR)
$(BACKUP_COMPOSE_CMD) run -e BACKUP_TIMESTAMP=$(BACKUP_TIMESTAMP) --rm backup-database

backup-file-storage:
mkdir -p ./backups
docker compose run -e BACKUP_TIMESTAMP=$(BACKUP_TIMESTAMP) --rm backup-file-storage
mkdir -p $(BACKUP_DIR)
$(BACKUP_COMPOSE_CMD) run -e BACKUP_TIMESTAMP=$(BACKUP_TIMESTAMP) --rm backup-file-storage

backup: backup-database backup-file-storage

restore-database:
docker compose stop app
docker compose run --rm restore-database
docker compose start app
$(BACKUP_COMPOSE_CMD) stop app
$(BACKUP_COMPOSE_CMD) run --rm restore-database
$(BACKUP_COMPOSE_CMD) start app

restore-file-storage:
docker compose stop app
docker compose run --rm restore-file-storage
docker compose start app
$(BACKUP_COMPOSE_CMD) stop app
$(BACKUP_COMPOSE_CMD) run --rm restore-file-storage
$(BACKUP_COMPOSE_CMD) start app

restore:
docker compose stop app
docker compose run --rm restore-database
docker compose run --rm restore-file-storage
docker compose start app
$(BACKUP_COMPOSE_CMD) stop app
$(BACKUP_COMPOSE_CMD) run --rm restore-database
$(BACKUP_COMPOSE_CMD) run --rm restore-file-storage
$(BACKUP_COMPOSE_CMD) start app

docs:
mkdir -p ./docs
docker compose run --rm compose-docs > docs/environment-variables.md

COMPOSE_CMD = docker compose -f docker-compose.yml -f overlays/traefik-dashboard/docker-compose.yml -f overlays/monitoring/docker-compose.yml -f overlays/profiling/docker-compose.yml -f overlays/glowroot/docker-compose.yml
docker compose -f stacks/docs/docker-compose.yml run --rm compose-docs > docs/environment-variables.md

COMPOSE_CMD = docker compose \
--project-name $(PROJECT_NAME) \
--env-file $(ENV_FILE) \
-f docker-compose.yml \
-f overlays/profiling/docker-compose.yml \
-f overlays/glowroot/docker-compose.yml

# Create the shared Docker networks required by the multi-instance workflow.
# Safe to run multiple times (ignores errors if networks already exist).
ensure-networks:
docker network create proxy 2>/dev/null || true
docker network create monitoring 2>/dev/null || true

# Start the standalone Traefik gateway (run once; watches stacks/traefik/conf.d/ for route changes)
start-traefik: ensure-networks
docker compose -f stacks/traefik/docker-compose.yml --env-file stacks/traefik/.env up $(COMPOSE_OPTS)

# Start the standalone monitoring stack (run once; watches stacks/monitoring/targets/ for new instances)
start-monitoring: ensure-networks
GRAFANA_HOSTNAME=$$(grep -E '^GRAFANA_HOSTNAME=' stacks/monitoring/.env | cut -d= -f2-) \
envsubst < stacks/traefik/conf.d/monitoring.yml.template > stacks/traefik/conf.d/monitoring.yml
docker compose -f stacks/monitoring/docker-compose.yml --env-file stacks/monitoring/.env up $(COMPOSE_OPTS)

# Generate the env file for a new instance.
# Example: APP_HOSTNAME=dhis2.example.com PROJECT_NAME=prod make create-instance
create-instance:
@test -n "$(APP_HOSTNAME)" || (echo "Error: APP_HOSTNAME must be set" >&2; exit 1)
GEN_PROJECT_NAME=$(PROJECT_NAME) GEN_APP_HOSTNAME=$(APP_HOSTNAME) ./scripts/generate-env.sh

# List all configured instances with their hostname and running container count.
list-instances:
@envs=$$(ls instances/*.env 2>/dev/null); \
if [ -z "$$envs" ]; then \
echo "No instances found in instances/"; \
else \
printf "%-20s %-40s %s\n" "INSTANCE" "HOSTNAME" "CONTAINERS"; \
for env in $$envs; do \
name=$$(basename $$env .env); \
hostname=$$(grep -E '^APP_HOSTNAME=' $$env | cut -d= -f2-); \
running=$$(docker compose --project-name $$name -f docker-compose.yml ps -q 2>/dev/null | wc -l | tr -d ' '); \
printf "%-20s %-40s %s running\n" "$$name" "$$hostname" "$$running"; \
done; \
fi

launch: install-loki-driver
# Start the PostgreSQL stack for a named instance.
# Creates a per-instance db network (PROJECT_NAME-db) and waits until healthy.
start-postgres:
docker network create $(PROJECT_NAME)-db 2>/dev/null || true
$(POSTGRES_COMPOSE_CMD) up --wait -d

# Start a named DHIS2 instance connected to the standalone Traefik, monitoring, and postgres stacks.
# Requires: PROJECT_NAME and ENV_FILE to be set, ensure-networks and start-traefik and
# start-monitoring to have been run first.
# Example: PROJECT_NAME=dev ENV_FILE=instances/dev.env make start-instance
start-instance: ensure-networks install-loki-driver start-postgres
PROJECT_NAME=$(PROJECT_NAME) APP_HOSTNAME=$$(grep -E '^APP_HOSTNAME=' $(ENV_FILE) | cut -d= -f2-) \
envsubst < stacks/traefik/conf.d/instance.yml.template > stacks/traefik/conf.d/$(PROJECT_NAME).yml
PROJECT_NAME=$(PROJECT_NAME) \
envsubst < stacks/monitoring/targets/dhis2/instance.json.template > stacks/monitoring/targets/dhis2/$(PROJECT_NAME).json
PROJECT_NAME=$(PROJECT_NAME) \
envsubst < stacks/monitoring/targets/postgres/instance.json.template > stacks/monitoring/targets/postgres/$(PROJECT_NAME).json
$(COMPOSE_CMD) up $(COMPOSE_OPTS)

# Stop a named DHIS2 instance and remove its Traefik routes and Prometheus targets.
# Example: PROJECT_NAME=dev ENV_FILE=instances/dev.env make stop-instance
stop-instance:
$(COMPOSE_CMD) down --remove-orphans
$(POSTGRES_COMPOSE_CMD) down
docker network rm $(PROJECT_NAME)-db 2>/dev/null || true
rm -f stacks/traefik/conf.d/$(PROJECT_NAME).yml
rm -f stacks/monitoring/targets/dhis2/$(PROJECT_NAME).json
rm -f stacks/monitoring/targets/postgres/$(PROJECT_NAME).json

# Delete a named DHIS2 instance: stop containers, remove volumes, and delete env file.
# WARNING: This permanently destroys all data for the instance.
# Example: PROJECT_NAME=dev ENV_FILE=instances/dev.env make delete-instance
delete-instance:
@if [ -t 0 ]; then \
echo "WARNING: This will permanently destroy all data for instance '$(PROJECT_NAME)' (database, file storage, env file)."; \
echo "This action is irreversible."; \
read -p "Are you sure? [y/N] " confirm && [ "$$confirm" = "y" ] || (echo "Aborted." && exit 1); \
fi
$(COMPOSE_CMD) down --remove-orphans --volumes
$(POSTGRES_COMPOSE_CMD) down --volumes
docker network rm $(PROJECT_NAME)-db 2>/dev/null || true
rm -f stacks/traefik/conf.d/$(PROJECT_NAME).yml
rm -f stacks/monitoring/targets/dhis2/$(PROJECT_NAME).json
rm -f stacks/monitoring/targets/postgres/$(PROJECT_NAME).json
rm -f instances/$(PROJECT_NAME).env

clean:
$(COMPOSE_CMD) down --remove-orphans

Expand Down
Loading
Loading