Docker Compose orchestration layer for multiple independent application stacks on a shared host under munywele.co.ke. Public HTTP services bind loopback host ports and are routed by host Caddy snippets; TLS termination is handled by Caddy.
proxy-tool/
├── stacks/ ← one folder per stack, each self-contained
│ ├── databases/ ← postgres 17, pgbouncer, mariadb [deploy first]
│ ├── cache/ ← Redis [deploy before Redis consumers]
│ ├── automation/ ← n8n
│ ├── activepieces/ ← Activepieces app + worker
│ ├── monitoring/ ← Grafana, Prometheus, Loki, Alloy
│ ├── beszel/ ← Beszel hub and host agent
│ ├── netdata/ ← Netdata host/container metrics
│ ├── fuelrod/ ← Fuelrod service, SMS portal, SMS gateway
│ ├── farm/ ← Farm Manager API, web, migrations
│ ├── akilimo/ ← Akilimo API, use-uptake
│ ├── fees-prod/ ← Production Fee Syncer
│ ├── fees-dev/ ← Development Fee Syncer
│ ├── sonar/ ← SonarQube [optional]
│ ├── metabase/ ← Metabase BI [optional]
│ ├── mail/ ← Mailpit SMTP relay [optional]
│ ├── mqtt/ ← EMQX MQTT broker [optional]
│ ├── db-tools/ ← Adminer + RedisInsight [tunnel only]
│ └── dozzle/ ← Docker log viewer [tunnel only]
├── config/
│ ├── supervisor/ ← Supervisor process configs (common/, fuelrod/, fees/, akilimo/)
│ ├── nginx/ ← NGINX configs
│ ├── monitoring/ ← Grafana, Prometheus, Loki, Alloy
│ ├── beszel/ ← Beszel hub and host agent
│ ├── netdata/ ← Netdata host/container metrics
│ └── init/pgsql/ ← PostgreSQL init scripts (run on first container start)
├── log/
│ └── supervisor/ ← Bind-mounted log dirs (fees.prod/, fees.dev/)
├── stacks/databases/postgres/ ← postgres.conf
├── IMPROVEMENTS.md ← reliability/security checklist
├── BACKLOG.md ← deferred work items
└── .backup-example ← copy to .backup (backup credentials, gitignored)
EMQX deployment and WSS proxy routing are documented in
docs/mqtt.md.
| Network | Scope | Managed by |
|---|---|---|
dokploy-network |
External — inter-stack communication | Dokploy (created on install) |
internal |
Shared fixed-name network used by multiple stacks | Docker Compose |
Create dokploy-network manually when running without Dokploy:
docker network create dokploy-networkPublic HTTP services are bound to host ports, usually on 127.0.0.1. Each stack keeps the relevant routing snippets in stacks/<name>/Caddyfile; merge those snippets into the host Caddyfile. For Activepieces, flow.munywele.co.ke routes to 127.0.0.1:9710.
Validate the merged host configuration before restarting it:
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddyBind mount paths in each compose file are relative to that compose file's directory. Shared config at the repo root is referenced with ../../:
# From stacks/fuelrod/docker-compose.yml:
- ../../config/supervisor/common:/etc/supervisor/conf.d ✓
- ./config/supervisor/common:/etc/supervisor/conf.d ✗ (resolves to stacks/fuelrod/config/...)On first start (empty data volume) postgres runs config/init/pgsql/ in sorted order:
| Script | Purpose |
|---|---|
00-extensions.sql |
Enables uuid-ossp and pg_stat_statements on the primary DB |
01-databases.sh |
Creates each database in ADDITIONAL_DBS; enables uuid-ossp on each |
| Volume | Created by | Consumed by | Purpose |
|---|---|---|---|
uploads |
farm | — | Farm uploads |
fuelrod-uploads |
fuelrod | — | Fuelrod uploads |
Application services write logs to Docker stdout. Alloy discovers the selected Fuelrod, Fees, Fees Dev, and Akilimo containers through the Docker socket and forwards their logs to Loki. Netdata monitors host and Docker metrics independently.
# 1. Install Dokploy on the server (creates dokploy-network)
curl -sSL https://get.dokploy.com | sh
# 2. Copy and configure env files for each stack
for stack in databases cache activepieces automation monitoring fuelrod farm akilimo fees sonar metabase mail mqtt; do
cp stacks/$stack/.env.example stacks/$stack/.env
done
# Edit each .env — replace all placeholder values and domains
# 3. Deploy stacks in order (see Deployment Order below)# 1. External network required by the stacks
docker network create dokploy-network
# 2. Databases — must be first for PostgreSQL/MariaDB consumers
docker compose -f stacks/databases/docker-compose.yml up -d
# 3. Cache — separate Redis stack
docker compose -f stacks/cache/docker-compose.yml up -d
# 4. Automation / Activepieces
docker compose -f stacks/automation/docker-compose.yml up -d
docker compose -f stacks/activepieces/docker-compose.yml up -d
# 5. Monitoring, Beszel, Netdata, and applications
docker compose -f stacks/monitoring/docker-compose.yml up -d
docker compose -f stacks/beszel/docker-compose.yml up -d
docker compose -f stacks/netdata/docker-compose.yml up -d
docker compose -f stacks/fuelrod/docker-compose.yml up -d
docker compose -f stacks/farm/docker-compose.yml up -d
docker compose -f stacks/akilimo/docker-compose.yml up -d
docker compose -f stacks/fees-prod/docker-compose.yml up -d
docker compose -f stacks/fees-dev/docker-compose.yml up -dactivepieces must exist in PostgreSQL before starting the Activepieces stack. Adding it to ADDITIONAL_DBS only creates it when pgdata-main is empty; on an existing database volume, create it explicitly with the shared PostgreSQL owner.
Adminer, RedisInsight, and Dozzle are not exposed through Caddy. They bind only to 127.0.0.1 on the server and are accessed by forwarding a local port over SSH. This means no public URL or TLS certificate is needed.
# On the server — deploy only when needed
docker compose -f stacks/db-tools/docker-compose.yml up -d # Adminer + RedisInsight
docker compose -f stacks/dozzle/docker-compose.yml up -d # DozzleRun this on your local machine:
# Adminer (postgres / mariadb GUI) — opens at http://localhost:8080
ssh -L 8080:localhost:8080 user@your-server.munywele.co.ke
# RedisInsight — opens at http://localhost:5540
ssh -L 5540:localhost:5540 user@your-server.munywele.co.ke
# Dozzle (container log viewer) — opens at http://localhost:9999
ssh -L 9999:localhost:9999 user@your-server.munywele.co.ke
# All three at once (single SSH session)
ssh -L 8080:localhost:8080 \
-L 5540:localhost:5540 \
-L 9999:localhost:9999 \
user@your-server.munywele.co.keOpen your browser while the SSH session is active. The tunnel closes when you exit the session.
# On the server — never leave these running unattended
docker compose -f stacks/db-tools/docker-compose.yml down
docker compose -f stacks/dozzle/docker-compose.yml downIn ~/.ssh/config on your local machine:
Host munywele-tools
HostName your-server.munywele.co.ke
User your-user
LocalForward 8080 localhost:8080
LocalForward 5540 localhost:5540
LocalForward 9999 localhost:9999
Then just run ssh munywele-tools and all ports are forwarded automatically.
Use the stack-local guide when configuring or troubleshooting a specific stack:
- Fees production and Fees development — independent Fee Syncer deployments.
- Activepieces — app/worker split, database setup, secrets, and worker token.
- Netdata — host metrics, privileged mounts, and Caddy access.
- Monitoring — Grafana, Prometheus, Loki, Alloy, and log UI.
Each stack has its own .env (gitignored) sourced from .env.example. Stacks sharing postgres credentials must use matching values — copy from stacks/databases/.env.
| Stack | Key variables |
|---|---|
databases |
POSTGRES_USER/PASSWORD/DB, ADDITIONAL_DBS, MARIADB_* |
cache |
REDIS_PASSWORD, REDIS_DEV_PASSWORD |
automation |
POSTGRES_* (must match databases), n8n runtime settings |
activepieces |
AP_FRONTEND_URL, AP_ENCRYPTION_KEY, AP_JWT_SECRET, AP_WORKER_TOKEN, POSTGRES_*, optional REDIS_PASSWORD |
monitoring |
GRAFANA_*, LOKI_*, shared POSTGRES_* and REDIS_PASSWORD for exporters |
beszel |
BESZEL_*, host port 9625 |
netdata |
NETDATA_* |
fees-prod |
Production Fee Syncer settings |
fees-dev |
Development Fee Syncer settings |
fuelrod |
FUELROD_TAG, FUELROD_DOMAIN, PORTAL_DOMAIN, GATEWAY_DOMAIN |
farm |
FARM_TAG, POSTGRES_*, JWT_SECRET, DEFAULT_PASSWORD |
akilimo |
AKILIMO_TAG, USE_UPTAKE_TAG, AKILIMO_DOMAIN, MARIADB_* |
sonar |
SONAR_TAG, SONAR_DOMAIN, POSTGRES_* |
metabase |
METABASE_DOMAIN, POSTGRES_* |
mail |
MAILPIT_DOMAIN |
db-tools |
ADMINER_DEFAULT_SERVER, ADMINER_DESIGN |
dozzle |
DOZZLE_HOSTNAME |
The repository does not contain an active backup or migration toolchain. Use the database tooling appropriate for the deployment, and keep PostgreSQL/MariaDB backups outside Git. Never commit backup data or credentials.
Caddy is used as the host-level reverse proxy for WordPress-based stacks (Akilimo, and others as added). Each stack that uses Caddy keeps its own Caddyfile inside the stack directory (e.g. stacks/akilimo/Caddyfile). Copy the relevant blocks into the host's global Caddyfile.
Validate config before applying (dry run):
caddy validate --config /etc/caddy/CaddyfileFormat / auto-indent the Caddyfile in place:
caddy fmt --overwrite /etc/caddy/CaddyfileRestart Caddy after validation:
sudo systemctl restart caddyStop / start:
sudo systemctl stop caddy
sudo systemctl start caddyEnable Caddy to start on boot:
sudo systemctl enable caddyCheck service status and tail logs:
sudo systemctl status caddy
sudo journalctl -u caddy -fInspect the adapted (parsed) config:
caddy adapt --config /etc/caddy/Caddyfile --prettyView Caddy version:
caddy versionRun Caddy in the foreground (useful for debugging):
sudo caddy run --config /etc/caddy/CaddyfileCreate the log directory if missing (fixes log writer errors on first run):
sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/log/caddyDirectories are owned by akilimo:akilimo. The www-data user (PHP-FPM inside the container) is added to the akilimo group and gets write access via group permissions. The setgid bit (s) ensures files created by www-data inherit the akilimo group so the host user retains full control.
Run once on the host:
# Grant www-data group membership
sudo usermod -aG akilimo www-data# Set ownership and permissions (drwxrwsr-x = 2775)
sudo chown -R akilimo:akilimo /data/extra_storage/services/akilimo
sudo chown -R akilimo:akilimo /data/extra_storage/services/portal
sudo chown -R akilimo:akilimo /data/extra_storage/services/new_akilimo
sudo chown -R akilimo:akilimo /data/extra_storage/services/agwise_site
sudo chmod -R 2775 /data/extra_storage/services/akilimo
sudo chmod -R 2775 /data/extra_storage/services/portal
sudo chmod -R 2775 /data/extra_storage/services/new_akilimoThe wordpress:php8.4-fpm container runs as www-data (uid 33). Because the WordPress directories are bind-mounted from the host, all files must be owned by uid 33 on the host — group membership tricks do not cross the container boundary.
Fix wp-content/upgrade not writable:
sudo mkdir -p /data/extra_storage/services/akilimo/wp-content/upgrade
sudo mkdir -p /data/extra_storage/services/portal/wp-content/upgrade
sudo mkdir -p /data/extra_storage/services/new_akilimo/wp-content/upgrade
sudo chown 33:33 /data/extra_storage/services/akilimo/wp-content/upgrade
sudo chown 33:33 /data/extra_storage/services/portal/wp-content/upgrade
sudo chown 33:33 /data/extra_storage/services/new_akilimo/wp-content/upgradeFix core WordPress files not writable (full reset):
# akilimo-site
sudo chown -R 33:33 /data/extra_storage/services/akilimo
sudo find /data/extra_storage/services/akilimo -type d -exec chmod 755 {} \;
sudo find /data/extra_storage/services/akilimo -type f -exec chmod 644 {} \;
# akilimo-portal
sudo chown -R 33:33 /data/extra_storage/services/portal
sudo find /data/extra_storage/services/portal -type d -exec chmod 755 {} \;
sudo find /data/extra_storage/services/portal -type f -exec chmod 644 {} \;
# new-akilimo
sudo chown -R 33:33 /data/extra_storage/services/new_akilimo
sudo find /data/extra_storage/services/new_akilimo -type d -exec chmod 755 {} \;
sudo find /data/extra_storage/services/new_akilimo -type f -exec chmod 644 {} \;
# agwise
sudo chown -R 33:33 /data/extra_storage/services/agwise
sudo find /data/extra_storage/services/agwise -type d -exec chmod 755 {} \;
sudo find /data/extra_storage/services/agwise -type f -exec chmod 644 {} \;
Note:
755on directories and644on files is the standard WordPress permission pattern. After running this, WordPress auto-updates, plugin installs, and theme uploads will work correctly.
Each stack keeps its own Caddyfile. Copy the relevant blocks into the host's global Caddyfile.
| Stack | Service | Caddyfile | Port |
|---|---|---|---|
| akilimo | API | stacks/akilimo/Caddyfile |
90xx (PHP-FPM), 91xx (API) |
| fuelrod | API | stacks/fuelrod/Caddyfile |
92xx |
| farm | API | stacks/farm/Caddyfile |
93xx |
| fees-prod | Fee Syncer production | stacks/fees-prod/Caddyfile |
9400 |
| fees-dev | Fee Syncer development | stacks/fees-dev/Caddyfile |
9401 |
| use-uptake | Web | stacks/use-uptake/Caddyfile |
95xx |
| monitoring | Grafana | stacks/monitoring/Caddyfile |
9600 |
| netdata | Host metrics | stacks/netdata/Caddyfile |
19999 |
| automation | n8n | stacks/automation/Caddyfile |
9700 |
| activepieces | Activepieces app | stacks/activepieces/Caddyfile |
9710 |
- Commits to
maintrigger automatic SemVer tagging viamasgeek/github-tag-action - Commit message prefixes drive version bumps:
fix:→ patch,feat:→ minor,BREAKING CHANGE:→ major - Renovate Bot manages Docker image tag updates
- PRs from non-owner actors are auto-approved by the
pr-automationworkflow