Docker Compose orchestration layer for domain-based routing across multiple independent application stacks on a shared host under munywele.co.ke. Reverse proxying and TLS termination are handled by Dokploy + Traefik.
proxy-tool/
├── stacks/ ← one folder per stack, each self-contained
│ ├── databases/ ← postgres 17, pgbouncer, mariadb, redis [deploy first]
│ ├── automation/ ← n8n
│ ├── monitoring/ ← Grafana, Prometheus, Loki, Grafana Alloy
│ ├── fuelrod/ ← Fuelrod service, SMS portal, SMS gateway
│ ├── farm/ ← Farm Manager API, web, migrations
│ ├── akilimo/ ← Akilimo API, use-uptake
│ ├── fees/ ← Fee-syncer (prod + dev)
│ ├── 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 dashboards/datasources, Prometheus, Loki, Agent
│ └── 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 — Traefik routes here | Dokploy (created on install) |
internal |
Private — intra-stack only | Docker Compose (per stack) |
Create dokploy-network manually when running without Dokploy:
docker network create dokploy-networkAll public traffic flows through Traefik (managed by Dokploy). Each service declares its routing rules and TLS config via Docker labels:
labels:
- "traefik.enable=true"
- "traefik.http.routers.myservice.rule=Host(`myservice.munywele.co.ke`)"
- "traefik.http.routers.myservice.entrypoints=websecure"
- "traefik.http.routers.myservice.tls=true"
- "traefik.http.routers.myservice.tls.certresolver=letsencrypt"
- "traefik.http.services.myservice.loadbalancer.server.port=80"TLS certificates are issued automatically by Let's Encrypt.
Bind 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 |
fuelrod | farm | User file uploads |
Application services write logs to Docker stdout. Grafana Alloy discovers the Fuelrod, Fees, Fees Dev, and Akilimo containers through the Docker socket and forwards their logs to Loki.
# 1. Install Dokploy on the server (creates dokploy-network + Traefik)
curl -sSL https://get.dokploy.com | sh
# 2. Copy and configure env files for each stack
for stack in databases automation monitoring fuelrod farm akilimo fees sonar metabase mail mqtt; do
cp stacks/$stack/.env.example stacks/$stack/.env
done
cp .backup-example .backup
# Edit each .env — replace all placeholder values and domains
# 3. Deploy stacks in order (see Deployment Order below)# 1. Databases — must be first (provides postgres, pgbouncer, mariadb, redis)
docker compose -f stacks/databases/docker-compose.yml up -d
# 2. Automation — requires databases
docker compose -f stacks/automation/docker-compose.yml up -d
# 3. Monitoring
docker compose -f stacks/monitoring/docker-compose.yml up -d
# 4. Fuelrod — requires databases; creates the shared 'uploads' volume
docker compose -f stacks/fuelrod/docker-compose.yml up -d
# 5. Farm — requires databases + fuelrod (uses 'uploads' volume)
docker compose -f stacks/farm/docker-compose.yml up -d
# 6. Akilimo — requires databases (MariaDB)
docker compose -f stacks/akilimo/docker-compose.yml up -d
# 7. Fees — requires databases
docker compose -f stacks/fees/docker-compose.yml up -d
# Optional tooling — deploy independently as needed
docker compose -f stacks/sonar/docker-compose.yml up -d
docker compose -f stacks/metabase/docker-compose.yml up -d
docker compose -f stacks/mail/docker-compose.yml up -d
docker compose -f stacks/mqtt/docker-compose.yml up -dAdminer, RedisInsight, and Dozzle are not exposed through Traefik. 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, no TLS cert needed, and no risk of accidental exposure.
# 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.
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_*, REDIS_PASSWORD |
automation |
POSTGRES_* (must match databases), N8N_DOMAIN |
monitoring |
GRAFANA_ADMIN_PASSWORD, GRAFANA_DOMAIN |
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_* |
fees |
SYNCER_TAG, FEES_PROD_DOMAIN, FEES_DEV_DOMAIN |
sonar |
SONAR_TAG, SONAR_DOMAIN, POSTGRES_* |
metabase |
METABASE_DOMAIN, POSTGRES_* |
mail |
MAILPIT_DOMAIN |
db-tools |
ADMINER_DEFAULT_SERVER, ADMINER_DESIGN |
dozzle |
DOZZLE_HOSTNAME |
# Full automated backup (n8n → postgres → mariadb → Google Drive sync)
./autobackup.sh
# PostgreSQL — all databases, compressed, keep 7 days
cd fuelrod-backup && poetry run fuelrod-backup backup --db-type postgres --compress --keep-days 7
# PostgreSQL — specific databases and schemas
cd fuelrod-backup && poetry run fuelrod-backup backup --db-type postgres --db mydb --schemas public,audit --compress
# PostgreSQL restore
cd fuelrod-backup && poetry run fuelrod-backup restore --db-type postgres
# MariaDB backup / restore
cd fuelrod-backup && poetry run fuelrod-backup backup --db-type mariadb
cd fuelrod-backup && poetry run fuelrod-backup restore --db-type mariadb
# Google Drive sync only (dry run first)
./gbk.sh --dry-run && ./gbk.sh./migration/batch-exporter.sh # Export MySQL tables to CSV
./migration/execute-loads.sh # Load CSVs into PostgreSQL via pgloader
./migration/import_csv_to_pg.sh # Direct CSV importCaddy 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/CaddyfileReload config without downtime (no restart needed):
caddy reload --config /etc/caddy/CaddyfileRestart the Caddy service (when reload is not enough):
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 | Caddyfile | Port range |
|---|---|---|
| akilimo | stacks/akilimo/Caddyfile |
90xx (PHP-FPM), 91xx (API) |
| fuelrod | stacks/fuelrod/Caddyfile |
92xx |
| farm | stacks/farm/Caddyfile |
93xx |
| fees | stacks/fees/Caddyfile |
94xx |
| use-uptake | stacks/use-uptake/Caddyfile |
95xx |
| monitoring | stacks/monitoring/Caddyfile |
96xx |
| automation | stacks/automation/Caddyfile |
97xx |
- 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