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 {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
REGISTRY_IMAGE = 'git.aexoradao.com/epistemophiliac/erpnext'
|
||||
REGISTRY_HOST = 'git.aexoradao.com'
|
||||
FRAPPE_BRANCH = 'version-16'
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
|
|
@ -12,12 +18,14 @@ pipeline {
|
|||
stage('Verify') {
|
||||
steps {
|
||||
sh '''
|
||||
echo "=== ERPNext Coolify CI ==="
|
||||
echo "=== ERPNext custom image CI ==="
|
||||
echo "commit: $(git rev-parse --short HEAD)"
|
||||
echo "branch: ${BRANCH_NAME:-main}"
|
||||
test -f Jenkinsfile
|
||||
test -f docker-compose.yml
|
||||
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 {
|
||||
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 {
|
||||
success {
|
||||
archiveArtifacts artifacts: 'dist/docker-compose.coolify.yml', fingerprint: true, onlyIfSuccessful: true
|
||||
echo 'CI passed — safe to deploy docker-compose.yml on Coolify (set DB_PASSWORD, SITE_NAME, ADMIN_PASSWORD).'
|
||||
archiveArtifacts artifacts: 'dist/*', fingerprint: true, onlyIfSuccessful: true
|
||||
echo '''
|
||||
CI passed.
|
||||
Image: git.aexoradao.com/epistemophiliac/erpnext:main-<sha> (+ :main)
|
||||
Coolify: set CUSTOM_IMAGE / CUSTOM_TAG from dist/coolify-image.env
|
||||
'''
|
||||
}
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
## Quick start (Coolify)
|
||||
## Apps baked into the image
|
||||
|
||||
1. **New Resource** → **Docker Compose**
|
||||
2. **Git repository:** `https://git.aexoradao.com/epistemophiliac/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)
|
||||
| App | Source | Branch |
|
||||
|-----|--------|--------|
|
||||
| ERPNext | frappe/erpnext | version-16 |
|
||||
| Payments | frappe/payments | version-16 (required by LMS) |
|
||||
| HRMS | frappe/hrms | version-16 |
|
||||
| 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 |
|
||||
|---------|------|
|
||||
| `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 |
|
||||
1. Validate compose + readiness
|
||||
2. **Build** custom image (`images/layered/Containerfile`)
|
||||
3. **Push** to Forgejo registry: `git.aexoradao.com/epistemophiliac/erpnext:main-<sha>` and `:main`
|
||||
4. Archive `dist/coolify-image.env` with `CUSTOM_IMAGE` / `CUSTOM_TAG` for Coolify
|
||||
|
||||
## 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
|
||||
- `scripts/ci/validate-docker-compose.sh` — Coolify compose rules + `docker compose config`
|
||||
- `docker compose config` + pull pinned `frappe/erpnext` image
|
||||
1. Docker Compose from this git repo, file `docker-compose.yml`
|
||||
2. Env vars from [`example.env`](example.env) — use `CUSTOM_TAG` from latest green Jenkins build
|
||||
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
|
||||
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)
|
||||
- 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/)
|
||||
MariaDB, Redis, configurator, create-site, migrator, backend, frontend (8080), websocket, workers.
|
||||
|
||||
## 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.
|
||||
|
||||
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}
|
||||
restart: ${RESTART_POLICY:-unless-stopped}
|
||||
|
||||
|
|
@ -102,11 +102,18 @@ services:
|
|||
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
|
||||
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:
|
||||
- 'SITE_NAME=${SITE_NAME:-erp.example.com}'
|
||||
- 'ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme}'
|
||||
- 'DB_PASSWORD=${DB_PASSWORD:-changeme}'
|
||||
- 'INSTALL_APPS=${INSTALL_APPS:-erpnext,payments,hrms,lending,lms}'
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
# Coolify deployment — Production ERPNext
|
||||
# Coolify deployment — Production ERPNext (+ HRMS, Lending, LMS)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- 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`)
|
||||
|
||||
## 1. Create the Coolify service
|
||||
## 1. Create the Coolify service (you do this)
|
||||
|
||||
| Setting | Value |
|
||||
|---------|--------|
|
||||
|
|
@ -17,86 +18,51 @@
|
|||
|
||||
## 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 |
|
||||
|----------|----------|---------|-------|
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
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.
|
||||
1. Add domain: `erp.yourdomain.com`
|
||||
2. Attach to service **`frontend`**
|
||||
3. Port **`8080`**
|
||||
|
||||
## 4. First deploy timeline
|
||||
|
||||
```text
|
||||
db (healthy) → redis → configurator (exit 0)
|
||||
→ create-site (new-site + install-app erpnext, ~5–15 min)
|
||||
pull custom image → db (healthy) → redis → configurator
|
||||
→ create-site (install erpnext + payments + hrms + lending + lms, ~10–20 min)
|
||||
→ migrator → backend / workers / frontend
|
||||
```
|
||||
|
||||
Watch logs:
|
||||
## 5. Upgrades
|
||||
|
||||
- `create-site` — site creation progress
|
||||
- `backend` — gunicorn ready
|
||||
- `frontend` — nginx on 8080
|
||||
1. Push app changes to git → Jenkins builds new image
|
||||
2. Set `CUSTOM_TAG` in Coolify to new `main-<sha>`
|
||||
3. Redeploy — `migrator` runs `bench migrate`
|
||||
|
||||
## 5. Post-deploy verification
|
||||
## Apps in the image
|
||||
|
||||
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
|
||||
See [`apps.json`](../apps.json). Site install list: `INSTALL_APPS` in [`example.env`](../example.env).
|
||||
|
||||
## 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` should pass the Jenkins pipeline (`Jenkinsfile`) before Coolify deploy.
|
||||
|
||||
Local check:
|
||||
|
||||
```bash
|
||||
make ci
|
||||
```
|
||||
| Image pull failed | Check registry login on Coolify host; verify tag exists in Forgejo Packages |
|
||||
| create-site fails on LMS | Ensure `payments` is in `INSTALL_APPS` before `lms` |
|
||||
| 502 / unhealthy frontend | Wait for create-site; check `backend` health |
|
||||
| Wrong site | `SITE_NAME` and `FRAPPE_SITE_NAME_HEADER` must match Coolify domain |
|
||||
|
|
|
|||
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`
|
||||
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`
|
||||
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**
|
||||
**Artifacts:** `dist/coolify-image.env`, `dist/docker-compose.coolify.yml`, `dist/image-reference.txt`
|
||||
|
||||
## 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`
|
||||
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**
|
||||
Same as before — **Pipeline from SCM** or **Multibranch** with **Discover branches**.
|
||||
|
||||
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
|
||||
- Username: `epistemophiliac`
|
||||
- Password: Forgejo personal access token (repo read scope)
|
||||
Download `dist/coolify-image.env` from Jenkins artifacts, or use:
|
||||
|
||||
## Pipeline stages
|
||||
```env
|
||||
CUSTOM_IMAGE=git.aexoradao.com/epistemophiliac/erpnext
|
||||
CUSTOM_TAG=main-<commit-sha>
|
||||
PULL_POLICY=always
|
||||
```
|
||||
|
||||
| Stage | What it checks |
|
||||
|-------|----------------|
|
||||
| **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` |
|
||||
Set those in Coolify before deploy (or use `CUSTOM_TAG=main` for latest main build).
|
||||
|
||||
**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
|
||||
|
||||
### `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`
|
||||
|
||||
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
|
||||
stat -c '%g' /var/run/docker.sock
|
||||
```
|
||||
### Registry push 401/403
|
||||
|
||||
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).
|
||||
# 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
|
||||
ERPNEXT_VERSION=v16.22.0
|
||||
# Custom image built by Jenkins (apps: ERPNext, HRMS, Lending, LMS + payments)
|
||||
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)
|
||||
DB_PASSWORD=changeme
|
||||
|
|
@ -16,6 +21,9 @@ ADMIN_PASSWORD=changeme
|
|||
# Nginx site header — for single-site Coolify, same as SITE_NAME
|
||||
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)
|
||||
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
|
||||
# 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
|
||||
|
||||
echo "=== erpnext CI (local) ==="
|
||||
|
|
@ -10,4 +10,15 @@ bash scripts/ci/ci-readiness.sh .
|
|||
bash scripts/ci/validate-docker-compose.sh .
|
||||
bash scripts/ci/jenkins-bootstrap.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