Add production ERPNext Coolify stack with CI gates
Single compose file for Coolify: MariaDB, Redis, idempotent site creation, migrations on redeploy, SERVICE_URL_FRONTEND_8080 routing, and Forgejo Actions readiness validation vendored from production-ci-readiness skill.
This commit is contained in:
commit
0c8d593d40
9 changed files with 717 additions and 0 deletions
39
.github/workflows/production-readiness.yml
vendored
Normal file
39
.github/workflows/production-readiness.yml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
name: Production Readiness
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: production-erpnext-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
readiness:
|
||||||
|
name: Coolify compose readiness
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Production readiness scan
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/ci/*.sh
|
||||||
|
bash scripts/ci/ci-readiness.sh .
|
||||||
|
|
||||||
|
- name: Validate docker-compose (Coolify rules)
|
||||||
|
run: bash scripts/ci/validate-docker-compose.sh .
|
||||||
|
|
||||||
|
compose-smoke:
|
||||||
|
name: docker compose config
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: readiness
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Compose config (strip Coolify-only keys)
|
||||||
|
run: |
|
||||||
|
sed '/exclude_from_hc:/d' docker-compose.yml > docker-compose.validate.yml
|
||||||
|
docker compose -f docker-compose.validate.yml config -q
|
||||||
|
echo "compose config OK"
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
*.local
|
||||||
|
.DS_Store
|
||||||
11
Makefile
Normal file
11
Makefile
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
.PHONY: ci validate compose-config
|
||||||
|
|
||||||
|
ci:
|
||||||
|
bash scripts/ci/ci-readiness.sh .
|
||||||
|
bash scripts/ci/validate-docker-compose.sh .
|
||||||
|
|
||||||
|
validate:
|
||||||
|
bash scripts/ci/validate-docker-compose.sh .
|
||||||
|
|
||||||
|
compose-config:
|
||||||
|
sed '/exclude_from_hc:/d' docker-compose.yml | docker compose -f - config
|
||||||
58
README.md
Normal file
58
README.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Production ERPNext on Coolify
|
||||||
|
|
||||||
|
Validated Docker Compose stack for [ERPNext](https://erpnext.com) on [Coolify](https://coolify.io), derived from [frappe/frappe_docker](https://github.com/frappe/frappe_docker).
|
||||||
|
|
||||||
|
**Repository:** https://git.aexoradao.com/epistemophiliac/production-erpnext
|
||||||
|
|
||||||
|
## Quick start (Coolify)
|
||||||
|
|
||||||
|
1. **New Resource** → **Docker Compose**
|
||||||
|
2. **Git repository:** `https://git.aexoradao.com/epistemophiliac/production-erpnext`
|
||||||
|
3. **Compose file:** `docker-compose.yml`
|
||||||
|
4. Set environment variables from [`example.env`](example.env) (at minimum `DB_PASSWORD`, `SITE_NAME`, `ADMIN_PASSWORD`)
|
||||||
|
5. Assign your domain to service **`frontend`**, port **`8080`**
|
||||||
|
6. Deploy — first boot creates the site and installs ERPNext (~5–15 minutes)
|
||||||
|
|
||||||
|
Login: user `Administrator`, password = `ADMIN_PASSWORD`.
|
||||||
|
|
||||||
|
## What this stack includes
|
||||||
|
|
||||||
|
| Service | Role |
|
||||||
|
|---------|------|
|
||||||
|
| `db` | MariaDB 11.8 |
|
||||||
|
| `redis-cache` / `redis-queue` | Cache and job queue |
|
||||||
|
| `configurator` | One-shot bench config |
|
||||||
|
| `create-site` | Idempotent site + ERPNext install |
|
||||||
|
| `migrator` | `bench migrate` on redeploy |
|
||||||
|
| `backend` | Gunicorn API |
|
||||||
|
| `frontend` | Nginx (port **8080**) |
|
||||||
|
| `websocket` | Socket.IO realtime |
|
||||||
|
| `queue-short` / `queue-long` / `scheduler` | Background workers |
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Forgejo Actions runs on every push/PR to `main`:
|
||||||
|
|
||||||
|
- `scripts/ci/ci-readiness.sh` — secrets, docs, compose checks
|
||||||
|
- `scripts/ci/validate-docker-compose.sh` — Coolify compose rules + `docker compose config`
|
||||||
|
|
||||||
|
Run locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make ci
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Coolify server with **4 GB+ RAM** (8 GB recommended)
|
||||||
|
- Domain DNS pointing to your Coolify proxy
|
||||||
|
- `SITE_NAME` and `FRAPPE_SITE_NAME_HEADER` must match the Coolify domain
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Coolify deploy guide](docs/COOLIFY_DEPLOY.md)
|
||||||
|
- [Upstream frappe_docker docs](https://frappe.github.io/frappe_docker/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Compose and docs: MIT. ERPNext/Frappe images: see upstream licenses.
|
||||||
236
docker-compose.yml
Normal file
236
docker-compose.yml
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
# ERPNext production stack for Coolify.
|
||||||
|
# Based on https://github.com/frappe/frappe_docker (compose.yaml + mariadb + redis).
|
||||||
|
# Coolify: assign your domain to service `frontend` on port 8080.
|
||||||
|
# No ports: — routing uses SERVICE_URL_FRONTEND_8080.
|
||||||
|
|
||||||
|
x-customizable-image: &customizable_image
|
||||||
|
image: ${CUSTOM_IMAGE:-frappe/erpnext}:${CUSTOM_TAG:-${ERPNEXT_VERSION:-v16.22.0}}
|
||||||
|
pull_policy: ${PULL_POLICY:-always}
|
||||||
|
restart: ${RESTART_POLICY:-unless-stopped}
|
||||||
|
|
||||||
|
x-depends-on-configurator: &depends_on_configurator
|
||||||
|
depends_on:
|
||||||
|
configurator:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
x-backend-defaults: &backend_defaults
|
||||||
|
<<: [*depends_on_configurator, *customizable_image]
|
||||||
|
platform: linux/amd64
|
||||||
|
volumes:
|
||||||
|
- sites:/home/frappe/frappe-bench/sites
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb:11.8
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --character-set-server=utf8mb4
|
||||||
|
- --collation-server=utf8mb4_unicode_ci
|
||||||
|
- --skip-character-set-client-handshake
|
||||||
|
- --skip-innodb-read-only-compressed
|
||||||
|
environment:
|
||||||
|
- 'MYSQL_ROOT_PASSWORD=${DB_PASSWORD:-changeme}'
|
||||||
|
- 'MARIADB_AUTO_UPGRADE=1'
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
|
||||||
|
start_period: 10s
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/mysql
|
||||||
|
|
||||||
|
redis-cache:
|
||||||
|
image: redis:8.6-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis-queue:
|
||||||
|
image: redis:8.6-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- redis-queue-data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
configurator:
|
||||||
|
<<: *backend_defaults
|
||||||
|
exclude_from_hc: true
|
||||||
|
restart: 'no'
|
||||||
|
entrypoint: ['bash', '-c']
|
||||||
|
command:
|
||||||
|
- >
|
||||||
|
ls -1 apps > sites/apps.txt;
|
||||||
|
bench set-config -g db_host $$DB_HOST;
|
||||||
|
bench set-config -gp db_port $$DB_PORT;
|
||||||
|
bench set-config -g redis_cache "redis://$$REDIS_CACHE";
|
||||||
|
bench set-config -g redis_queue "redis://$$REDIS_QUEUE";
|
||||||
|
bench set-config -g redis_socketio "redis://$$REDIS_QUEUE";
|
||||||
|
bench set-config -gp socketio_port $$SOCKETIO_PORT;
|
||||||
|
bench set-config -g chromium_path /usr/bin/chromium-headless-shell;
|
||||||
|
environment:
|
||||||
|
- 'DB_HOST=db'
|
||||||
|
- 'DB_PORT=3306'
|
||||||
|
- 'REDIS_CACHE=redis-cache:6379'
|
||||||
|
- 'REDIS_QUEUE=redis-queue:6379'
|
||||||
|
- 'SOCKETIO_PORT=9000'
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-cache:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-queue:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
create-site:
|
||||||
|
<<: *customizable_image
|
||||||
|
exclude_from_hc: true
|
||||||
|
restart: 'no'
|
||||||
|
platform: linux/amd64
|
||||||
|
entrypoint: ['bash', '-c']
|
||||||
|
command:
|
||||||
|
- >
|
||||||
|
B=/usr/local/bin/bench;
|
||||||
|
SITE=$$SITE_NAME;
|
||||||
|
wait-for-it -t 120 db:3306;
|
||||||
|
wait-for-it -t 120 redis-cache:6379;
|
||||||
|
wait-for-it -t 120 redis-queue:6379;
|
||||||
|
if [ -d "sites/$$SITE" ]; then echo "[create-site] exists"; $$B use "$$SITE"; else echo "[create-site] creating"; $$B new-site "$$SITE" --mariadb-user-host-login-scope='%' --admin-password "$$ADMIN_PASSWORD" --db-root-password "$$DB_PASSWORD" --install-app erpnext --set-default; fi
|
||||||
|
environment:
|
||||||
|
- 'SITE_NAME=${SITE_NAME:-erp.example.com}'
|
||||||
|
- 'ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme}'
|
||||||
|
- 'DB_PASSWORD=${DB_PASSWORD:-changeme}'
|
||||||
|
volumes:
|
||||||
|
- sites:/home/frappe/frappe-bench/sites
|
||||||
|
depends_on:
|
||||||
|
configurator:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
migrator:
|
||||||
|
<<: *backend_defaults
|
||||||
|
exclude_from_hc: true
|
||||||
|
restart: 'no'
|
||||||
|
entrypoint: ['bash', '-c']
|
||||||
|
command:
|
||||||
|
- >
|
||||||
|
if [ "$$MIGRATE_SITES" != "true" ]; then echo "[migrator] disabled"; exit 0; fi;
|
||||||
|
if [ -z "$$(find sites -mindepth 2 -maxdepth 2 -name site_config.json 2>/dev/null)" ]; then echo "[migrator] no sites"; exit 0; fi;
|
||||||
|
echo "[migrator] migrating all sites";
|
||||||
|
bench --site all migrate;
|
||||||
|
environment:
|
||||||
|
- 'MIGRATE_SITES=${MIGRATE_SITES:-true}'
|
||||||
|
depends_on:
|
||||||
|
create-site:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
backend:
|
||||||
|
<<: *backend_defaults
|
||||||
|
environment:
|
||||||
|
- 'GUNICORN_THREADS=${GUNICORN_THREADS:-4}'
|
||||||
|
- 'GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}'
|
||||||
|
- 'GUNICORN_TIMEOUT=${GUNICORN_TIMEOUT:-120}'
|
||||||
|
depends_on:
|
||||||
|
configurator:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
create-site:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
migrator:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'curl -sf http://localhost:8000/api/method/ping || exit 1']
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
start_period: 120s
|
||||||
|
|
||||||
|
websocket:
|
||||||
|
<<: [*depends_on_configurator, *customizable_image]
|
||||||
|
platform: linux/amd64
|
||||||
|
command:
|
||||||
|
- node
|
||||||
|
- /home/frappe/frappe-bench/apps/frappe/socketio.js
|
||||||
|
volumes:
|
||||||
|
- sites:/home/frappe/frappe-bench/sites
|
||||||
|
depends_on:
|
||||||
|
create-site:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
<<: *customizable_image
|
||||||
|
platform: linux/amd64
|
||||||
|
command:
|
||||||
|
- nginx-entrypoint.sh
|
||||||
|
environment:
|
||||||
|
- SERVICE_URL_FRONTEND_8080
|
||||||
|
- 'BACKEND=backend:8000'
|
||||||
|
- 'SOCKETIO=websocket:9000'
|
||||||
|
- 'FRAPPE_SITE_NAME_HEADER=${FRAPPE_SITE_NAME_HEADER:-${SITE_NAME:-erp.example.com}}'
|
||||||
|
- 'UPSTREAM_REAL_IP_ADDRESS=${UPSTREAM_REAL_IP_ADDRESS:-127.0.0.1}'
|
||||||
|
- 'UPSTREAM_REAL_IP_HEADER=${UPSTREAM_REAL_IP_HEADER:-X-Forwarded-For}'
|
||||||
|
- 'UPSTREAM_REAL_IP_RECURSIVE=${UPSTREAM_REAL_IP_RECURSIVE:-off}'
|
||||||
|
- 'PROXY_READ_TIMEOUT=${PROXY_READ_TIMEOUT:-120}'
|
||||||
|
- 'CLIENT_MAX_BODY_SIZE=${CLIENT_MAX_BODY_SIZE:-50m}'
|
||||||
|
volumes:
|
||||||
|
- sites:/home/frappe/frappe-bench/sites
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
websocket:
|
||||||
|
condition: service_started
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'curl -sf http://localhost:8080/ || exit 1']
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 15
|
||||||
|
start_period: 90s
|
||||||
|
|
||||||
|
queue-short:
|
||||||
|
<<: *backend_defaults
|
||||||
|
command:
|
||||||
|
- bench
|
||||||
|
- worker
|
||||||
|
- --queue
|
||||||
|
- short,default
|
||||||
|
depends_on:
|
||||||
|
create-site:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
migrator:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
queue-long:
|
||||||
|
<<: *backend_defaults
|
||||||
|
command:
|
||||||
|
- bench
|
||||||
|
- worker
|
||||||
|
- --queue
|
||||||
|
- long,default,short
|
||||||
|
depends_on:
|
||||||
|
create-site:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
migrator:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
<<: *backend_defaults
|
||||||
|
command:
|
||||||
|
- bench
|
||||||
|
- schedule
|
||||||
|
depends_on:
|
||||||
|
create-site:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
migrator:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sites:
|
||||||
|
db-data:
|
||||||
|
redis-queue-data:
|
||||||
102
docs/COOLIFY_DEPLOY.md
Normal file
102
docs/COOLIFY_DEPLOY.md
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Coolify deployment — Production ERPNext
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Coolify v4+ with Docker Compose support
|
||||||
|
- Server: **minimum 4 GB RAM**, **8 GB+** for production workloads
|
||||||
|
- Public domain (e.g. `erp.yourdomain.com`)
|
||||||
|
|
||||||
|
## 1. Create the Coolify service
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|--------|
|
||||||
|
| Type | Docker Compose |
|
||||||
|
| Repository | `https://git.aexoradao.com/epistemophiliac/production-erpnext` |
|
||||||
|
| Branch | `main` |
|
||||||
|
| Compose file | `docker-compose.yml` |
|
||||||
|
|
||||||
|
## 2. Environment variables
|
||||||
|
|
||||||
|
Set these in **Coolify → Service → Environment Variables** before first deploy:
|
||||||
|
|
||||||
|
| Variable | Required | Example | Notes |
|
||||||
|
|----------|----------|---------|-------|
|
||||||
|
| `ERPNEXT_VERSION` | yes | `v16.22.0` | Pin image tag |
|
||||||
|
| `DB_PASSWORD` | yes | strong secret | MariaDB root |
|
||||||
|
| `SITE_NAME` | yes | `erp.yourdomain.com` | Must match domain |
|
||||||
|
| `ADMIN_PASSWORD` | yes | strong secret | Frappe login |
|
||||||
|
| `FRAPPE_SITE_NAME_HEADER` | yes | same as `SITE_NAME` | Single-site routing |
|
||||||
|
| `MIGRATE_SITES` | no | `true` | Run migrate on redeploy |
|
||||||
|
|
||||||
|
> **Coolify env cache:** Changing defaults in `docker-compose.yml` does **not** update values already stored in Coolify. Edit them in the UI after changes.
|
||||||
|
|
||||||
|
## 3. Domain routing
|
||||||
|
|
||||||
|
1. Open the deployed service in Coolify
|
||||||
|
2. Add domain: `erp.yourdomain.com`
|
||||||
|
3. Attach domain to service **`frontend`**
|
||||||
|
4. Internal port: **`8080`** (Frappe nginx — not 80)
|
||||||
|
|
||||||
|
The compose file sets `SERVICE_URL_FRONTEND_8080` so Coolify routes HTTPS to nginx correctly.
|
||||||
|
|
||||||
|
## 4. First deploy timeline
|
||||||
|
|
||||||
|
```text
|
||||||
|
db (healthy) → redis → configurator (exit 0)
|
||||||
|
→ create-site (new-site + install-app erpnext, ~5–15 min)
|
||||||
|
→ migrator → backend / workers / frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Watch logs:
|
||||||
|
|
||||||
|
- `create-site` — site creation progress
|
||||||
|
- `backend` — gunicorn ready
|
||||||
|
- `frontend` — nginx on 8080
|
||||||
|
|
||||||
|
## 5. Post-deploy verification
|
||||||
|
|
||||||
|
From Coolify terminal on `frontend`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sI http://localhost:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
From your machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sI https://erp.yourdomain.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
Login at `https://erp.yourdomain.com` — user `Administrator`.
|
||||||
|
|
||||||
|
## 6. Upgrades
|
||||||
|
|
||||||
|
1. Bump `ERPNEXT_VERSION` in Coolify env vars
|
||||||
|
2. Redeploy — `migrator` runs `bench --site all migrate`
|
||||||
|
3. Confirm `migrator` logs show success
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Fix |
|
||||||
|
|---------|-----|
|
||||||
|
| Coolify 404 | Domain on wrong service — must be `frontend:8080` |
|
||||||
|
| Site not found | `SITE_NAME` ≠ domain; fix `FRAPPE_SITE_NAME_HEADER` in UI |
|
||||||
|
| Stack unhealthy | Healthcheck port must be **8080** on frontend |
|
||||||
|
| create-site fails on redeploy | Should be idempotent — check `sites/$SITE_NAME` exists |
|
||||||
|
| Env change ignored | Update variable in Coolify UI, not only in git |
|
||||||
|
|
||||||
|
## What we intentionally omit
|
||||||
|
|
||||||
|
- No Traefik / nginx-proxy / Let's Encrypt in compose — Coolify handles TLS
|
||||||
|
- No `ports:` — Coolify proxy only
|
||||||
|
- No `pwd.yml` demo stack
|
||||||
|
|
||||||
|
## CI / production gate
|
||||||
|
|
||||||
|
Every merge to `main` must pass `.github/workflows/production-readiness.yml` before Coolify auto-deploy (if wired).
|
||||||
|
|
||||||
|
Local check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make ci
|
||||||
|
```
|
||||||
32
example.env
Normal file
32
example.env
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Copy to Coolify Environment Variables (Service > Environment).
|
||||||
|
# Upstream reference: https://github.com/frappe/frappe_docker/blob/main/docs/02-setup/04-env-variables.md
|
||||||
|
|
||||||
|
# Image tag — pin for reproducible deploys
|
||||||
|
ERPNEXT_VERSION=v16.22.0
|
||||||
|
|
||||||
|
# MariaDB root password (required — change before production)
|
||||||
|
DB_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Frappe site name — MUST match your Coolify domain
|
||||||
|
SITE_NAME=erp.example.com
|
||||||
|
|
||||||
|
# Frappe Administrator password
|
||||||
|
ADMIN_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Nginx site header — for single-site Coolify, same as SITE_NAME
|
||||||
|
FRAPPE_SITE_NAME_HEADER=erp.example.com
|
||||||
|
|
||||||
|
# Run bench migrate on every deploy (set false to skip)
|
||||||
|
MIGRATE_SITES=true
|
||||||
|
|
||||||
|
# Gunicorn tuning (optional)
|
||||||
|
GUNICORN_THREADS=4
|
||||||
|
GUNICORN_WORKERS=2
|
||||||
|
GUNICORN_TIMEOUT=120
|
||||||
|
|
||||||
|
# Proxy / upload limits (optional)
|
||||||
|
PROXY_READ_TIMEOUT=120
|
||||||
|
CLIENT_MAX_BODY_SIZE=50m
|
||||||
|
UPSTREAM_REAL_IP_ADDRESS=127.0.0.1
|
||||||
|
UPSTREAM_REAL_IP_HEADER=X-Forwarded-For
|
||||||
|
UPSTREAM_REAL_IP_RECURSIVE=off
|
||||||
127
scripts/ci/ci-readiness.sh
Executable file
127
scripts/ci/ci-readiness.sh
Executable file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Production readiness scanner — run from repo root or pass path as $1
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="${1:-.}"
|
||||||
|
REPO="$(cd "$REPO" && pwd)"
|
||||||
|
STRICT="${STRICT:-0}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
ERR=0
|
||||||
|
WARN=0
|
||||||
|
INFO=0
|
||||||
|
|
||||||
|
err() { echo "ERROR $*"; ERR=$((ERR + 1)); }
|
||||||
|
warn() { echo "WARN $*"; WARN=$((WARN + 1)); }
|
||||||
|
info() { echo "INFO $*"; }
|
||||||
|
pass() { echo "OK $*"; }
|
||||||
|
|
||||||
|
echo "=== Production readiness: $REPO ==="
|
||||||
|
echo "strict=$STRICT"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- Secrets in tracked files ---
|
||||||
|
if command -v git >/dev/null 2>&1 && git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
if git -C "$REPO" ls-files | grep -qE '^\.env$|credentials\.json|\.pem$'; then
|
||||||
|
err "[SEC-01] Tracked sensitive files (.env, credentials, pem)"
|
||||||
|
else
|
||||||
|
pass "[SEC-01] No obvious secret filenames tracked"
|
||||||
|
fi
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -f "$REPO/$f" ] || continue
|
||||||
|
if grep -qE 'BEGIN (RSA |EC )?PRIVATE KEY|api[_-]?key\s*=\s*["\x27][a-zA-Z0-9]{20,}' "$REPO/$f" 2>/dev/null; then
|
||||||
|
err "[SEC-01] Possible secret in tracked file: $f"
|
||||||
|
fi
|
||||||
|
done < <(git -C "$REPO" ls-files '*.yml' '*.yaml' '*.json' '*.md' '*.sh' '*.js' '*.go' '*.env*' 2>/dev/null | head -500)
|
||||||
|
else
|
||||||
|
warn "[SEC-01] Not a git repo — skip secret scan"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- node_modules in git ---
|
||||||
|
if command -v git >/dev/null 2>&1 && git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
NM_COUNT=$(git -C "$REPO" ls-files '**/node_modules/**' 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
if [ "${NM_COUNT:-0}" -gt 0 ]; then
|
||||||
|
err "[DC-06] node_modules tracked ($NM_COUNT files) — git rm -r --cached and .gitignore"
|
||||||
|
else
|
||||||
|
pass "[DC-06] node_modules not tracked"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Node Dockerfiles: lockfile + npm ci ---
|
||||||
|
while IFS= read -r df; do
|
||||||
|
[ -f "$df" ] || continue
|
||||||
|
dir="$(dirname "$df")"
|
||||||
|
if grep -qE 'npm install' "$df" && ! grep -qE 'npm ci' "$df"; then
|
||||||
|
warn "[DC-07] $df uses npm install without npm ci"
|
||||||
|
fi
|
||||||
|
if grep -qE 'npm ci|npm install' "$df"; then
|
||||||
|
if [ ! -f "$dir/package-lock.json" ] && [ ! -f "$dir/yarn.lock" ] && [ ! -f "$dir/pnpm-lock.yaml" ]; then
|
||||||
|
warn "[DC-07] $df has no lockfile in $dir"
|
||||||
|
else
|
||||||
|
pass "[DC-07] $df has lockfile"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(find "$REPO" -name Dockerfile \
|
||||||
|
-not -path '*/node_modules/*' \
|
||||||
|
-not -path '*/fabric-samples-upstream/*' \
|
||||||
|
-not -path '*/.git/*' \
|
||||||
|
2>/dev/null)
|
||||||
|
|
||||||
|
# --- Gateway patterns (if present) ---
|
||||||
|
GW="$REPO/gateway/server.js"
|
||||||
|
if [ -f "$GW" ]; then
|
||||||
|
if grep -q 'withChainLock' "$GW" && grep -q 'evaluateChaincodeRaw' "$GW"; then
|
||||||
|
if grep -A2 'evaluateChaincodeRaw' "$GW" | grep -q 'withChainLock'; then
|
||||||
|
warn "[GW-02] gateway evaluates may be serialized behind withChainLock"
|
||||||
|
else
|
||||||
|
pass "[GW-02] evaluates not behind global chain lock"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if grep -qE "app\.get\(['\"]/health" "$GW"; then
|
||||||
|
pass "[GW-01] /health route present"
|
||||||
|
else
|
||||||
|
warn "[GW-01] no /health in gateway/server.js"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Deploy docs ---
|
||||||
|
for doc in README.md docs/COOLIFY_DEPLOY.md docs/GATEWAY.md AGENTS.md; do
|
||||||
|
if [ -f "$REPO/$doc" ]; then
|
||||||
|
pass "[HY-01] found $doc"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Compose (delegate) ---
|
||||||
|
if [ -f "$REPO/docker-compose.yml" ] || [ -f "$REPO/docker-compose.yaml" ]; then
|
||||||
|
echo
|
||||||
|
echo "--- docker-compose validation ---"
|
||||||
|
COMPOSE_ERR=0
|
||||||
|
bash "$SCRIPT_DIR/validate-docker-compose.sh" "$REPO" || COMPOSE_ERR=$?
|
||||||
|
if [ "$COMPOSE_ERR" -ne 0 ]; then
|
||||||
|
ERR=$((ERR + COMPOSE_ERR))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Go tests hint ---
|
||||||
|
if [ -f "$REPO/Makefile" ] && grep -q 'test' "$REPO/Makefile"; then
|
||||||
|
info "[HY-03] Makefile has test target — run in CI"
|
||||||
|
fi
|
||||||
|
if [ -d "$REPO/chaincode" ] || find "$REPO" -name go.mod -print -quit 2>/dev/null | grep -q .; then
|
||||||
|
info "Consider: cd chaincode && go test ./..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Summary ==="
|
||||||
|
echo "errors=$ERR warnings=$WARN"
|
||||||
|
if [ "$ERR" -gt 0 ]; then
|
||||||
|
echo "VERDICT: FAIL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$STRICT" = "1" ] && [ "$WARN" -gt 0 ]; then
|
||||||
|
echo "VERDICT: FAIL (strict mode, warnings treated as errors)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "VERDICT: PASS"
|
||||||
|
exit 0
|
||||||
109
scripts/ci/validate-docker-compose.sh
Executable file
109
scripts/ci/validate-docker-compose.sh
Executable file
|
|
@ -0,0 +1,109 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Validate docker-compose for production/Coolify deploy safety
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="${1:-.}"
|
||||||
|
REPO="$(cd "$REPO" && pwd)"
|
||||||
|
COOLIFY_STRICT="${COOLIFY_STRICT:-0}"
|
||||||
|
|
||||||
|
ERR=0
|
||||||
|
WARN=0
|
||||||
|
|
||||||
|
err() { echo "ERROR $*"; ERR=$((ERR + 1)); }
|
||||||
|
warn() { echo "WARN $*"; WARN=$((WARN + 1)); }
|
||||||
|
info() { echo "INFO $*"; }
|
||||||
|
pass() { echo "OK $*"; }
|
||||||
|
|
||||||
|
COMPOSE_FILE=""
|
||||||
|
for f in docker-compose.yml docker-compose.yaml compose.yml; do
|
||||||
|
if [ -f "$REPO/$f" ]; then
|
||||||
|
COMPOSE_FILE="$REPO/$f"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$COMPOSE_FILE" ]; then
|
||||||
|
echo "INFO No docker-compose file — skip"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== validate-docker-compose: $COMPOSE_FILE ==="
|
||||||
|
|
||||||
|
# Coolify-only keys (exclude_from_hc) are stripped before docker compose config
|
||||||
|
COMPOSE_VALIDATE_FILE="$COMPOSE_FILE"
|
||||||
|
if grep -q 'exclude_from_hc:' "$COMPOSE_FILE"; then
|
||||||
|
COMPOSE_VALIDATE_FILE="$(mktemp)"
|
||||||
|
sed '/exclude_from_hc:/d' "$COMPOSE_FILE" > "$COMPOSE_VALIDATE_FILE"
|
||||||
|
info "[DC-00] stripped exclude_from_hc for docker compose config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# docker compose config
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
if docker compose -f "$COMPOSE_VALIDATE_FILE" config -q 2>/dev/null; then
|
||||||
|
pass "[DC-01] docker compose config OK"
|
||||||
|
else
|
||||||
|
err "[DC-01] docker compose config failed"
|
||||||
|
docker compose -f "$COMPOSE_VALIDATE_FILE" config 2>&1 | tail -20 || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "[DC-01] docker not available — syntax not fully validated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$COMPOSE_VALIDATE_FILE" != "$COMPOSE_FILE" ] && rm -f "$COMPOSE_VALIDATE_FILE"
|
||||||
|
|
||||||
|
# Coolify: no host ports on HTTP services
|
||||||
|
if grep -E '^\s+ports:' "$COMPOSE_FILE" >/dev/null 2>&1; then
|
||||||
|
err "[DC-02] ports: found — Coolify uses SERVICE_URL_* instead"
|
||||||
|
else
|
||||||
|
pass "[DC-02] no host ports (Coolify-safe)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SERVICE_URL on frontend with port 8080 (Frappe nginx default)
|
||||||
|
if grep -q 'SERVICE_URL_FRONTEND_8080' "$COMPOSE_FILE"; then
|
||||||
|
pass "[DC-03] SERVICE_URL_FRONTEND_8080 present"
|
||||||
|
else
|
||||||
|
err "[DC-03] missing SERVICE_URL_FRONTEND_8080 on frontend"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bind mounts ./scripts on long-running services (heuristic)
|
||||||
|
if grep -E '^\s+-\s+['\''"]?\./scripts' "$COMPOSE_FILE" >/dev/null 2>&1; then
|
||||||
|
if grep -B30 './scripts' "$COMPOSE_FILE" | grep -qE 'restart:\s+(unless-stopped|always)'; then
|
||||||
|
if [ "$COOLIFY_STRICT" = "1" ]; then
|
||||||
|
err "[DC-04] ./scripts bind mount on restarting service — Coolify may miss files"
|
||||||
|
else
|
||||||
|
warn "[DC-04] ./scripts bind mount — prefer inline compose for Coolify daemons"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "[DC-04] ./scripts mount on non-restarting service (may be OK)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SERVICE_URL on compose (Coolify magic vars)
|
||||||
|
if grep -qE 'SERVICE_URL_[A-Z0-9_]+' "$COMPOSE_FILE"; then
|
||||||
|
if grep -qE 'networks:' "$COMPOSE_FILE" && ! grep -qE '^\s+default:' "$COMPOSE_FILE"; then
|
||||||
|
warn "[DC-05] SERVICE_URL_* with custom networks — risk Traefik 504; see coolify-docker-compose skill"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# fabric-price-oracle / gateway port hints
|
||||||
|
if grep -q 'fabric-gateway' "$COMPOSE_FILE"; then
|
||||||
|
if grep -A80 'fabric-gateway:' "$COMPOSE_FILE" | grep -qE "['\"]?3000:3000|GATEWAY_PORT"; then
|
||||||
|
pass "[DC-08] fabric-gateway publishes port 3000"
|
||||||
|
else
|
||||||
|
warn "[DC-08] fabric-gateway may not publish host port 3000"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retired oracle stub
|
||||||
|
if grep -q 'fabric-oracle:' "$COMPOSE_FILE"; then
|
||||||
|
if grep -A15 'fabric-oracle:' "$COMPOSE_FILE" | grep -qE 'profiles:|legacy|Retired|exit 0'; then
|
||||||
|
pass "[DC-09] fabric-oracle retired/profiled"
|
||||||
|
else
|
||||||
|
warn "[DC-09] fabric-oracle still active — should be legacy stub"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "compose errors=$ERR warnings=$WARN"
|
||||||
|
[ "$ERR" -eq 0 ] || exit 1
|
||||||
|
exit 0
|
||||||
Loading…
Reference in a new issue