Add custom Frappe image build with HRMS, Lending, and LMS.
Jenkins builds from apps.json, pushes to Forgejo registry, and archives Coolify image tags; compose installs all apps on first site creation.
This commit is contained in:
parent
3eefb73727
commit
17c2c5ead8
16 changed files with 355 additions and 182 deletions
6
.dockerignore
Normal file
6
.dockerignore
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
.git
|
||||||
|
.ci-bin
|
||||||
|
dist
|
||||||
|
*.md
|
||||||
|
Jenkinsfile
|
||||||
|
Makefile
|
||||||
44
Jenkinsfile
vendored
44
Jenkinsfile
vendored
|
|
@ -1,7 +1,13 @@
|
||||||
// ERPNext → Coolify production CI (Forgejo: epistemophiliac/erpnext)
|
// ERPNext → Coolify: validate, build custom image, push to Forgejo registry
|
||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
REGISTRY_IMAGE = 'git.aexoradao.com/epistemophiliac/erpnext'
|
||||||
|
REGISTRY_HOST = 'git.aexoradao.com'
|
||||||
|
FRAPPE_BRANCH = 'version-16'
|
||||||
|
}
|
||||||
|
|
||||||
options {
|
options {
|
||||||
timestamps()
|
timestamps()
|
||||||
disableConcurrentBuilds()
|
disableConcurrentBuilds()
|
||||||
|
|
@ -12,12 +18,14 @@ pipeline {
|
||||||
stage('Verify') {
|
stage('Verify') {
|
||||||
steps {
|
steps {
|
||||||
sh '''
|
sh '''
|
||||||
echo "=== ERPNext Coolify CI ==="
|
echo "=== ERPNext custom image CI ==="
|
||||||
echo "commit: $(git rev-parse --short HEAD)"
|
echo "commit: $(git rev-parse --short HEAD)"
|
||||||
echo "branch: ${BRANCH_NAME:-main}"
|
echo "branch: ${BRANCH_NAME:-main}"
|
||||||
test -f Jenkinsfile
|
test -f Jenkinsfile
|
||||||
test -f docker-compose.yml
|
test -f docker-compose.yml
|
||||||
test -f example.env
|
test -f example.env
|
||||||
|
test -f apps.json
|
||||||
|
test -f images/layered/Containerfile
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,20 +52,42 @@ pipeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Verify ERPNext image') {
|
stage('Build custom image') {
|
||||||
steps {
|
steps {
|
||||||
sh 'bash scripts/ci/jenkins-pull-image.sh'
|
sh 'bash scripts/ci/jenkins-build-image.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Push to Forgejo registry') {
|
||||||
|
steps {
|
||||||
|
withCredentials([usernamePassword(
|
||||||
|
credentialsId: 'forgejo-erpnext',
|
||||||
|
usernameVariable: 'REGISTRY_USER',
|
||||||
|
passwordVariable: 'REGISTRY_PASSWORD'
|
||||||
|
)]) {
|
||||||
|
sh 'bash scripts/ci/jenkins-push-image.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Verify registry pull') {
|
||||||
|
steps {
|
||||||
|
sh 'bash scripts/ci/jenkins-verify-image.sh'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
success {
|
success {
|
||||||
archiveArtifacts artifacts: 'dist/docker-compose.coolify.yml', fingerprint: true, onlyIfSuccessful: true
|
archiveArtifacts artifacts: 'dist/*', fingerprint: true, onlyIfSuccessful: true
|
||||||
echo 'CI passed — safe to deploy docker-compose.yml on Coolify (set DB_PASSWORD, SITE_NAME, ADMIN_PASSWORD).'
|
echo '''
|
||||||
|
CI passed.
|
||||||
|
Image: git.aexoradao.com/epistemophiliac/erpnext:main-<sha> (+ :main)
|
||||||
|
Coolify: set CUSTOM_IMAGE / CUSTOM_TAG from dist/coolify-image.env
|
||||||
|
'''
|
||||||
}
|
}
|
||||||
failure {
|
failure {
|
||||||
echo 'CI failed — do not deploy to Coolify until this build is green.'
|
echo 'CI failed — image was not published.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
README.md
66
README.md
|
|
@ -1,61 +1,49 @@
|
||||||
# Production ERPNext on Coolify
|
# 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).
|
Custom Docker image and Compose stack for [ERPNext](https://erpnext.com) plus **HRMS**, **Lending**, and **LMS (Learning)** on [Coolify](https://coolify.io). Derived from [frappe/frappe_docker](https://github.com/frappe/frappe_docker).
|
||||||
|
|
||||||
**Repository:** https://git.aexoradao.com/epistemophiliac/erpnext
|
**Repository:** https://git.aexoradao.com/epistemophiliac/erpnext
|
||||||
|
|
||||||
## Quick start (Coolify)
|
## Apps baked into the image
|
||||||
|
|
||||||
1. **New Resource** → **Docker Compose**
|
| App | Source | Branch |
|
||||||
2. **Git repository:** `https://git.aexoradao.com/epistemophiliac/erpnext`
|
|-----|--------|--------|
|
||||||
3. **Compose file:** `docker-compose.yml`
|
| ERPNext | frappe/erpnext | version-16 |
|
||||||
4. Set environment variables from [`example.env`](example.env) (at minimum `DB_PASSWORD`, `SITE_NAME`, `ADMIN_PASSWORD`)
|
| Payments | frappe/payments | version-16 (required by LMS) |
|
||||||
5. Assign your domain to service **`frontend`**, port **`8080`**
|
| HRMS | frappe/hrms | version-16 |
|
||||||
6. Deploy — first boot creates the site and installs ERPNext (~5–15 minutes)
|
| Lending | frappe/lending | version-16 |
|
||||||
|
| LMS (Learning) | frappe/lms | v2.55.0 |
|
||||||
|
|
||||||
Login: user `Administrator`, password = `ADMIN_PASSWORD`.
|
Defined in [`apps.json`](apps.json). Edit that file and push to change apps; Jenkins rebuilds the image.
|
||||||
|
|
||||||
## What this stack includes
|
## Pipeline (Jenkins)
|
||||||
|
|
||||||
| Service | Role |
|
1. Validate compose + readiness
|
||||||
|---------|------|
|
2. **Build** custom image (`images/layered/Containerfile`)
|
||||||
| `db` | MariaDB 11.8 |
|
3. **Push** to Forgejo registry: `git.aexoradao.com/epistemophiliac/erpnext:main-<sha>` and `:main`
|
||||||
| `redis-cache` / `redis-queue` | Cache and job queue |
|
4. Archive `dist/coolify-image.env` with `CUSTOM_IMAGE` / `CUSTOM_TAG` for Coolify
|
||||||
| `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 (Jenkins)
|
See [docs/JENKINS.md](docs/JENKINS.md).
|
||||||
|
|
||||||
Jenkins runs the same checks on every build via [`Jenkinsfile`](Jenkinsfile):
|
## Coolify deploy (you configure)
|
||||||
|
|
||||||
- `scripts/ci/ci-readiness.sh` — secrets, docs, compose checks
|
1. Docker Compose from this git repo, file `docker-compose.yml`
|
||||||
- `scripts/ci/validate-docker-compose.sh` — Coolify compose rules + `docker compose config`
|
2. Env vars from [`example.env`](example.env) — use `CUSTOM_TAG` from latest green Jenkins build
|
||||||
- `docker compose config` + pull pinned `frappe/erpnext` image
|
3. Domain on service **`frontend`**, port **`8080`**
|
||||||
|
|
||||||
**Jenkins:** see [docs/JENKINS.md](docs/JENKINS.md) — Multibranch needs **Discover branches** behaviour, or use a simple **Pipeline** job on `main`.
|
See [docs/COOLIFY_DEPLOY.md](docs/COOLIFY_DEPLOY.md).
|
||||||
|
|
||||||
Run locally:
|
## Local checks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make ci
|
make ci # validate only
|
||||||
|
BUILD_IMAGE=1 bash scripts/ci/jenkins-run.sh # build image locally (slow)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Requirements
|
## Stack services
|
||||||
|
|
||||||
- Coolify server with **4 GB+ RAM** (8 GB recommended)
|
MariaDB, Redis, configurator, create-site, migrator, backend, frontend (8080), websocket, workers.
|
||||||
- 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
|
## License
|
||||||
|
|
||||||
Compose and docs: MIT. ERPNext/Frappe images: see upstream licenses.
|
Compose and docs: MIT. Frappe/ERPNext apps: see upstream licenses.
|
||||||
|
|
|
||||||
22
apps.json
Normal file
22
apps.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"url": "https://github.com/frappe/erpnext",
|
||||||
|
"branch": "version-16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/frappe/payments",
|
||||||
|
"branch": "version-16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/frappe/hrms",
|
||||||
|
"branch": "version-16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/frappe/lending",
|
||||||
|
"branch": "version-16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/frappe/lms",
|
||||||
|
"branch": "v2.55.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
# No ports: — routing uses SERVICE_URL_FRONTEND_8080.
|
# No ports: — routing uses SERVICE_URL_FRONTEND_8080.
|
||||||
|
|
||||||
x-customizable-image: &customizable_image
|
x-customizable-image: &customizable_image
|
||||||
image: ${CUSTOM_IMAGE:-frappe/erpnext}:${CUSTOM_TAG:-${ERPNEXT_VERSION:-v16.22.0}}
|
image: ${CUSTOM_IMAGE:-git.aexoradao.com/epistemophiliac/erpnext}:${CUSTOM_TAG:-main}
|
||||||
pull_policy: ${PULL_POLICY:-always}
|
pull_policy: ${PULL_POLICY:-always}
|
||||||
restart: ${RESTART_POLICY:-unless-stopped}
|
restart: ${RESTART_POLICY:-unless-stopped}
|
||||||
|
|
||||||
|
|
@ -102,11 +102,18 @@ services:
|
||||||
wait-for-it -t 120 db:3306;
|
wait-for-it -t 120 db:3306;
|
||||||
wait-for-it -t 120 redis-cache:6379;
|
wait-for-it -t 120 redis-cache:6379;
|
||||||
wait-for-it -t 120 redis-queue: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
|
if [ -d "sites/$$SITE" ]; then echo "[create-site] exists"; $$B use "$$SITE"; else
|
||||||
|
echo "[create-site] creating";
|
||||||
|
INSTALL_ARGS="";
|
||||||
|
IFS=',' read -r -a apps <<< "$$INSTALL_APPS";
|
||||||
|
for app in "$${apps[@]}"; do INSTALL_ARGS="$$INSTALL_ARGS --install-app $$app"; done;
|
||||||
|
$$B new-site "$$SITE" --mariadb-user-host-login-scope='%' --admin-password "$$ADMIN_PASSWORD" --db-root-password "$$DB_PASSWORD" $$INSTALL_ARGS --set-default;
|
||||||
|
fi
|
||||||
environment:
|
environment:
|
||||||
- 'SITE_NAME=${SITE_NAME:-erp.example.com}'
|
- 'SITE_NAME=${SITE_NAME:-erp.example.com}'
|
||||||
- 'ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme}'
|
- 'ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme}'
|
||||||
- 'DB_PASSWORD=${DB_PASSWORD:-changeme}'
|
- 'DB_PASSWORD=${DB_PASSWORD:-changeme}'
|
||||||
|
- 'INSTALL_APPS=${INSTALL_APPS:-erpnext,payments,hrms,lending,lms}'
|
||||||
volumes:
|
volumes:
|
||||||
- sites:/home/frappe/frappe-bench/sites
|
- sites:/home/frappe/frappe-bench/sites
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
# Coolify deployment — Production ERPNext
|
# Coolify deployment — Production ERPNext (+ HRMS, Lending, LMS)
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Coolify v4+ with Docker Compose support
|
- Coolify v4+ with Docker Compose support
|
||||||
- Server: **minimum 4 GB RAM**, **8 GB+** for production workloads
|
- Jenkins green build published image to `git.aexoradao.com/epistemophiliac/erpnext`
|
||||||
|
- Server: **minimum 4 GB RAM**, **8 GB+** recommended (custom image + LMS frontend assets)
|
||||||
- Public domain (e.g. `erp.yourdomain.com`)
|
- Public domain (e.g. `erp.yourdomain.com`)
|
||||||
|
|
||||||
## 1. Create the Coolify service
|
## 1. Create the Coolify service (you do this)
|
||||||
|
|
||||||
| Setting | Value |
|
| Setting | Value |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
|
|
@ -17,86 +18,51 @@
|
||||||
|
|
||||||
## 2. Environment variables
|
## 2. Environment variables
|
||||||
|
|
||||||
Set these in **Coolify → Service → Environment Variables** before first deploy:
|
From latest **green Jenkins build**, use `dist/coolify-image.env` or:
|
||||||
|
|
||||||
| Variable | Required | Example | Notes |
|
| Variable | Required | Example | Notes |
|
||||||
|----------|----------|---------|-------|
|
|----------|----------|---------|-------|
|
||||||
| `ERPNEXT_VERSION` | yes | `v16.22.0` | Pin image tag |
|
| `CUSTOM_IMAGE` | yes | `git.aexoradao.com/epistemophiliac/erpnext` | Forgejo registry |
|
||||||
|
| `CUSTOM_TAG` | yes | `main-3eefb73` or `main` | Pin SHA for prod; `main` = latest CI |
|
||||||
|
| `PULL_POLICY` | yes | `always` | Pull from registry on deploy |
|
||||||
| `DB_PASSWORD` | yes | strong secret | MariaDB root |
|
| `DB_PASSWORD` | yes | strong secret | MariaDB root |
|
||||||
| `SITE_NAME` | yes | `erp.yourdomain.com` | Must match domain |
|
| `SITE_NAME` | yes | `erp.yourdomain.com` | Must match domain |
|
||||||
| `ADMIN_PASSWORD` | yes | strong secret | Frappe login |
|
| `ADMIN_PASSWORD` | yes | strong secret | Frappe login |
|
||||||
| `FRAPPE_SITE_NAME_HEADER` | yes | same as `SITE_NAME` | Single-site routing |
|
| `FRAPPE_SITE_NAME_HEADER` | yes | same as `SITE_NAME` | Single-site routing |
|
||||||
| `MIGRATE_SITES` | no | `true` | Run migrate on redeploy |
|
| `INSTALL_APPS` | yes | `erpnext,payments,hrms,lending,lms` | First site only |
|
||||||
|
| `MIGRATE_SITES` | no | `true` | 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.
|
> **Coolify env cache:** Changing defaults in `docker-compose.yml` does not update values already stored in Coolify. Edit them in the UI.
|
||||||
|
|
||||||
## 3. Domain routing
|
## 3. Domain routing
|
||||||
|
|
||||||
1. Open the deployed service in Coolify
|
1. Add domain: `erp.yourdomain.com`
|
||||||
2. Add domain: `erp.yourdomain.com`
|
2. Attach to service **`frontend`**
|
||||||
3. Attach domain to service **`frontend`**
|
3. Port **`8080`**
|
||||||
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
|
## 4. First deploy timeline
|
||||||
|
|
||||||
```text
|
```text
|
||||||
db (healthy) → redis → configurator (exit 0)
|
pull custom image → db (healthy) → redis → configurator
|
||||||
→ create-site (new-site + install-app erpnext, ~5–15 min)
|
→ create-site (install erpnext + payments + hrms + lending + lms, ~10–20 min)
|
||||||
→ migrator → backend / workers / frontend
|
→ migrator → backend / workers / frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Watch logs:
|
## 5. Upgrades
|
||||||
|
|
||||||
- `create-site` — site creation progress
|
1. Push app changes to git → Jenkins builds new image
|
||||||
- `backend` — gunicorn ready
|
2. Set `CUSTOM_TAG` in Coolify to new `main-<sha>`
|
||||||
- `frontend` — nginx on 8080
|
3. Redeploy — `migrator` runs `bench migrate`
|
||||||
|
|
||||||
## 5. Post-deploy verification
|
## Apps in the image
|
||||||
|
|
||||||
From Coolify terminal on `frontend`:
|
See [`apps.json`](../apps.json). Site install list: `INSTALL_APPS` in [`example.env`](../example.env).
|
||||||
|
|
||||||
```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
|
## Troubleshooting
|
||||||
|
|
||||||
| Symptom | Fix |
|
| Symptom | Fix |
|
||||||
|---------|-----|
|
|---------|-----|
|
||||||
| Coolify 404 | Domain on wrong service — must be `frontend:8080` |
|
| Image pull failed | Check registry login on Coolify host; verify tag exists in Forgejo Packages |
|
||||||
| Site not found | `SITE_NAME` ≠ domain; fix `FRAPPE_SITE_NAME_HEADER` in UI |
|
| create-site fails on LMS | Ensure `payments` is in `INSTALL_APPS` before `lms` |
|
||||||
| Stack unhealthy | Healthcheck port must be **8080** on frontend |
|
| 502 / unhealthy frontend | Wait for create-site; check `backend` health |
|
||||||
| create-site fails on redeploy | Should be idempotent — check `sites/$SITE_NAME` exists |
|
| Wrong site | `SITE_NAME` and `FRAPPE_SITE_NAME_HEADER` must match Coolify domain |
|
||||||
| 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` should pass the Jenkins pipeline (`Jenkinsfile`) before Coolify deploy.
|
|
||||||
|
|
||||||
Local check:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make ci
|
|
||||||
```
|
|
||||||
|
|
|
||||||
108
docs/JENKINS.md
108
docs/JENKINS.md
|
|
@ -1,89 +1,77 @@
|
||||||
# Jenkins setup (Forgejo)
|
# Jenkins CI — custom image build + Forgejo registry
|
||||||
|
|
||||||
Repo: `https://git.aexoradao.com/epistemophiliac/erpnext.git`
|
Repo: `https://git.aexoradao.com/epistemophiliac/erpnext.git`
|
||||||
Branch: `main`
|
Branch: `main`
|
||||||
Pipeline file: `Jenkinsfile` (repo root)
|
Registry image: `git.aexoradao.com/epistemophiliac/erpnext`
|
||||||
|
|
||||||
## Option A — Simple Pipeline (fastest)
|
## What Jenkins does
|
||||||
|
|
||||||
If Multibranch shows an empty folder, use this instead.
|
| Stage | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| **Verify** | Required files including `apps.json`, `Containerfile` |
|
||||||
|
| **Production readiness** | Secrets/docs/compose checks |
|
||||||
|
| **Bootstrap Docker tools** | Static docker CLI + compose, socket access |
|
||||||
|
| **Compose validate** | Coolify-safe `docker compose config` |
|
||||||
|
| **Build custom image** | `bench init` from `apps.json` (ERPNext, HRMS, Lending, LMS, payments) |
|
||||||
|
| **Push to Forgejo registry** | Tags `main-<git-sha>` and `main` |
|
||||||
|
| **Verify registry pull** | Confirms the pushed image is pullable |
|
||||||
|
|
||||||
1. **New Item** → **Pipeline** → name `erpnext`
|
**Artifacts:** `dist/coolify-image.env`, `dist/docker-compose.coolify.yml`, `dist/image-reference.txt`
|
||||||
2. **Pipeline** → Definition: **Pipeline script from SCM**
|
|
||||||
3. SCM: **Git**
|
|
||||||
- Repository URL: `https://git.aexoradao.com/epistemophiliac/erpnext.git`
|
|
||||||
- Credentials: Forgejo user + access token
|
|
||||||
- Branch: `*/main`
|
|
||||||
4. Script Path: `Jenkinsfile`
|
|
||||||
5. **Save** → **Build Now**
|
|
||||||
|
|
||||||
## Option B — Multibranch Pipeline
|
First image build can take **30–60+ minutes** (compiles assets). Later builds use Docker layer cache unless `apps.json` changes.
|
||||||
|
|
||||||
Indexing succeeds but the folder stays empty when **Discover branches** is missing.
|
## Jenkins job setup
|
||||||
|
|
||||||
1. **New Item** → **Multibranch Pipeline** → name `erpnext`
|
Same as before — **Pipeline from SCM** or **Multibranch** with **Discover branches**.
|
||||||
2. **Branch Sources** → **Git**
|
|
||||||
- URL + credentials (same as above)
|
|
||||||
3. **Behaviours** → **Add** → **Discover branches**
|
|
||||||
- Strategy: **All branches** (or include `main` via wildcard filter)
|
|
||||||
4. **Build Configuration**
|
|
||||||
- Mode: **by Jenkinsfile**
|
|
||||||
- Script Path: `Jenkinsfile`
|
|
||||||
5. **Save** → **Scan Repository Now**
|
|
||||||
|
|
||||||
You should see a `main` branch under the folder. Click it → **Build Now**.
|
**Credentials:** `forgejo-erpnext` (username + Forgejo token) — used for git checkout **and** `docker login git.aexoradao.com`.
|
||||||
|
|
||||||
### Optional: Forgejo webhook
|
Token needs:
|
||||||
|
|
||||||
Install the **Gitea** plugin in Jenkins, then use **Gitea** as the branch source (Forgejo-compatible) for automatic scans on push.
|
- Repo read (checkout)
|
||||||
|
- **Package write** (push container images to Forgejo registry)
|
||||||
|
|
||||||
## Credentials
|
Enable **Packages** on the Forgejo repo if pushes fail with 404/403.
|
||||||
|
|
||||||
**Manage Jenkins → Credentials → Add**
|
## After a green build
|
||||||
|
|
||||||
- Kind: Username with password
|
Download `dist/coolify-image.env` from Jenkins artifacts, or use:
|
||||||
- Username: `epistemophiliac`
|
|
||||||
- Password: Forgejo personal access token (repo read scope)
|
|
||||||
|
|
||||||
## Pipeline stages
|
```env
|
||||||
|
CUSTOM_IMAGE=git.aexoradao.com/epistemophiliac/erpnext
|
||||||
|
CUSTOM_TAG=main-<commit-sha>
|
||||||
|
PULL_POLICY=always
|
||||||
|
```
|
||||||
|
|
||||||
| Stage | What it checks |
|
Set those in Coolify before deploy (or use `CUSTOM_TAG=main` for latest main build).
|
||||||
|-------|----------------|
|
|
||||||
| **Verify** | `Jenkinsfile`, `docker-compose.yml`, `example.env` present |
|
|
||||||
| **Production readiness** | `ci-readiness.sh` + `validate-docker-compose.sh` (Coolify rules, no secrets tracked) |
|
|
||||||
| **Bootstrap Docker tools** | Static `docker` + `docker-compose` in `.ci-bin/`, socket access |
|
|
||||||
| **Compose validate** | `docker compose config` on Coolify-stripped compose (no `exclude_from_hc`, no host ports) |
|
|
||||||
| **Verify ERPNext image** | `docker pull frappe/erpnext:<ERPNEXT_VERSION>` from `example.env` |
|
|
||||||
|
|
||||||
**Post-success:** archives `dist/docker-compose.coolify.yml` (the compose file Coolify actually parses).
|
## Changing apps (HRMS, Lending, LMS, …)
|
||||||
|
|
||||||
This validates the repo before deploy; **Coolify deploy is separate** (set `DB_PASSWORD`, `SITE_NAME`, `ADMIN_PASSWORD`, domain on `frontend:8080`).
|
1. Edit [`apps.json`](../apps.json) (branches must match `FRAPPE_BRANCH=version-16` where applicable)
|
||||||
|
2. Push to `main`
|
||||||
|
3. Jenkins rebuilds and pushes a new image tag
|
||||||
|
4. Update `CUSTOM_TAG` in Coolify and redeploy
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### `fatal: not in a git directory` (branch indexing)
|
|
||||||
|
|
||||||
Usually a **corrupt Jenkins git cache** after changing container user. In the **jenkins** container terminal (Coolify):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm -rf /var/jenkins_home/caches/git-*
|
|
||||||
chown -R jenkins:jenkins /var/jenkins_home
|
|
||||||
```
|
|
||||||
|
|
||||||
Then **Scan Repository Now** on the multibranch job.
|
|
||||||
|
|
||||||
### `permission denied` on `/var/run/docker.sock`
|
### `permission denied` on `/var/run/docker.sock`
|
||||||
|
|
||||||
Jenkins must be in the host **docker** group. On the Coolify host:
|
Set `DOCKER_GID` on the Jenkins Coolify service to the host docker group GID (`stat -c '%g' /var/run/docker.sock`), redeploy Jenkins.
|
||||||
|
|
||||||
```bash
|
### Registry push 401/403
|
||||||
stat -c '%g' /var/run/docker.sock
|
|
||||||
```
|
|
||||||
|
|
||||||
Set that number as `DOCKER_GID` on the **jenkins** Coolify service (was wrong at `999` on this host — use **`991`**), redeploy Jenkins, rebuild.
|
- Token needs **write:package** (or full repo scope including packages)
|
||||||
|
- `docker login git.aexoradao.com` with same credentials as git
|
||||||
|
|
||||||
Do **not** run Jenkins as `user: 0:0` — it breaks `jenkins_home` ownership and git caches.
|
### Build fails on `bench init`
|
||||||
|
|
||||||
### Always use **Build Now** on `main`, not **Rebuild** on old builds
|
- All apps in `apps.json` must be compatible with `version-16`
|
||||||
|
- LMS has no `version-16` branch — pinned to tag `v2.55.0` in `apps.json`
|
||||||
|
|
||||||
Old rebuilds replay old commits with old `Jenkinsfile` content.
|
### `source: not found`
|
||||||
|
|
||||||
|
All pipeline steps use `bash scripts/ci/*.sh` — do not use `source` in bare `sh '''` blocks.
|
||||||
|
|
||||||
|
### Use **Build Now**, not **Rebuild** on old runs
|
||||||
|
|
||||||
|
Rebuild replays an old commit.
|
||||||
|
|
|
||||||
14
example.env
14
example.env
|
|
@ -1,8 +1,13 @@
|
||||||
# Copy to Coolify Environment Variables (Service > Environment).
|
# 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 tags come from Jenkins (Forgejo container registry).
|
||||||
|
|
||||||
# Image tag — pin for reproducible deploys
|
# Custom image built by Jenkins (apps: ERPNext, HRMS, Lending, LMS + payments)
|
||||||
ERPNEXT_VERSION=v16.22.0
|
CUSTOM_IMAGE=git.aexoradao.com/epistemophiliac/erpnext
|
||||||
|
CUSTOM_TAG=main
|
||||||
|
PULL_POLICY=always
|
||||||
|
|
||||||
|
# Frappe major line — must match apps.json branches (version-16)
|
||||||
|
FRAPPE_BRANCH=version-16
|
||||||
|
|
||||||
# MariaDB root password (required — change before production)
|
# MariaDB root password (required — change before production)
|
||||||
DB_PASSWORD=changeme
|
DB_PASSWORD=changeme
|
||||||
|
|
@ -16,6 +21,9 @@ ADMIN_PASSWORD=changeme
|
||||||
# Nginx site header — for single-site Coolify, same as SITE_NAME
|
# Nginx site header — for single-site Coolify, same as SITE_NAME
|
||||||
FRAPPE_SITE_NAME_HEADER=erp.example.com
|
FRAPPE_SITE_NAME_HEADER=erp.example.com
|
||||||
|
|
||||||
|
# Apps installed on first site creation (comma-separated, order matters)
|
||||||
|
INSTALL_APPS=erpnext,payments,hrms,lending,lms
|
||||||
|
|
||||||
# Run bench migrate on every deploy (set false to skip)
|
# Run bench migrate on every deploy (set false to skip)
|
||||||
MIGRATE_SITES=true
|
MIGRATE_SITES=true
|
||||||
|
|
||||||
|
|
|
||||||
58
images/layered/Containerfile
Normal file
58
images/layered/Containerfile
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Custom ERPNext image (HRMS, Lending, LMS + dependencies).
|
||||||
|
# Pattern: https://github.com/frappe/frappe_docker/tree/main/images/layered
|
||||||
|
ARG FRAPPE_BRANCH=version-16
|
||||||
|
ARG FRAPPE_IMAGE_PREFIX=frappe
|
||||||
|
|
||||||
|
FROM ${FRAPPE_IMAGE_PREFIX}/build:${FRAPPE_BRANCH} AS builder
|
||||||
|
|
||||||
|
ARG FRAPPE_BRANCH=version-16
|
||||||
|
ARG FRAPPE_PATH=https://github.com/frappe/frappe
|
||||||
|
ARG CACHE_BUST=""
|
||||||
|
|
||||||
|
USER frappe
|
||||||
|
|
||||||
|
RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1000 \
|
||||||
|
: "${CACHE_BUST}" && \
|
||||||
|
export APP_INSTALL_ARGS="" && \
|
||||||
|
if [ -f /opt/frappe/apps.json ] && [ -s /opt/frappe/apps.json ]; then \
|
||||||
|
export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \
|
||||||
|
fi && \
|
||||||
|
bench init ${APP_INSTALL_ARGS}\
|
||||||
|
--frappe-branch=${FRAPPE_BRANCH} \
|
||||||
|
--frappe-path=${FRAPPE_PATH} \
|
||||||
|
--no-procfile \
|
||||||
|
--no-backups \
|
||||||
|
--skip-redis-config-generation \
|
||||||
|
--verbose \
|
||||||
|
/home/frappe/frappe-bench && \
|
||||||
|
cd /home/frappe/frappe-bench && \
|
||||||
|
echo "{}" > sites/common_site_config.json && \
|
||||||
|
find apps -mindepth 1 -path "*/.git" | xargs rm -fr
|
||||||
|
|
||||||
|
FROM ${FRAPPE_IMAGE_PREFIX}/base:${FRAPPE_BRANCH} AS backend
|
||||||
|
|
||||||
|
USER frappe
|
||||||
|
|
||||||
|
COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench
|
||||||
|
|
||||||
|
WORKDIR /home/frappe/frappe-bench
|
||||||
|
|
||||||
|
RUN cp -r /home/frappe/frappe-bench/sites/assets /home/frappe/frappe-bench/assets && \
|
||||||
|
rm -rf /home/frappe/frappe-bench/sites/assets
|
||||||
|
|
||||||
|
VOLUME [ \
|
||||||
|
"/home/frappe/frappe-bench/sites", \
|
||||||
|
"/home/frappe/frappe-bench/logs" \
|
||||||
|
]
|
||||||
|
|
||||||
|
USER root
|
||||||
|
COPY resources/core/main-entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod 755 /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
COPY resources/core/start.sh /usr/local/bin/start.sh
|
||||||
|
RUN chmod 755 /usr/local/bin/start.sh
|
||||||
|
|
||||||
|
USER frappe
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
|
||||||
|
CMD ["start.sh"]
|
||||||
12
resources/core/main-entrypoint.sh
Normal file
12
resources/core/main-entrypoint.sh
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ASSETS_PATH="/home/frappe/frappe-bench/sites/assets"
|
||||||
|
BAKED_PATH="/home/frappe/frappe-bench/assets"
|
||||||
|
|
||||||
|
echo "Linking fresh assets to volume..."
|
||||||
|
rm -rf "$ASSETS_PATH"
|
||||||
|
mkdir -p "$(dirname "$ASSETS_PATH")"
|
||||||
|
ln -s "$BAKED_PATH" "$ASSETS_PATH"
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
19
resources/core/start.sh
Normal file
19
resources/core/start.sh
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GUNICORN_THREADS=${GUNICORN_THREADS:-4}
|
||||||
|
GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
|
||||||
|
GUNICORN_TIMEOUT=${GUNICORN_TIMEOUT:-120}
|
||||||
|
|
||||||
|
echo "Booting Gunicorn with $GUNICORN_WORKERS workers and $GUNICORN_THREADS threads..."
|
||||||
|
|
||||||
|
exec /home/frappe/frappe-bench/env/bin/gunicorn \
|
||||||
|
--chdir=/home/frappe/frappe-bench/sites \
|
||||||
|
--bind=0.0.0.0:8000 \
|
||||||
|
--threads="$GUNICORN_THREADS" \
|
||||||
|
--workers="$GUNICORN_WORKERS" \
|
||||||
|
--worker-class=gthread \
|
||||||
|
--worker-tmp-dir=/dev/shm \
|
||||||
|
--timeout="$GUNICORN_TIMEOUT" \
|
||||||
|
--preload \
|
||||||
|
frappe.app:application
|
||||||
35
scripts/ci/jenkins-build-image.sh
Executable file
35
scripts/ci/jenkins-build-image.sh
Executable file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source .ci-bin/ci-env.sh
|
||||||
|
|
||||||
|
FRAPPE_BRANCH="${FRAPPE_BRANCH:-version-16}"
|
||||||
|
REGISTRY_IMAGE="${REGISTRY_IMAGE:-git.aexoradao.com/epistemophiliac/erpnext}"
|
||||||
|
GIT_SHA="$(git rev-parse --short HEAD)"
|
||||||
|
IMAGE_TAG="${IMAGE_TAG:-main-${GIT_SHA}}"
|
||||||
|
CACHE_BUST="$(sha256sum apps.json | awk '{print $1}')"
|
||||||
|
|
||||||
|
export DOCKER_BUILDKIT=1
|
||||||
|
|
||||||
|
echo "=== Building ${REGISTRY_IMAGE}:${IMAGE_TAG} ==="
|
||||||
|
echo "FRAPPE_BRANCH=${FRAPPE_BRANCH}"
|
||||||
|
echo "apps.json sha256=${CACHE_BUST}"
|
||||||
|
|
||||||
|
$DOCKER build \
|
||||||
|
--build-arg="FRAPPE_BRANCH=${FRAPPE_BRANCH}" \
|
||||||
|
--build-arg="CACHE_BUST=${CACHE_BUST}" \
|
||||||
|
--secret=id=apps_json,src=apps.json \
|
||||||
|
--tag="${REGISTRY_IMAGE}:${IMAGE_TAG}" \
|
||||||
|
--tag="${REGISTRY_IMAGE}:main" \
|
||||||
|
--file=images/layered/Containerfile .
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
echo "${REGISTRY_IMAGE}:${IMAGE_TAG}" > dist/image-reference.txt
|
||||||
|
cat > dist/coolify-image.env <<EOF
|
||||||
|
CUSTOM_IMAGE=${REGISTRY_IMAGE}
|
||||||
|
CUSTOM_TAG=${IMAGE_TAG}
|
||||||
|
PULL_POLICY=always
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Built ${REGISTRY_IMAGE}:${IMAGE_TAG} and ${REGISTRY_IMAGE}:main"
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
source .ci-bin/ci-env.sh
|
|
||||||
|
|
||||||
VERSION="$(grep -E '^ERPNEXT_VERSION=' example.env | cut -d= -f2)"
|
|
||||||
$DOCKER pull "frappe/erpnext:${VERSION}"
|
|
||||||
echo "frappe/erpnext:${VERSION} OK"
|
|
||||||
23
scripts/ci/jenkins-push-image.sh
Executable file
23
scripts/ci/jenkins-push-image.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source .ci-bin/ci-env.sh
|
||||||
|
|
||||||
|
REGISTRY_IMAGE="${REGISTRY_IMAGE:-git.aexoradao.com/epistemophiliac/erpnext}"
|
||||||
|
REGISTRY_HOST="${REGISTRY_HOST:-git.aexoradao.com}"
|
||||||
|
GIT_SHA="$(git rev-parse --short HEAD)"
|
||||||
|
IMAGE_TAG="${IMAGE_TAG:-main-${GIT_SHA}}"
|
||||||
|
|
||||||
|
if [ -z "${REGISTRY_USER:-}" ] || [ -z "${REGISTRY_PASSWORD:-}" ]; then
|
||||||
|
echo "ERROR: set REGISTRY_USER and REGISTRY_PASSWORD (Jenkins forgejo-erpnext credentials)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$REGISTRY_PASSWORD" | $DOCKER login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin
|
||||||
|
|
||||||
|
$DOCKER push "${REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||||
|
$DOCKER push "${REGISTRY_IMAGE}:main"
|
||||||
|
|
||||||
|
echo "Pushed ${REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||||
|
echo "Pushed ${REGISTRY_IMAGE}:main"
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Local / all-in-one CI runner (same checks as Jenkinsfile stages).
|
# Local CI. Full image build + push: BUILD_IMAGE=1 REGISTRY_USER=... REGISTRY_PASSWORD=... bash scripts/ci/jenkins-run.sh
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "=== erpnext CI (local) ==="
|
echo "=== erpnext CI (local) ==="
|
||||||
|
|
@ -10,4 +10,15 @@ bash scripts/ci/ci-readiness.sh .
|
||||||
bash scripts/ci/validate-docker-compose.sh .
|
bash scripts/ci/validate-docker-compose.sh .
|
||||||
bash scripts/ci/jenkins-bootstrap.sh
|
bash scripts/ci/jenkins-bootstrap.sh
|
||||||
bash scripts/ci/jenkins-compose-validate.sh
|
bash scripts/ci/jenkins-compose-validate.sh
|
||||||
bash scripts/ci/jenkins-pull-image.sh
|
|
||||||
|
if [ "${BUILD_IMAGE:-0}" = "1" ]; then
|
||||||
|
bash scripts/ci/jenkins-build-image.sh
|
||||||
|
if [ -n "${REGISTRY_USER:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then
|
||||||
|
bash scripts/ci/jenkins-push-image.sh
|
||||||
|
bash scripts/ci/jenkins-verify-image.sh
|
||||||
|
else
|
||||||
|
echo "Skip push: set REGISTRY_USER and REGISTRY_PASSWORD to publish"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Skip image build (set BUILD_IMAGE=1 to build locally)"
|
||||||
|
fi
|
||||||
|
|
|
||||||
9
scripts/ci/jenkins-verify-image.sh
Executable file
9
scripts/ci/jenkins-verify-image.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source .ci-bin/ci-env.sh
|
||||||
|
|
||||||
|
REF="$(cat dist/image-reference.txt)"
|
||||||
|
$DOCKER pull "$REF"
|
||||||
|
echo "Verified pull: $REF"
|
||||||
Loading…
Reference in a new issue