1 Commits
main ... compat

Author SHA1 Message Date
Revant Nandgaonkar
09060341c7 fix: make changes for backward compat
make changes for refactor to work on existing setups
2022-03-07 12:30:23 +05:30
140 changed files with 5373 additions and 5024 deletions

View File

@@ -1,4 +1,7 @@
# frappe_docker .dockerignore file
.travis.yml
README.md README.md
LICENSE LICENSE.md
.gitignore .gitignore
compose*.yaml docker-*.yml

View File

@@ -6,21 +6,36 @@ updates:
interval: daily interval: daily
- package-ecosystem: docker - package-ecosystem: docker
directory: images/bench directory: build/bench
schedule: schedule:
interval: daily interval: daily
- package-ecosystem: docker - package-ecosystem: docker
directory: images/production directory: build/erpnext-nginx
schedule: schedule:
interval: daily interval: daily
- package-ecosystem: docker - package-ecosystem: docker
directory: images/custom directory: build/erpnext-worker
schedule: schedule:
interval: daily interval: daily
- package-ecosystem: pip - package-ecosystem: docker
directory: / directory: build/frappe-nginx
schedule:
interval: daily
- package-ecosystem: docker
directory: build/frappe-socketio
schedule:
interval: daily
- package-ecosystem: docker
directory: build/frappe-worker
schedule:
interval: daily
- package-ecosystem: npm
directory: build/frappe-socketio
schedule: schedule:
interval: daily interval: daily

20
.github/scripts/get-latest-tags.sh vendored Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -e
set -x
get_tag() {
tags=$(git ls-remote --refs --tags --sort='v:refname' "https://github.com/$1" "v$2.*")
tag=$(echo "$tags" | tail -n1 | sed 's/.*\///')
echo "$tag"
}
FRAPPE_VERSION=$(get_tag frappe/frappe "$VERSION")
ERPNEXT_VERSION=$(get_tag frappe/erpnext "$VERSION")
cat <<EOL >>"$GITHUB_ENV"
FRAPPE_VERSION=$FRAPPE_VERSION
ERPNEXT_VERSION=$ERPNEXT_VERSION
GIT_BRANCH=version-$VERSION
VERSION=$VERSION
EOL

View File

@@ -1,78 +0,0 @@
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from typing import Literal
Repo = Literal["frappe", "erpnext"]
MajorVersion = Literal["12", "13", "14", "15", "develop"]
def get_latest_tag(repo: Repo, version: MajorVersion) -> str:
if version == "develop":
return "develop"
regex = rf"v{version}.*"
refs = subprocess.check_output(
(
"git",
"-c",
"versionsort.suffix=-",
"ls-remote",
"--refs",
"--tags",
"--sort=v:refname",
f"https://github.com/frappe/{repo}",
str(regex),
),
encoding="UTF-8",
).split()[1::2]
if not refs:
raise RuntimeError(f'No tags found for version "{regex}"')
ref = refs[-1]
matches: list[str] = re.findall(regex, ref)
if not matches:
raise RuntimeError(f'Can\'t parse tag from ref "{ref}"')
return matches[0]
def update_env(file_name: str, frappe_tag: str, erpnext_tag: str | None = None):
text = f"\nFRAPPE_VERSION={frappe_tag}"
if erpnext_tag:
text += f"\nERPNEXT_VERSION={erpnext_tag}"
with open(file_name, "a") as f:
f.write(text)
def _print_resp(frappe_tag: str, erpnext_tag: str | None = None):
print(json.dumps({"frappe": frappe_tag, "erpnext": erpnext_tag}))
def main(_args: list[str]) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo", choices=["frappe", "erpnext"], required=True)
parser.add_argument(
"--version", choices=["12", "13", "14", "15", "develop"], required=True
)
args = parser.parse_args(_args)
frappe_tag = get_latest_tag("frappe", args.version)
if args.repo == "erpnext":
erpnext_tag = get_latest_tag("erpnext", args.version)
else:
erpnext_tag = None
file_name = os.getenv("GITHUB_ENV")
if file_name:
update_env(file_name, frappe_tag, erpnext_tag)
_print_resp(frappe_tag, erpnext_tag)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))

View File

@@ -1,28 +0,0 @@
import os
import re
def get_erpnext_version():
erpnext_version = os.getenv("ERPNEXT_VERSION")
assert erpnext_version, "No ERPNext version set"
return erpnext_version
def update_env(erpnext_version: str):
with open("example.env", "r+") as f:
content = f.read()
content = re.sub(
rf"ERPNEXT_VERSION=.*", f"ERPNEXT_VERSION={erpnext_version}", content
)
f.seek(0)
f.truncate()
f.write(content)
def main() -> int:
update_env(get_erpnext_version())
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,30 +0,0 @@
import os
import re
def get_versions():
frappe_version = os.getenv("FRAPPE_VERSION")
erpnext_version = os.getenv("ERPNEXT_VERSION")
assert frappe_version, "No Frappe version set"
assert erpnext_version, "No ERPNext version set"
return frappe_version, erpnext_version
def update_pwd(frappe_version: str, erpnext_version: str):
with open("pwd.yml", "r+") as f:
content = f.read()
content = re.sub(
rf"frappe/erpnext:.*", f"frappe/erpnext:{erpnext_version}", content
)
f.seek(0)
f.truncate()
f.write(content)
def main() -> int:
update_pwd(*get_versions())
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -5,9 +5,8 @@ on:
branches: branches:
- main - main
paths: paths:
- images/bench/** - build/bench/**
- docker-bake.hcl - docker-bake.hcl
- .github/workflows/build_bench.yml
schedule: schedule:
# Every day at 12:00 pm # Every day at 12:00 pm
@@ -20,40 +19,28 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v3
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:latest
platforms: all
- name: Setup Buildx - name: Setup Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v1
- name: Set Environment Variables
run: cat example.env | grep -o '^[^#]*' >> "$GITHUB_ENV"
- name: Get Bench Latest Version
run: echo "LATEST_BENCH_RELEASE=$(curl -s 'https://api.github.com/repos/frappe/bench/releases/latest' | jq -r '.tag_name')" >> "$GITHUB_ENV"
- name: Build and test - name: Build and test
uses: docker/bake-action@v6.9.0 uses: docker/bake-action@v1.7.0
with: with:
source: . files: docker-bake.hcl
targets: bench-test targets: bench-test
- name: Login - name: Login
if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
uses: docker/login-action@v3 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push - name: Push
if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
uses: docker/bake-action@v6.9.0 uses: docker/bake-action@v1.7.0
with: with:
targets: bench targets: bench
files: docker-bake.hcl
push: true push: true
set: "*.platform=linux/amd64,linux/arm64"

View File

@@ -1,17 +1,17 @@
name: Develop build name: Build Develop
on: on:
pull_request: pull_request:
branches: branches:
- main - main
paths: paths:
- images/production/**
- overrides/**
- tests/**
- compose.yaml
- docker-bake.hcl
- example.env
- .github/workflows/build_develop.yml - .github/workflows/build_develop.yml
- build/**
- installation/**
- tests/**
- .dockerignore
- docker-bake.hcl
- env-example
schedule: schedule:
# Every day at 12:00 pm # Every day at 12:00 pm
@@ -19,15 +19,65 @@ on:
workflow_dispatch: workflow_dispatch:
env:
IS_AUTHORIZED_RUN: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
jobs: jobs:
build: build:
uses: ./.github/workflows/docker-build-push.yml name: Frappe & ERPNext
with: runs-on: ubuntu-latest
repo: erpnext services:
version: develop registry:
push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} image: registry:2
python_version: 3.11.6 ports:
node_version: 20.19.2 - 5000:5000
secrets: steps:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - name: Checkout
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} uses: actions/checkout@v3
- name: Setup Buildx
uses: docker/setup-buildx-action@v1
with:
driver-opts: network=host
- name: Login
uses: docker/login-action@v1
if: env.IS_AUTHORIZED_RUN == 'true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build Frappe
uses: docker/bake-action@v1.7.0
with:
files: docker-bake.hcl
targets: frappe-develop-test
load: true
- name: Push Frappe to local registry
uses: docker/bake-action@v1.7.0
with:
files: docker-bake.hcl
targets: frappe-develop-test-local
push: true
- name: Test Frappe
run: ./tests/test-frappe.sh
- name: Build ERPNext
uses: docker/bake-action@v1.7.0
with:
files: docker-bake.hcl
targets: erpnext-develop-test
load: true
- name: Test ERPNext
run: ./tests/test-erpnext.sh
- name: Push
if: env.IS_AUTHORIZED_RUN == 'true'
uses: docker/bake-action@v1.7.0
with:
files: docker-bake.hcl
targets: frappe-develop,erpnext-develop
push: true

View File

@@ -1,106 +1,131 @@
name: Stable build name: Build Stable
on: on:
pull_request: pull_request:
branches: branches:
- main - main
paths: paths:
- images/production/**
- overrides/**
- tests/**
- compose.yaml
- docker-bake.hcl
- example.env
- .github/workflows/build_stable.yml - .github/workflows/build_stable.yml
- .github/scripts/get-latest-tags.sh
- build/**
- installation/**
- tests/**
- .dockerignore
- docker-bake.hcl
- env-example
push: push:
branches: branches:
- main - main
paths: paths:
- images/production/** - .github/workflows/build_stable.yml
- overrides/** - .github/scripts/get-latest-tags.sh
- build/**
- installation/**
- tests/** - tests/**
- compose.yaml - .dockerignore
- docker-bake.hcl - docker-bake.hcl
- example.env - env-example
# Triggered from frappe/frappe and frappe/erpnext on releases # Triggered from frappe/frappe and frappe/erpnext on releases
repository_dispatch: repository_dispatch:
workflow_dispatch: workflow_dispatch:
env:
IS_AUTHORIZED_RUN: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
jobs: jobs:
v14: build:
uses: ./.github/workflows/docker-build-push.yml name: Frappe & ERPNext
with:
repo: erpnext
version: "14"
push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
python_version: 3.10.13
node_version: 16.20.2
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
v15:
uses: ./.github/workflows/docker-build-push.yml
with:
repo: erpnext
version: "15"
push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
python_version: 3.11.6
node_version: 20.19.2
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
update_versions:
name: Update example.env and pwd.yml
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} strategy:
needs: v15 matrix:
version: [12, 13]
services:
registry:
image: registry:2
ports:
- 5000:5000
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v3
- name: Setup Python - name: Setup Buildx
uses: actions/setup-python@v6 uses: docker/setup-buildx-action@v1
with: with:
python-version: "3.10" driver-opts: network=host
- name: Login
uses: docker/login-action@v1
if: env.IS_AUTHORIZED_RUN == 'true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get latest versions - name: Get latest versions
run: python3 ./.github/scripts/get_latest_tags.py --repo erpnext --version 15 run: ./.github/scripts/get-latest-tags.sh
env:
VERSION: ${{ matrix.version }}
- name: Update - name: Build Frappe
run: | uses: docker/bake-action@v1.7.0
python3 ./.github/scripts/update_example_env.py with:
python3 ./.github/scripts/update_pwd.py files: docker-bake.hcl
targets: frappe-stable-test
load: true
- name: Push - name: Push Frappe to local registry
run: | uses: docker/bake-action@v1.7.0
git config --global user.name github-actions with:
git config --global user.email github-actions@github.com files: docker-bake.hcl
git add example.env pwd.yml targets: frappe-stable-test-local
if [ -z "$(git status --porcelain)" ]; then push: true
echo "versions did not change, exiting."
exit 0 - name: Test Frappe
else if: github.event_name == 'pull_request'
echo "version changed, pushing changes..." run: ./tests/test-frappe.sh
git commit -m "chore: Update example.env"
git pull --rebase - name: Build ERPNext
git push origin main uses: docker/bake-action@v1.7.0
fi with:
files: docker-bake.hcl
targets: erpnext-stable-test
load: true
- name: Test ERPNext
if: github.event_name == 'pull_request'
run: ./tests/test-erpnext.sh
- name: Push Frappe
if: env.IS_AUTHORIZED_RUN == 'true'
uses: docker/bake-action@v1.7.0
with:
files: docker-bake.hcl
targets: frappe-stable
push: true
env:
GIT_TAG: ${{ env.FRAPPE_VERSION }}
- name: Push ERPNext
if: env.IS_AUTHORIZED_RUN == 'true'
uses: docker/bake-action@v1.7.0
with:
files: docker-bake.hcl
targets: erpnext-stable
push: true
env:
GIT_TAG: ${{ env.ERPNEXT_VERSION }}
release_helm: release_helm:
name: Release Helm name: Release Helm
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} if: github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request'
needs: v15 needs: build
steps: steps:
- name: Setup deploy key - name: Setup deploy key
uses: webfactory/ssh-agent@v0.9.1 uses: webfactory/ssh-agent@v0.5.4
with: with:
ssh-private-key: ${{ secrets.HELM_DEPLOY_KEY }} ssh-private-key: ${{ secrets.HELM_DEPLOY_KEY }}
@@ -113,4 +138,4 @@ jobs:
run: | run: |
git clone git@github.com:frappe/helm.git && cd helm git clone git@github.com:frappe/helm.git && cd helm
pip install -r release_wizard/requirements.txt pip install -r release_wizard/requirements.txt
./release_wizard/wizard 15 patch --remote origin --ci ./release_wizard/wizard 13 patch --remote origin --ci

View File

@@ -1,101 +0,0 @@
name: Build
on:
workflow_call:
inputs:
repo:
required: true
type: string
description: "'erpnext' or 'frappe'"
version:
required: true
type: string
description: "Major version, git tags should match 'v{version}.*'; or 'develop'"
push:
required: true
type: boolean
python_version:
required: true
type: string
description: Python Version
node_version:
required: true
type: string
description: NodeJS Version
secrets:
DOCKERHUB_USERNAME:
required: true
DOCKERHUB_TOKEN:
required: true
jobs:
build:
name: Build
runs-on: ubuntu-latest
services:
registry:
image: docker.io/registry:2
ports:
- 5000:5000
strategy:
matrix:
arch: [amd64, arm64]
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:latest
platforms: all
- name: Setup Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
platforms: linux/${{ matrix.arch }}
- name: Get latest versions
run: python3 ./.github/scripts/get_latest_tags.py --repo ${{ inputs.repo }} --version ${{ inputs.version }}
- name: Set build args
run: |
echo "PYTHON_VERSION=${{ inputs.python_version }}" >> "$GITHUB_ENV"
echo "NODE_VERSION=${{ inputs.node_version }}" >> "$GITHUB_ENV"
- name: Build
uses: docker/bake-action@v6.9.0
with:
source: .
push: true
env:
REGISTRY_USER: localhost:5000/frappe
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m venv venv
venv/bin/pip install -r requirements-test.txt
- name: Test
run: venv/bin/pytest --color=yes
- name: Login
if: ${{ inputs.push }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push
if: ${{ inputs.push }}
uses: docker/bake-action@v6.9.0
with:
push: true
set: "*.platform=linux/amd64,linux/arm64"

View File

@@ -13,19 +13,25 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v3
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v3
with:
python-version: "3.10.6"
# For shfmt pre-commit hook # For shfmt pre-commit hook
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v2
with: with:
go-version: "^1.14" go-version: "^1.14"
- name: Cache
uses: actions/cache@v2
with:
path: |
~/.cache/pre-commit
~/.cache/pip
key: lint-${{ hashFiles('.pre-commit-config.yaml') }}
- name: Install pre-commit - name: Install pre-commit
run: pip install -U pre-commit run: pip install -U pre-commit

View File

@@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v3
- name: Update pre-commit hooks - name: Update pre-commit hooks
uses: vrslev/pre-commit-autoupdate@v1.0.0 uses: vrslev/pre-commit-autoupdate@v1.0.0
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v3
with: with:
branch: pre-commit-autoupdate branch: pre-commit-autoupdate
title: "chore(deps): Update pre-commit hooks" title: "chore(deps): Update pre-commit hooks"

View File

@@ -9,7 +9,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v10 - uses: actions/stale@v4
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: This issue has been automatically marked as stale. You have a week to explain why you believe this is an error. stale-issue-message: This issue has been automatically marked as stale. You have a week to explain why you believe this is an error.

62
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Integration Test
on:
push:
branches:
- main
paths:
- .github/workflows/test.yml
- .github/scripts/get-latest-tags.sh
- build/**
- installation/**
- tests/**
- .dockerignore
- docker-bake.hcl
- env-example
pull_request:
branches:
- main
paths:
- .github/workflows/test.yml
- .github/scripts/get-latest-tags.sh
- build/**
- installation/**
- tests/**
- .dockerignore
- docker-bake.hcl
- env-example
workflow_dispatch:
schedule:
# Every day at 01:00 am
# Develop images are built at 12:00 pm, we want to use them
# Also, we don't build new images on this event
- cron: 0 1 * * *
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get latest versions
if: github.event_name != 'schedule'
run: ./.github/scripts/get-latest-tags.sh
env:
VERSION: 13
- name: Build
if: github.event_name != 'schedule'
uses: docker/bake-action@v1.7.0
with:
files: docker-bake.hcl
targets: frappe-develop,frappe-stable
load: true
env:
GIT_TAG: ${{ env.FRAPPE_VERSION }}
- name: Test
run: ./tests/integration-test.sh

22
.gitignore vendored
View File

@@ -1,30 +1,18 @@
*.code-workspace
# Environment Variables # Environment Variables
.env .env
# mounted volume # mounted volume
sites sites
development/* development
!development/README.md !development/README.md
!development/installer.py deploy_key
!development/apps-example.json deploy_key.pub
!development/vscode-example/
# Pycharm # Pycharm
.idea .idea
# VS Code
.vscode/**
!.vscode/extensions.json
# VS Code devcontainer # VS Code devcontainer
.devcontainer .devcontainer
*.code-workspace
# Python
*.pyc
__pycache__
venv
# NodeJS
node_modules

View File

@@ -1,42 +1,35 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v4.1.0
hooks: hooks:
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.19.1 rev: v2.31.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 25.1.0 rev: 22.1.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: 6.0.1 rev: 5.10.1
hooks: hooks:
- id: isort - id: isort
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8 rev: v2.5.1
hooks: hooks:
- id: prettier - id: prettier
additional_dependencies:
- prettier@3.5.2
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.4.1 rev: v2.1.0
hooks: hooks:
- id: codespell - id: codespell
args:
- -L
- "ro"
- repo: local - repo: local
hooks: hooks:
@@ -49,7 +42,7 @@ repos:
types: [shell] types: [shell]
- repo: https://github.com/shellcheck-py/shellcheck-py - repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1 rev: v0.8.0.4
hooks: hooks:
- id: shellcheck - id: shellcheck
args: [-x] args: [-x]

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
installation/docker-compose-custom.yml

View File

@@ -6,7 +6,7 @@ In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression, size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socioeconomic status, nationality, personal level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation. appearance, race, religion, or sexual identity and orientation.
## Our Standards ## Our Standards

View File

@@ -1,6 +1,10 @@
# Contribution Guidelines # Contribution Guidelines
Before publishing a PR, please test builds locally. Before publishing a PR, please test builds locally:
- with docker-compose for production,
- with and without nginx proxy,
- with VSCode for testing environments (only for frappe/bench image).
On each PR that contains changes relevant to Docker builds, images are being built and tested in our CI (GitHub Actions). On each PR that contains changes relevant to Docker builds, images are being built and tested in our CI (GitHub Actions).
@@ -35,47 +39,48 @@ pre-commit run --all-files
## Build ## Build
We use [Docker Buildx Bake](https://docs.docker.com/engine/reference/commandline/buildx_bake/). To build the images, run command below:
```shell ```shell
FRAPPE_VERSION=... ERPNEXT_VERSION=... docker buildx bake <targets> # *...* — targets from docker-bake.hcl,
# e.g. bench-build, frappe-socketio-develop or erpnext-nginx-stable.
# Stable builds require GIT_BRANCH (e.g. v13.15.0), IMAGE_TAG (version-13), VERSION (13)
# environment variables set.
docker buildx bake -f docker-bake.hcl *...*
``` ```
Available targets can be found in `docker-bake.hcl`.
## Test ## Test
We use [pytest](https://pytest.org) for our integration tests. ### Ping site
Install Python test requirements: Lightweight test that just checks if site will be available after creation.
Frappe:
```shell ```shell
python3 -m venv venv ./tests/test-frappe.sh
source venv/bin/activate
pip install -r requirements-test.txt
``` ```
Run pytest: ERPNext:
```shell ```shell
pytest ./tests/test-erpnext.sh
```
### Integration test
Tests frappe-bench-like commands, for example, `backup` and `restore`.
```shell
./tests/integration-test.sh
``` ```
# Documentation # Documentation
Place relevant markdown files in the `docs` directory and index them in README.md located at the root of repo. Place relevant markdown file(s) in the `docs` directory and index them in README.md located at the root of repo.
# Wiki
Add alternatives that can be used optionally along with frappe_docker. Add articles to list on home page as well.
# Frappe and ERPNext updates # Frappe and ERPNext updates
Each Frappe/ERPNext release triggers new stable images builds as well as bump to helm chart. Each Frappe/ERPNext release triggers new stable images builds as well as bump to helm chart.
# Maintenance
In case of new release of Debian. e.g. bullseye to bookworm. Change following files:
- `images/erpnext/Containerfile` and `images/custom/Containerfile`: Change the files to use new debian release, make sure new python version tag that is available on new debian release image. e.g. 3.9.9 (bullseye) to 3.9.17 (bookworm) or 3.10.5 (bullseye) to 3.10.12 (bookworm). Make sure apt-get packages and wkhtmltopdf version are also upgraded accordingly.
- `images/bench/Dockerfile`: Change the files to use new debian release. Make sure apt-get packages and wkhtmltopdf version are also upgraded accordingly.
Change following files on release of ERPNext
- `.github/workflows/build_stable.yml`: Add the new release step under `jobs` and remove the unmaintained one. e.g. In case v12, v13 available, v14 will be added and v12 will be removed on release of v14. Also change the `needs:` for later steps to `v14` from `v13`.

104
README.md
View File

@@ -1,89 +1,69 @@
[![Build Stable](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml) [![Build Stable](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml)
[![Build Develop](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml) [![Build Develop](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml)
Everything about [Frappe](https://github.com/frappe/frappe) and [ERPNext](https://github.com/frappe/erpnext) in containers. ## Getting Started
# Getting Started
To get started you need [Docker](https://docs.docker.com/get-docker/), [docker-compose](https://docs.docker.com/compose/), and [git](https://docs.github.com/en/get-started/getting-started-with-git/set-up-git) setup on your machine. For Docker basics and best practices refer to Docker's [documentation](http://docs.docker.com).
Once completed, chose one of the following two sections for next steps.
### Try in Play With Docker ### Try in Play With Docker
To play in an already set up sandbox, in your browser, click the button below:
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml"> <a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD"/> <img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD"/>
</a> </a>
### Try on your Dev environment Wait for 5 minutes for ERPNext site to be created or check `site-creator` container logs before opening browser on port 80. (username: `Administrator`, password: `admin`)
First clone the repo: ### Setting up Pre-requisites
This repository requires Docker, docker-compose and Git to be setup on the instance to be used.
For Docker basics and best practices. Refer Docker [documentation](http://docs.docker.com).
### Cloning the repository and preliminary steps
Clone this repository somewhere in your system:
```sh ```sh
git clone https://github.com/frappe/frappe_docker git clone https://github.com/frappe/frappe_docker.git
cd frappe_docker cd frappe_docker
``` ```
Then run: `docker compose -f pwd.yml up -d` ## Production Setup
### To run on ARM64 architecture follow this instructions It takes care of the following:
After you clone the repo and `cd frappe_docker`, run this command to build multi-architecture images specifically for ARM64. - Setting up the desired version of Frappe/ERPNext.
- Setting up all the system requirements: eg. MariaDB, Node, Redis.
- Configure networking for remote access and setting up LetsEncrypt.
`docker buildx bake --no-cache --set "*.platform=linux/arm64"` It doesn't take care of the following:
and then - Cron Job to backup sites is not created by default.
- Use `CronJob` on k8s or refer wiki for alternatives.
- add `platform: linux/arm64` to all services in the `pwd.yml` 1. Single Server Installs
- replace the current specified versions of erpnext image on `pwd.yml` with `:latest` 1. [Single bench](docs/single-bench.md). Easiest Install!
2. [Multi bench](docs/multi-bench.md)
2. Multi Server Installs
1. [Docker Swarm](docs/docker-swarm.md)
2. [Kubernetes](https://helm.erpnext.com)
3. [Site Operations](docs/site-operations.md)
4. [Environment Variables](docs/environment-variables.md)
5. [Custom apps for production](docs/custom-apps-for-production.md)
6. [Tips for moving deployments](docs/tips-for-moving-deployments.md)
7. [Wiki for optional recipes](https://github.com/frappe/frappe_docker/wiki)
Then run: `docker compose -f pwd.yml up -d` ## Development Setup
## Final steps It takes care of complete setup to develop with Frappe/ERPNext and Bench, Including the following features:
Wait for 5 minutes for ERPNext site to be created or check `create-site` container logs before opening browser on port 8080. (username: `Administrator`, password: `admin`) - VSCode containers integration
- VSCode Python debugger
- Pre-configured Docker containers for an easy start
If you ran in a Dev Docker environment, to view container logs: `docker compose -f pwd.yml logs -f create-site`. Don't worry about some of the initial error messages, some services take a while to become ready, and then they go away. [Start development](development).
# Documentation ## Contributing
### [Frequently Asked Questions](https://github.com/frappe/frappe_docker/wiki/Frequently-Asked-Questions) - [Frappe Docker Images](CONTRIBUTING.md)
- [Frappe Framework](https://github.com/frappe/frappe#contributing)
### [Production](#production) - [ERPNext](https://github.com/frappe/erpnext#contributing)
- [frappe/bench](https://github.com/frappe/bench)
- [List of containers](docs/container-setup/01-overview.md)
- [Single Compose Setup](docs/single-compose-setup.md)
- [Environment Variables](docs/container-setup/env-variables.md)
- [Single Server Example](docs/single-server-example.md)
- [Setup Options](docs/setup-options.md)
- [Site Operations](docs/site-operations.md)
- [Backup and Push Cron Job](docs/backup-and-push-cronjob.md)
- [Port Based Multi Tenancy](docs/port-based-multi-tenancy.md)
- [Migrate from multi-image setup](docs/migrate-from-multi-image-setup.md)
- [running on linux/mac](docs/setup_for_linux_mac.md)
- [TLS for local deployment](docs/tls-for-local-deployment.md)
### [Custom Images](#custom-images)
- [Custom Apps](docs/container-setup/02-build-setup.md)
- [Build Version 10 Images](docs/build-version-10-images.md)
### [Development](#development)
- [Development using containers](docs/development.md)
- [Bench Console and VSCode Debugger](docs/bench-console-and-vscode-debugger.md)
- [Connect to localhost services](docs/connect-to-localhost-services-from-containers-for-local-app-development.md)
### [Troubleshoot](docs/troubleshoot.md)
# Contributing
If you want to contribute to this repo refer to [CONTRIBUTING.md](CONTRIBUTING.md)
This repository is only for container related stuff. You also might want to contribute to:
- [Frappe framework](https://github.com/frappe/frappe#contributing),
- [ERPNext](https://github.com/frappe/erpnext#contributing),
- [Frappe Bench](https://github.com/frappe/bench).

View File

@@ -1,18 +0,0 @@
[
{
"url": "https://github.com/frappe/erpnext",
"branch": "version-15"
},
{
"url": "https://github.com/frappe/hrms",
"branch": "version-15"
},
{
"url": "https://github.com/frappe/helpdesk",
"branch": "main"
},
{
"url": "https://github.com/frappe/payments",
"branch": "version-15"
}
]

View File

@@ -1,9 +1,9 @@
FROM debian:bookworm-slim AS bench FROM debian:bullseye-slim as bench
LABEL author=frappé LABEL author=frappé
ARG GIT_REPO=https://github.com/frappe/bench.git ARG GIT_REPO=https://github.com/frappe/bench.git
ARG GIT_BRANCH=v5.x ARG GIT_BRANCH=develop
RUN apt-get update \ RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
@@ -13,16 +13,14 @@ RUN apt-get update \
postgresql-client \ postgresql-client \
gettext-base \ gettext-base \
wget \ wget \
# for ERPNext v12
# TODO: Remove after v12 is deprecated
python2 \
# for PDF # for PDF
libssl-dev \ libssl-dev \
fonts-cantarell \ fonts-cantarell \
xfonts-75dpi \ xfonts-75dpi \
xfonts-base \ xfonts-base \
# weasyprint dependencies
libpango-1.0-0 \
libharfbuzz0b \
libpangoft2-1.0-0 \
libpangocairo-1.0-0 \
# to work inside the container # to work inside the container
locales \ locales \
build-essential \ build-essential \
@@ -47,41 +45,22 @@ RUN apt-get update \
libsasl2-dev \ libsasl2-dev \
libtiff5-dev \ libtiff5-dev \
libwebp-dev \ libwebp-dev \
pkg-config \
redis-tools \ redis-tools \
rlwrap \ rlwrap \
tk8.6-dev \ tk8.6-dev \
ssh-client \ ssh-client \
# VSCode container requirements # VSCode container requirements
net-tools \ net-tools \
# For pyenv build dependencies
# https://github.com/frappe/frappe_docker/issues/840#issuecomment-1185206895
make \
# For pandas
libbz2-dev \
# For bench execute
libsqlite3-dev \
# For other dependencies
zlib1g-dev \
libreadline-dev \
llvm \
libncurses5-dev \
libncursesw5-dev \
xz-utils \
tk-dev \
liblzma-dev \
file \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
&& dpkg-reconfigure --frontend=noninteractive locales && dpkg-reconfigure --frontend=noninteractive locales
# Detect arch and install wkhtmltopdf # Detect arch and install wkhtmltopdf
ARG WKHTMLTOPDF_VERSION=0.12.6.1-3 ENV WKHTMLTOPDF_VERSION 0.12.6-1
ARG WKHTMLTOPDF_DISTRO=bookworm
RUN if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \ RUN if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \
&& if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \ && if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \
&& downloaded_file=wkhtmltox_${WKHTMLTOPDF_VERSION}.${WKHTMLTOPDF_DISTRO}_${ARCH}.deb \ && downloaded_file=wkhtmltox_$WKHTMLTOPDF_VERSION.buster_${ARCH}.deb \
&& wget -q https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \ && wget -q https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \
&& dpkg -i $downloaded_file \ && dpkg -i $downloaded_file \
&& rm $downloaded_file && rm $downloaded_file
@@ -97,42 +76,42 @@ USER frappe
WORKDIR /home/frappe WORKDIR /home/frappe
# Install Python via pyenv # Install Python via pyenv
ENV PYTHON_VERSION_V14=3.10.13 # Python 3.7 sits here for ERPNext version-12
ENV PYTHON_VERSION=3.11.6 # TODO: Remove Python 3.7 when version-12 will not be supported
ENV PYENV_ROOT=/home/frappe/.pyenv ENV PYTHON_VERSION_V12=3.7.12
ENV PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH ENV PYTHON_VERSION=3.9.9
ENV PYENV_ROOT /home/frappe/.pyenv
ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
# From https://github.com/pyenv/pyenv#basic-github-checkout # From https://github.com/pyenv/pyenv#basic-github-checkout
RUN git clone --depth 1 https://github.com/pyenv/pyenv.git .pyenv \ RUN git clone --depth 1 https://github.com/pyenv/pyenv.git .pyenv \
&& pyenv install $PYTHON_VERSION_V14 \ && pyenv install $PYTHON_VERSION_V12 \
&& pyenv install $PYTHON_VERSION \ && pyenv install $PYTHON_VERSION \
&& PYENV_VERSION=$PYTHON_VERSION_V14 pip install --no-cache-dir virtualenv \ && pyenv global $PYTHON_VERSION $PYTHON_VERSION_V12 \
&& PYENV_VERSION=$PYTHON_VERSION pip install --no-cache-dir virtualenv \
&& pyenv global $PYTHON_VERSION $PYTHON_VERSION_v14 \
&& sed -Ei -e '/^([^#]|$)/ {a export PYENV_ROOT="/home/frappe/.pyenv" a export PATH="$PYENV_ROOT/bin:$PATH" a ' -e ':a' -e '$!{n;ba};}' ~/.profile \ && sed -Ei -e '/^([^#]|$)/ {a export PYENV_ROOT="/home/frappe/.pyenv" a export PATH="$PYENV_ROOT/bin:$PATH" a ' -e ':a' -e '$!{n;ba};}' ~/.profile \
&& echo 'eval "$(pyenv init --path)"' >>~/.profile \ && echo 'eval "$(pyenv init --path)"' >>~/.profile \
&& echo 'eval "$(pyenv init -)"' >>~/.bashrc && echo 'eval "$(pyenv init -)"' >>~/.bashrc
# Clone and install bench in the local user home directory # Clone and install bench in the local user home directory
# For development, bench source is located in ~/.bench # For development, bench source is located in ~/.bench
ENV PATH=/home/frappe/.local/bin:$PATH ENV PATH /home/frappe/.local/bin:$PATH
# Skip editable-bench warning # Skip editable-bench warning
# https://github.com/frappe/bench/commit/20560c97c4246b2480d7358c722bc9ad13606138 # https://github.com/frappe/bench/commit/20560c97c4246b2480d7358c722bc9ad13606138
RUN git clone ${GIT_REPO} --depth 1 -b ${GIT_BRANCH} .bench \ RUN git clone ${GIT_REPO} --depth 1 -b ${GIT_BRANCH} .bench \
&& pip install --no-cache-dir --user -e .bench \ && pip install --user -e .bench \
&& echo "export PATH=/home/frappe/.local/bin:\$PATH" >>/home/frappe/.bashrc \ && echo "export PATH=/home/frappe/.local/bin:\$PATH" >>/home/frappe/.bashrc \
&& echo "export BENCH_DEVELOPER=1" >>/home/frappe/.bashrc && echo "export BENCH_DEVELOPER=1" >>/home/frappe/.bashrc
# Install Node via nvm # Install Node via nvm
ENV NODE_VERSION_14=16.20.2 ENV NODE_VERSION=14.18.1
ENV NODE_VERSION=20.19.2 ENV NODE_VERSION_FRAPPEV11=10.24.1
ENV NVM_DIR=/home/frappe/.nvm ENV NVM_DIR /home/frappe/.nvm
ENV PATH=${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH} ENV PATH ${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}
RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \ RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash \
&& . ${NVM_DIR}/nvm.sh \ && . ${NVM_DIR}/nvm.sh \
&& nvm install ${NODE_VERSION_14} \ && nvm install ${NODE_VERSION_FRAPPEV11} \
&& nvm use v${NODE_VERSION_14} \ && nvm use v${NODE_VERSION_FRAPPEV11} \
&& npm install -g yarn \ && npm install -g yarn \
&& nvm install ${NODE_VERSION} \ && nvm install ${NODE_VERSION} \
&& nvm use v${NODE_VERSION} \ && nvm use v${NODE_VERSION} \
@@ -146,7 +125,7 @@ RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh |
EXPOSE 8000-8005 9000-9005 6787 EXPOSE 8000-8005 9000-9005 6787
FROM bench AS bench-test FROM bench as bench-test
# Print version and verify bashrc is properly sourced so that everything works # Print version and verify bashrc is properly sourced so that everything works
# in the interactive shell and Dockerfile # in the interactive shell and Dockerfile

View File

@@ -0,0 +1,35 @@
ARG NODE_IMAGE_TAG=14-bullseye-slim
ARG DOCKER_REGISTRY_PREFIX=frappe
ARG IMAGE_TAG=develop
FROM node:${NODE_IMAGE_TAG} as builder
ARG GIT_REPO=https://github.com/frappe/erpnext
ARG GIT_BRANCH=develop
ARG FRAPPE_BRANCH=${GIT_BRANCH}
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
python2 \
git \
build-essential \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY build/erpnext-nginx/install_app.sh /install_app
RUN chmod +x /install_app \
&& /install_app erpnext ${GIT_REPO} ${GIT_BRANCH} ${FRAPPE_BRANCH}
FROM ${DOCKER_REGISTRY_PREFIX}/frappe-nginx:${IMAGE_TAG}
COPY --from=builder --chown=1000:1000 /home/frappe/frappe-bench/sites/ /var/www/html/
COPY --from=builder /rsync /rsync
RUN echo "erpnext" >> /var/www/html/apps.txt
VOLUME [ "/assets" ]
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,40 @@
#!/bin/bash
set -e
APP_NAME=${1}
APP_REPO=${2}
APP_BRANCH=${3}
FRAPPE_BRANCH=${4}
[ "${APP_BRANCH}" ] && BRANCH="-b ${APP_BRANCH}"
mkdir -p /home/frappe/frappe-bench
cd /home/frappe/frappe-bench
mkdir -p apps "sites/assets/${APP_NAME}"
echo -ne "frappe\n${APP_NAME}" >sites/apps.txt
git clone --depth 1 -b "${FRAPPE_BRANCH}" https://github.com/frappe/frappe apps/frappe
# shellcheck disable=SC2086
git clone --depth 1 ${BRANCH} ${APP_REPO} apps/${APP_NAME}
echo "Install frappe NodeJS dependencies . . ."
cd apps/frappe
yarn --pure-lockfile
echo "Install ${APP_NAME} NodeJS dependencies . . ."
yarn --pure-lockfile --cwd "../${APP_NAME}"
echo "Build ${APP_NAME} assets . . ."
yarn production --app "${APP_NAME}"
cd /home/frappe/frappe-bench
# shellcheck disable=SC2086
cp -R apps/${APP_NAME}/${APP_NAME}/public/* sites/assets/${APP_NAME}
# Add frappe and all the apps available under in frappe-bench here
echo "rsync -a --delete /var/www/html/assets/frappe /assets" >/rsync
echo "rsync -a --delete /var/www/html/assets/${APP_NAME} /assets" >>/rsync
chmod +x /rsync
rm sites/apps.txt

View File

@@ -0,0 +1,15 @@
ARG IMAGE_TAG=develop
ARG DOCKER_REGISTRY_PREFIX=frappe
FROM ${DOCKER_REGISTRY_PREFIX}/frappe-worker:${IMAGE_TAG}
ARG GIT_REPO=https://github.com/frappe/erpnext
ARG GIT_BRANCH=develop
USER root
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
USER frappe
RUN install_app erpnext ${GIT_REPO} ${GIT_BRANCH}

View File

@@ -0,0 +1,75 @@
# This image uses nvm and same base image as the worker image.
# This is done to ensures that node-sass binary remains common.
# node-sass is required to enable website theme feature used
# by Website Manager role in Frappe Framework
ARG PYTHON_VERSION=3.9
FROM python:${PYTHON_VERSION}-slim-bullseye as builder
ARG GIT_REPO=https://github.com/frappe/frappe
ARG GIT_BRANCH=develop
ENV NODE_VERSION=14.18.1
ENV NVM_DIR=/root/.nvm
ENV PATH ${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
git \
build-essential \
wget \
# python2 for version-12 builds
python2 \
&& rm -rf /var/lib/apt/lists/*
# Install nvm with node and yarn
RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash \
&& . ${NVM_DIR}/nvm.sh \
&& nvm install ${NODE_VERSION} \
&& npm install -g yarn \
&& rm -rf ${NVM_DIR}/.cache
WORKDIR /home/frappe/frappe-bench
RUN mkdir -p apps sites/assets/css sites/assets/frappe /var/www/error_pages
RUN echo "frappe" > sites/apps.txt
RUN git clone --depth 1 -b ${GIT_BRANCH} ${GIT_REPO} apps/frappe
RUN cd apps/frappe \
&& yarn \
&& yarn run production \
&& yarn install --production=true
RUN git clone --depth 1 https://github.com/frappe/bench /tmp/bench \
&& cp -r /tmp/bench/bench/config/templates /var/www/error_pages
RUN cp -R apps/frappe/frappe/public/* sites/assets/frappe \
&& cp -R apps/frappe/node_modules sites/assets/frappe/
FROM nginxinc/nginx-unprivileged:latest
USER root
RUN usermod -u 1000 nginx && groupmod -g 1000 nginx
COPY --from=builder --chown=1000:1000 /home/frappe/frappe-bench/sites /var/www/html/
COPY --from=builder --chown=1000:1000 /var/www/error_pages /var/www/
COPY build/frappe-nginx/nginx-default.conf.template /etc/nginx/conf.d/default.conf.template
COPY build/frappe-nginx/docker-entrypoint.sh /
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
rsync \
&& rm -rf /var/lib/apt/lists/*
RUN echo "#!/bin/bash" > /rsync \
&& chmod +x /rsync
RUN mkdir /assets
VOLUME [ "/assets" ]
RUN chown -R nginx:nginx /assets /etc/nginx/conf.d/ /var/www/html/
USER nginx
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,66 @@
#!/bin/bash -ae
## Thanks
# https://serverfault.com/a/919212
##
rsync -a --delete /var/www/html/assets/* /assets
/rsync
# shellcheck disable=SC2012
touch /var/www/html/sites/.build -r "$(ls -td /assets/* | head -n 1)"
[[ -z "${FRAPPE_PY}" ]] && FRAPPE_PY='0.0.0.0'
[[ -z "${FRAPPE_PY_PORT}" ]] && FRAPPE_PY_PORT='8000'
[[ -z "${FRAPPE_SOCKETIO}" ]] && FRAPPE_SOCKETIO='0.0.0.0'
[[ -z "${SOCKETIO_PORT}" ]] && SOCKETIO_PORT='9000'
[[ -z "${HTTP_TIMEOUT}" ]] && HTTP_TIMEOUT='120'
[[ -z "${UPSTREAM_REAL_IP_ADDRESS}" ]] && UPSTREAM_REAL_IP_ADDRESS='127.0.0.1'
[[ -z "${UPSTREAM_REAL_IP_RECURSIVE}" ]] && UPSTREAM_REAL_IP_RECURSIVE='off'
[[ -z "${UPSTREAM_REAL_IP_HEADER}" ]] && UPSTREAM_REAL_IP_HEADER='X-Forwarded-For'
[[ -z "${FRAPPE_SITE_NAME_HEADER}" ]] && FRAPPE_SITE_NAME_HEADER="\$host"
[[ -z "${HTTP_HOST}" ]] && HTTP_HOST="\$http_host"
[[ -z "${WEB_PORT}" ]] && WEB_PORT="8080"
[[ -z "${SKIP_NGINX_TEMPLATE_GENERATION}" ]] && SKIP_NGINX_TEMPLATE_GENERATION='0'
if [[ ${SKIP_NGINX_TEMPLATE_GENERATION} == 1 ]]; then
echo "Skipping default NGINX template generation. Please mount your own NGINX config file inside /etc/nginx/conf.d"
else
echo "Generating default template"
# shellcheck disable=SC2016
envsubst '${FRAPPE_PY}
${FRAPPE_PY_PORT}
${FRAPPE_SOCKETIO}
${SOCKETIO_PORT}
${HTTP_TIMEOUT}
${UPSTREAM_REAL_IP_ADDRESS}
${UPSTREAM_REAL_IP_RECURSIVE}
${FRAPPE_SITE_NAME_HEADER}
${HTTP_HOST}
${UPSTREAM_REAL_IP_HEADER}
${WEB_PORT}' \
</etc/nginx/conf.d/default.conf.template >/etc/nginx/conf.d/default.conf
fi
echo "Waiting for frappe-python to be available on ${FRAPPE_PY} port ${FRAPPE_PY_PORT}"
# shellcheck disable=SC2016
timeout 10 bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' ${FRAPPE_PY} ${FRAPPE_PY_PORT}
echo "Frappe-python available on ${FRAPPE_PY} port ${FRAPPE_PY_PORT}"
echo "Waiting for frappe-socketio to be available on ${FRAPPE_SOCKETIO} port ${SOCKETIO_PORT}"
# shellcheck disable=SC2016
timeout 10 bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' ${FRAPPE_SOCKETIO} ${SOCKETIO_PORT}
echo "Frappe-socketio available on ${FRAPPE_SOCKETIO} port ${SOCKETIO_PORT}"
exec "$@"

View File

@@ -0,0 +1,118 @@
upstream frappe-server {
server ${FRAPPE_PY}:${FRAPPE_PY_PORT} fail_timeout=0;
}
upstream socketio-server {
server ${FRAPPE_SOCKETIO}:${SOCKETIO_PORT} fail_timeout=0;
}
# Parse the X-Forwarded-Proto header - if set - defaulting to $scheme.
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
default $scheme;
https https;
}
server {
listen ${WEB_PORT};
server_name $http_host;
root /var/www/html;
add_header X-Frame-Options "SAMEORIGIN";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# Define ${UPSTREAM_REAL_IP_ADDRESS} as our trusted upstream address, so we will be using
# its ${UPSTREAM_REAL_IP_HEADER} address as our remote address
set_real_ip_from ${UPSTREAM_REAL_IP_ADDRESS};
real_ip_header ${UPSTREAM_REAL_IP_HEADER};
real_ip_recursive ${UPSTREAM_REAL_IP_RECURSIVE};
location /assets {
try_files $uri =404;
}
location ~ ^/protected/(.*) {
internal;
try_files /sites/$http_host/$1 =404;
}
location /socket.io {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Origin $proxy_x_forwarded_proto://$http_host;
proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER};
proxy_set_header Host ${HTTP_HOST};
proxy_pass http://socketio-server;
}
location / {
rewrite ^(.+)/$ $1 permanent;
rewrite ^(.+)/index\.html$ $1 permanent;
rewrite ^(.+)\.html$ $1 permanent;
location ~ ^/files/.*.(htm|html|svg|xml) {
add_header Content-disposition "attachment";
try_files /sites/$http_host/public/$uri @webserver;
}
try_files /sites/$http_host/public/$uri @webserver;
}
location @webserver {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER};
proxy_set_header Host ${HTTP_HOST};
proxy_set_header X-Use-X-Accel-Redirect True;
proxy_read_timeout ${HTTP_TIMEOUT};
proxy_redirect off;
proxy_pass http://frappe-server;
}
# error pages
error_page 502 /502.html;
location /502.html {
root /var/www/templates;
internal;
}
# optimizations
sendfile on;
keepalive_timeout 15;
client_max_body_size 50m;
client_body_buffer_size 16K;
client_header_buffer_size 1k;
# enable gzip compression
# based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge
gzip on;
gzip_http_version 1.1;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/atom+xml
application/javascript
application/json
application/rss+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/font-woff
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/svg+xml
image/x-icon
text/css
text/plain
text/x-component;
# text/html is always compressed by HttpGzipModule
}

View File

@@ -0,0 +1,35 @@
FROM alpine/git as builder
ARG GIT_REPO=https://github.com/frappe/frappe.git
ARG GIT_BRANCH=develop
RUN git clone --depth 1 -b ${GIT_BRANCH} ${GIT_REPO} /opt/frappe
FROM node:bullseye-slim
# Add frappe user
RUN useradd -ms /bin/bash frappe
WORKDIR /home/frappe/frappe-bench
# Create bench directories and set ownership
RUN mkdir -p sites apps/frappe \
&& chown -R frappe:frappe /home/frappe
# Download socketio
COPY build/frappe-socketio/package.json apps/frappe
COPY --from=builder /opt/frappe/socketio.js apps/frappe/socketio.js
COPY --from=builder /opt/frappe/node_utils.js apps/frappe/node_utils.js
RUN cd apps/frappe \
&& npm install --only=prod
# Setup docker-entrypoint
COPY build/frappe-socketio/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
# backwards compat
RUN ln -s /usr/local/bin/docker-entrypoint.sh /
USER frappe
WORKDIR /home/frappe/frappe-bench/sites
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["start"]

View File

@@ -0,0 +1,24 @@
#!/bin/bash
set -e
function checkConfigExists() {
COUNTER=0
while [[ ! -e /home/frappe/frappe-bench/sites/common_site_config.json && ${COUNTER} -le 30 ]]; do
sleep 1
((COUNTER = COUNTER + 1))
echo "config file not created, retry ${COUNTER}" >&2
done
if [[ ! -e /home/frappe/frappe-bench/sites/common_site_config.json ]]; then
echo "timeout: config file not created" >&2
exit 1
fi
}
if [[ "$1" == 'start' ]]; then
checkConfigExists
node /home/frappe/frappe-bench/apps/frappe/socketio.js
else
exec -c "$@"
fi

View File

@@ -0,0 +1,17 @@
{
"name": "frappe-socketio",
"version": "1.0.1",
"description": "Frappe SocketIO Server",
"main": "socketio.js",
"scripts": {
"start": "node socketio.js"
},
"author": "Revant Nandgaonkar",
"license": "MIT",
"dependencies": {
"express": "^4.17.1",
"redis": "^3.1.1",
"socket.io": "^2.4.0",
"superagent": "^5.1.0"
}
}

View File

@@ -0,0 +1,108 @@
ARG PYTHON_VERSION=3.9
FROM python:${PYTHON_VERSION}-slim-bullseye
# Add non root user without password
RUN useradd -ms /bin/bash frappe
ARG GIT_REPO=https://github.com/frappe/frappe
ARG GIT_BRANCH=develop
ARG ARCH=amd64
ENV PYTHONUNBUFFERED 1
ENV NODE_VERSION=14.18.1
ENV NVM_DIR /home/frappe/.nvm
ENV PATH ${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}
ENV WKHTMLTOPDF_VERSION 0.12.6-1
# Install apt dependencies
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
# For frappe framework
git \
mariadb-client \
postgresql-client \
gettext-base \
wget \
wait-for-it \
# For PDF
libjpeg62-turbo \
libx11-6 \
libxcb1 \
libxext6 \
libxrender1 \
libssl-dev \
fonts-cantarell \
xfonts-75dpi \
xfonts-base \
libxml2 \
libffi-dev \
libjpeg-dev \
zlib1g-dev \
# For psycopg2
libpq-dev \
# For arm64 python wheel builds
&& if [ "$(uname -m)" = "aarch64" ]; then \
apt-get install --no-install-recommends -y \
gcc \
g++; \
fi \
# Install additional requirements for develop branch
&& if [ "${GIT_BRANCH}" = 'develop' ]; then \
apt-get install --no-install-recommends -y \
libcairo2 \
python3-cffi \
python3-brotli \
libpango-1.0-0 \
libpangoft2-1.0-0 \
libpangocairo-1.0-0; \
fi \
&& rm -rf /var/lib/apt/lists/*
# Detect arch, download and install wkhtmltopdf
RUN if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \
&& if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \
&& downloaded_file=wkhtmltox_$WKHTMLTOPDF_VERSION.buster_${ARCH}.deb \
&& wget -q https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \
&& dpkg -i $downloaded_file \
&& rm $downloaded_file
# Setup docker-entrypoint
COPY build/frappe-worker/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN ln -s /usr/local/bin/docker-entrypoint.sh / # backwards compat
WORKDIR /home/frappe/frappe-bench
RUN chown -R frappe:frappe /home/frappe
USER frappe
# Create frappe-bench directories
RUN mkdir -p apps logs commands sites /home/frappe/backups
# Setup python environment
RUN python -m venv env
RUN env/bin/pip install --no-cache-dir wheel gevent
# Install nvm with node
RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash \
&& . ${NVM_DIR}/nvm.sh \
&& nvm install ${NODE_VERSION} \
&& rm -rf ${NVM_DIR}/.cache
# Install Frappe
RUN git clone --depth 1 -o upstream -b ${GIT_BRANCH} ${GIT_REPO} apps/frappe \
&& env/bin/pip install --no-cache-dir -e apps/frappe
# Copy scripts and templates
COPY build/frappe-worker/commands/* /home/frappe/frappe-bench/commands/
COPY build/frappe-worker/common_site_config.json.template /opt/frappe/common_site_config.json.template
COPY build/frappe-worker/install_app.sh /usr/local/bin/install_app
COPY build/frappe-worker/bench /usr/local/bin/bench
COPY build/frappe-worker/healthcheck.sh /usr/local/bin/healthcheck.sh
# Use sites volume as working directory
WORKDIR /home/frappe/frappe-bench/sites
VOLUME [ "/home/frappe/frappe-bench/sites", "/home/frappe/backups", "/home/frappe/frappe-bench/logs" ]
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["start"]

22
build/frappe-worker/bench Executable file
View File

@@ -0,0 +1,22 @@
#!/home/frappe/frappe-bench/env/bin/python
import os
import subprocess
import sys
if __name__ == "__main__":
bench_dir = os.path.join(os.sep, "home", "frappe", "frappe-bench")
sites_dir = os.path.join(bench_dir, "sites")
bench_helper = os.path.join(
bench_dir,
"apps",
"frappe",
"frappe",
"utils",
"bench_helper.py",
)
cwd = os.getcwd()
os.chdir(sites_dir)
subprocess.check_call(
[sys.executable, bench_helper, "frappe"] + sys.argv[1:],
)

View File

@@ -0,0 +1,64 @@
import os
import git
import semantic_version
from migrate import migrate_sites
from utils import (
get_apps,
get_config,
get_container_versions,
get_version_file,
save_version_file,
)
def main():
is_ready = False
apps = get_apps()
container_versions = get_container_versions(apps)
version_file = get_version_file()
if not version_file:
version_file = container_versions
save_version_file(version_file)
for app in apps:
container_version = None
file_version = None
version_file_hash = None
container_hash = None
repo = git.Repo(os.path.join("..", "apps", app))
branch = repo.active_branch.name
if branch == "develop":
version_file_hash = version_file.get(app + "_git_hash")
container_hash = container_versions.get(app + "_git_hash")
if container_hash and version_file_hash:
if container_hash != version_file_hash:
is_ready = True
break
if version_file.get(app):
file_version = semantic_version.Version(version_file.get(app))
if container_versions.get(app):
container_version = semantic_version.Version(container_versions.get(app))
if file_version and container_version:
if container_version > file_version:
is_ready = True
break
config = get_config()
if is_ready and config.get("maintenance_mode") != 1:
migrate_sites(maintenance_mode=True)
version_file = container_versions
save_version_file(version_file)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,45 @@
import os
import frappe
from frappe.utils import cint, get_sites, now
from frappe.utils.backups import scheduled_backup
def backup(sites, with_files=False):
for site in sites:
frappe.init(site)
frappe.connect()
odb = scheduled_backup(
ignore_files=not with_files,
backup_path_db=None,
backup_path_files=None,
backup_path_private_files=None,
force=True,
)
print("database backup taken -", odb.backup_path_db, "- on", now())
if with_files:
print("files backup taken -", odb.backup_path_files, "- on", now())
print(
"private files backup taken -",
odb.backup_path_private_files,
"- on",
now(),
)
frappe.destroy()
def main():
installed_sites = ":".join(get_sites())
sites = os.environ.get("SITES", installed_sites).split(":")
with_files = cint(os.environ.get("WITH_FILES"))
backup(sites, with_files)
if frappe.redis_server:
frappe.redis_server.connection_pool.disconnect()
exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,124 @@
import socket
import time
from constants import (
DB_HOST_KEY,
DB_PORT,
DB_PORT_KEY,
REDIS_CACHE_KEY,
REDIS_QUEUE_KEY,
REDIS_SOCKETIO_KEY,
)
from six.moves.urllib.parse import urlparse
from utils import get_config
def is_open(ip, port, timeout=30):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
try:
s.connect((ip, int(port)))
s.shutdown(socket.SHUT_RDWR)
return True
except Exception:
return False
finally:
s.close()
def check_host(ip, port, retry=10, delay=3, print_attempt=True):
ipup = False
for i in range(retry):
if print_attempt:
print(f"Attempt {i+1} to connect to {ip}:{port}")
if is_open(ip, port):
ipup = True
break
else:
time.sleep(delay)
return ipup
# Check service
def check_service(
retry=10, delay=3, print_attempt=True, service_name=None, service_port=None
):
config = get_config()
if not service_name:
service_name = config.get(DB_HOST_KEY, "mariadb")
if not service_port:
service_port = config.get(DB_PORT_KEY, DB_PORT)
is_db_connected = False
is_db_connected = check_host(
service_name, service_port, retry, delay, print_attempt
)
if not is_db_connected:
print(
"Connection to {service_name}:{service_port} timed out".format(
service_name=service_name,
service_port=service_port,
)
)
exit(1)
# Check redis queue
def check_redis_queue(retry=10, delay=3, print_attempt=True):
check_redis_queue = False
config = get_config()
redis_queue_url = urlparse(
config.get(REDIS_QUEUE_KEY, "redis://redis-queue:6379")
).netloc
redis_queue, redis_queue_port = redis_queue_url.split(":")
check_redis_queue = check_host(
redis_queue, redis_queue_port, retry, delay, print_attempt
)
if not check_redis_queue:
print("Connection to redis queue timed out")
exit(1)
# Check redis cache
def check_redis_cache(retry=10, delay=3, print_attempt=True):
check_redis_cache = False
config = get_config()
redis_cache_url = urlparse(
config.get(REDIS_CACHE_KEY, "redis://redis-cache:6379")
).netloc
redis_cache, redis_cache_port = redis_cache_url.split(":")
check_redis_cache = check_host(
redis_cache, redis_cache_port, retry, delay, print_attempt
)
if not check_redis_cache:
print("Connection to redis cache timed out")
exit(1)
# Check redis socketio
def check_redis_socketio(retry=10, delay=3, print_attempt=True):
check_redis_socketio = False
config = get_config()
redis_socketio_url = urlparse(
config.get(REDIS_SOCKETIO_KEY, "redis://redis-socketio:6379")
).netloc
redis_socketio, redis_socketio_port = redis_socketio_url.split(":")
check_redis_socketio = check_host(
redis_socketio, redis_socketio_port, retry, delay, print_attempt
)
if not check_redis_socketio:
print("Connection to redis socketio timed out")
exit(1)
def main():
check_service()
check_redis_queue()
check_redis_cache()
check_redis_socketio()
print("Connections OK")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,13 @@
REDIS_QUEUE_KEY = "redis_queue"
REDIS_CACHE_KEY = "redis_cache"
REDIS_SOCKETIO_KEY = "redis_socketio"
DB_HOST_KEY = "db_host"
DB_PORT_KEY = "db_port"
DB_PORT = 3306
APP_VERSIONS_JSON_FILE = "app_versions.json"
APPS_TXT_FILE = "apps.txt"
COMMON_SITE_CONFIG_FILE = "common_site_config.json"
DATE_FORMAT = "%Y%m%d_%H%M%S"
RDS_DB = "rds_db"
RDS_PRIVILEGES = "SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES"
ARCHIVE_SITES_PATH = "/home/frappe/frappe-bench/sites/archive_sites"

View File

@@ -0,0 +1,61 @@
import argparse
from check_connection import (
check_redis_cache,
check_redis_queue,
check_redis_socketio,
check_service,
)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"-p",
"--ping-service",
dest="ping_services",
action="append",
type=str,
help='list of services to ping, e.g. doctor -p "postgres:5432" --ping-service "mariadb:3306"',
)
args = parser.parse_args()
return args
def main():
args = parse_args()
check_service(retry=1, delay=0, print_attempt=False)
print("Bench database Connected")
check_redis_cache(retry=1, delay=0, print_attempt=False)
print("Redis Cache Connected")
check_redis_queue(retry=1, delay=0, print_attempt=False)
print("Redis Queue Connected")
check_redis_socketio(retry=1, delay=0, print_attempt=False)
print("Redis SocketIO Connected")
if args.ping_services:
for service in args.ping_services:
service_name = None
service_port = None
try:
service_name, service_port = service.split(":")
except ValueError:
print("Service should be in format host:port, e.g postgres:5432")
exit(1)
check_service(
retry=1,
delay=0,
print_attempt=False,
service_name=service_name,
service_port=service_port,
)
print(f"{service_name}:{service_port} Connected")
print("Health check successful")
exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,3 @@
import gevent.monkey
gevent.monkey.patch_all()

View File

@@ -0,0 +1,52 @@
import os
import frappe
from frappe.utils import cint, get_sites
from utils import get_config, save_config
def set_maintenance_mode(enable=True):
conf = get_config()
if enable:
conf.update({"maintenance_mode": 1, "pause_scheduler": 1})
save_config(conf)
if not enable:
conf.update({"maintenance_mode": 0, "pause_scheduler": 0})
save_config(conf)
def migrate_sites(maintenance_mode=False):
installed_sites = ":".join(get_sites())
sites = os.environ.get("SITES", installed_sites).split(":")
if not maintenance_mode:
maintenance_mode = cint(os.environ.get("MAINTENANCE_MODE"))
if maintenance_mode:
set_maintenance_mode(True)
for site in sites:
print("Migrating", site)
frappe.init(site=site)
frappe.connect()
try:
from frappe.migrate import migrate
migrate()
finally:
frappe.destroy()
# Disable maintenance mode after migration
set_maintenance_mode(False)
def main():
migrate_sites()
if frappe.redis_server:
frappe.redis_server.connection_pool.disconnect()
exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,132 @@
import os
import frappe
import semantic_version
from constants import COMMON_SITE_CONFIG_FILE, RDS_DB, RDS_PRIVILEGES
from frappe.installer import update_site_config
from utils import get_config, get_password, get_site_config, run_command
# try to import _new_site from frappe, which could possibly
# exist in either commands.py or installer.py, and so we need
# to maintain compatibility across all frappe versions.
try:
# <= version-{11,12}
from frappe.commands.site import _new_site
except ImportError:
# >= version-13 and develop
from frappe.installer import _new_site
def main():
config = get_config()
db_type = "mariadb"
db_port = config.get("db_port", 3306)
db_host = config.get("db_host")
site_name = os.environ.get("SITE_NAME", "site1.localhost")
db_root_username = os.environ.get("DB_ROOT_USER", "root")
mariadb_root_password = get_password("MYSQL_ROOT_PASSWORD", "admin")
postgres_root_password = get_password("POSTGRES_PASSWORD")
db_root_password = mariadb_root_password
if postgres_root_password:
db_type = "postgres"
db_host = os.environ.get("POSTGRES_HOST")
db_port = 5432
db_root_password = postgres_root_password
if not db_host:
db_host = config.get("db_host")
print("Environment variable POSTGRES_HOST not found.")
print("Using db_host from common_site_config.json")
sites_path = os.getcwd()
common_site_config_path = os.path.join(sites_path, COMMON_SITE_CONFIG_FILE)
update_site_config(
"root_login",
db_root_username,
validate=False,
site_config_path=common_site_config_path,
)
update_site_config(
"root_password",
db_root_password,
validate=False,
site_config_path=common_site_config_path,
)
force = True if os.environ.get("FORCE", None) else False
install_apps = os.environ.get("INSTALL_APPS", None)
install_apps = install_apps.split(",") if install_apps else []
frappe.init(site_name, new_site=True)
if semantic_version.Version(frappe.__version__).major > 11:
_new_site(
None,
site_name,
mariadb_root_username=db_root_username,
mariadb_root_password=db_root_password,
admin_password=get_password("ADMIN_PASSWORD", "admin"),
verbose=True,
install_apps=install_apps,
source_sql=None,
force=force,
db_type=db_type,
reinstall=False,
db_host=db_host,
db_port=db_port,
)
else:
_new_site(
None,
site_name,
mariadb_root_username=db_root_username,
mariadb_root_password=db_root_password,
admin_password=get_password("ADMIN_PASSWORD", "admin"),
verbose=True,
install_apps=install_apps,
source_sql=None,
force=force,
reinstall=False,
)
if db_type == "mariadb":
site_config = get_site_config(site_name)
db_name = site_config.get("db_name")
db_password = site_config.get("db_password")
mysql_command = [
"mysql",
f"-h{db_host}",
f"-u{db_root_username}",
f"-p{mariadb_root_password}",
"-e",
]
# Drop User if exists
command = mysql_command + [
f"DROP USER IF EXISTS '{db_name}'; FLUSH PRIVILEGES;"
]
run_command(command)
# Grant permission to database and set password
grant_privileges = "ALL PRIVILEGES"
# for Amazon RDS
if config.get(RDS_DB) or site_config.get(RDS_DB):
grant_privileges = RDS_PRIVILEGES
command = mysql_command + [
f"\
CREATE USER IF NOT EXISTS '{db_name}'@'%' IDENTIFIED BY '{db_password}'; \
GRANT {grant_privileges} ON `{db_name}`.* TO '{db_name}'@'%'; \
FLUSH PRIVILEGES;"
]
run_command(command)
if frappe.redis_server:
frappe.redis_server.connection_pool.disconnect()
exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,183 @@
import datetime
import os
import time
from glob import glob
import boto3
from constants import DATE_FORMAT
from frappe.utils import get_sites
from utils import check_s3_environment_variables, get_s3_config, upload_file_to_s3
def get_file_ext():
return {
"database": "-database.sql.gz",
"private_files": "-private-files.tar",
"public_files": "-files.tar",
"site_config": "-site_config_backup.json",
}
def get_backup_details(sitename):
backup_details = dict()
file_ext = get_file_ext()
# add trailing slash https://stackoverflow.com/a/15010678
site_backup_path = os.path.join(os.getcwd(), sitename, "private", "backups", "")
if os.path.exists(site_backup_path):
for filetype, ext in file_ext.items():
site_slug = sitename.replace(".", "_")
pattern = site_backup_path + "*-" + site_slug + ext
backup_files = list(filter(os.path.isfile, glob(pattern)))
if len(backup_files) > 0:
backup_files.sort(
key=lambda file: os.stat(
os.path.join(site_backup_path, file)
).st_ctime
)
backup_date = datetime.datetime.strptime(
time.ctime(os.path.getmtime(backup_files[0])),
"%a %b %d %H:%M:%S %Y",
)
backup_details[filetype] = {
"sitename": sitename,
"file_size_in_bytes": os.stat(backup_files[-1]).st_size,
"file_path": os.path.abspath(backup_files[-1]),
"filename": os.path.basename(backup_files[-1]),
"backup_date": backup_date.date().strftime("%Y-%m-%d %H:%M:%S"),
}
return backup_details
def delete_old_backups(limit, bucket, site_name):
all_backups = list()
all_backup_dates = list()
backup_limit = int(limit)
check_s3_environment_variables()
bucket_dir = os.environ.get("BUCKET_DIR")
oldest_backup_date = None
s3 = boto3.resource(
"s3",
region_name=os.environ.get("REGION"),
aws_access_key_id=os.environ.get("ACCESS_KEY_ID"),
aws_secret_access_key=os.environ.get("SECRET_ACCESS_KEY"),
endpoint_url=os.environ.get("ENDPOINT_URL"),
)
bucket = s3.Bucket(bucket)
objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter="/")
if objects:
for obj in objects.get("CommonPrefixes"):
if obj.get("Prefix") == bucket_dir + "/":
for backup_obj in bucket.objects.filter(Prefix=obj.get("Prefix")):
if backup_obj.get()["ContentType"] == "application/x-directory":
continue
try:
# backup_obj.key is bucket_dir/site/date_time/backupfile.extension
(
bucket_dir,
site_slug,
date_time,
backupfile,
) = backup_obj.key.split("/")
date_time_object = datetime.datetime.strptime(
date_time, DATE_FORMAT
)
if site_name in backup_obj.key:
all_backup_dates.append(date_time_object)
all_backups.append(backup_obj.key)
except IndexError as error:
print(error)
exit(1)
if len(all_backup_dates) > 0:
oldest_backup_date = min(all_backup_dates)
if len(all_backups) / 3 > backup_limit:
oldest_backup = None
for backup in all_backups:
try:
# backup is bucket_dir/site/date_time/backupfile.extension
backup_dir, site_slug, backup_dt_string, filename = backup.split("/")
backup_datetime = datetime.datetime.strptime(
backup_dt_string, DATE_FORMAT
)
if backup_datetime == oldest_backup_date:
oldest_backup = backup
except IndexError as error:
print(error)
exit(1)
if oldest_backup:
for obj in bucket.objects.filter(Prefix=oldest_backup):
# delete all keys that are inside the oldest_backup
if bucket_dir in obj.key:
print("Deleting " + obj.key)
s3.Object(bucket.name, obj.key).delete()
def main():
details = dict()
sites = get_sites()
conn, bucket = get_s3_config()
for site in sites:
details = get_backup_details(site)
db_file = details.get("database", {}).get("file_path")
folder = os.environ.get("BUCKET_DIR") + "/" + site + "/"
if db_file:
folder = (
os.environ.get("BUCKET_DIR")
+ "/"
+ site
+ "/"
+ os.path.basename(db_file)[:15]
+ "/"
)
upload_file_to_s3(db_file, folder, conn, bucket)
# Archive site_config.json
site_config_file = details.get("site_config", {}).get("file_path")
if not site_config_file:
site_config_file = os.path.join(os.getcwd(), site, "site_config.json")
upload_file_to_s3(site_config_file, folder, conn, bucket)
public_files = details.get("public_files", {}).get("file_path")
if public_files:
folder = (
os.environ.get("BUCKET_DIR")
+ "/"
+ site
+ "/"
+ os.path.basename(public_files)[:15]
+ "/"
)
upload_file_to_s3(public_files, folder, conn, bucket)
private_files = details.get("private_files", {}).get("file_path")
if private_files:
folder = (
os.environ.get("BUCKET_DIR")
+ "/"
+ site
+ "/"
+ os.path.basename(private_files)[:15]
+ "/"
)
upload_file_to_s3(private_files, folder, conn, bucket)
delete_old_backups(os.environ.get("BACKUP_LIMIT", "3"), bucket, site)
print("push-backup complete")
exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,313 @@
import datetime
import hashlib
import os
import tarfile
import boto3
import frappe
from constants import COMMON_SITE_CONFIG_FILE, DATE_FORMAT, RDS_DB, RDS_PRIVILEGES
from frappe.installer import (
get_conf_params,
make_conf,
make_site_dirs,
update_site_config,
)
from frappe.utils import get_sites, random_string
from utils import (
check_s3_environment_variables,
get_config,
get_password,
get_site_config,
list_directories,
run_command,
set_key_in_site_config,
)
def get_backup_dir():
return os.path.join(os.path.expanduser("~"), "backups")
def decompress_db(database_file, site):
command = ["gunzip", "-c", database_file]
with open(database_file.replace(".gz", ""), "w") as db_file:
print(f"Extract Database GZip for site {site}")
run_command(command, stdout=db_file)
def restore_database(files_base, site_config_path, site):
# restore database
database_file = files_base + "-database.sql.gz"
decompress_db(database_file, site)
config = get_config()
# Set db_type if it exists in backup site_config.json
set_key_in_site_config("db_type", site, site_config_path)
# Set db_host if it exists in backup site_config.json
set_key_in_site_config("db_host", site, site_config_path)
# Set db_port if it exists in backup site_config.json
set_key_in_site_config("db_port", site, site_config_path)
# get updated site_config
site_config = get_site_config(site)
# if no db_type exists, default to mariadb
db_type = site_config.get("db_type", "mariadb")
is_database_restored = False
if db_type == "mariadb":
restore_mariadb(
config=config, site_config=site_config, database_file=database_file
)
is_database_restored = True
elif db_type == "postgres":
restore_postgres(
config=config, site_config=site_config, database_file=database_file
)
is_database_restored = True
if is_database_restored:
# Set encryption_key if it exists in backup site_config.json
set_key_in_site_config("encryption_key", site, site_config_path)
def restore_files(files_base):
public_files = files_base + "-files.tar"
# extract tar
public_tar = tarfile.open(public_files)
print(f"Extracting {public_files}")
public_tar.extractall()
def restore_private_files(files_base):
private_files = files_base + "-private-files.tar"
private_tar = tarfile.open(private_files)
print(f"Extracting {private_files}")
private_tar.extractall()
def pull_backup_from_s3():
check_s3_environment_variables()
# https://stackoverflow.com/a/54672690
s3 = boto3.resource(
"s3",
region_name=os.environ.get("REGION"),
aws_access_key_id=os.environ.get("ACCESS_KEY_ID"),
aws_secret_access_key=os.environ.get("SECRET_ACCESS_KEY"),
endpoint_url=os.environ.get("ENDPOINT_URL"),
)
bucket_dir = os.environ.get("BUCKET_DIR")
bucket_name = os.environ.get("BUCKET_NAME")
bucket = s3.Bucket(bucket_name)
# Change directory to /home/frappe/backups
os.chdir(get_backup_dir())
backup_files = []
sites = set()
site_timestamps = set()
download_backups = []
for obj in bucket.objects.filter(Prefix=bucket_dir):
if obj.get()["ContentType"] == "application/x-directory":
continue
backup_file = obj.key.replace(os.path.join(bucket_dir, ""), "")
backup_files.append(backup_file)
site_name, timestamp, backup_type = backup_file.split("/")
site_timestamp = site_name + "/" + timestamp
sites.add(site_name)
site_timestamps.add(site_timestamp)
# sort sites for latest backups
for site in sites:
backup_timestamps = []
for site_timestamp in site_timestamps:
site_name, timestamp = site_timestamp.split("/")
if site == site_name:
timestamp_datetime = datetime.datetime.strptime(timestamp, DATE_FORMAT)
backup_timestamps.append(timestamp)
download_backups.append(site + "/" + max(backup_timestamps))
# Only download latest backups
for backup_file in backup_files:
for backup in download_backups:
if backup in backup_file:
if not os.path.exists(os.path.dirname(backup_file)):
os.makedirs(os.path.dirname(backup_file))
print(f"Downloading {backup_file}")
bucket.download_file(bucket_dir + "/" + backup_file, backup_file)
os.chdir(os.path.join(os.path.expanduser("~"), "frappe-bench", "sites"))
def restore_postgres(config, site_config, database_file):
# common config
common_site_config_path = os.path.join(os.getcwd(), COMMON_SITE_CONFIG_FILE)
db_root_user = config.get("root_login")
if not db_root_user:
postgres_user = os.environ.get("DB_ROOT_USER")
if not postgres_user:
print("Variable DB_ROOT_USER not set")
exit(1)
db_root_user = postgres_user
update_site_config(
"root_login",
db_root_user,
validate=False,
site_config_path=common_site_config_path,
)
db_root_password = config.get("root_password")
if not db_root_password:
root_password = get_password("POSTGRES_PASSWORD")
if not root_password:
print("Variable POSTGRES_PASSWORD not set")
exit(1)
db_root_password = root_password
update_site_config(
"root_password",
db_root_password,
validate=False,
site_config_path=common_site_config_path,
)
# site config
db_host = site_config.get("db_host")
db_port = site_config.get("db_port", 5432)
db_name = site_config.get("db_name")
db_password = site_config.get("db_password")
psql_command = ["psql"]
psql_uri = f"postgres://{db_root_user}:{db_root_password}@{db_host}:{db_port}"
print("Restoring PostgreSQL")
run_command(psql_command + [psql_uri, "-c", f'DROP DATABASE IF EXISTS "{db_name}"'])
run_command(psql_command + [psql_uri, "-c", f"DROP USER IF EXISTS {db_name}"])
run_command(psql_command + [psql_uri, "-c", f'CREATE DATABASE "{db_name}"'])
run_command(
psql_command
+ [psql_uri, "-c", f"CREATE user {db_name} password '{db_password}'"]
)
run_command(
psql_command
+ [psql_uri, "-c", f'GRANT ALL PRIVILEGES ON DATABASE "{db_name}" TO {db_name}']
)
with open(database_file.replace(".gz", "")) as db_file:
run_command(psql_command + [f"{psql_uri}/{db_name}", "<"], stdin=db_file)
def restore_mariadb(config, site_config, database_file):
db_root_password = get_password("MYSQL_ROOT_PASSWORD")
if not db_root_password:
print("Variable MYSQL_ROOT_PASSWORD not set")
exit(1)
db_root_user = os.environ.get("DB_ROOT_USER", "root")
db_host = site_config.get("db_host", config.get("db_host"))
db_port = site_config.get("db_port", config.get("db_port", 3306))
db_name = site_config.get("db_name")
db_password = site_config.get("db_password")
# mysql command prefix
mysql_command = [
"mysql",
f"-u{db_root_user}",
f"-h{db_host}",
f"-p{db_root_password}",
f"-P{db_port}",
]
# drop db if exists for clean restore
drop_database = mysql_command + ["-e", f"DROP DATABASE IF EXISTS `{db_name}`;"]
run_command(drop_database)
# create db
create_database = mysql_command + [
"-e",
f"CREATE DATABASE IF NOT EXISTS `{db_name}`;",
]
run_command(create_database)
# create user
create_user = mysql_command + [
"-e",
f"CREATE USER IF NOT EXISTS '{db_name}'@'%' IDENTIFIED BY '{db_password}'; FLUSH PRIVILEGES;",
]
run_command(create_user)
# grant db privileges to user
grant_privileges = "ALL PRIVILEGES"
# for Amazon RDS
if config.get(RDS_DB) or site_config.get(RDS_DB):
grant_privileges = RDS_PRIVILEGES
grant_privileges_command = mysql_command + [
"-e",
f"GRANT {grant_privileges} ON `{db_name}`.* TO '{db_name}'@'%' IDENTIFIED BY '{db_password}'; FLUSH PRIVILEGES;",
]
run_command(grant_privileges_command)
print("Restoring MariaDB")
with open(database_file.replace(".gz", "")) as db_file:
run_command(mysql_command + [f"{db_name}"], stdin=db_file)
def main():
backup_dir = get_backup_dir()
if len(list_directories(backup_dir)) == 0:
pull_backup_from_s3()
for site in list_directories(backup_dir):
site_slug = site.replace(".", "_")
backups = [
datetime.datetime.strptime(backup, DATE_FORMAT)
for backup in list_directories(os.path.join(backup_dir, site))
]
latest_backup = max(backups).strftime(DATE_FORMAT)
files_base = os.path.join(backup_dir, site, latest_backup, "")
files_base += latest_backup + "-" + site_slug
site_config_path = files_base + "-site_config_backup.json"
if not os.path.exists(site_config_path):
site_config_path = os.path.join(backup_dir, site, "site_config.json")
if site in get_sites():
print(f"Overwrite site {site}")
restore_database(files_base, site_config_path, site)
restore_private_files(files_base)
restore_files(files_base)
else:
site_config = get_conf_params(
db_name="_" + hashlib.sha1(site.encode()).hexdigest()[:16],
db_password=random_string(16),
)
frappe.local.site = site
frappe.local.sites_path = os.getcwd()
frappe.local.site_path = os.getcwd() + "/" + site
make_conf(
db_name=site_config.get("db_name"),
db_password=site_config.get("db_password"),
)
make_site_dirs()
print(f"Create site {site}")
restore_database(files_base, site_config_path, site)
restore_private_files(files_base)
restore_files(files_base)
if frappe.redis_server:
frappe.redis_server.connection_pool.disconnect()
exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,208 @@
import json
import os
import subprocess
import boto3
import git
from constants import APP_VERSIONS_JSON_FILE, APPS_TXT_FILE, COMMON_SITE_CONFIG_FILE
from frappe.installer import update_site_config
def run_command(command, stdout=None, stdin=None, stderr=None):
stdout = stdout or subprocess.PIPE
stderr = stderr or subprocess.PIPE
stdin = stdin or subprocess.PIPE
process = subprocess.Popen(command, stdout=stdout, stdin=stdin, stderr=stderr)
out, error = process.communicate()
if process.returncode:
print("Something went wrong:")
print(f"return code: {process.returncode}")
print(f"stdout:\n{out}")
print(f"\nstderr:\n{error}")
exit(process.returncode)
def save_version_file(versions):
with open(APP_VERSIONS_JSON_FILE, "w") as f:
return json.dump(versions, f, indent=1, sort_keys=True)
def get_apps():
apps = []
try:
with open(APPS_TXT_FILE) as apps_file:
for app in apps_file.readlines():
if app.strip():
apps.append(app.strip())
except FileNotFoundError as exception:
print(exception)
exit(1)
except Exception:
print(APPS_TXT_FILE + " is not valid")
exit(1)
return apps
def get_container_versions(apps):
versions = {}
for app in apps:
try:
version = __import__(app).__version__
versions.update({app: version})
except Exception:
pass
try:
path = os.path.join("..", "apps", app)
repo = git.Repo(path)
commit_hash = repo.head.object.hexsha
versions.update({app + "_git_hash": commit_hash})
except Exception:
pass
return versions
def get_version_file():
versions = None
try:
with open(APP_VERSIONS_JSON_FILE) as versions_file:
versions = json.load(versions_file)
except Exception:
pass
return versions
def get_config():
config = None
try:
with open(COMMON_SITE_CONFIG_FILE) as config_file:
config = json.load(config_file)
except FileNotFoundError as exception:
print(exception)
exit(1)
except Exception:
print(COMMON_SITE_CONFIG_FILE + " is not valid")
exit(1)
return config
def get_site_config(site_name):
site_config = None
with open(f"{site_name}/site_config.json") as site_config_file:
site_config = json.load(site_config_file)
return site_config
def save_config(config):
with open(COMMON_SITE_CONFIG_FILE, "w") as f:
return json.dump(config, f, indent=1, sort_keys=True)
def get_password(env_var, default=None):
return (
os.environ.get(env_var)
or get_password_from_secret(f"{env_var}_FILE")
or default
)
def get_password_from_secret(env_var):
"""Fetches the secret value from the docker secret file
usually located inside /run/secrets/
Arguments:
env_var {str} -- Name of the environment variable
containing the path to the secret file.
Returns:
[str] -- Secret value
"""
passwd = None
secret_file_path = os.environ.get(env_var)
if secret_file_path:
with open(secret_file_path) as secret_file:
passwd = secret_file.read().strip()
return passwd
def get_s3_config():
check_s3_environment_variables()
bucket = os.environ.get("BUCKET_NAME")
conn = boto3.client(
"s3",
region_name=os.environ.get("REGION"),
aws_access_key_id=os.environ.get("ACCESS_KEY_ID"),
aws_secret_access_key=os.environ.get("SECRET_ACCESS_KEY"),
endpoint_url=os.environ.get("ENDPOINT_URL"),
)
return conn, bucket
def upload_file_to_s3(filename, folder, conn, bucket):
destpath = os.path.join(folder, os.path.basename(filename))
try:
print("Uploading file:", filename)
conn.upload_file(filename, bucket, destpath)
except Exception as e:
print("Error uploading: %s" % (e))
exit(1)
def list_directories(path):
directories = []
for name in os.listdir(path):
if os.path.isdir(os.path.join(path, name)):
directories.append(name)
return directories
def get_site_config_from_path(site_config_path):
site_config = dict()
if os.path.exists(site_config_path):
with open(site_config_path) as sc:
site_config = json.load(sc)
return site_config
def set_key_in_site_config(key, site, site_config_path):
site_config = get_site_config_from_path(site_config_path)
value = site_config.get(key)
if value:
print(f"Set {key} in site config for site: {site}")
update_site_config(
key,
value,
site_config_path=os.path.join(os.getcwd(), site, "site_config.json"),
)
def check_s3_environment_variables():
if "BUCKET_NAME" not in os.environ:
print("Variable BUCKET_NAME not set")
exit(1)
if "ACCESS_KEY_ID" not in os.environ:
print("Variable ACCESS_KEY_ID not set")
exit(1)
if "SECRET_ACCESS_KEY" not in os.environ:
print("Variable SECRET_ACCESS_KEY not set")
exit(1)
if "ENDPOINT_URL" not in os.environ:
print("Variable ENDPOINT_URL not set")
exit(1)
if "BUCKET_DIR" not in os.environ:
print("Variable BUCKET_DIR not set")
exit(1)
if "REGION" not in os.environ:
print("Variable REGION not set")
exit(1)

View File

@@ -0,0 +1,8 @@
{
"db_host": "${DB_HOST}",
"db_port": ${DB_PORT},
"redis_cache": "redis://${REDIS_CACHE}",
"redis_queue": "redis://${REDIS_QUEUE}",
"redis_socketio": "redis://${REDIS_SOCKETIO}",
"socketio_port": ${SOCKETIO_PORT}
}

View File

@@ -0,0 +1,191 @@
#!/bin/bash
function configureEnv() {
if [[ ! -f /home/frappe/frappe-bench/sites/common_site_config.json ]]; then
if [[ -z "${MARIADB_HOST}" && -z "${POSTGRES_HOST}" ]]; then
echo "MARIADB_HOST or POSTGRES_HOST is not set" >&2
exit 1
fi
if [[ -z "${REDIS_CACHE}" ]]; then
echo "REDIS_CACHE is not set" >&2
exit 1
fi
if [[ -z "${REDIS_QUEUE}" ]]; then
echo "REDIS_QUEUE is not set" >&2
exit 1
fi
if [[ -z "${REDIS_SOCKETIO}" ]]; then
echo "REDIS_SOCKETIO is not set" >&2
exit 1
fi
if [[ -z "${SOCKETIO_PORT}" ]]; then
echo "SOCKETIO_PORT is not set" >&2
exit 1
fi
if [[ -z "${DB_PORT}" ]]; then
export DB_PORT=3306
fi
export DB_HOST="${MARIADB_HOST:-$POSTGRES_HOST}"
# shellcheck disable=SC2016
envsubst '${DB_HOST}
${DB_PORT}
${REDIS_CACHE}
${REDIS_QUEUE}
${REDIS_SOCKETIO}
${SOCKETIO_PORT}' </opt/frappe/common_site_config.json.template >/home/frappe/frappe-bench/sites/common_site_config.json
fi
}
function checkConnection() {
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/commands/check_connection.py
}
function checkConfigExists() {
COUNTER=0
while [[ ! -e /home/frappe/frappe-bench/sites/common_site_config.json && ${COUNTER} -le 30 ]]; do
sleep 1
((COUNTER = COUNTER + 1))
echo "config file not created, retry ${COUNTER}" >&2
done
if [[ ! -e /home/frappe/frappe-bench/sites/common_site_config.json ]]; then
echo "timeout: config file not created" >&2
exit 1
fi
}
if [[ ! -e /home/frappe/frappe-bench/sites/apps.txt ]]; then
find /home/frappe/frappe-bench/apps -mindepth 1 -maxdepth 1 -type d -printf '%f\n' |
sort -r >/home/frappe/frappe-bench/sites/apps.txt
fi
# symlink node_modules
ln -sfn /home/frappe/frappe-bench/sites/assets/frappe/node_modules \
/home/frappe/frappe-bench/apps/frappe/node_modules
case "$1" in
start)
configureEnv
checkConnection
[[ -z "${WORKERS}" ]] && WORKERS='2'
[[ -z "${FRAPPE_PORT}" ]] && FRAPPE_PORT='8000'
[[ -z "${WORKER_CLASS}" ]] && WORKER_CLASS='gthread'
LOAD_CONFIG_FILE=""
[[ "${WORKER_CLASS}" == "gevent" ]] &&
LOAD_CONFIG_FILE="-c /home/frappe/frappe-bench/commands/gevent_patch.py"
if [[ -n "${AUTO_MIGRATE}" ]]; then
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/commands/auto_migrate.py
fi
# shellcheck disable=SC2086
/home/frappe/frappe-bench/env/bin/gunicorn ${LOAD_CONFIG_FILE} -b 0.0.0.0:${FRAPPE_PORT} \
--worker-tmp-dir /dev/shm \
--threads=4 \
--workers ${WORKERS} \
--worker-class=${WORKER_CLASS} \
--log-file=- \
-t 120 frappe.app:application --preload
;;
worker)
checkConfigExists
checkConnection
: "${WORKER_TYPE:=default}"
bench worker --queue $WORKER_TYPE
;;
schedule)
checkConfigExists
checkConnection
bench schedule
;;
new)
checkConfigExists
checkConnection
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/commands/new.py
exit
;;
drop)
checkConfigExists
checkConnection
: "${SITE_NAME:=site1.localhost}"
: "${DB_ROOT_USER:=root}"
: "${DB_ROOT_PASSWORD:=$POSTGRES_PASSWORD}"
: "${DB_ROOT_PASSWORD:=$MYSQL_ROOT_PASSWORD}"
: "${DB_ROOT_PASSWORD:=admin}"
FLAGS=
if [[ ${NO_BACKUP} == 1 ]]; then
FLAGS="${FLAGS} --no-backup"
fi
if [[ ${FORCE} == 1 ]]; then
FLAGS="${FLAGS} --force"
fi
# shellcheck disable=SC2086
bench drop-site \
${SITE_NAME} \
--root-login ${DB_ROOT_USER} \
--root-password ${DB_ROOT_PASSWORD} \
--archived-sites-path /home/frappe/frappe-bench/sites/archive_sites \
${FLAGS}
;;
migrate)
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/commands/migrate.py
exit
;;
doctor)
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/commands/doctor.py "${@:2}"
exit
;;
backup)
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/commands/backup.py
exit
;;
console)
if [[ -z "$2" ]]; then
echo "Need to specify a sitename with the command:" >&2
echo "console <sitename>" >&2
exit 1
fi
bench --site "$2" console
;;
push-backup)
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/commands/push_backup.py
exit
;;
restore-backup)
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/commands/restore_backup.py
exit
;;
*)
exec "$@"
;;
esac

View File

@@ -0,0 +1,45 @@
#!/bin/bash
set -ea
function getUrl() {
grep "$2" "$1" | awk -v word="$2" '$word { gsub(/[",]/,"",$2); print $2}' | tr -d '\n'
}
COMMON_SITE_CONFIG_JSON='/home/frappe/frappe-bench/sites/common_site_config.json'
# Set DB Host and port
DB_HOST=$(getUrl "${COMMON_SITE_CONFIG_JSON}" "db_host")
DB_PORT=$(getUrl "${COMMON_SITE_CONFIG_JSON}" "db_port")
if [[ -z "${DB_PORT}" ]]; then
DB_PORT=3306
fi
# Set REDIS host:port
REDIS_CACHE=$(getUrl "${COMMON_SITE_CONFIG_JSON}" "redis_cache" | sed 's|redis://||g')
if [[ "${REDIS_CACHE}" == *"/"* ]]; then
REDIS_CACHE=$(echo "${REDIS_CACHE}" | cut -f1 -d"/")
fi
REDIS_QUEUE=$(getUrl "${COMMON_SITE_CONFIG_JSON}" "redis_queue" | sed 's|redis://||g')
if [[ "${REDIS_QUEUE}" == *"/"* ]]; then
REDIS_QUEUE=$(echo "${REDIS_QUEUE}" | cut -f1 -d"/")
fi
REDIS_SOCKETIO=$(getUrl "${COMMON_SITE_CONFIG_JSON}" "redis_socketio" | sed 's|redis://||g')
if [[ "${REDIS_SOCKETIO}" == *"/"* ]]; then
REDIS_SOCKETIO=$(echo "${REDIS_SOCKETIO}" | cut -f1 -d"/")
fi
echo "Check ${DB_HOST}:${DB_PORT}"
wait-for-it "${DB_HOST}:${DB_PORT}" -t 1
echo "Check ${REDIS_CACHE}"
wait-for-it "${REDIS_CACHE}" -t 1
echo "Check ${REDIS_QUEUE}"
wait-for-it "${REDIS_QUEUE}" -t 1
echo "Check ${REDIS_SOCKETIO}"
wait-for-it "${REDIS_SOCKETIO}" -t 1
if [[ "$1" = "-p" || "$1" = "--ping-service" ]]; then
echo "Check $2"
wait-for-it "$2" -t 1
fi

View File

@@ -0,0 +1,11 @@
#!/bin/bash -ex
APP_NAME=${1}
APP_REPO=${2}
APP_BRANCH=${3}
[[ -n "${APP_BRANCH}" ]] && BRANCH="-b ${APP_BRANCH}"
# shellcheck disable=SC2086
git clone --depth 1 -o upstream "${APP_REPO}" ${BRANCH} "/home/frappe/frappe-bench/apps/${APP_NAME}"
/home/frappe/frappe-bench/env/bin/pip3 install --no-cache-dir -e "/home/frappe/frappe-bench/apps/${APP_NAME}"

View File

@@ -1,237 +0,0 @@
name: frappe_docker
services:
backend:
depends_on:
configurator:
condition: service_completed_successfully
required: true
image: frappe/erpnext:v15.83.0
networks:
default: null
platform: linux/amd64
pull_policy: always
restart: unless-stopped
volumes:
- type: volume
source: sites
target: /home/frappe/frappe-bench/sites
volume: {}
configurator:
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;
depends_on:
db:
condition: service_healthy
required: true
redis-cache:
condition: service_started
required: true
redis-queue:
condition: service_started
required: true
entrypoint:
- bash
- -c
environment:
DB_HOST: db
DB_PORT: "3306"
REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379
SOCKETIO_PORT: "9000"
image: frappe/erpnext:v15.83.0
networks:
default: null
platform: linux/amd64
pull_policy: always
restart: on-failure
volumes:
- type: volume
source: sites
target: /home/frappe/frappe-bench/sites
volume: {}
db:
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed
environment:
MARIADB_AUTO_UPGRADE: "1"
MYSQL_ROOT_PASSWORD: khaelicloud
healthcheck:
test:
- CMD
- healthcheck.sh
- --connect
- --innodb_initialized
timeout: 5s
interval: 5s
retries: 5
start_period: 5s
image: mariadb:11.8
networks:
default: null
restart: unless-stopped
volumes:
- type: volume
source: db-data
target: /var/lib/mysql
volume: {}
frontend:
command:
- nginx-entrypoint.sh
depends_on:
backend:
condition: service_started
required: true
websocket:
condition: service_started
required: true
environment:
BACKEND: backend:8000
CLIENT_MAX_BODY_SIZE: 50m
FRAPPE_SITE_NAME_HEADER: $$host
PROXY_READ_TIMEOUT: "120"
SOCKETIO: websocket:9000
UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
UPSTREAM_REAL_IP_RECURSIVE: "off"
image: frappe/erpnext:v15.83.0
networks:
default: null
platform: linux/amd64
ports:
- mode: ingress
target: 8080
published: "8080"
protocol: tcp
pull_policy: always
restart: unless-stopped
volumes:
- type: volume
source: sites
target: /home/frappe/frappe-bench/sites
volume: {}
queue-long:
command:
- bench
- worker
- --queue
- long,default,short
depends_on:
configurator:
condition: service_completed_successfully
required: true
image: frappe/erpnext:v15.83.0
networks:
default: null
platform: linux/amd64
pull_policy: always
restart: unless-stopped
volumes:
- type: volume
source: sites
target: /home/frappe/frappe-bench/sites
volume: {}
queue-short:
command:
- bench
- worker
- --queue
- short,default
depends_on:
configurator:
condition: service_completed_successfully
required: true
image: frappe/erpnext:v15.83.0
networks:
default: null
platform: linux/amd64
pull_policy: always
restart: unless-stopped
volumes:
- type: volume
source: sites
target: /home/frappe/frappe-bench/sites
volume: {}
redis-cache:
image: redis:6.2-alpine
networks:
default: null
restart: unless-stopped
redis-queue:
image: redis:6.2-alpine
networks:
default: null
restart: unless-stopped
volumes:
- type: volume
source: redis-queue-data
target: /data
volume: {}
scheduler:
command:
- bench
- schedule
depends_on:
configurator:
condition: service_completed_successfully
required: true
image: frappe/erpnext:v15.83.0
networks:
default: null
platform: linux/amd64
pull_policy: always
restart: unless-stopped
volumes:
- type: volume
source: sites
target: /home/frappe/frappe-bench/sites
volume: {}
websocket:
command:
- node
- /home/frappe/frappe-bench/apps/frappe/socketio.js
depends_on:
configurator:
condition: service_completed_successfully
required: true
image: frappe/erpnext:v15.83.0
networks:
default: null
platform: linux/amd64
pull_policy: always
restart: unless-stopped
volumes:
- type: volume
source: sites
target: /home/frappe/frappe-bench/sites
volume: {}
networks:
default:
name: frappe_docker_default
volumes:
db-data:
name: frappe_docker_db-data
redis-queue-data:
name: frappe_docker_redis-queue-data
sites:
name: frappe_docker_sites
x-backend-defaults:
depends_on:
configurator:
condition: service_completed_successfully
image: frappe/erpnext:v15.83.0
pull_policy: always
restart: unless-stopped
volumes:
- sites:/home/frappe/frappe-bench/sites
x-customizable-image:
image: frappe/erpnext:v15.83.0
pull_policy: always
restart: unless-stopped
x-depends-on-configurator:
depends_on:
configurator:
condition: service_completed_successfully

View File

@@ -1,95 +0,0 @@
x-customizable-image: &customizable_image
# By default the image used only contains the `frappe` and `erpnext` apps.
# See https://github.com/frappe/frappe_docker/blob/main/docs/custom-apps.md
# about using custom images.
image: ${CUSTOM_IMAGE:-frappe/erpnext}:${CUSTOM_TAG:-$ERPNEXT_VERSION}
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]
volumes:
- sites:/home/frappe/frappe-bench/sites
services:
configurator:
<<: *backend_defaults
platform: linux/amd64
entrypoint:
- bash
- -c
# add redis_socketio for backward compatibility
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;
environment:
DB_HOST: ${DB_HOST:-}
DB_PORT: ${DB_PORT:-}
REDIS_CACHE: ${REDIS_CACHE:-}
REDIS_QUEUE: ${REDIS_QUEUE:-}
SOCKETIO_PORT: 9000
depends_on: {}
restart: on-failure
backend:
<<: *backend_defaults
platform: linux/amd64
frontend:
<<: *customizable_image
platform: linux/amd64
command:
- nginx-entrypoint.sh
environment:
BACKEND: backend:8000
SOCKETIO: websocket:9000
FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host}
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
- websocket
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
queue-short:
<<: *backend_defaults
platform: linux/amd64
command: bench worker --queue short,default
queue-long:
<<: *backend_defaults
platform: linux/amd64
command: bench worker --queue long,default,short
scheduler:
<<: *backend_defaults
platform: linux/amd64
command: bench schedule
# ERPNext requires local assets access (Frappe does not)
volumes:
sites:

View File

@@ -1,32 +1,19 @@
{ {
"name": "Frappe Bench", "name": "Frappe Bench",
"forwardPorts": [8000, 9000, 6787], "appPort": [8000, 9000, 6787],
"remoteUser": "frappe", "remoteUser": "frappe",
"customizations": { "settings": {
"vscode": { "terminal.integrated.shell.linux": "/bin/bash"
"extensions": [
"ms-python.python",
"ms-vscode.live-server",
"grapecity.gc-excelviewer",
"mtxr.sqltools",
"visualstudioexptteam.vscodeintellicode"
],
"settings": {
"terminal.integrated.profiles.linux": {
"frappe bash": {
"path": "/bin/bash"
}
},
"terminal.integrated.defaultProfile.linux": "frappe bash",
"debug.node.autoAttach": "disabled"
}
}
}, },
"dockerComposeFile": "./docker-compose.yml", "dockerComposeFile": "./docker-compose.yml",
"service": "frappe", "service": "frappe",
"workspaceFolder": "/workspace/development", "workspaceFolder": "/workspace/development",
"shutdownAction": "stopCompose", "shutdownAction": "stopCompose",
"mounts": [ "extensions": [
"source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/frappe/.ssh,type=bind,consistency=cached" "ms-python.python",
"auchenberg.vscode-browser-preview",
"grapecity.gc-excelviewer",
"mtxr.sqltools",
"visualstudioexptteam.vscodeintellicode"
] ]
} }

View File

@@ -1,90 +1,51 @@
version: "3.7" version: "3.7"
services: services:
mariadb: mariadb:
image: docker.io/mariadb:11.8 image: mariadb:10.6
command: command:
- --character-set-server=utf8mb4 - --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci - --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake - --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
environment: environment:
MYSQL_ROOT_PASSWORD: 123 - MYSQL_ROOT_PASSWORD=123
MARIADB_AUTO_UPGRADE: 1 - MYSQL_USER=root
# Sometimes db initialization takes longer than 10 seconds and site-creator goes away.
# Frappe doesn't use CONVERT_TZ() function that requires time zone info, so we can just skip it.
- MYSQL_INITDB_SKIP_TZINFO=1
volumes: volumes:
- mariadb-data:/var/lib/mysql - mariadb-vol:/var/lib/mysql
# Enable PostgreSQL only if you use it, see development/README.md for more information. # Enable PostgreSQL only if you use it, see development/README.md for more information.
# postgresql: # postgresql:
# image: postgres:14 # image: postgres:11.8
# restart: on-failure
# environment: # environment:
# POSTGRES_PASSWORD: 123 # - POSTGRES_PASSWORD=123
# volumes: # volumes:
# - postgresql-data:/var/lib/postgresql/data # - postgresql-vol:/var/lib/postgresql/data
# Enable Mailpit if you need to test outgoing mail services
# See https://mailpit.axllent.org/
# mailpit:
# image: axllent/mailpit
# volumes:
# - mailpit-data:/data
# ports:
# - 8025:8025
# - 1025:1025
# environment:
# MP_MAX_MESSAGES: 5000
# MP_DATA_FILE: /data/mailpit.db
# MP_SMTP_AUTH_ACCEPT_ANY: 1
# MP_SMTP_AUTH_ALLOW_INSECURE: 1
redis-cache: redis-cache:
image: docker.io/redis:alpine image: redis:alpine
redis-queue: redis-queue:
image: docker.io/redis:alpine image: redis:alpine
redis-socketio:
image: redis:alpine
frappe: frappe:
image: docker.io/frappe/bench:latest image: frappe/bench:latest
# If you want to build the current bench image the Containerfile is in this Repo.
# build: ../images/bench
command: sleep infinity command: sleep infinity
environment: environment:
- SHELL=/bin/bash - SHELL=/bin/bash
volumes: volumes:
- ..:/workspace:cached - ..:/workspace:cached
# Enable if you require git cloning
# - ${HOME}/.ssh:/home/frappe/.ssh
working_dir: /workspace/development working_dir: /workspace/development
ports: ports:
- 8000-8005:8000-8005 - "8000-8005:8000-8005"
- 9000-9005:9000-9005 - "9000-9005:9000-9005"
# enable the below service if you need Cypress UI Tests to be executed
# Before enabling ensure install_x11_deps.sh has been executed and display variable is exported.
# Run install_x11_deps.sh again if DISPLAY is not set
# ui-tester:
# # pass custom command to start Cypress otherwise it will use the entrypoint
# # specified in the Cypress Docker image.
# # also pass "--project <folder>" so that when Cypress opens
# # it can find file "cypress.json" and show integration specs
# # https://on.cypress.io/command-line#cypress-open
# entrypoint: 'sleep infinity'
# image: "docker.io/cypress/included:latest"
# environment:
# - SHELL=/bin/bash
# # get the IP address of the host machine and allow X11 to accept
# # incoming connections from that IP address
# # IP=$(ipconfig getifaddr en0) or mac or \
# # IP=$($(hostname -I | awk '{print $1}') ) for Ubuntu
# # /usr/X11/bin/xhost + $IP
# # then pass the environment variable DISPLAY to show Cypress GUI on the host system
# # DISPLAY=$IP:0
# - DISPLAY
# volumes:
# # for Cypress to communicate with the X11 server pass this socket file
# # in addition to any other mapped volumes
# - /tmp/.X11-unix:/tmp/.X11-unix
# - ..:/workspace:z,cached
# network_mode: "host"
volumes: volumes:
mariadb-data: mariadb-vol:
#postgresql-data: postgresql-vol:
#mailpit-data:

299
development/README.md Normal file
View File

@@ -0,0 +1,299 @@
# Getting Started
## Prerequisites
In order to start developing you need to satisfy the following prerequisites:
- Docker
- docker-compose
- user added to docker group
It is recommended you allocate at least 4GB of RAM to docker:
- [Instructions for Windows](https://docs.docker.com/docker-for-windows/#resources)
- [Instructions for macOS](https://docs.docker.com/docker-for-mac/#resources)
## Bootstrap Containers for development
Clone and change directory to frappe_docker directory
```shell
git clone https://github.com/frappe/frappe_docker.git
cd frappe_docker
```
Copy example devcontainer config from `devcontainer-example` to `.devcontainer`
```shell
cp -R devcontainer-example .devcontainer
```
Copy example vscode config for devcontainer from `development/vscode-example` to `development/.vscode`. This will setup basic configuration for debugging.
```shell
cp -R development/vscode-example development/.vscode
```
## Use VSCode Remote Containers extension
For most people getting started with Frappe development, the best solution is to use [VSCode Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
Before opening the folder in container, determine the database that you want to use. The default is MariaDB.
If you want to use PostgreSQL instead, edit `.devcontainer/docker-compose.yml` and uncomment the section for `postgresql` service, and you may also want to comment `mariadb` as well.
VSCode should automatically inquire you to install the required extensions, that can also be installed manually as follows:
- Install Remote - Containers for VSCode
- through command line `code --install-extension ms-vscode-remote.remote-containers`
- clicking on the Install button in the Vistual Studio Marketplace: [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
- View: Extensions command in VSCode (Windows: Ctrl+Shift+X; macOS: Cmd+Shift+X) then search for extension `ms-vscode-remote.remote-containers`
After the extensions are installed, you can:
- Open frappe_docker folder in VS Code.
- `code .`
- Launch the command, from Command Palette (Ctrl + Shift + P) `Execute Remote Containers : Reopen in Container`. You can also click in the bottom left corner to access the remote container menu.
Notes:
- The `development` directory is ignored by git. It is mounted and available inside the container. Create all your benches (installations of bench, the tool that manages frappe) inside this directory.
- nvm with node v12 and v10 is installed. Check with `nvm ls`. Node v12 is used by default.
### Setup first bench
Run the following commands in the terminal inside the container. You might need to create a new terminal in VSCode.
```shell
bench init --skip-redis-config-generation --frappe-branch version-13 frappe-bench
cd frappe-bench
```
Note: For version 12 use python 3.7 by passing option to `bench init` command, e.g. `bench init --skip-redis-config-generation --frappe-branch version-12 --python python3.7 frappe-bench`
### Setup hosts
We need to tell bench to use the right containers instead of localhost. Run the following commands inside the container:
```shell
bench set-mariadb-host mariadb
bench set-redis-cache-host redis-cache:6379
bench set-redis-queue-host redis-queue:6379
bench set-redis-socketio-host redis-socketio:6379
```
### Edit Honcho's Procfile
Note : With the option '--skip-redis-config-generation' during bench init, these actions are no more needed. But at least, take a look to ProcFile to see what going on when bench launch honcho on start command
Honcho is the tool used by Bench to manage all the processes Frappe requires. Usually, these all run in localhost, but in this case, we have external containers for Redis. For this reason, we have to stop Honcho from trying to start Redis processes.
Open the Procfile file and remove the three lines containing the configuration from Redis, either by editing manually the file:
```shell
code Procfile
```
Or running the following command:
```shell
sed -i '/redis/d' ./Procfile
```
### Create a new site with bench
You can create a new site with the following command:
```shell
bench new-site sitename --no-mariadb-socket
```
sitename MUST end with .localhost for trying deployments locally.
for example:
```shell
bench new-site mysite.localhost --no-mariadb-socket
```
The same command can be run non-interactively as well:
```shell
bench new-site mysite.localhost --mariadb-root-password 123 --admin-password admin --no-mariadb-socket
```
The command will ask the MariaDB root password. The default root password is `123`.
This will create a new site and a `mysite.localhost` directory under `frappe-bench/sites`.
The option `--no-mariadb-socket` will configure site's database credentials to work with docker.
You may need to configure your system /etc/hosts if you're on Linux, Mac, or its Windows equivalent.
To setup site with PostgreSQL as database use option `--db-type postgres` and `--db-host postgresql`. (Available only v12 onwards, currently NOT available for ERPNext).
Example:
```shell
bench new-site mypgsql.localhost --db-type postgres --db-host postgresql
```
To avoid entering postgresql username and root password, set it in `common_site_config.json`,
```shell
bench config set-common-config -c root_login postgres
bench config set-common-config -c root_password '"123"'
```
Note: If PostgreSQL is not required, the postgresql service / container can be stopped.
### Set bench developer mode on the new site
To develop a new app, the last step will be setting the site into developer mode. Documentation is available at [this link](https://frappe.io/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe).
```shell
bench --site mysite.localhost set-config developer_mode 1
bench --site mysite.localhost clear-cache
```
### Install an app
To install an app we need to fetch it from the appropriate git repo, then install in on the appropriate site:
You can check [VSCode container remote extension documentation](https://code.visualstudio.com/docs/remote/containers#_sharing-git-credentials-with-your-container) regarding git credential sharing.
To install custom app
```shell
# --branch is optional, use it to point to branch on custom app repository
bench get-app --branch version-12 myapp https://github.com/myusername/myapp.git
bench --site mysite.localhost install-app myapp
```
To install ERPNext (from the version-12 branch):
```shell
bench get-app --branch version-12 erpnext https://github.com/frappe/erpnext.git
bench --site mysite.localhost install-app erpnext
```
Note: Both frappe and erpnext must be on branch with same name. e.g. version-12
### Start Frappe without debugging
Execute following command from the `frappe-bench` directory.
```shell
bench start
```
You can now login with user `Administrator` and the password you choose when creating the site.
Your website will now be accessible at location [mysite.localhost:8000](http://mysite.localhost:8000)
Note: To start bench with debugger refer section for debugging.
### Start Frappe with Visual Studio Code Python Debugging
To enable Python debugging inside Visual Studio Code, you must first install the `ms-python.python` extension inside the container. This should have already happened automatically, but depending on your VSCode config, you can force it by:
- Click on the extension icon inside VSCode
- Search `ms-python.python`
- Click on `Install on Dev Container: Frappe Bench`
- Click on 'Reload'
We need to start bench separately through the VSCode debugger. For this reason, **instead** of running `bench start` you should run the following command inside the frappe-bench directory:
```shell
honcho start \
socketio \
watch \
schedule \
worker_short \
worker_long \
worker_default
```
Alternatively you can use the VSCode launch configuration "Honcho SocketIO Watch Schedule Worker" which launches the same command as above.
This command starts all processes with the exception of Redis (which is already running in separate container) and the `web` process. The latter can can finally be started from the debugger tab of VSCode by clicking on the "play" button.
You can now login with user `Administrator` and the password you choose when creating the site, if you followed this guide's unattended install that password is going to be `admin`.
To debug workers, skip starting worker with honcho and start it with VSCode debugger.
For advance vscode configuration in the devcontainer, change the config files in `development/.vscode`.
## Developing using the interactive console
You can launch a simple interactive shell console in the terminal with:
```shell
bench --site mysite.localhost console
```
More likely, you may want to launch VSCode interactive console based on Jupyter kernel.
Launch VSCode command palette (cmd+shift+p or ctrl+shift+p), run the command `Python: Select interpreter to start Jupyter server` and select `/workspace/development/frappe-bench/env/bin/python`.
The first step is installing and updating the required software. Usually the frappe framework may require an older version of Jupyter, while VSCode likes to move fast, this can [cause issues](https://github.com/jupyter/jupyter_console/issues/158). For this reason we need to run the following command.
```shell
/workspace/development/frappe-bench/env/bin/python -m pip install --upgrade jupyter ipykernel ipython
```
Then, run the command `Python: Show Python interactive window` from the VSCode command palette.
Replace `mysite.localhost` with your site and run the following code in a Jupyter cell:
```python
import frappe
frappe.init(site='mysite.localhost', sites_path='/workspace/development/frappe-bench/sites')
frappe.connect()
frappe.local.lang = frappe.db.get_default('lang')
frappe.db.connect()
```
The first command can take a few seconds to be executed, this is to be expected.
### Fixing MariaDB issues after rebuilding the container
For any reason after rebuilding the container if you are not be able to access MariaDB correctly with the previous configuration. Follow these instructions.
The parameter `'db_name'@'%'` needs to be set in MariaDB and permission to the site database suitably assigned to the user.
This step has to be repeated for all sites available under the current bench.
Example shows the queries to be executed for site `localhost`
Open sites/localhost/site_config.json:
```shell
code sites/localhost/site_config.json
```
and take note of the parameters `db_name` and `db_password`.
Enter MariaDB Interactive shell:
```shell
mysql -uroot -p123 -hmariadb
```
Execute following queries replacing `db_name` and `db_password` with the values found in site_config.json.
```sql
UPDATE mysql.global_priv SET Host = '%' where User = 'db_name'; FLUSH PRIVILEGES;
SET PASSWORD FOR 'db_name'@'%' = PASSWORD('db_password'); FLUSH PRIVILEGES;
GRANT ALL PRIVILEGES ON `db_name`.* TO 'db_name'@'%'; FLUSH PRIVILEGES;
EXIT;
```
## Manually start containers
In case you don't use VSCode, you may start the containers manually with the following command:
### Running the containers
```shell
docker-compose -f .devcontainer/docker-compose.yml up -d
```
And enter the interactive shell for the development container with the following command:
```shell
docker exec -e "TERM=xterm-256color" -w /workspace/development -it devcontainer_frappe_1 bash
```

View File

@@ -1,6 +0,0 @@
[
{
"url": "https://github.com/frappe/erpnext.git",
"branch": "version-15"
}
]

View File

@@ -1,245 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import subprocess
def cprint(*args, level: int = 1):
"""
logs colorful messages
level = 1 : RED
level = 2 : GREEN
level = 3 : YELLOW
default level = 1
"""
CRED = "\033[31m"
CGRN = "\33[92m"
CYLW = "\33[93m"
reset = "\033[0m"
message = " ".join(map(str, args))
if level == 1:
print(CRED, message, reset) # noqa: T001, T201
if level == 2:
print(CGRN, message, reset) # noqa: T001, T201
if level == 3:
print(CYLW, message, reset) # noqa: T001, T201
def main():
parser = get_args_parser()
args = parser.parse_args()
init_bench_if_not_exist(args)
create_site_in_bench(args)
def get_args_parser():
parser = argparse.ArgumentParser()
parser.add_argument(
"-j",
"--apps-json",
action="store",
type=str,
help="Path to apps.json, default: apps-example.json",
default="apps-example.json",
) # noqa: E501
parser.add_argument(
"-b",
"--bench-name",
action="store",
type=str,
help="Bench directory name, default: frappe-bench",
default="frappe-bench",
) # noqa: E501
parser.add_argument(
"-s",
"--site-name",
action="store",
type=str,
help="Site name, should end with .localhost, default: development.localhost", # noqa: E501
default="development.localhost",
)
parser.add_argument(
"-r",
"--frappe-repo",
action="store",
type=str,
help="frappe repo to use, default: https://github.com/frappe/frappe", # noqa: E501
default="https://github.com/frappe/frappe",
)
parser.add_argument(
"-t",
"--frappe-branch",
action="store",
type=str,
help="frappe repo to use, default: version-15", # noqa: E501
default="version-15",
)
parser.add_argument(
"-p",
"--py-version",
action="store",
type=str,
help="python version, default: Not Set", # noqa: E501
default=None,
)
parser.add_argument(
"-n",
"--node-version",
action="store",
type=str,
help="node version, default: Not Set", # noqa: E501
default=None,
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="verbose output", # noqa: E501
)
parser.add_argument(
"-a",
"--admin-password",
action="store",
type=str,
help="admin password for site, default: admin", # noqa: E501
default="admin",
)
parser.add_argument(
"-d",
"--db-type",
action="store",
type=str,
help="Database type to use (e.g., mariadb or postgres)",
default="mariadb", # Set your default database type here
)
return parser
def init_bench_if_not_exist(args):
if os.path.exists(args.bench_name):
cprint("Bench already exists. Only site will be created", level=3)
return
try:
env = os.environ.copy()
if args.py_version:
env["PYENV_VERSION"] = args.py_version
init_command = ""
if args.node_version:
init_command = f"nvm use {args.node_version};"
if args.py_version:
init_command += f"PYENV_VERSION={args.py_version} "
init_command += "bench init "
init_command += "--skip-redis-config-generation "
init_command += "--verbose " if args.verbose else " "
init_command += f"--frappe-path={args.frappe_repo} "
init_command += f"--frappe-branch={args.frappe_branch} "
init_command += f"--apps_path={args.apps_json} "
init_command += args.bench_name
command = [
"/bin/bash",
"-i",
"-c",
init_command,
]
subprocess.call(command, env=env, cwd=os.getcwd())
cprint("Configuring Bench ...", level=2)
cprint("Set db_host", level=3)
if args.db_type:
cprint(f"Setting db_type to {args.db_type}", level=3)
subprocess.call(
["bench", "set-config", "-g", "db_type", args.db_type],
cwd=os.path.join(os.getcwd(), args.bench_name),
)
cprint("Set redis_cache to redis://redis-cache:6379", level=3)
subprocess.call(
[
"bench",
"set-config",
"-g",
"redis_cache",
"redis://redis-cache:6379",
],
cwd=os.getcwd() + "/" + args.bench_name,
)
cprint("Set redis_queue to redis://redis-queue:6379", level=3)
subprocess.call(
[
"bench",
"set-config",
"-g",
"redis_queue",
"redis://redis-queue:6379",
],
cwd=os.getcwd() + "/" + args.bench_name,
)
cprint(
"Set redis_socketio to redis://redis-queue:6379 for backward compatibility", # noqa: E501
level=3,
)
subprocess.call(
[
"bench",
"set-config",
"-g",
"redis_socketio",
"redis://redis-queue:6379",
],
cwd=os.getcwd() + "/" + args.bench_name,
)
cprint("Set developer_mode", level=3)
subprocess.call(
["bench", "set-config", "-gp", "developer_mode", "1"],
cwd=os.getcwd() + "/" + args.bench_name,
)
except subprocess.CalledProcessError as e:
cprint(e.output, level=1)
def create_site_in_bench(args):
if "mariadb" == args.db_type:
cprint("Set db_host", level=3)
subprocess.call(
["bench", "set-config", "-g", "db_host", "mariadb"],
cwd=os.getcwd() + "/" + args.bench_name,
)
new_site_cmd = [
"bench",
"new-site",
f"--db-root-username=root",
f"--db-host=mariadb", # Should match the compose service name
f"--db-type={args.db_type}", # Add the selected database type
f"--mariadb-user-host-login-scope=%",
f"--db-root-password=123", # Replace with your MariaDB password
f"--admin-password={args.admin_password}",
]
else:
cprint("Set db_host", level=3)
subprocess.call(
["bench", "set-config", "-g", "db_host", "postgresql"],
cwd=os.getcwd() + "/" + args.bench_name,
)
new_site_cmd = [
"bench",
"new-site",
f"--db-root-username=root",
f"--db-host=postgresql", # Should match the compose service name
f"--db-type={args.db_type}", # Add the selected database type
f"--db-root-password=123", # Replace with your PostgreSQL password
f"--admin-password={args.admin_password}",
]
apps = os.listdir(f"{os.getcwd()}/{args.bench_name}/apps")
apps.remove("frappe")
for app in apps:
new_site_cmd.append(f"--install-app={app}")
new_site_cmd.append(args.site_name)
cprint(f"Creating Site {args.site_name} ...", level=2)
subprocess.call(
new_site_cmd,
cwd=os.getcwd() + "/" + args.bench_name,
)
if __name__ == "__main__":
main()

View File

@@ -6,7 +6,7 @@
"configurations": [ "configurations": [
{ {
"name": "Bench Web", "name": "Bench Web",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": [ "args": [
@@ -17,17 +17,7 @@
"--noreload", "--noreload",
"--nothreading" "--nothreading"
], ],
"cwd": "${workspaceFolder}/frappe-bench/sites", "pythonPath": "${workspaceFolder}/frappe-bench/env/bin/python",
"env": {
"DEV_SERVER": "1"
}
},
{
"name": "Bench Short Worker",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "worker", "--queue", "short"],
"cwd": "${workspaceFolder}/frappe-bench/sites", "cwd": "${workspaceFolder}/frappe-bench/sites",
"env": { "env": {
"DEV_SERVER": "1" "DEV_SERVER": "1"
@@ -35,10 +25,23 @@
}, },
{ {
"name": "Bench Default Worker", "name": "Bench Default Worker",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "worker", "--queue", "default"], "args": ["frappe", "worker", "--queue", "default"],
"pythonPath": "${workspaceFolder}/frappe-bench/env/bin/python",
"cwd": "${workspaceFolder}/frappe-bench/sites",
"env": {
"DEV_SERVER": "1"
}
},
{
"name": "Bench Short Worker",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "worker", "--queue", "short"],
"pythonPath": "${workspaceFolder}/frappe-bench/env/bin/python",
"cwd": "${workspaceFolder}/frappe-bench/sites", "cwd": "${workspaceFolder}/frappe-bench/sites",
"env": { "env": {
"DEV_SERVER": "1" "DEV_SERVER": "1"
@@ -46,10 +49,11 @@
}, },
{ {
"name": "Bench Long Worker", "name": "Bench Long Worker",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "worker", "--queue", "long"], "args": ["frappe", "worker", "--queue", "long"],
"pythonPath": "${workspaceFolder}/frappe-bench/env/bin/python",
"cwd": "${workspaceFolder}/frappe-bench/sites", "cwd": "${workspaceFolder}/frappe-bench/sites",
"env": { "env": {
"DEV_SERVER": "1" "DEV_SERVER": "1"
@@ -57,21 +61,21 @@
}, },
{ {
"name": "Honcho SocketIO Watch Schedule Worker", "name": "Honcho SocketIO Watch Schedule Worker",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"python": "/home/frappe/.pyenv/shims/python",
"program": "/home/frappe/.local/bin/honcho", "program": "/home/frappe/.local/bin/honcho",
"pythonPath": "${workspaceFolder}/frappe-bench/env/bin/python",
"cwd": "${workspaceFolder}/frappe-bench", "cwd": "${workspaceFolder}/frappe-bench",
"console": "internalConsole", "console": "internalConsole",
"args": ["start", "socketio", "watch", "schedule", "worker"], "args": [
"postDebugTask": "Clean Honcho SocketIO Watch Schedule Worker" "start",
} "socketio",
], "watch",
"compounds": [ "schedule",
{ "worker_short",
"name": "Honcho + Web debug", "worker_long",
"configurations": ["Bench Web", "Honcho SocketIO Watch Schedule Worker"], "worker_default"
"stopAll": true ]
} }
] ]
} }

View File

@@ -1,3 +1,5 @@
{ {
"python.defaultInterpreterPath": "${workspaceFolder}/frappe-bench/env/bin/python" "debug.node.autoAttach": "disabled",
"python.pythonPath": "/workspace/frappe-bench/env/bin/python",
"python.analysis.extraPaths": ["./frappe-bench/apps/frappe"]
} }

View File

@@ -1,22 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Clean Honcho SocketIO Watch Schedule Worker",
"detail": "When stopping the debug process from vscode window, the honcho won't receive the SIGINT signal. This task will send the SIGINT signal to the honcho processes.",
"type": "shell",
"command": "pkill -SIGINT -f bench; pkill -SIGINT -f socketio",
"isBackground": false,
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"close": true
}
}
]
}

View File

@@ -1,53 +1,13 @@
# Docker Buildx Bake build definition file # Docker Buildx Bake build definition file
# Reference: https://github.com/docker/buildx/blob/master/docs/reference/buildx_bake.md # Reference: https://github.com/docker/buildx/blob/master/docs/reference/buildx_bake.md
variable "REGISTRY_USER" {
default = "frappe"
}
variable PYTHON_VERSION {
default = "3.11.6"
}
variable NODE_VERSION {
default = "20.19.2"
}
variable "FRAPPE_VERSION" {
default = "develop"
}
variable "ERPNEXT_VERSION" {
default = "develop"
}
variable "FRAPPE_REPO" {
default = "https://github.com/frappe/frappe"
}
variable "ERPNEXT_REPO" {
default = "https://github.com/frappe/erpnext"
}
variable "BENCH_REPO" {
default = "https://github.com/frappe/bench"
}
variable "LATEST_BENCH_RELEASE" {
default = "latest"
}
# Bench image # Bench image
target "bench" { target "bench" {
args = { context = "build/bench"
GIT_REPO = "${BENCH_REPO}"
}
context = "images/bench"
target = "bench" target = "bench"
tags = [ tags = ["frappe/bench:latest"]
"frappe/bench:${LATEST_BENCH_RELEASE}",
"frappe/bench:latest",
]
} }
target "bench-test" { target "bench-test" {
@@ -58,56 +18,255 @@ target "bench-test" {
# Main images # Main images
# Base for all other targets # Base for all other targets
group "default" { target "frappe-nginx" {
targets = ["erpnext", "base", "build"] dockerfile = "build/frappe-nginx/Dockerfile"
} }
function "tag" { target "frappe-worker" {
params = [repo, version] dockerfile = "build/frappe-worker/Dockerfile"
result = [
# Push frappe or erpnext branch as tag
"${REGISTRY_USER}/${repo}:${version}",
# If `version` param is develop (development build) then use tag `latest`
"${version}" == "develop" ? "${REGISTRY_USER}/${repo}:latest" : "${REGISTRY_USER}/${repo}:${version}",
# Make short tag for major version if possible. For example, from v13.16.0 make v13.
can(regex("(v[0-9]+)[.]", "${version}")) ? "${REGISTRY_USER}/${repo}:${regex("(v[0-9]+)[.]", "${version}")[0]}" : "",
# Make short tag for major version if possible. For example, from v13.16.0 make version-13.
can(regex("(v[0-9]+)[.]", "${version}")) ? "${REGISTRY_USER}/${repo}:version-${regex("([0-9]+)[.]", "${version}")[0]}" : "",
]
} }
target "default-args" { target "frappe-socketio" {
dockerfile = "build/frappe-socketio/Dockerfile"
}
target "erpnext-nginx" {
dockerfile = "build/erpnext-nginx/Dockerfile"
}
target "erpnext-worker" {
dockerfile = "build/erpnext-worker/Dockerfile"
}
# Helpers
target "develop-args" {
args = { args = {
FRAPPE_PATH = "${FRAPPE_REPO}" GIT_BRANCH = "develop"
ERPNEXT_PATH = "${ERPNEXT_REPO}" IMAGE_TAG = "develop"
BENCH_REPO = "${BENCH_REPO}"
FRAPPE_BRANCH = "${FRAPPE_VERSION}"
ERPNEXT_BRANCH = "${ERPNEXT_VERSION}"
PYTHON_VERSION = "${PYTHON_VERSION}"
NODE_VERSION = "${NODE_VERSION}"
} }
} }
target "erpnext" { function "set_develop_tags" {
inherits = ["default-args"] params = [repo]
context = "." result = ["${repo}:latest", "${repo}:edge", "${repo}:develop"]
dockerfile = "images/production/Containerfile"
target = "erpnext"
tags = tag("erpnext", "${ERPNEXT_VERSION}")
} }
target "base" { # NOTE: Variable are used only for stable builds
inherits = ["default-args"] variable "GIT_TAG" {} # git tag, e.g. v13.15.0
context = "." variable "GIT_BRANCH" {} # git branch, e.g. version-13
dockerfile = "images/production/Containerfile" variable "VERSION" {} # Frappe and ERPNext version, e.g. 13
target = "base"
tags = tag("base", "${FRAPPE_VERSION}") target "stable-args" {
args = {
GIT_BRANCH = "${GIT_BRANCH}"
IMAGE_TAG = "${GIT_BRANCH}"
# ERPNext build fails on v12
# TODO: Remove PYTHON_VERSION argument when v12 will stop being supported
PYTHON_VERSION = "${VERSION}" == "12" ? "3.7" : "3.9"
}
} }
target "build" { function "set_stable_tags" {
inherits = ["default-args"] # e.g. base_image:v13.15.0, base_image:v13, base_image:version-13
context = "." params = [repo]
dockerfile = "images/production/Containerfile" result = ["${repo}:${GIT_TAG}", "${repo}:v${VERSION}", "${repo}:${GIT_BRANCH}"]
target = "build" }
tags = tag("build", "${ERPNEXT_VERSION}")
target "test-erpnext-args" {
args = {
IMAGE_TAG = "test"
DOCKER_REGISTRY_PREFIX = "localhost:5000/frappe"
}
}
function "set_local_test_tags" {
params = [repo]
result = ["localhost:5000/${repo}:test"]
}
function "set_test_tags" {
params = [repo]
result = ["${repo}:test"]
}
# Develop images
target "frappe-nginx-develop" {
inherits = ["frappe-nginx", "develop-args"]
tags = set_develop_tags("frappe/frappe-nginx")
}
target "frappe-worker-develop" {
inherits = ["frappe-worker", "develop-args"]
tags = set_develop_tags("frappe/frappe-worker")
}
target "frappe-socketio-develop" {
inherits = ["frappe-socketio", "develop-args"]
tags = set_develop_tags("frappe/frappe-socketio")
}
target "erpnext-nginx-develop" {
inherits = ["erpnext-nginx", "develop-args"]
tags = set_develop_tags("frappe/erpnext-nginx")
}
target "erpnext-worker-develop" {
inherits = ["erpnext-worker", "develop-args"]
tags = set_develop_tags("frappe/erpnext-worker")
}
group "frappe-develop" {
targets = ["frappe-nginx-develop", "frappe-worker-develop", "frappe-socketio-develop"]
}
group "erpnext-develop" {
targets = ["erpnext-nginx-develop", "erpnext-worker-develop"]
}
# Test develop images
target "frappe-nginx-develop-test-local" {
inherits = ["frappe-nginx-develop"]
tags = set_local_test_tags("frappe/frappe-nginx")
}
target "frappe-worker-develop-test-local" {
inherits = ["frappe-worker-develop"]
tags = set_local_test_tags("frappe/frappe-worker")
}
target "frappe-socketio-develop-test-local" {
inherits = ["frappe-socketio-develop"]
tags = set_local_test_tags("frappe/frappe-socketio")
}
target "frappe-nginx-develop-test" {
inherits = ["frappe-nginx-develop"]
tags = set_test_tags("frappe/frappe-nginx")
}
target "frappe-worker-develop-test" {
inherits = ["frappe-worker-develop"]
tags = set_test_tags("frappe/frappe-worker")
}
target "frappe-socketio-develop-test" {
inherits = ["frappe-socketio-develop"]
tags = set_test_tags("frappe/frappe-socketio")
}
target "erpnext-nginx-develop-test" {
inherits = ["erpnext-nginx-develop", "test-erpnext-args"]
tags = set_test_tags("frappe/erpnext-nginx")
}
target "erpnext-worker-develop-test" {
inherits = ["erpnext-worker-develop", "test-erpnext-args"]
tags = set_test_tags("frappe/erpnext-worker")
}
group "frappe-develop-test-local" {
targets = ["frappe-nginx-develop-test-local", "frappe-worker-develop-test-local", "frappe-socketio-develop-test-local"]
}
group "frappe-develop-test" {
targets = ["frappe-nginx-develop-test", "frappe-worker-develop-test", "frappe-socketio-develop-test"]
}
group "erpnext-develop-test" {
targets = ["erpnext-nginx-develop-test", "erpnext-worker-develop-test"]
}
# Stable images
target "frappe-nginx-stable" {
inherits = ["frappe-nginx", "stable-args"]
tags = set_stable_tags("frappe/frappe-nginx")
}
target "frappe-worker-stable" {
inherits = ["frappe-worker", "stable-args"]
tags = set_stable_tags("frappe/frappe-worker")
}
target "frappe-socketio-stable" {
inherits = ["frappe-socketio", "stable-args"]
tags = set_stable_tags("frappe/frappe-socketio")
}
target "erpnext-nginx-stable" {
inherits = ["erpnext-nginx", "stable-args"]
tags = set_stable_tags("frappe/erpnext-nginx")
}
target "erpnext-worker-stable" {
inherits = ["erpnext-worker", "stable-args"]
tags = set_stable_tags("frappe/erpnext-worker")
}
group "frappe-stable" {
targets = ["frappe-nginx-stable", "frappe-worker-stable", "frappe-socketio-stable"]
}
group "erpnext-stable" {
targets = ["erpnext-nginx-stable", "erpnext-worker-stable"]
}
# Test stable images
target "frappe-nginx-stable-test-local" {
inherits = ["frappe-nginx-stable"]
tags = set_local_test_tags("frappe/frappe-nginx")
}
target "frappe-worker-stable-test-local" {
inherits = ["frappe-worker-stable"]
tags = set_local_test_tags("frappe/frappe-worker")
}
target "frappe-socketio-stable-test-local" {
inherits = ["frappe-socketio-stable"]
tags = set_local_test_tags("frappe/frappe-socketio")
}
target "frappe-nginx-stable-test" {
inherits = ["frappe-nginx-stable"]
tags = set_test_tags("frappe/frappe-nginx")
}
target "frappe-worker-stable-test" {
inherits = ["frappe-worker-stable"]
tags = set_test_tags("frappe/frappe-worker")
}
target "frappe-socketio-stable-test" {
inherits = ["frappe-socketio-stable"]
tags = set_test_tags("frappe/frappe-socketio")
}
target "erpnext-nginx-stable-test" {
inherits = ["erpnext-nginx-stable", "test-erpnext-args"]
tags = set_test_tags("frappe/erpnext-nginx")
}
target "erpnext-worker-stable-test" {
inherits = ["erpnext-worker-stable", "test-erpnext-args"]
tags = set_test_tags("frappe/erpnext-worker")
}
group "frappe-stable-test-local" {
targets = ["frappe-nginx-stable-test-local", "frappe-worker-stable-test-local", "frappe-socketio-stable-test-local"]
}
group "frappe-stable-test" {
targets = ["frappe-nginx-stable-test", "frappe-worker-stable-test", "frappe-socketio-stable-test"]
}
group "erpnext-stable-test" {
targets = ["erpnext-nginx-stable-test", "erpnext-worker-stable-test"]
} }

167
docker-compose.yml Normal file
View File

@@ -0,0 +1,167 @@
version: "3"
services:
traefik:
image: "traefik:v2.2"
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.myresolver.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
labels:
# enable traefik
- "traefik.enable=true"
# global redirect to https for production only
- "${HTTPS_REDIRECT_RULE_LABEL}"
- "${HTTPS_REDIRECT_ENTRYPOINT_LABEL}"
- "${HTTPS_REDIRECT_MIDDLEWARE_LABEL}"
# middleware redirect for production only
- "${HTTPS_USE_REDIRECT_MIDDLEWARE_LABEL}"
ports:
- "80:80"
- "443:443"
volumes:
- cert-vol:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
userns_mode: "host"
erpnext-nginx:
image: frappe/erpnext-nginx:${ERPNEXT_VERSION}
restart: on-failure
environment:
- FRAPPE_PY=erpnext-python
- FRAPPE_PY_PORT=8000
- FRAPPE_SOCKETIO=frappe-socketio
- SOCKETIO_PORT=9000
- SKIP_NGINX_TEMPLATE_GENERATION=${SKIP_NGINX_TEMPLATE_GENERATION}
# For refactored images
- BACKEND=erpnext-python:8000
- SOCKETIO=frappe-socketio:9000
- FRAPPE_SITE_NAME_HEADER=${FRAPPE_SITE_NAME_HEADER:-$$host}
- 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}
labels:
- "traefik.enable=true"
- "traefik.http.routers.erpnext-nginx.rule=Host(${SITES})"
- "${ENTRYPOINT_LABEL}"
- "${CERT_RESOLVER_LABEL}"
- "traefik.http.services.erpnext-nginx.loadbalancer.server.port=8080"
volumes:
- sites-vol:/var/www/html/sites:rw
- assets-vol:/assets:rw
erpnext-python:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
environment:
- MARIADB_HOST=${MARIADB_HOST}
- REDIS_CACHE=redis-cache:6379
- REDIS_QUEUE=redis-queue:6379
- REDIS_SOCKETIO=redis-socketio:6379
- SOCKETIO_PORT=9000
- AUTO_MIGRATE=1
- WORKER_CLASS=${WORKER_CLASS}
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- assets-vol:/home/frappe/frappe-bench/sites/assets:rw
frappe-socketio:
image: frappe/frappe-socketio:${FRAPPE_VERSION}
restart: on-failure
depends_on:
- redis-socketio
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
erpnext-worker-default:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
command: ["bench", "worker", "--queue", "default"]
depends_on:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
erpnext-worker-short:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
command: ["bench", "worker", "--queue", "short"]
depends_on:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
erpnext-worker-long:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
command: ["bench", "worker", "--queue", "long"]
depends_on:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
erpnext-schedule:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
command: ["bench", "schedule"]
depends_on:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
redis-cache:
image: redis:latest
restart: on-failure
volumes:
- redis-cache-vol:/data
redis-queue:
image: redis:latest
restart: on-failure
volumes:
- redis-queue-vol:/data
redis-socketio:
image: redis:latest
restart: on-failure
volumes:
- redis-socketio-vol:/data
mariadb:
image: mariadb:10.6
restart: on-failure
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
# Sometimes db initialization takes longer than 10 seconds and site creation can cause error.
# Frappe doesn't use CONVERT_TZ() function that requires time zone info, so we can just skip it.
- MYSQL_INITDB_SKIP_TZINFO=1
volumes:
- mariadb-vol:/var/lib/mysql
volumes:
mariadb-vol:
redis-cache-vol:
redis-queue-vol:
redis-socketio-vol:
assets-vol:
sites-vol:
cert-vol:
logs-vol:

View File

@@ -1,58 +0,0 @@
Create backup service or stack.
```yaml
# backup-job.yml
version: "3.7"
services:
backup:
image: frappe/erpnext:${VERSION}
entrypoint: ["bash", "-c"]
command:
- |
bench --site all backup
## Uncomment for restic snapshots.
# restic snapshots || restic init
# restic backup sites
## Uncomment to keep only last n=30 snapshots.
# restic forget --group-by=paths --keep-last=30 --prune
environment:
# Set correct environment variables for restic
- RESTIC_REPOSITORY=s3:https://s3.endpoint.com/restic
- AWS_ACCESS_KEY_ID=access_key
- AWS_SECRET_ACCESS_KEY=secret_access_key
- RESTIC_PASSWORD=restic_password
volumes:
- "sites:/home/frappe/frappe-bench/sites"
networks:
- erpnext-network
networks:
erpnext-network:
external: true
name: ${PROJECT_NAME:-erpnext}_default
volumes:
sites:
external: true
name: ${PROJECT_NAME:-erpnext}_sites
```
In case of single docker host setup, add crontab entry for backup every 6 hours.
```
0 */6 * * * /usr/local/bin/docker-compose -f /path/to/backup-job.yml up -d > /dev/null
```
Or
```
0 */6 * * * docker compose -p erpnext exec backend bench --site all backup --with-files > /dev/null
```
Notes:
- Make sure `docker-compose` or `docker compose` is available in path during execution.
- Change the cron string as per need.
- Set the correct project name in place of `erpnext`.
- For Docker Swarm add it as a [swarm-cronjob](https://github.com/crazy-max/swarm-cronjob)
- Add it as a `CronJob` in case of Kubernetes cluster.

View File

@@ -1,16 +0,0 @@
Add the following configuration to `launch.json` `configurations` array to start bench console and use debugger. Replace `development.localhost` with appropriate site. Also replace `frappe-bench` with name of the bench directory.
```json
{
"name": "Bench Console",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "--site", "development.localhost", "console"],
"pythonPath": "${workspaceFolder}/frappe-bench/env/bin/python",
"cwd": "${workspaceFolder}/frappe-bench/sites",
"env": {
"DEV_SERVER": "1"
}
}
```

View File

@@ -1,16 +0,0 @@
Clone the version-10 branch of this repo
```shell
git clone https://github.com/frappe/frappe_docker.git -b version-10 && cd frappe_docker
```
Build the images
```shell
export DOCKER_REGISTRY_PREFIX=frappe
docker build -t ${DOCKER_REGISTRY_PREFIX}/frappe-socketio:v10 -f build/frappe-socketio/Dockerfile .
docker build -t ${DOCKER_REGISTRY_PREFIX}/frappe-nginx:v10 -f build/frappe-nginx/Dockerfile .
docker build -t ${DOCKER_REGISTRY_PREFIX}/erpnext-nginx:v10 -f build/erpnext-nginx/Dockerfile .
docker build -t ${DOCKER_REGISTRY_PREFIX}/frappe-worker:v10 -f build/frappe-worker/Dockerfile .
docker build -t ${DOCKER_REGISTRY_PREFIX}/erpnext-worker:v10 -f build/erpnext-worker/Dockerfile .
```

View File

@@ -1,13 +0,0 @@
Add following to frappe container from the `.devcontainer/docker-compose.yml`:
```yaml
...
frappe:
...
extra_hosts:
app1.localhost: 172.17.0.1
app2.localhost: 172.17.0.1
...
```
This is makes the domain names `app1.localhost` and `app2.localhost` connect to docker host and connect to services running on `localhost`.

View File

@@ -1,47 +0,0 @@
The purpose of this document is to give you an overview of how the Frappe Docker containers are structured.
# 🐳 Images
There are **four predefined Dockerfiles** available in the `/images` directory.
| Dockerfile | Ingredients | Purpose & Use Case |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **bench** | Sets up only the Bench CLI. | Used for **development** or debugging. Provides the command-line tooling but does not include runtime services. |
| **custom** | Multi-purpose Python backend built from a plain Python image. Includes everything needed to run a Frappe instance via a Compose setup. Installs apps defined in `apps.json`. | Suitable for **production** and **testing**. Ideal when you need control over dependencies (e.g. trying new Python or Node versions). |
| **layered** | Final contents are the same as `custom`, but it is based on **prebuilt images from [Docker Hub](https://hub.docker.com/u/frappe)**. | Great for **production builds** when youre fine with the dependency versions managed by Frappe. Builds much faster since the base layers are already prepared. |
| **production** | Similar to `custom` (built from a Python base image), but installs **only Frappe and ERPNext**. Not customizable with `apps.json`. | Best for **quick starts** or exploration. For real deployments or CI/CD pipelines, `custom` or `layered` are preferred because they offer more flexibility. |
---
These images include everything needed to run all processes required by the Frappe framework
(see [Bench Procfile reference](https://frappeframework.com/docs/v14/user/en/bench/resources/bench-procfile)).
- The `bench` image only sets up the CLI tool.
- The other images (`custom`, `layered`, and `production`) go further — enabling a nearly **plug-and-play** setup for ERPNext and custom apps.
> We use [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/) and [Docker Buildx](https://docs.docker.com/engine/reference/commandline/buildx/) to maximize layer reuse and make our builds more efficient.
# 🏗️ Compose
Once images are built, containers are orchestrated using a [compose file](https://docs.docker.com/compose/compose-file/). The main compose.yaml provides core services, networking, and volumes for any Frappe setup.
## 🛠️ Services
| Service | Role | Purpose |
| ---------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **configurator** | Setup | Updates `common_site_config.json` so Frappe knows how to access db and redis. It is executed on every `docker-compose up` (and exited immediately). Other services start after this container exits successfully |
| **backend** | Runtime | [Werkzeug server](https://werkzeug.palletsprojects.com/en/2.0.x/) |
| **frontend** | Proxy | [nginx](https://www.nginx.com) server that serves JS/CSS assets and routes incoming requests |
| **websocket** | Real-time | Node server that runs [Socket.IO](https://socket.io) |
| **queue-\_** | Background Jobs | Python servers that run job queues using [rq](https://python-rq.org) |
| **scheduler** | Task Automation | Python server that runs tasks on schedule using [schedule](https://schedule.readthedocs.io/en/stable/) |
## 🧩 Overrides
Additional functionality can be added using [overrides](https://docs.docker.com/compose/extends/). These files modify existing services or add new ones without changing the main `compose.yaml`.
Example: The main compose file has no database service, but `compose.mariadb.yaml` adds MariaDB. See [overrider.md](overrider.md) for the complete list of available overrides and how to use them.
---
**Next:** [Build Setup →](02-build-setup.md)

View File

@@ -1,121 +0,0 @@
This guide walks you through building Frappe images from the repository resources.
# Prerequisites
- git
- docker or podman
- docker compose v2 or podman compose
> Install containerization software according to the official maintainer documentation. Avoid package managers when not recommended, as they frequently cause compatibility issues.
# Clone this repo
```bash
git clone https://github.com/frappe/frappe_docker
cd frappe_docker
```
# Define custom apps
If you dont want to install specific apps to the image skip this section.
To include custom apps in your image, create an `apps.json` file in the repository root:
```json
[
{
"url": "https://github.com/frappe/erpnext",
"branch": "version-15"
},
{
"url": "https://github.com/frappe/hrms",
"branch": "version-15"
},
{
"url": "https://github.com/frappe/helpdesk",
"branch": "main"
}
]
```
Then generate a base64-encoded string from this file:
```bash
export APPS_JSON_BASE64=$(base64 -w 0 apps.json)
```
# Build the image
Choose the appropriate build command based on your container runtime and desired image type. This example builds the `layered` image with the custom `apps.json` you created.
`Docker`:
```bash
docker build \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=version-15 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
--tag=custom:15 \
--file=images/layered/Containerfile .
```
`Podman`:
```bash
podman build \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=version-15 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
--tag=custom:15 \
--file=images/layered/Containerfile .
```
## Build args
| Arg | Purpose |
| -------------------- | --------------------------------------------------------------------------------------------- |
| **Frappe Framework** | |
| FRAPPE_PATH | Repository URL for Frappe framework source code. Defaults to https://github.com/frappe/frappe |
| FRAPPE_BRANCH | Branch to use for Frappe framework. Defaults to version-15 |
| **Custom Apps** | |
| APPS_JSON_BASE64 | Base64-encoded JSON string from apps.json defining apps to install |
| **Dependencies** | |
| PYTHON_VERSION | Python version for the base image |
| NODE_VERSION | Node.js version |
| WKHTMLTOPDF_VERSION | wkhtmltopdf version |
| **bench only** | |
| DEBIAN_BASE | Debian base version for the bench image, defaults to `bookworm` |
| WKHTMLTOPDF_DISTRO | use the specified distro for debian package. Default is `bookworm` |
# env file
The compose file requires several environment variables. You can either export them on your system or create a `.env` file.
```bash
cp example.env custom.env
```
Edit `custom.env` to customize variables for your setup. The template includes common variables, but you can add, modify, or remove any as needed. See [env-variables.md](env-variables.md) for detailed descriptions of all available variables.
# Creating the final compose file
Combine the base compose file with appropriate overrides for your use case. This example adds MariaDB, Redis, and exposes ports on `:8080`:
```bash
docker compose --env.file example.env \
-f compose.yaml \
-f overrides/compose.mariadb.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.noproxy.yaml \
config > compose.custom.yaml
```
This generates `compose.custom.yaml`, which you'll use to start all containers. Customize the overrides and environment variables according to your requirements.
> **NOTE**: podman compose is just a wrapper, it uses docker-compose if it is available or podman-compose if not. podman-compose have an issue reading .env files ([Issue](https://github.com/containers/podman-compose/issues/475)) and might create an issue when running the containers.
---
**Next:** [Start Setup →](03-start-setup.md)
**Back:** [Container Overview ←](01-overview.md)

View File

@@ -1,42 +0,0 @@
# start Container
Once your compose file is ready, start all containers with a single command:
```bash
docker compose -p frappe -f compose.custom.yaml up -d
```
```bash
podman-compose --in-pod=1 --project-name frappe -f compose.custom.yaml up -d
```
The `-p` (or `--project-name`) flag names the project `frappe`, allowing you to easily reference and manage all containers together.
# Create a site and install apps
Frappe is now running, but it's not yet configured. You need to create a site and install your apps.
```bash
docker compose -p frappe exec backend bench new-site <sitename> --mariadb-user-host-login-scope='172.%.%.%'
docker compose -p frappe exec backend bench --site <sitename> install-app erpnext
```
```bash
podman exec -ti erpnext_backend_1 /bin/bash
bench new-site <sitename> --mariadb-user-host-login-scope='172.%.%.%'
bench --site <sitename> install-app erpnext
```
Replace `<sitename>` with your desired site name.
> ## Understanding the MariaDB User Scope
>
> The flag --mariadb-user-host-login-scope='172.%.%.%' allows database connections from any IP address within the 172.0.0.0/8 range. This includes all containers and virtual machines running on your machine.
>
> **Why is this necessary?** Docker and Podman assign dynamic IP addresses to containers. If you set a fixed IP address instead, database connections will fail when the container restarts and receives a new IP. The wildcard pattern ensures connections always work, regardless of IP changes.
>
> **Security note:** This scope is sufficient because only the backend container accesses the database. If you need external database access, adjust the scope accordingly, but be cautious with overly permissive settings.
---
**Back:** [Build Setup →](02-build-setup.md)

View File

@@ -1,112 +0,0 @@
# Environment Variables Reference
Environment variables configure your Frappe Docker setup. They can be set directly in the container or defined in a `.env` file referenced by Docker Compose.
**Getting Started:**
```bash
cp example.env .env
```
Then edit `.env` and set variables according to your needs.
---
## Required Variables
| Variable | Purpose | Example | Notes |
| ----------------- | ------------------------------------------------ | -------------------------------- | ---------------------------------------------------------------- |
| `FRAPPE_PATH` | Frappe framework path | https://github.com/frappe/frappe | |
| `FRAPPE_BRANCH` | Frappe Branch | `version-15` | See [Frappe releases](https://github.com/frappe/frappe/releases) |
| `ERPNEXT_VERSION` | ERPNext release version | `v15.67.0` | Required although its never used |
| `DB_PASSWORD` | Password for database root (MariaDB or Postgres) | `secure_password_123` | Not needed if using `DB_PASSWORD_SECRETS_FILE` |
---
## Database Configuration
| Variable | Purpose | Default | When to Set |
| -------------------------- | ----------------------------------------- | ------------------------------------ | ---------------------------------- |
| `DB_PASSWORD` | Database root user password | 123 | Always (unless using secrets file) |
| `DB_PASSWORD_SECRETS_FILE` | Path to file containing database password | — | Setup mariadb-secrets overrider |
| `DB_HOST` | Database hostname or IP | `db` (service name) | Only if using external database |
| `DB_PORT` | Database port | `3306` (MariaDB) / `5432` (Postgres) | Only if using external database |
---
## Redis Configuration
| Variable | Purpose | Default | When to Set |
| ------------- | --------------------------------------------------- | ---------------------------- | ------------------------------------- |
| `REDIS_CACHE` | Redis hostname for caching | `redis-cache` (service name) | Only if using external Redis instance |
| `REDIS_QUEUE` | Redis hostname for job queues and real-time updates | `redis-queue` (service name) | Only if using external Redis instance |
---
## HTTPS & SSL Configuration
| Variable | Purpose | Default | When to Set |
| ------------------- | ------------------------------------------------ | ------- | ---------------------------------------- |
| `LETSENCRYPT_EMAIL` | Email for Let's Encrypt certificate registration | — | Required if using HTTPS override |
| `SITES` | List of domains for SSL certificates | — | Required if using reverse proxy override |
**Format for `SITES`:**
```bash
# Single site
SITES=`mysite.example.com`
# Wildcard (any subdomain)
SITES=`{any:.+}`
```
---
## Site Configuration
| Variable | Purpose | Default | When to Set |
| ------------------------- | -------------------------------- | ---------------------------------------- | ----------------------------------------------- |
| `FRAPPE_SITE_NAME_HEADER` | Site name for multi-tenant setup | `$host` (resolved from request hostname) | When accessing by IP or need explicit site name |
**Examples:**
If your site is named `mysite` but you want to access it via `127.0.0.1`:
```bash
FRAPPE_SITE_NAME_HEADER=mysite
```
If your site is named `example.com` and you access it via that domain, no need to set this (defaults to hostname).
---
## Image Configuration
| Variable | Purpose | Default | Notes |
| ---------------- | ------------------------------ | --------------------- | ------------------------------------------------------- |
| `CUSTOM_IMAGE` | Custom Docker image repository | Frappe official image | Leave empty to use default |
| `CUSTOM_TAG` | Custom Docker image tag | Latest stable | Corresponds to `FRAPPE_VERSION` |
| `PULL_POLICY` | Image pull behavior | `always` | Options: `always`, `never`, `if-not-present` |
| `RESTART_POLICY` | Container restart behavior | `unless-stopped` | Options: `no`, `always`, `unless-stopped`, `on-failure` |
---
## Nginx Proxy Configuration
| Variable | Purpose | Default | Allowed Values |
| ---------------------- | ---------------------------------- | -------------- | -------------------------------------------- |
| `BACKEND` | Backend service address and port | `0.0.0.0:8000` | `{host}:{port}` |
| `SOCKETIO` | Socket.IO service address and port | `0.0.0.0:9000` | `{host}:{port}` |
| `HTTP_PUBLISH_PORT` | Published HTTP port | `8080` | Any available port |
| `PROXY_READ_TIMEOUT` | Upstream request timeout | `120s` | Any nginx timeout value (e.g., `300s`, `5m`) |
| `CLIENT_MAX_BODY_SIZE` | Maximum upload file size | `50m` | Any nginx size value (e.g., `100m`, `1g`) |
### Real IP Configuration (Behind Proxy)
Use these variables when running behind a reverse proxy or load balancer:
| Variable | Purpose | Default |
| ---------------------------- | ------------------------------------------------- | ----------------- |
| `UPSTREAM_REAL_IP_ADDRESS` | Trusted upstream IP address for real IP detection | `127.0.0.1` |
| `UPSTREAM_REAL_IP_HEADER` | Request header containing client IP | `X-Forwarded-For` |
| `UPSTREAM_REAL_IP_RECURSIVE` | Enable recursive IP search | `off` |

View File

@@ -1,27 +0,0 @@
Overrides extend the base compose.yaml with additional services or modify existing behavior. Include them in your compose command using multiple -f flags.
```bash
docker compose -f compose.yaml -f overrides/compose.mariadb.yaml -f overrides/compose.redis.yaml config > compose.custom.yaml
```
| Overrider | Purpose | Additional Info |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Database** | | |
| compose.mariadb.yaml | Adds MariaDB database service | set `DB_PASSWORD` or default Password will be used |
| compose.mariadb-secrets.yaml | Adds MariaDB with password from a secret file instead of environment variable | Set `DB_PASSWORD_SECRETS_FILE` to the path of your secret file |
| compose.mariadb-shared.yaml | Makes MariaDB available on a shared network (mariadb-network) for other services | set `DB_PASSWORD` |
| compose.postgres.yaml | Uses PostgreSQL instead of MariaDB as the database | set `DB_PASSWORD` |
| **Proxy** | | |
| compose.noproxy.yaml | Exposes the application directly on port `:8080` without a reverse proxy | |
| compose.proxy.yaml | Uses Traefik as HTTP reverse proxy on port `:80` | You can change the published port by setting `HTTP_PUBLISH_PORT` |
| compose.https.yaml | Uses Traefik as HTTPS reverse proxy on Port `:443` with automatic HTTP-to-HTTPS redirect | `SITES` and `LETSENCRYPT_EMAIL` must be set. `HTTP_PUBLISH_PORT` and `HTTPS_PUBLISH_PORT` can be set. |
| **Redis** | | |
| compose.redis.yaml | Adds Redis service for caching and background job queuing |
| **TBD** | **The following overrides are available but lack documentation. If you use them and understand their purpose, please consider contributing to this documentation.** |
| compose.backup-cron.yaml | | |
| compose.custom-domain-ssl.yaml | | |
| compose.custom-domain.yaml | | |
| compose.multi-bench-ssl.yaml | | |
| compose.multi-bench.yaml | | |
| compose.traefik-ssl.yaml | | |
| compose.traefik.yaml | | |

View File

@@ -0,0 +1,58 @@
# Custom apps
To add your own Frappe/ERPNext apps to the image, we'll need to create a custom image with the help of a unique wrapper script
> For the sake of simplicity, in this example, we'll be using a place holder called `[custom]`, and we'll be building off the edge image.
Create two directories called `[custom]-worker` and `[custom]-nginx` in the `build` directory.
```shell
cd frappe_docker
mkdir ./build/[custom]-worker ./build/[custom]-nginx
```
Create a `Dockerfile` in `./build/[custom]-worker` with the following content:
```Dockerfile
FROM frappe/erpnext-worker:edge
RUN install_app [custom] https://github.com/[username]/[custom] [branch]
# Only add the branch if you are using a specific tag or branch.
```
**Note:** Replace `https://github.com/[username]/[custom]` above with your custom app's Git repository URL (may include credentials if needed). Your custom app Git repository **must** be named exactly as the custom app's name, and use the same branch name as Frappe/ERPNext branch name that you use.
Create a `Dockerfile` in `./build/[custom]-nginx` with the following content:
```Dockerfile
FROM bitnami/node:12-prod
COPY build/[custom]-nginx/install_app.sh /install_app
RUN /install_app [custom] https://github.com/[username]/[custom] [branch]
FROM frappe/erpnext-nginx:edge
COPY --from=0 /home/frappe/frappe-bench/sites/ /var/www/html/
COPY --from=0 /rsync /rsync
RUN echo -n "\n[custom]" >> /var/www/html/apps.txt
VOLUME [ "/assets" ]
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
```
Copy over the `install_app.sh` file from `./build/erpnext-nginx`
```shell
cp ./build/erpnext-nginx/install_app.sh ./build/[custom]-nginx
```
Open up `./installation/docker-compose-custom.yml` and replace all instances of `[app]` with the name of your app.
```shell
sed -i "s#\[app\]#[custom]#" ./installation/docker-compose-custom.yml
```
Install like usual, except that when you set the `INSTALL_APPS` variable to `erpnext,[custom]`.

View File

@@ -1,422 +0,0 @@
# Getting Started
## Prerequisites
In order to start developing you need to satisfy the following prerequisites:
- Docker
- docker-compose
- user added to docker group
It is recommended you allocate at least 4GB of RAM to docker:
- [Instructions for Windows](https://docs.docker.com/docker-for-windows/#resources)
- [Instructions for macOS](https://docs.docker.com/desktop/settings/mac/#advanced)
Here is a screenshot showing the relevant setting in the Help Manual
![image](images/Docker%20Manual%20Screenshot%20-%20Resources%20section.png)
Here is a screenshot showing the settings in Docker Desktop on Mac
![images](images/Docker%20Desktop%20Screenshot%20-%20Resources%20section.png)
## Bootstrap Containers for development
Clone and change directory to frappe_docker directory
```shell
git clone https://github.com/frappe/frappe_docker.git
cd frappe_docker
```
Copy example devcontainer config from `devcontainer-example` to `.devcontainer`
```shell
cp -R devcontainer-example .devcontainer
```
Copy example vscode config for devcontainer from `development/vscode-example` to `development/.vscode`. This will setup basic configuration for debugging.
```shell
cp -R development/vscode-example development/.vscode
```
## Use VSCode Remote Containers extension
For most people getting started with Frappe development, the best solution is to use [VSCode Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
Before opening the folder in container, determine the database that you want to use. The default is MariaDB.
If you want to use PostgreSQL instead, edit `.devcontainer/docker-compose.yml` and uncomment the section for `postgresql` service, and you may also want to comment `mariadb` as well.
VSCode should automatically inquire you to install the required extensions, that can also be installed manually as follows:
- Install Dev Containers for VSCode
- through command line `code --install-extension ms-vscode-remote.remote-containers`
- clicking on the Install button in the Vistual Studio Marketplace: [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
- View: Extensions command in VSCode (Windows: Ctrl+Shift+X; macOS: Cmd+Shift+X) then search for extension `ms-vscode-remote.remote-containers`
After the extensions are installed, you can:
- Open frappe_docker folder in VS Code.
- `code .`
- Launch the command, from Command Palette (Ctrl + Shift + P) `Dev Containers: Reopen in Container`. You can also click in the bottom left corner to access the remote container menu.
Notes:
- The `development` directory is ignored by git. It is mounted and available inside the container. Create all your benches (installations of bench, the tool that manages frappe) inside this directory.
- Node v14 and v10 are installed. Check with `nvm ls`. Node v14 is used by default.
### Setup first bench
> Jump to [scripts](#setup-bench--new-site-using-script) section to setup a bench automatically. Alternatively, you can setup a bench manually using below guide.
Run the following commands in the terminal inside the container. You might need to create a new terminal in VSCode.
NOTE: Prior to doing the following, make sure the user is **frappe**.
```shell
bench init --skip-redis-config-generation frappe-bench
cd frappe-bench
```
To setup frappe framework version 14 bench set `PYENV_VERSION` environment variable to `3.10.5` (default) and use NodeJS version 16 (default),
```shell
# Use default environments
bench init --skip-redis-config-generation --frappe-branch version-14 frappe-bench
# Or set environment versions explicitly
nvm use v16
PYENV_VERSION=3.10.13 bench init --skip-redis-config-generation --frappe-branch version-14 frappe-bench
# Switch directory
cd frappe-bench
```
To setup frappe framework version 13 bench set `PYENV_VERSION` environment variable to `3.9.17` and use NodeJS version 14,
```shell
nvm use v14
PYENV_VERSION=3.9.17 bench init --skip-redis-config-generation --frappe-branch version-13 frappe-bench
cd frappe-bench
```
### Setup hosts
We need to tell bench to use the right containers instead of localhost. Run the following commands inside the container:
```shell
bench set-config -g db_host mariadb
bench set-config -g redis_cache redis://redis-cache:6379
bench set-config -g redis_queue redis://redis-queue:6379
bench set-config -g redis_socketio redis://redis-queue:6379
```
For any reason the above commands fail, set the values in `common_site_config.json` manually.
```json
{
"db_host": "mariadb",
"redis_cache": "redis://redis-cache:6379",
"redis_queue": "redis://redis-queue:6379",
"redis_socketio": "redis://redis-queue:6379"
}
```
### Edit Honcho's Procfile
Note : With the option '--skip-redis-config-generation' during bench init, these actions are no more needed. But at least, take a look to ProcFile to see what going on when bench launch honcho on start command
Honcho is the tool used by Bench to manage all the processes Frappe requires. Usually, these all run in localhost, but in this case, we have external containers for Redis. For this reason, we have to stop Honcho from trying to start Redis processes.
Honcho is installed in global python environment along with bench. To make it available locally you've to install it in every `frappe-bench/env` you create. Install it using command `./env/bin/pip install honcho`. It is required locally if you wish to use is as part of VSCode launch configuration.
Open the Procfile file and remove the three lines containing the configuration from Redis, either by editing manually the file:
```shell
code Procfile
```
Or running the following command:
```shell
sed -i '/redis/d' ./Procfile
```
### Create a new site with bench
You can create a new site with the following command:
```shell
bench new-site --mariadb-user-host-login-scope=% sitename
```
sitename MUST end with .localhost for trying deployments locally.
for example:
```shell
bench new-site --mariadb-user-host-login-scope=% development.localhost
```
The same command can be run non-interactively as well:
```shell
bench new-site --db-root-password 123 --admin-password admin --mariadb-user-host-login-scope=% development.localhost
```
The command will ask the MariaDB root password. The default root password is `123`.
This will create a new site and a `development.localhost` directory under `frappe-bench/sites`.
The option `--mariadb-user-host-login-scope=%` will configure site's database credentials to work with docker.
You may need to configure your system /etc/hosts if you're on Linux, Mac, or its Windows equivalent.
To setup site with PostgreSQL as database use option `--db-type postgres` and `--db-host postgresql`. (Available only v12 onwards, currently NOT available for ERPNext).
Example:
```shell
bench new-site --db-type postgres --db-host postgresql mypgsql.localhost
```
To avoid entering postgresql username and root password, set it in `common_site_config.json`,
```shell
bench config set-common-config -c root_login postgres
bench config set-common-config -c root_password '"123"'
```
Note: If PostgreSQL is not required, the postgresql service / container can be stopped.
### Set bench developer mode on the new site
To develop a new app, the last step will be setting the site into developer mode. Documentation is available at [this link](https://frappe.io/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe).
```shell
bench --site development.localhost set-config developer_mode 1
bench --site development.localhost clear-cache
```
### Install an app
To install an app we need to fetch it from the appropriate git repo, then install in on the appropriate site:
You can check [VSCode container remote extension documentation](https://code.visualstudio.com/docs/remote/containers#_sharing-git-credentials-with-your-container) regarding git credential sharing.
To install custom app
```shell
# --branch is optional, use it to point to branch on custom app repository
bench get-app --branch version-12 https://github.com/myusername/myapp
bench --site development.localhost install-app myapp
```
At the time of this writing, the Payments app has been factored out of the Version 14 ERPNext app and is now a separate app. ERPNext will not install it.
```shell
bench get-app --branch version-14 --resolve-deps erpnext
bench --site development.localhost install-app erpnext
```
To install ERPNext (from the version-13 branch):
```shell
bench get-app --branch version-13 erpnext
bench --site development.localhost install-app erpnext
```
Note: Both frappe and erpnext must be on branch with same name. e.g. version-14
You can use the `switch-to-branch` command to align versions if you get an error about mismatching versions.
```shell
bench switch-to-branch version-xx
```
### Start Frappe without debugging
Execute following command from the `frappe-bench` directory.
```shell
bench start
```
You can now login with user `Administrator` and the password you choose when creating the site.
Your website will now be accessible at location [development.localhost:8000](http://development.localhost:8000)
Note: To start bench with debugger refer section for debugging.
### Setup bench / new site using script
Most developers work with numerous clients and versions. Moreover, apps may be required to be installed by everyone on the team working for a client.
This is simplified using a script to automate the process of creating a new bench / site and installing the required apps. The `Administrator` password for created sites is `admin`.
Sample `apps-example.json` is used by default, it installs erpnext on current stable release. To install custom apps, copy the `apps-example.json` to custom json file and make changes to list of apps. Pass this file to the `installer.py` script.
> You may have apps in private repos which may require ssh access. You may use SSH from your home directory on linux (configurable in docker-compose.yml).
```shell
python installer.py #pass --db-type postgres for postgresdb
```
For command help
```shell
python installer.py --help
usage: installer.py [-h] [-j APPS_JSON] [-b BENCH_NAME] [-s SITE_NAME] [-r FRAPPE_REPO] [-t FRAPPE_BRANCH] [-p PY_VERSION] [-n NODE_VERSION] [-v] [-a ADMIN_PASSWORD] [-d DB_TYPE]
options:
-h, --help show this help message and exit
-j APPS_JSON, --apps-json APPS_JSON
Path to apps.json, default: apps-example.json
-b BENCH_NAME, --bench-name BENCH_NAME
Bench directory name, default: frappe-bench
-s SITE_NAME, --site-name SITE_NAME
Site name, should end with .localhost, default: development.localhost
-r FRAPPE_REPO, --frappe-repo FRAPPE_REPO
frappe repo to use, default: https://github.com/frappe/frappe
-t FRAPPE_BRANCH, --frappe-branch FRAPPE_BRANCH
frappe repo to use, default: version-15
-p PY_VERSION, --py-version PY_VERSION
python version, default: Not Set
-n NODE_VERSION, --node-version NODE_VERSION
node version, default: Not Set
-v, --verbose verbose output
-a ADMIN_PASSWORD, --admin-password ADMIN_PASSWORD
admin password for site, default: admin
-d DB_TYPE, --db-type DB_TYPE
Database type to use (e.g., mariadb or postgres)
```
A new bench and / or site is created for the client with following defaults.
- MariaDB root password: `123`
- Admin password: `admin`
> To use Postegres DB, comment the mariabdb service and uncomment postegres service.
### Start Frappe with Visual Studio Code Python Debugging
To enable Python debugging inside Visual Studio Code, you must first install the `ms-python.python` extension inside the container. This should have already happened automatically, but depending on your VSCode config, you can force it by:
- Click on the extension icon inside VSCode
- Search `ms-python.python`
- Click on `Install on Dev Container: Frappe Bench`
- Click on 'Reload'
We need to start bench separately through the VSCode debugger. For this reason, **instead** of running `bench start` you should run the following command inside the frappe-bench directory:
```shell
honcho start \
socketio \
watch \
schedule \
worker_short \
worker_long
```
Alternatively you can use the VSCode launch configuration "Honcho SocketIO Watch Schedule Worker" which launches the same command as above.
This command starts all processes with the exception of Redis (which is already running in separate container) and the `web` process. The latter can can finally be started from the debugger tab of VSCode by clicking on the "play" button.
You can now login with user `Administrator` and the password you choose when creating the site, if you followed this guide's unattended install that password is going to be `admin`.
To debug workers, skip starting worker with honcho and start it with VSCode debugger.
For advance vscode configuration in the devcontainer, change the config files in `development/.vscode`.
## Developing using the interactive console
You can launch a simple interactive shell console in the terminal with:
```shell
bench --site development.localhost console
```
More likely, you may want to launch VSCode interactive console based on Jupyter kernel.
Launch VSCode command palette (cmd+shift+p or ctrl+shift+p), run the command `Python: Select interpreter to start Jupyter server` and select `/workspace/development/frappe-bench/env/bin/python`.
The first step is installing and updating the required software. Usually the frappe framework may require an older version of Jupyter, while VSCode likes to move fast, this can [cause issues](https://github.com/jupyter/jupyter_console/issues/158). For this reason we need to run the following command.
```shell
/workspace/development/frappe-bench/env/bin/python -m pip install --upgrade jupyter ipykernel ipython
```
Then, run the command `Python: Show Python interactive window` from the VSCode command palette.
Replace `development.localhost` with your site and run the following code in a Jupyter cell:
```python
import frappe
frappe.init(site='development.localhost', sites_path='/workspace/development/frappe-bench/sites')
frappe.connect()
frappe.local.lang = frappe.db.get_default('lang')
frappe.db.connect()
```
The first command can take a few seconds to be executed, this is to be expected.
## Manually start containers
In case you don't use VSCode, you may start the containers manually with the following command:
### Running the containers
```shell
docker-compose -f .devcontainer/docker-compose.yml up -d
```
And enter the interactive shell for the development container with the following command:
```shell
docker exec -e "TERM=xterm-256color" -w /workspace/development -it devcontainer-frappe-1 bash
```
## Use additional services during development
Add any service that is needed for development in the `.devcontainer/docker-compose.yml` then rebuild and reopen in devcontainer.
e.g.
```yaml
...
services:
...
postgresql:
image: postgres:11.8
environment:
POSTGRES_PASSWORD: 123
volumes:
- postgresql-data:/var/lib/postgresql/data
ports:
- 5432:5432
volumes:
...
postgresql-data:
```
Access the service by service name from the `frappe` development container. The above service will be accessible via hostname `postgresql`. If ports are published on to host, access it via `localhost:5432`.
## Using Cypress UI tests
To run cypress based UI tests in a docker environment, follow the below steps:
1. Install and setup X11 tooling on VM using the script `install_x11_deps.sh`
```shell
sudo bash ./install_x11_deps.sh
```
This script will install required deps, enable X11Forwarding and restart SSH daemon and export `DISPLAY` variable.
2. Run X11 service `startx` or `xquartz`
3. Start docker compose services.
4. SSH into ui-tester service using `docker exec..` command
5. Export CYPRESS_baseUrl and other required env variables
6. Start Cypress UI console by issuing `cypress run command`
> More references : [Cypress Official Documentation](https://www.cypress.io/blog/2019/05/02/run-cypress-with-a-single-docker-command)
> Ensure DISPLAY environment is always exported.
## Using Mailpit to test mail services
To use Mailpit just uncomment the service in the docker-compose.yml file.
The Interface is then available under port 8025 and the smtp service can be used as mailpit:1025.

295
docs/docker-swarm.md Normal file
View File

@@ -0,0 +1,295 @@
### Prerequisites
IMPORTANT: All commands are executed on live server with public IP and DNS Configured.
#### Setup docker swarm
Follow [dockerswarm.rocks](https://dockerswarm.rocks) guide to setup Docker swarm, Traefik and Portainer.
Use Portainer for rest of the guide
### Create Config
Configs > Add Config > `frappe-mariadb-config`
```
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
```
### Create Secret
Secret > Add Secret > `frappe-mariadb-root-password`
```
longsecretpassword
```
Note down this password.
It is only available in mariadb containers at location `/run/secrets/frappe-mariadb-root-password` later
### Deploy MariaDB Replication
Stacks > Add Stacks > `frappe-mariadb`
```yaml
version: "3.7"
services:
mariadb-master:
image: "bitnami/mariadb:10.3"
deploy:
restart_policy:
condition: on-failure
configs:
- source: frappe-mariadb-config
target: /opt/bitnami/mariadb/conf/bitnami/my_custom.cnf
networks:
- frappe-network
secrets:
- frappe-mariadb-root-password
volumes:
- "mariadb_master_data:/bitnami/mariadb"
environment:
- MARIADB_REPLICATION_MODE=master
- MARIADB_REPLICATION_USER=repl_user
- MARIADB_REPLICATION_PASSWORD_FILE=/run/secrets/frappe-mariadb-root-password
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/frappe-mariadb-root-password
mariadb-slave:
image: "bitnami/mariadb:10.3"
deploy:
restart_policy:
condition: on-failure
configs:
- source: frappe-mariadb-config
target: /opt/bitnami/mariadb/conf/bitnami/my_custom.cnf
networks:
- frappe-network
secrets:
- frappe-mariadb-root-password
volumes:
- "mariadb_slave_data:/bitnami/mariadb"
environment:
- MARIADB_REPLICATION_MODE=slave
- MARIADB_REPLICATION_USER=repl_user
- MARIADB_REPLICATION_PASSWORD_FILE=/run/secrets/frappe-mariadb-root-password
- MARIADB_MASTER_HOST=mariadb-master
- MARIADB_MASTER_PORT_NUMBER=3306
- MARIADB_MASTER_ROOT_PASSWORD_FILE=/run/secrets/frappe-mariadb-root-password
volumes:
mariadb_master_data:
mariadb_slave_data:
configs:
frappe-mariadb-config:
external: true
secrets:
frappe-mariadb-root-password:
external: true
networks:
frappe-network:
name: frappe-network
attachable: true
```
### Deploy Frappe/ERPNext
Stacks > Add Stacks > `frappe-bench-v13`
```yaml
version: "3.7"
services:
redis-cache:
image: redis:latest
volumes:
- redis-cache-vol:/data
deploy:
restart_policy:
condition: on-failure
networks:
- frappe-network
redis-queue:
image: redis:latest
volumes:
- redis-queue-vol:/data
deploy:
restart_policy:
condition: on-failure
networks:
- frappe-network
redis-socketio:
image: redis:latest
volumes:
- redis-socketio-vol:/data
deploy:
restart_policy:
condition: on-failure
networks:
- frappe-network
erpnext-nginx:
image: frappe/erpnext-nginx:${ERPNEXT_VERSION?Variable ERPNEXT_VERSION not set}
environment:
- UPSTREAM_REAL_IP_ADDRESS=10.0.0.0/8
- FRAPPE_PY=erpnext-python
- FRAPPE_PY_PORT=8000
- FRAPPE_SOCKETIO=frappe-socketio
- SOCKETIO_PORT=9000
volumes:
- sites-vol:/var/www/html/sites:rw
- assets-vol:/assets:rw
networks:
- frappe-network
- traefik-public
deploy:
restart_policy:
condition: on-failure
labels:
- "traefik.docker.network=traefik-public"
- "traefik.enable=true"
- "traefik.constraint-label=traefik-public"
- "traefik.http.routers.erpnext-nginx.rule=Host(${SITES?Variable SITES not set})"
- "traefik.http.routers.erpnext-nginx.entrypoints=http"
- "traefik.http.routers.erpnext-nginx.middlewares=https-redirect"
- "traefik.http.routers.erpnext-nginx-https.rule=Host(${SITES?Variable SITES not set})"
- "traefik.http.routers.erpnext-nginx-https.entrypoints=https"
- "traefik.http.routers.erpnext-nginx-https.tls=true"
- "traefik.http.routers.erpnext-nginx-https.tls.certresolver=le"
- "traefik.http.services.erpnext-nginx.loadbalancer.server.port=8080"
erpnext-python:
image: frappe/erpnext-worker:${ERPNEXT_VERSION?Variable ERPNEXT_VERSION not set}
deploy:
restart_policy:
condition: on-failure
environment:
- MARIADB_HOST=${MARIADB_HOST?Variable MARIADB_HOST not set}
- REDIS_CACHE=redis-cache:6379
- REDIS_QUEUE=redis-queue:6379
- REDIS_SOCKETIO=redis-socketio:6379
- SOCKETIO_PORT=9000
- AUTO_MIGRATE=1
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- assets-vol:/home/frappe/frappe-bench/sites/assets:rw
networks:
- frappe-network
frappe-socketio:
image: frappe/frappe-socketio:${FRAPPE_VERSION?Variable FRAPPE_VERSION not set}
deploy:
restart_policy:
condition: on-failure
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
networks:
- frappe-network
erpnext-worker-default:
image: frappe/erpnext-worker:${ERPNEXT_VERSION?Variable ERPNEXT_VERSION not set}
deploy:
restart_policy:
condition: on-failure
command: worker
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
networks:
- frappe-network
erpnext-worker-short:
image: frappe/erpnext-worker:${ERPNEXT_VERSION?Variable ERPNEXT_VERSION not set}
deploy:
restart_policy:
condition: on-failure
command: worker
environment:
- WORKER_TYPE=short
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
networks:
- frappe-network
erpnext-worker-long:
image: frappe/erpnext-worker:${ERPNEXT_VERSION?Variable ERPNEXT_VERSION not set}
deploy:
restart_policy:
condition: on-failure
command: worker
environment:
- WORKER_TYPE=long
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
networks:
- frappe-network
frappe-schedule:
image: frappe/erpnext-worker:${ERPNEXT_VERSION?Variable ERPNEXT_VERSION not set}
deploy:
restart_policy:
condition: on-failure
command: schedule
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
networks:
- frappe-network
volumes:
redis-cache-vol:
redis-queue-vol:
redis-socketio-vol:
assets-vol:
sites-vol:
networks:
traefik-public:
external: true
frappe-network:
external: true
```
Use environment variables:
- `ERPNEXT_VERSION` variable to be set to desired version of ERPNext. e.g. 12.10.0
- `FRAPPE_VERSION` variable to be set to desired version of Frappe Framework. e.g. 12.7.0
- `MARIADB_HOST=frappe-mariadb_mariadb-master`
- `SITES` variable is list of sites in back tick and separated by comma
```
SITES=`site1.example.com`,`site2.example.com`
```
### Create new site job
1. Containers > Add Container > `add-site1-example-com`
2. Select Image frappe/erpnext-worker:v13
3. Set command as `new`
4. Select network `frappe-network`
5. Select Volume `frappe-bench-v13_sites-vol` and mount in container `/home/frappe/frappe-bench/sites`
6. Env variables:
- MYSQL_ROOT_PASSWORD=longsecretpassword
- SITE_NAME=site1.example.com
- INSTALL_APPS=erpnext
7. Start container
### Migrate Sites job
1. Containers > Add Container > `migrate-sites`
2. Select Image frappe/erpnext-worker:v13
3. Set command as `migrate`
4. Select network `frappe-network`
5. Select Volume `frappe-bench-v13_sites-vol` and mount in container `/home/frappe/frappe-bench/sites`
6. Env variables:
- MAINTENANCE_MODE=1
7. Start container

View File

@@ -0,0 +1,33 @@
List of environment variables for containers
### frappe-worker and erpnext-worker
Following environment variables are set into sites volume as `common_site_config.json`. It means once the file is created in volume, the variables won't have any effect, you need to edit the common_site_config.json to update the configuration
- `DB_HOST`: MariaDB host, domain name or ip address.
- `DB_PORT`: MariaDB port.
- `REDIS_CACHE`: Redis cache host, domain name or ip address.
- `REDIS_QUEUE`: Redis queue host, domain name or ip address.
- `REDIS_SOCKETIO`: Redis queue host, domain name or ip address.
- `SOCKETIO_PORT: `: Port on which the SocketIO should start.
- `WORKER_CLASS`: Optionally set gunicorn worker-class. Supports gevent only, defaults to gthread.
### frappe-nginx and erpnext-nginx
These variables are set on every container start. Change in these variables will reflect on every container start.
- `FRAPPE_PY`: Gunicorn host to reverse proxy. Default: 0.0.0.0
- `FRAPPE_PY_PORT`: Gunicorn port to reverse proxy. Default: 8000
- `FRAPPE_SOCKETIO`: SocketIO host to reverse proxy. Default: 0.0.0.0
- `SOCKETIO_PORT`: SocketIO port to reverse proxy. Default: 9000
- `HTTP_TIMEOUT`: Nginx http timeout. Default: 120
- `UPSTREAM_REAL_IP_ADDRESS `: The trusted address (or ip range) of upstream proxy servers. If set, this will tell nginx to trust the X-Forwarded-For header from these proxy servers in determining the real IP address of connecting clients. Default: 127.0.0.1
- `UPSTREAM_REAL_IP_RECURSIVE`: When set to `on`, this will tell nginx to not just look to the last upstream proxy server in determining the real IP address. Default: off
- `UPSTREAM_REAL_IP_HEADER`: Set this to the header name sent by your upstream proxy server to indicate the real IP of connecting clients. Default: X-Forwarded-For
- `FRAPPE_SITE_NAME_HEADER`: NGINX `X-Frappe-Site-Name` header in the HTTP request which matches a site name. Default: `$host`
- `HTTP_HOST`: NGINX `Host` header in the HTTP request which matches a site name. Default: `$http_host`
- `SKIP_NGINX_TEMPLATE_GENERATION`: When set to `1`, this will not generate a default NGINX configuration. The config file must be mounted inside the container (`/etc/nginx/conf.d`) by the user in this case. Default: `0`
### frappe-socketio
This container takes configuration from `common_site_config.json` which is already created by erpnext gunicorn container. It doesn't use any environment variables.

View File

@@ -1,12 +0,0 @@
# Resolving Docker `nginx-entrypoint.sh` Script Not Found Error on Windows
If you're encountering the error `exec /usr/local/bin/nginx-entrypoint.sh: no such file or directory` in a Docker container on Windows, follow these steps to resolve the issue.
## 1. Check Line Endings
On Windows, files often have `CRLF` line endings, while Linux systems expect `LF`. This can cause issues when executing shell scripts in Linux containers.
- **Convert Line Endings using `dos2unix`:**
```bash
dos2unix resources/nginx-entrypoint.sh
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -1,112 +0,0 @@
## Migrate from multi-image setup
All the containers now use same image. Use `frappe/erpnext` instead of `frappe/frappe-worker`, `frappe/frappe-nginx` , `frappe/frappe-socketio` , `frappe/erpnext-worker` and `frappe/erpnext-nginx`.
Now you need to specify command and environment variables for following containers:
### Frontend
For `frontend` service to act as static assets frontend and reverse proxy, you need to pass `nginx-entrypoint.sh` as container `command` and `BACKEND` and `SOCKETIO` environment variables pointing `{host}:{port}` for gunicorn and websocket services. Check [environment variables](environment-variables.md)
Now you only need to mount the `sites` volume at location `/home/frappe/frappe-bench/sites`. No need for `assets` volume and asset population script or steps.
Example change:
```yaml
# ... removed for brevity
frontend:
image: frappe/erpnext:${ERPNEXT_VERSION:?ERPNext version not set}
command:
- nginx-entrypoint.sh
environment:
BACKEND: backend:8000
SOCKETIO: websocket:9000
volumes:
- sites:/home/frappe/frappe-bench/sites
# ... removed for brevity
```
### Websocket
For `websocket` service to act as socketio backend, you need to pass `["node", "/home/frappe/frappe-bench/apps/frappe/socketio.js"]` as container `command`
Example change:
```yaml
# ... removed for brevity
websocket:
image: frappe/erpnext:${ERPNEXT_VERSION:?ERPNext version not set}
command:
- node
- /home/frappe/frappe-bench/apps/frappe/socketio.js
# ... removed for brevity
```
### Configurator
For `configurator` service to act as run once configuration job, you need to pass `["bash", "-c"]` as container `entrypoint` and bash script inline to yaml. There is no `configure.py` in the container now.
Example change:
```yaml
# ... removed for brevity
configurator:
image: frappe/erpnext:${ERPNEXT_VERSION:?ERPNext version not set}
restart: "no"
entrypoint:
- bash
- -c
command:
- >
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 -gp socketio_port $$SOCKETIO_PORT;
environment:
DB_HOST: db
DB_PORT: "3306"
REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379
SOCKETIO_PORT: "9000"
# ... removed for brevity
```
### Site Creation
For `create-site` service to act as run once site creation job, you need to pass `["bash", "-c"]` as container `entrypoint` and bash script inline to yaml. Make sure to use `--mariadb-user-host-login-scope=%` as upstream bench is installed in container.
The `WORKDIR` has changed to `/home/frappe/frappe-bench` like `bench` setup we are used to. So the path to find `common_site_config.json` has changed to `sites/common_site_config.json`.
Example change:
```yaml
# ... removed for brevity
create-site:
image: frappe/erpnext:${ERPNEXT_VERSION:?ERPNext version not set}
restart: "no"
entrypoint:
- bash
- -c
command:
- >
wait-for-it -t 120 db:3306;
wait-for-it -t 120 redis-cache:6379;
wait-for-it -t 120 redis-queue:6379;
export start=`date +%s`;
until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]];
do
echo "Waiting for sites/common_site_config.json to be created";
sleep 5;
if (( `date +%s`-start > 120 )); then
echo "could not find sites/common_site_config.json with required keys";
exit 1
fi
done;
echo "sites/common_site_config.json found";
bench new-site --mariadb-user-host-login-scope=% --admin-password=admin --db-root-password=admin --install-app erpnext --set-default frontend;
# ... removed for brevity
```

197
docs/multi-bench.md Normal file
View File

@@ -0,0 +1,197 @@
# Multi bench
This setup separates all services such that only required ones can be deployed.
This is suitable when multiple services are installed on cluster with shared proxy/router, database, cache etc.
Make sure you've cloned this repository and switch to the directory before executing following commands.
## Setup Environment Variables
Copy the example docker environment file to `.env`:
```sh
cp env-example .env
```
To get started, copy the existing `env-example` file to `.env`. By default, the file will contain the following variables:
- `VERSION=edge`
- In this case, `edge` corresponds to `develop`. To setup any other version, you may use the branch name or version specific tags. (eg. v13.0.0, version-12, v11.1.15, v11)
- `MYSQL_ROOT_PASSWORD=admin`
- Bootstraps a MariaDB container with this value set as the root password. If a managed MariaDB instance is used, there is no need to set the password here.
- `MARIADB_HOST=mariadb`
- Sets the hostname to `mariadb`. This is required if the database is managed by the containerized MariaDB instance.
- In case of a separately managed database setups, set the value to the database's hostname/IP/domain.
- `SITES=site1.domain.com,site2.domain.com`
- List of sites that are part of the deployment "bench" Each site is separated by a comma(,).
- If LetsEncrypt is being setup, make sure that the DNS for all the site's domains correctly point to the current instance.
- `LETSENCRYPT_EMAIL=your.email@your.domain.com`
- Email for LetsEncrypt expiry notification. This is only required if you are setting up LetsEncrypt.
Notes:
- docker-compose-erpnext.yml and docker-compose-frappe.yml set `AUTO_MIGRATE` environment variable to `1`.
- `AUTO_MIGRATE` checks if there is semver bump or git hash change in case of develop branch and automatically migrates the sites on container start up.
- It is good practice to use image tag for specific version instead of latest. e.g `frappe-socketio:v12.5.1`, `erpnext-nginx:v12.7.1`.
## Local deployment for testing
For trying out locally or to develop apps using ERPNext REST API port 80 must be published.
Following command will start the needed containers and expose ports.
For Erpnext:
```sh
docker-compose \
--project-name <project-name> \
-f installation/docker-compose-common.yml \
-f installation/docker-compose-erpnext.yml \
-f installation/erpnext-publish.yml \
up -d
```
For Frappe:
```sh
docker-compose \
--project-name <project-name> \
-f installation/docker-compose-common.yml \
-f installation/docker-compose-frappe.yml \
-f installation/frappe-publish.yml \
up -d
```
Make sure to replace `<project-name>` with the desired name you wish to set for the project.
Notes:
- New site (first site) needs to be added after starting the services.
- The local deployment is for testing and REST API development purpose only
- A complete development environment is available [here](../development)
- The site names are limited to patterns matching \*.localhost by default
- Additional site name patterns can be added by editing /etc/hosts of your host machine
## Deployment for production
### Setup Letsencrypt Nginx Proxy Companion
Letsencrypt Nginx Proxy Companion can optionally be setup to provide SSL. This is recommended for instances accessed over the internet.
Your DNS will need to be configured correctly for Letsencrypt to verify your domain.
To setup the proxy companion, run the following commands:
```sh
cd $HOME
git clone https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion.git
cd docker-compose-letsencrypt-nginx-proxy-companion
cp .env.sample .env
./start.sh
```
It will create the required network and configure containers for Letencrypt ACME.
For more details, see the [Letsencrypt Nginx Proxy Companion github repo](https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion). Letsencrypt Nginx Proxy Companion github repo works by automatically proxying to containers with the `VIRTUAL_HOST` environmental variable.
Notes:
- `SITES` variables from `env-example` is set as `VIRTUAL_HOST`
- `LETSENCRYPT_EMAIL` variables from `env-example` is used as it is.
- This is simple nginx + letsencrypt solution. Any other solution can be setup. Above two variables can be re-used or removed in case any other reverse-proxy is used.
### Start Frappe/ERPNext Services
To start the Frappe/ERPNext services for production, run the following command:
```sh
docker-compose \
--project-name <project-name> \
-f installation/docker-compose-common.yml \
-f installation/docker-compose-erpnext.yml \
-f installation/docker-compose-networks.yml \
up -d
```
Make sure to replace `<project-name>` with any desired name you wish to set for the project.
Notes:
- Use `docker-compose-frappe.yml` in case you need only Frappe without ERPNext.
- New site (first site) needs to be added after starting the services.
## Docker containers
This repository contains the following docker-compose files, each one containing the described images:
- docker-compose-common.yml
- redis-cache
- volume: redis-cache-vol
- redis-queue
- volume: redis-queue-vol
- redis-socketio
- volume: redis-socketio-vol
- mariadb: main database
- volume: mariadb-vol
- docker-compose-erpnext.yml
- erpnext-nginx: serves static assets and proxies web request to the appropriate container, allowing to offer all services on the same port.
- volume: assets-vol
- erpnext-python: main application code
- frappe-socketio: enables realtime communication to the user interface through websockets
- frappe-worker-default: background runner
- frappe-worker-short: background runner for short-running jobs
- frappe-worker-long: background runner for long-running jobs
- frappe-schedule
- docker-compose-frappe.yml
- frappe-nginx: serves static assets and proxies web request to the appropriate container, allowing to offer all services on the same port.
- volume: assets-vol, sites-vol
- erpnext-python: main application code
- volume: sites-vol
- frappe-socketio: enables realtime communication to the user interface through websockets
- volume: sites-vol
- frappe-worker-default: background runner
- volume: sites-vol
- frappe-worker-short: background runner for short-running jobs
- volume: sites-vol
- frappe-worker-long: background runner for long-running jobs
- volume: sites-vol
- frappe-schedule
- volume: sites-vol
- docker-compose-networks.yml: this yaml define the network to communicate with _Letsencrypt Nginx Proxy Companion_.
- erpnext-publish.yml: this yml extends erpnext-nginx service to publish port 80, can only be used with docker-compose-erpnext.yml
- frappe-publish.yml: this yml extends frappe-nginx service to publish port 80, can only be used with docker-compose-frappe.yml
## Updating and Migrating Sites
Switch to the root of the `frappe_docker` directory before running the following commands:
```sh
# Update environment variable VERSION
nano .env
# Pull new images
docker-compose \
-f installation/docker-compose-common.yml \
-f installation/docker-compose-erpnext.yml \
pull
# Restart containers
docker-compose \
--project-name <project-name> \
-f installation/docker-compose-common.yml \
-f installation/docker-compose-erpnext.yml \
-f installation/docker-compose-networks.yml \
up -d
docker run \
-e "MAINTENANCE_MODE=1" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
frappe/erpnext-worker:$VERSION migrate
```

View File

@@ -1,69 +0,0 @@
WARNING: Do not use this in production if the site is going to be served over plain http.
### Step 1
Remove the traefik service from docker-compose.yml
### Step 2
Add service for each port that needs to be exposed.
e.g. `port-site-1`, `port-site-2`, `port-site-3`.
```yaml
# ... removed for brevity
services:
# ... removed for brevity
port-site-1:
image: frappe/erpnext:v14.11.1
deploy:
restart_policy:
condition: on-failure
command:
- nginx-entrypoint.sh
environment:
BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: site1.local
SOCKETIO: websocket:9000
volumes:
- sites:/home/frappe/frappe-bench/sites
ports:
- "8080:8080"
port-site-2:
image: frappe/erpnext:v14.11.1
deploy:
restart_policy:
condition: on-failure
command:
- nginx-entrypoint.sh
environment:
BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: site2.local
SOCKETIO: websocket:9000
volumes:
- sites:/home/frappe/frappe-bench/sites
ports:
- "8081:8080"
port-site-3:
image: frappe/erpnext:v14.11.1
deploy:
restart_policy:
condition: on-failure
command:
- nginx-entrypoint.sh
environment:
BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: site3.local
SOCKETIO: websocket:9000
volumes:
- sites:/home/frappe/frappe-bench/sites
ports:
- "8082:8080"
```
Notes:
- Above setup will expose `site1.local`, `site2.local`, `site3.local` on port `8080`, `8081`, `8082` respectively.
- Change `site1.local` to site name to serve from bench.
- Change the `BACKEND` and `SOCKETIO` environment variables as per your service names.
- Make sure `sites:` volume is available as part of yaml.

View File

@@ -1,131 +0,0 @@
# Containerized Production Setup
Make sure you've cloned this repository and switch to the directory before executing following commands.
Commands will generate YAML as per the environment for setup.
## Prerequisites
- [docker](https://docker.com/get-started)
- [docker compose v2](https://docs.docker.com/compose/cli-command)
## Setup Environment Variables
Copy the example docker environment file to `.env`:
```sh
cp example.env .env
```
Note: To know more about environment variable [read here](./environment-variables.md). Set the necessary variables in the `.env` file.
## Generate docker-compose.yml for variety of setups
Notes:
- Make sure to replace `<project-name>` with the desired name you wish to set for the project.
- This setup is not to be used for development. A complete development environment is available [here](../development)
### Store the yaml files
YAML files generated by `docker compose config` command can be stored in a directory. We will create a directory called `gitops` in the user's home.
```shell
mkdir ~/gitops
```
You can make the directory into a private git repo which stores the yaml and secrets. It can help in tracking changes.
Instead of `docker compose config`, you can directly use `docker compose up` to start the containers and skip storing the yamls in `gitops` directory.
### Setup Frappe without proxy and external MariaDB and Redis
In this case make sure you've set `DB_HOST`, `DB_PORT`, `REDIS_CACHE` and `REDIS_QUEUE` environment variables or the `configurator` will fail.
```sh
# Generate YAML
docker compose -f compose.yaml -f overrides/compose.noproxy.yaml config > ~/gitops/docker-compose.yml
# Start containers
docker compose --project-name <project-name> -f ~/gitops/docker-compose.yml up -d
```
### Setup ERPNext with proxy and external MariaDB and Redis
In this case make sure you've set `DB_HOST`, `DB_PORT`, `REDIS_CACHE` and `REDIS_QUEUE` environment variables or the `configurator` will fail.
```sh
# Generate YAML
docker compose -f compose.yaml \
-f overrides/compose.proxy.yaml \
config > ~/gitops/docker-compose.yml
# Start containers
docker compose --project-name <project-name> -f ~/gitops/docker-compose.yml up -d
```
### Setup Frappe using containerized MariaDB and Redis with Letsencrypt certificates.
In this case make sure you've set `LETSENCRYPT_EMAIL` and `SITES` environment variables are set or certificates won't work.
```sh
# Generate YAML
docker compose -f compose.yaml \
-f overrides/compose.mariadb.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.https.yaml \
config > ~/gitops/docker-compose.yml
# Start containers
docker compose --project-name <project-name> -f ~/gitops/docker-compose.yml up -d
```
### Setup ERPNext using containerized MariaDB and Redis with Letsencrypt certificates.
In this case make sure you've set `LETSENCRYPT_EMAIL` and `SITES` environment variables are set or certificates won't work.
```sh
# Generate YAML
docker compose -f compose.yaml \
-f overrides/compose.mariadb.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.https.yaml \
config > ~/gitops/docker-compose.yml
# Start containers
docker compose --project-name <project-name> -f ~/gitops/docker-compose.yml up -d
```
## Create first site
After starting containers, the first site needs to be created. Refer [site operations](./site-operations.md#setup-new-site).
## Updating Images
Switch to the root of the `frappe_docker` directory before running the following commands:
```sh
# Update environment variables ERPNEXT_VERSION and FRAPPE_VERSION
nano .env
# Pull new images
docker compose -f compose.yaml \
# ... your other overrides
config > ~/gitops/docker-compose.yml
# Pull images
docker compose --project-name <project-name> -f ~/gitops/docker-compose.yml pull
# Stop containers
docker compose --project-name <project-name> -f ~/gitops/docker-compose.yml down
# Restart containers
docker compose --project-name <project-name> -f ~/gitops/docker-compose.yml up -d
```
Note:
- pull and stop container commands can be skipped if immutable image tags are used
- `docker compose up -d` will pull new immutable tags if not found.
To migrate sites refer [site operations](./site-operations.md#migrate-site)

View File

@@ -1,226 +0,0 @@
# How to install ERPNext on linux/mac using Frappe_docker ?
step1: clone the repo
```
git clone https://github.com/frappe/frappe_docker
```
step2: add platform: linux/amd64 to all services in the /pwd.yaml
here is the update pwd.yml file
```yml
version: "3"
services:
backend:
image: frappe/erpnext:v15
platform: linux/amd64
deploy:
restart_policy:
condition: on-failure
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
configurator:
image: frappe/erpnext:v15
platform: linux/amd64
deploy:
restart_policy:
condition: none
entrypoint:
- bash
- -c
# add redis_socketio for backward compatibility
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;
environment:
DB_HOST: db
DB_PORT: "3306"
REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379
SOCKETIO_PORT: "9000"
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
create-site:
image: frappe/erpnext:v15
platform: linux/amd64
deploy:
restart_policy:
condition: none
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
entrypoint:
- bash
- -c
command:
- >
wait-for-it -t 120 db:3306;
wait-for-it -t 120 redis-cache:6379;
wait-for-it -t 120 redis-queue:6379;
export start=`date +%s`;
until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]];
do
echo "Waiting for sites/common_site_config.json to be created";
sleep 5;
if (( `date +%s`-start > 120 )); then
echo "could not find sites/common_site_config.json with required keys";
exit 1
fi
done;
echo "sites/common_site_config.json found";
bench new-site --mariadb-user-host-login-scope=% --admin-password=admin --db-root-password=admin --install-app erpnext --set-default frontend;
db:
image: mariadb:10.6
platform: linux/amd64
healthcheck:
test: mysqladmin ping -h localhost --password=admin
interval: 1s
retries: 20
deploy:
restart_policy:
condition: on-failure
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
environment:
MYSQL_ROOT_PASSWORD: admin
volumes:
- db-data:/var/lib/mysql
frontend:
image: frappe/erpnext:v15
platform: linux/amd64
depends_on:
- websocket
deploy:
restart_policy:
condition: on-failure
command:
- nginx-entrypoint.sh
environment:
BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: frontend
SOCKETIO: websocket:9000
UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
UPSTREAM_REAL_IP_RECURSIVE: "off"
PROXY_READ_TIMEOUT: 120
CLIENT_MAX_BODY_SIZE: 50m
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
ports:
- "8080:8080"
queue-long:
image: frappe/erpnext:v15
platform: linux/amd64
deploy:
restart_policy:
condition: on-failure
command:
- bench
- worker
- --queue
- long,default,short
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
queue-short:
image: frappe/erpnext:v15
platform: linux/amd64
deploy:
restart_policy:
condition: on-failure
command:
- bench
- worker
- --queue
- short,default
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
redis-queue:
image: redis:6.2-alpine
platform: linux/amd64
deploy:
restart_policy:
condition: on-failure
volumes:
- redis-queue-data:/data
redis-cache:
image: redis:6.2-alpine
platform: linux/amd64
deploy:
restart_policy:
condition: on-failure
scheduler:
image: frappe/erpnext:v15
platform: linux/amd64
deploy:
restart_policy:
condition: on-failure
command:
- bench
- schedule
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
websocket:
image: frappe/erpnext:v15
platform: linux/amd64
deploy:
restart_policy:
condition: on-failure
command:
- node
- /home/frappe/frappe-bench/apps/frappe/socketio.js
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
volumes:
db-data:
redis-queue-data:
sites:
logs:
```
step3: run the docker
```
cd frappe_docker
```
```
docker-compose -f ./pwd.yml up
```
---
Wait for couple of minutes.
Open localhost:8080

149
docs/single-bench.md Normal file
View File

@@ -0,0 +1,149 @@
# Single Bench
This setup starts traefik service as part of single docker-compose project. It is quick to get started locally or on production for a single server with single deployment.
This is not suitable when multiple services are installed on cluster with shared proxy/router, database, cache etc.
Make sure you've cloned this repository and switch to the directory before executing following commands.
## Setup Environment Variables
Copy the example docker environment file to `.env`:
For local setup
```sh
cp env-local .env
```
For production
```sh
cp env-production .env
```
To get started, copy the existing `env-local` or `env-production` file to `.env`. By default, the file will contain the following variables:
- `ERPNEXT_VERSION=edge`
- In this case, `edge` corresponds to `develop`. To setup any other version, you may use the branch name or version specific tags. (eg. v13.0.0, version-12, v11.1.15, v11).
- `FRAPPE_VERSION=edge`
- In this case, `edge` corresponds to `develop`. To setup any other version, you may use the branch name or version specific tags. (eg. v13.0.0, version-12, v11.1.15, v11).
- `MARIADB_HOST=mariadb`
- Sets the hostname to `mariadb`. This is required if the database is managed by the containerized MariaDB instance.
- `MYSQL_ROOT_PASSWORD=admin`
- Bootstraps a MariaDB container with this value set as the root password. If a managed MariaDB instance is used, there is no need to set the password here.
- In case of a separately managed database setups, set the value to the database's hostname/IP/domain.
- `SITE_NAME=erp.example.com`
- Creates this site after starting all services and installs ERPNext. Site name must be resolvable by users machines and the ERPNext components. e.g. `erp.example.com` or `mysite.localhost`.
- `` SITES=`erp.example.com` ``
- List of sites that are part of the deployment "bench" Each site is separated by a comma(,) and quoted in backtick (`). By default site created by `SITE_NAME` variable is added here.
- If LetsEncrypt is being setup, make sure that the DNS for all the site's domains correctly point to the current instance.
- `DB_ROOT_USER=root`
- MariaDB root username
- `ADMIN_PASSWORD=admin`
- Password for the `Administrator` user, credentials after install `Administrator:$ADMIN_PASSWORD`.
- `INSTALL_APPS=erpnext`
- Apps to install, the app must be already in the container image, to install other application read the [instructions on installing custom apps](./custom-apps-for-production.md).
- `LETSENCRYPT_EMAIL=email@example.com`
- Email for LetsEncrypt expiry notification. This is only required if you are setting up LetsEncrypt.
- `ENTRYPOINT_LABEL=traefik.http.routers.erpnext-nginx.entrypoints=websecure`
- Related to the traefik configuration, says all traffic from outside should come from HTTP or HTTPS, for local development should be web, for production websecure. if redirection is needed, read below.
- `CERT_RESOLVER_LABEL=traefik.http.routers.erpnext-nginx.tls.certresolver=myresolver`
- Which traefik resolver to use to get TLS certificate, sets `erpnext.local.no-cert-resolver` for local setup.
- `` HTTPS_REDIRECT_RULE_LABEL=traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`) ``
- Related to the traefik https redirection configuration, sets `erpnext.local.no-redirect-rule` for local setup.
- `HTTPS_REDIRECT_ENTRYPOINT_LABEL=traefik.http.routers.http-catchall.entrypoints=web`
- Related to the traefik https redirection configuration, sets `erpnext.local.no-entrypoint` for local setup.
- `HTTPS_REDIRECT_MIDDLEWARE_LABEL=traefik.http.routers.http-catchall.middlewares=redirect-to-https`
- Related to the traefik https redirection configuration, sets `erpnext.local.no-middleware` for local setup.
- `HTTPS_USE_REDIRECT_MIDDLEWARE_LABEL=traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https`
- Related to the traefik https redirection configuration, sets `erpnext.local-no-redirect-middleware` for local setup.
Notes:
- `AUTO_MIGRATE` variable is set to `1` by default. It checks if there is semver bump or git hash change in case of develop branch and automatically migrates the sites on container start up.
- It is good practice to use image tag for specific version instead of latest. e.g `frappe-socketio:v12.5.1`, `erpnext-nginx:v12.7.1`.
## Start containers
Execute the following command:
```sh
docker-compose --project-name <project-name> up -d
```
Make sure to replace `<project-name>` with the desired name you wish to set for the project.
Notes:
- If it is the first time running and site is being initialized, _it can take multiple minutes for the site to be up_. Monitor `site-creator` container logs to check progress. Use command `docker logs <project-name>_site-creator_1 -f`
- After the site is ready the username is `Administrator` and the password is `$ADMIN_PASSWORD`
- The local deployment is for testing and REST API development purpose only
- A complete development environment is available [here](../development)
## Create Site
Set environment variables
```sh
export $(cat .env | xargs)
```
Create site as per use and install apps optionally.
```sh
docker-compose exec erpnext-python bench new-site ${SITE_NAME} --mariadb-root-username=${DB_ROOT_USER} --mariadb-root-password=${MYSQL_ROOT_PASSWORD} --admin-password=${ADMIN_PASSWORD} --install-app=${INSTALL_APPS} --db-type=mariadb --no-mariadb-socket
```
If this is the first site created then remove `currentsite.txt` to unset default site.
```sh
docker-compose exec erpnext-python rm -f currentsite.txt
```
## Docker containers
The docker-compose file contains following services:
- traefik: manages letsencrypt
- volume: cert-vol
- redis-cache: cache store
- volume: redis-cache-vol
- redis-queue: used by workers
- volume: redis-queue-vol
- redis-socketio: used by socketio service
- volume: redis-socketio-vol
- mariadb: main database
- volume: mariadb-vol
- erpnext-nginx: serves static assets and proxies web request to the appropriate container, allowing to offer all services on the same port.
- volume: assets-vol and sites-vol
- erpnext-python: main application code
- volume: assets-vol and sites-vol
- frappe-socketio: enables realtime communication to the user interface through websockets
- volume: sites-vol
- erpnext-worker-default: background runner
- volume: sites-vol
- erpnext-worker-short: background runner for short-running jobs
- volume: sites-vol
- erpnext-worker-long: background runner for long-running jobs
- volume: sites-vol
- erpnext-schedule
- volume: sites-vol
- site-creator: run once container to create new site.
- volume: sites-vol
## Updating and Migrating Sites
Switch to the root of the `frappe_docker` directory before running the following commands:
```sh
# Update environment variables ERPNEXT_VERSION and FRAPPE_VERSION
nano .env
# Pull new images
docker-compose pull
# Restart containers
docker-compose --project-name <project-name> up -d
```

View File

@@ -1,38 +0,0 @@
# Single Compose Setup
This setup is a very simple single compose file that does everything to start required services and a frappe-bench. It is used to start play with docker instance with a site. The file is located in the root of repo and named `pwd.yml`.
## Services
### frappe-bench components
- backend, serves gunicorn backend
- frontend, serves static assets through nginx frontend reverse proxies websocket and gunicorn.
- queue-long, long default and short rq worker.
- queue-short, default and short rq worker.
- schedule, event scheduler.
- websocket, socketio websocket for realtime communication.
### Run once configuration
- configurator, configures `common_site_config.json` to set db and redis hosts.
- create-site, creates one site to serve as default site for the frappe-bench.
### Service dependencies
- db, mariadb, container with frappe specific configuration.
- redis-cache, redis for cache data.
- redis-queue, redis for rq data and pub/sub.
## Volumes
- sites: Volume for bench data. Common config, all sites, all site configs and site files will be stored here.
- logs: Volume for bench logs. all process logs are dumped here. No need to mount it. Each container will create a temporary volume for logs if not specified.
## Adaptation
If you understand containers use the `pwd.yml` as a reference to build more complex setup like, single server example, Docker Swarm stack, Kubernetes Helm chart, etc.
This serves only site called `frontend` through the nginx. `FRAPPE_SITE_NAME_HEADER` is set to `frontend` and a default site called `frontend` is created.
Change the `$$host` will allow container to accept any host header and serve that site. To escape `$` in compose yaml use it like `$$`. To unset default site remove `currentsite.txt` file from `sites` directory.

View File

@@ -1,288 +0,0 @@
### Single Server Example
In this use case we have a single server with a static IP attached to it. It can be used in scenarios where one powerful VM has multiple benches and applications or one entry level VM with single site. For single bench, single site setup follow only up to the point where first bench and first site is added. If you choose this setup you can only scale vertically. If you need to scale horizontally you'll need to backup the sites and restore them on to cluster setup.
We will setup the following:
- Install docker and docker compose v2 on linux server.
- Install traefik service for internal load balancer and letsencrypt.
- Install MariaDB with containers.
- Setup project called `erpnext-one` and create sites `one.example.com` and `two.example.com` in the project.
- Setup project called `erpnext-two` and create sites `three.example.com` and `four.example.com` in the project.
Explanation:
Single instance of **Traefik** will be installed and act as internal loadbalancer for multiple benches and sites hosted on the server. It can also load balance other applications along with frappe benches, e.g. wordpress, metabase, etc. We only expose the ports `80` and `443` once with this instance of traefik. Traefik will also take care of letsencrypt automation for all sites installed on the server. _Why choose Traefik over Nginx Proxy Manager?_ Traefik doesn't need additional DB service and can store certificates in a json file in a volume.
Single instance of **MariaDB** will be installed and act as database service for all the benches/projects installed on the server.
Each instance of ERPNext project (bench) will have its own redis, socketio, gunicorn, nginx, workers and scheduler. It will connect to internal MariaDB by connecting to MariaDB network. It will expose sites to public through Traefik by connecting to Traefik network.
### Install Docker
Easiest way to install docker is to use the [convenience script](https://docs.docker.com/engine/install/ubuntu/#install-using-the-convenience-script).
```shell
curl -fsSL https://get.docker.com | bash
```
Note: The documentation assumes Ubuntu LTS server is used. Use any distribution as long as the docker convenience script works. If the convenience script doesn't work, you'll need to install docker manually.
### Install Compose V2
Refer [original documentation](https://docs.docker.com/compose/cli-command/#install-on-linux) for updated version.
```shell
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
curl -SL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
```
### Prepare
Clone `frappe_docker` repo for the needed YAMLs and change the current working directory of your shell to the cloned repo.
```shell
git clone https://github.com/frappe/frappe_docker
cd frappe_docker
```
Create configuration and resources directory
```shell
mkdir ~/gitops
```
The `~/gitops` directory will store all the resources that we use for setup. We will also keep the environment files in this directory as there will be multiple projects with different environment variables. You can create a private repo for this directory and track the changes there.
### Install Traefik
Basic Traefik setup using docker compose.
Create a file called `traefik.env` in `~/gitops`
```shell
echo 'TRAEFIK_DOMAIN=traefik.example.com' > ~/gitops/traefik.env
echo 'EMAIL=admin@example.com' >> ~/gitops/traefik.env
echo "HASHED_PASSWORD='$(openssl passwd -apr1 changeit)'" >> ~/gitops/traefik.env
```
Note:
- Change the domain from `traefik.example.com` to the one used in production. DNS entry needs to point to the Server IP.
- Change the letsencrypt notification email from `admin@example.com` to correct email.
- Change the password from `changeit` to more secure.
env file generated at location `~/gitops/traefik.env` will look like following:
```env
TRAEFIK_DOMAIN=traefik.example.com
EMAIL=admin@example.com
HASHED_PASSWORD=$apr1$K.4gp7RT$tj9R2jHh0D4Gb5o5fIAzm/
```
If Container does not deploy put the HASHED_PASSWORD in ''.
Deploy the traefik container with letsencrypt SSL
```shell
docker compose --project-name traefik \
--env-file ~/gitops/traefik.env \
-f overrides/compose.traefik.yaml \
-f overrides/compose.traefik-ssl.yaml up -d
```
This will make the traefik dashboard available on `traefik.example.com` and all certificates will reside in the Docker volume `cert-data`.
For LAN setup deploy the traefik container without overriding `overrides/compose.traefik-ssl.yaml`.
### Install MariaDB
Basic MariaDB setup using docker compose.
Create a file called `mariadb.env` in `~/gitops`
```shell
echo "DB_PASSWORD=changeit" > ~/gitops/mariadb.env
```
Note:
- Change the password from `changeit` to more secure.
env file generated at location `~/gitops/mariadb.env` will look like following:
```env
DB_PASSWORD=changeit
```
Note: Change the password from `changeit` to more secure one.
Deploy the mariadb container
```shell
docker compose --project-name mariadb --env-file ~/gitops/mariadb.env -f overrides/compose.mariadb-shared.yaml up -d
```
This will make `mariadb-database` service available under `mariadb-network`. Data will reside in `/data/mariadb`.
### Install ERPNext
#### Create first bench
Create first bench called `erpnext-one` with `one.example.com` and `two.example.com`
Create a file called `erpnext-one.env` in `~/gitops`
```shell
cp example.env ~/gitops/erpnext-one.env
sed -i 's/DB_PASSWORD=123/DB_PASSWORD=changeit/g' ~/gitops/erpnext-one.env
sed -i 's/DB_HOST=/DB_HOST=mariadb-database/g' ~/gitops/erpnext-one.env
sed -i 's/DB_PORT=/DB_PORT=3306/g' ~/gitops/erpnext-one.env
sed -i 's/SITES=`erp.example.com`/SITES=\`one.example.com\`,\`two.example.com\`/g' ~/gitops/erpnext-one.env
echo 'ROUTER=erpnext-one' >> ~/gitops/erpnext-one.env
echo "BENCH_NETWORK=erpnext-one" >> ~/gitops/erpnext-one.env
```
Note:
- Change the password from `changeit` to the one set for MariaDB compose in the previous step.
env file is generated at location `~/gitops/erpnext-one.env`.
Create a yaml file called `erpnext-one.yaml` in `~/gitops` directory:
```shell
docker compose --project-name erpnext-one \
--env-file ~/gitops/erpnext-one.env \
-f compose.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.multi-bench.yaml \
-f overrides/compose.multi-bench-ssl.yaml config > ~/gitops/erpnext-one.yaml
```
For LAN setup do not override `compose.multi-bench-ssl.yaml`.
Use the above command after any changes are made to `erpnext-one.env` file to regenerate `~/gitops/erpnext-one.yaml`. e.g. after changing version to migrate the bench.
Deploy `erpnext-one` containers:
```shell
docker compose --project-name erpnext-one -f ~/gitops/erpnext-one.yaml up -d
```
Create sites `one.example.com` and `two.example.com`:
```shell
# one.example.com
docker compose --project-name erpnext-one exec backend \
bench new-site --mariadb-user-host-login-scope=% --db-root-password changeit --install-app erpnext --admin-password changeit one.example.com
```
You can stop here and have a single bench single site setup complete. Continue to add one more site to the current bench.
```shell
# two.example.com
docker compose --project-name erpnext-one exec backend \
bench new-site --mariadb-user-host-login-scope=% --db-root-password changeit --install-app erpnext --admin-password changeit two.example.com
```
#### Create second bench
Setting up additional bench is optional. Continue only if you need multi bench setup.
Create second bench called `erpnext-two` with `three.example.com` and `four.example.com`
Create a file called `erpnext-two.env` in `~/gitops`
```shell
curl -sL https://raw.githubusercontent.com/frappe/frappe_docker/main/example.env -o ~/gitops/erpnext-two.env
sed -i 's/DB_PASSWORD=123/DB_PASSWORD=changeit/g' ~/gitops/erpnext-two.env
sed -i 's/DB_HOST=/DB_HOST=mariadb-database/g' ~/gitops/erpnext-two.env
sed -i 's/DB_PORT=/DB_PORT=3306/g' ~/gitops/erpnext-two.env
echo "ROUTER=erpnext-two" >> ~/gitops/erpnext-two.env
echo "SITES=\`three.example.com\`,\`four.example.com\`" >> ~/gitops/erpnext-two.env
echo "BENCH_NETWORK=erpnext-two" >> ~/gitops/erpnext-two.env
```
Note:
- Change the password from `changeit` to the one set for MariaDB compose in the previous step.
env file is generated at location `~/gitops/erpnext-two.env`.
Create a yaml file called `erpnext-two.yaml` in `~/gitops` directory:
```shell
docker compose --project-name erpnext-two \
--env-file ~/gitops/erpnext-two.env \
-f compose.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.multi-bench.yaml \
-f overrides/compose.multi-bench-ssl.yaml config > ~/gitops/erpnext-two.yaml
```
Use the above command after any changes are made to `erpnext-two.env` file to regenerate `~/gitops/erpnext-two.yaml`. e.g. after changing version to migrate the bench.
Deploy `erpnext-two` containers:
```shell
docker compose --project-name erpnext-two -f ~/gitops/erpnext-two.yaml up -d
```
Create sites `three.example.com` and `four.example.com`:
```shell
# three.example.com
docker compose --project-name erpnext-two exec backend \
bench new-site --mariadb-user-host-login-scope=% --db-root-password changeit --install-app erpnext --admin-password changeit three.example.com
# four.example.com
docker compose --project-name erpnext-two exec backend \
bench new-site --mariadb-user-host-login-scope=% --db-root-password changeit --install-app erpnext --admin-password changeit four.example.com
```
#### Create custom domain to existing site
In case you need to point custom domain to existing site follow these steps.
Also useful if custom domain is required for LAN based access.
Create environment file
```shell
echo "ROUTER=custom-one-example" > ~/gitops/custom-one-example.env
echo "SITES=\`custom-one.example.com\`" >> ~/gitops/custom-one-example.env
echo "BASE_SITE=one.example.com" >> ~/gitops/custom-one-example.env
echo "BENCH_NETWORK=erpnext-one" >> ~/gitops/custom-one-example.env
```
Note:
- Change the file name from `custom-one-example.env` to a logical one.
- Change `ROUTER` variable from `custom-one.example.com` to the one being added.
- Change `SITES` variable from `custom-one.example.com` to the one being added. You can add multiple sites quoted in backtick (`) and separated by commas.
- Change `BASE_SITE` variable from `one.example.com` to the one which is being pointed to.
- Change `BENCH_NETWORK` variable from `erpnext-one` to the one which was created with the bench.
env file is generated at location mentioned in command.
Generate yaml to reverse proxy:
```shell
docker compose --project-name custom-one-example \
--env-file ~/gitops/custom-one-example.env \
-f overrides/compose.custom-domain.yaml \
-f overrides/compose.custom-domain-ssl.yaml config > ~/gitops/custom-one-example.yaml
```
For LAN setup do not override `compose.custom-domain-ssl.yaml`.
Deploy `erpnext-two` containers:
```shell
docker compose --project-name custom-one-example -f ~/gitops/custom-one-example.yaml up -d
```
### Site operations
Refer: [site operations](./site-operations.md)

View File

@@ -1,42 +1,177 @@
# Site operations # Site operations
> 💡 You should setup `--project-name` option in `docker-compose` commands if you have non-standard project name. Create and use env file to pass environment variables to containers,
## Setup new site ```sh
source .env
```
Or specify environment variables instead of passing secrets as command arguments. Refer notes section for environment variables required
## Setup New Site
Note: Note:
- Wait for the `db` service to start and `configurator` to exit before trying to create a new site. Usually this takes up to 10 seconds. - Wait for the database service to start before trying to create a new site.
- Also you have to pass `-p <project_name>` if `-p` passed previously eg. `docker-compose -p <project_name> exec (rest of the command)`. - If new site creation fails, retry after the MariaDB container is up and running.
- If you're using a managed database instance, make sure that the database is running before setting up a new site.
#### MariaDB Site
```sh ```sh
docker-compose exec backend bench new-site --mariadb-user-host-login-scope=% --db-root-password <db-password> --admin-password <admin-password> <site-name> # Create ERPNext site
docker run \
-e "SITE_NAME=$SITE_NAME" \
-e "DB_ROOT_USER=$DB_ROOT_USER" \
-e "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD" \
-e "ADMIN_PASSWORD=$ADMIN_PASSWORD" \
-e "INSTALL_APPS=erpnext" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
frappe/erpnext-worker:$ERPNEXT_VERSION new
``` ```
If you need to install some app, specify `--install-app`. To see all options, just run `bench new-site --help`. #### PostgreSQL Site
To create Postgres site (assuming you already use [Postgres compose override](images-and-compose-files.md#overrides)) you need have to do set `root_login` and `root_password` in common config before that: PostgreSQL is only available v12 onwards. It is NOT available for ERPNext.
It is available as part of `frappe/erpnext-worker`. It inherits from `frappe/frappe-worker`.
```sh ```sh
docker-compose exec backend bench set-config -g root_login <root-login> # Create ERPNext site
docker-compose exec backend bench set-config -g root_password <root-password> docker run \
-e "SITE_NAME=$SITE_NAME" \
-e "DB_ROOT_USER=$DB_ROOT_USER" \
-e "POSTGRES_HOST=$POSTGRES_HOST" \
-e "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" \
-e "ADMIN_PASSWORD=$ADMIN_PASSWORD" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
frappe/erpnext-worker:$ERPNEXT_VERSION new
``` ```
Also command is slightly different: Environment Variables needed:
- `SITE_NAME`: name of the new site to create. Site name is domain name that resolves. e.g. `erp.example.com` or `erp.localhost`.
- `DB_ROOT_USER`: MariaDB/PostgreSQL Root user.
- `MYSQL_ROOT_PASSWORD`: In case of the MariaDB docker container use the one set in `MYSQL_ROOT_PASSWORD` in previous steps. In case of a managed database use the appropriate password.
- `MYSQL_ROOT_PASSWORD_FILE` - When the MariaDB root password is stored using docker secrets.
- `ADMIN_PASSWORD`: set the administrator password for the new site.
- `ADMIN_PASSWORD_FILE`: set the administrator password for the new site using docker secrets.
- `INSTALL_APPS=erpnext`: available only in erpnext-worker and erpnext containers (or other containers with custom apps). Installs ERPNext (and/or the specified apps, comma-delinieated) on this new site.
- `FORCE=1`: optional variable which force installation of the same site.
Environment Variables for PostgreSQL only:
- `POSTGRES_HOST`: host for PostgreSQL server
- `POSTGRES_PASSWORD`: Password for `postgres`. The database root user.
Notes:
- To setup existing frappe-bench deployment with default database as PostgreSQL edit the common_site_config.json and set `db_host` to PostgreSQL hostname and `db_port` to PostgreSQL port.
- To create new frappe-bench deployment with default database as PostgreSQL use `POSTGRES_HOST` and `DB_PORT` environment variables in `erpnext-python` service instead of `MARIADB_HOST`
## Add sites to proxy
Change `SITES` variable to the list of sites created encapsulated in backtick and separated by comma with no space. e.g. `` SITES=`site1.example.com`,`site2.example.com` ``.
Reload variables with following command.
```sh ```sh
docker-compose exec backend bench new-site --mariadb-user-host-login-scope=% --db-type postgres --admin-password <admin-password> <site-name> docker-compose --project-name <project-name> up -d
``` ```
## Push backup to S3 storage ## Backup Sites
We have the script that helps to push latest backup to S3. Environment Variables
- `SITES` is list of sites separated by `:` colon to migrate. e.g. `SITES=site1.domain.com` or `SITES=site1.domain.com:site2.domain.com` By default all sites in bench will be backed up.
- `WITH_FILES` if set to 1, it will backup user-uploaded files.
- By default `backup` takes mariadb dump and gzips it. Example file, `20200325_221230-test_localhost-database.sql.gz`
- If `WITH_FILES` is set then it will also backup public and private files of each site as uncompressed tarball. Example files, `20200325_221230-test_localhost-files.tar` and `20200325_221230-test_localhost-private-files.tar`
- All the files generated by backup are placed at volume location `sites-vol:/{site-name}/private/backups/*`
```sh ```sh
docker-compose exec backend push_backup.py --site-name <site-name> --bucket <bucket> --region-name <region> --endpoint-url <endpoint-url> --aws-access-key-id <access-key> --aws-secret-access-key <secret-key> docker run \
-e "SITES=site1.domain.com:site2.domain.com" \
-e "WITH_FILES=1" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
frappe/erpnext-worker:$ERPNEXT_VERSION backup
``` ```
Note that you can restore backup only manually. The backup will be available in the `sites-vol` volume.
## Push backup to s3 compatible storage
Environment Variables
- `BUCKET_NAME`, Required to set bucket created on S3 compatible storage.
- `REGION`, Required to set region for S3 compatible storage.
- `ACCESS_KEY_ID`, Required to set access key.
- `SECRET_ACCESS_KEY`, Required to set secret access key.
- `ENDPOINT_URL`, Required to set URL of S3 compatible storage.
- `BUCKET_DIR`, Required to set directory in bucket where sites from this deployment will be backed up.
- `BACKUP_LIMIT`, Optionally set this to limit number of backups in bucket directory. Defaults to 3.
```sh
docker run \
-e "BUCKET_NAME=backups" \
-e "REGION=region" \
-e "ACCESS_KEY_ID=access_id_from_provider" \
-e "SECRET_ACCESS_KEY=secret_access_from_provider" \
-e "ENDPOINT_URL=https://region.storage-provider.com" \
-e "BUCKET_DIR=frappe-bench" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
frappe/frappe-worker:$FRAPPE_VERSION push-backup
```
Note:
- Above example will backup files in bucket called `backup` at location `frappe-bench-v13/site.name.com/DATE_TIME/DATE_TIME-site_name_com-{filetype}.{extension}`,
- example DATE_TIME: 20200325_042020.
- example filetype: database, files or private-files
- example extension: sql.gz or tar
## Restore backups
Environment Variables
- `MYSQL_ROOT_PASSWORD` or `MYSQL_ROOT_PASSWORD_FILE`(when using docker secrets), Required to restore mariadb backups.
- `BUCKET_NAME`, Required to set bucket created on S3 compatible storage.
- `ACCESS_KEY_ID`, Required to set access key.
- `SECRET_ACCESS_KEY`, Required to set secret access key.
- `ENDPOINT_URL`, Required to set URL of S3 compatible storage.
- `REGION`, Required to set region for s3 compatible storage.
- `BUCKET_DIR`, Required to set directory in bucket where sites from this deployment will be backed up.
```sh
docker run \
-e "MYSQL_ROOT_PASSWORD=admin" \
-e "BUCKET_NAME=backups" \
-e "REGION=region" \
-e "ACCESS_KEY_ID=access_id_from_provider" \
-e "SECRET_ACCESS_KEY=secret_access_from_provider" \
-e "ENDPOINT_URL=https://region.storage-provider.com" \
-e "BUCKET_DIR=frappe-bench" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
-v ./backups:/home/frappe/backups \
--network <project-name>_default \
frappe/frappe-worker:$FRAPPE_VERSION restore-backup
```
Note:
- Volume must be mounted at location `/home/frappe/backups` for restoring sites
- If no backup files are found in volume, it will use s3 credentials to pull backups
- Backup structure for mounted volume or downloaded from s3:
- /home/frappe/backups
- site1.domain.com
- 20200420_162000
- 20200420_162000-site1_domain_com-\*
- site2.domain.com
- 20200420_162000
- 20200420_162000-site2_domain_com-\*
## Edit configs ## Edit configs
@@ -44,11 +179,12 @@ Editing config manually might be required in some cases,
one such case is to use Amazon RDS (or any other DBaaS). one such case is to use Amazon RDS (or any other DBaaS).
For full instructions, refer to the [wiki](<https://github.com/frappe/frappe/wiki/Using-Frappe-with-Amazon-RDS-(or-any-other-DBaaS)>). Common question can be found in Issues and on forum. For full instructions, refer to the [wiki](<https://github.com/frappe/frappe/wiki/Using-Frappe-with-Amazon-RDS-(or-any-other-DBaaS)>). Common question can be found in Issues and on forum.
`common_site_config.json` or `site_config.json` from `sites` volume has to be edited using following command: `common_site_config.json` or `site_config.json` from `sites-vol` volume has to be edited using following command:
```sh ```sh
docker run --rm -it \ docker run \
-v <project-name>_sites:/sites \ -it \
-v <project-name>_sites-vol:/sites \
alpine vi /sites/common_site_config.json alpine vi /sites/common_site_config.json
``` ```
@@ -59,7 +195,8 @@ Instead of `alpine` use any image of your choice.
For socketio and gunicorn service ping the hostname:port and that will be sufficient. For workers and scheduler, there is a command that needs to be executed. For socketio and gunicorn service ping the hostname:port and that will be sufficient. For workers and scheduler, there is a command that needs to be executed.
```shell ```shell
docker-compose exec backend healthcheck.sh --ping-service mongodb:27017 docker exec -it <project-name>_erpnext-worker-d \
docker-entrypoint.sh doctor -p postgresql:5432 --ping-service mongodb:27017
``` ```
Additional services can be pinged as part of health check with option `-p` or `--ping-service`. Additional services can be pinged as part of health check with option `-p` or `--ping-service`.
@@ -67,20 +204,86 @@ Additional services can be pinged as part of health check with option `-p` or `-
This check ensures that given service should be connected along with services in common_site_config.json. This check ensures that given service should be connected along with services in common_site_config.json.
If connection to service(s) fails, the command fails with exit code 1. If connection to service(s) fails, the command fails with exit code 1.
--- ## Frappe internal commands using bench helper
For reference of commands like `backup`, `drop-site` or `migrate` check [official guide](https://frappeframework.com/docs/v13/user/en/bench/frappe-commands) or run: To execute commands using bench helper.
```sh ```shell
docker-compose exec backend bench --help docker run \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
--user frappe \
frappe/frappe-worker:$FRAPPE_VERSION bench --help
``` ```
## Migrate site Example command to clear cache
Note: ```shell
docker run \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
--user frappe \
frappe/frappe-worker:$FRAPPE_VERSION bench --site erp.mysite.com clear-cache
```
- Wait for the `db` service to start and `configurator` to exit before trying to migrate a site. Usually this takes up to 10 seconds. Notes:
- Use it to install/uninstall custom apps, add system manager user, etc.
- To run the command as non root user add the command option `--user frappe`.
## Delete/Drop Site
#### MariaDB Site
```sh ```sh
docker-compose exec backend bench --site <site-name> migrate # Delete/Drop ERPNext site
docker run \
-e "SITE_NAME=$SITE_NAME" \
-e "DB_ROOT_USER=$DB_ROOT_USER" \
-e "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
frappe/erpnext-worker:$ERPNEXT_VERSION drop
``` ```
#### PostgreSQL Site
```sh
# Delete/Drop ERPNext site
docker run \
-e "SITE_NAME=$SITE_NAME" \
-e "DB_ROOT_USER=$DB_ROOT_USER" \
-e "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
--network <project-name>_default \
frappe/erpnext-worker:$ERPNEXT_VERSION drop
```
Environment Variables needed:
- `SITE_NAME`: name of the site to be deleted. Site name is domain name that resolves. e.g. `erp.example.com` or `erp.localhost`.
- `DB_ROOT_USER`: MariaDB/PostgreSQL Root user.
- `MYSQL_ROOT_PASSWORD`: Root User password for MariaDB.
- `FORCE=1`: optional variable which force deletion of the same site.
- `NO_BACKUP=1`: option variable to skip the process of taking backup before deleting the site.
Environment Variables for PostgreSQL only:
- `POSTGRES_PASSWORD`: Password for `postgres`. The database root user.
## Migrate Site
```sh
# Migrate ERPNext site
docker run \
-e "MAINTENANCE_MODE=1" \
-v <project-name>_sites-vol:/home/frappe/frappe-bench/sites \
-v <project-name>_assets-vol:/home/frappe/frappe-bench/sites/assets \
--network <project-name>_default \
frappe/erpnext-worker:$ERPNEXT_VERSION migrate
```
Environment Variables needed:
- `MAINTENANCE_MODE`: If set to `1`, this will ensure the bench is switched to maintenance mode during migration.
- `SITES`: Optional list of sites to be migrated, separated by colon (`:`). e.g. `erp.site1.com:erp.site2.com`. If not used, all sites will be migrated by default.

View File

@@ -0,0 +1,15 @@
# Tips for moving deployments
- Take regular automatic backups and push the files to S3 compatible cloud. Setup backup and push with cronjobs
- Use regular cron for single machine installs
- Use [swarm-cronjob](https://github.com/crazy-max/swarm-cronjob) for docker swarm
- Use Kubernetes CronJob
- It makes it easy to transfer data from cloud to any new deployment.
- They are just [site operations](site-operations.md) that can be manually pipelined as per need.
- Remember to restore encryption keys and other custom configuration from `site_config.json`.
- Steps to move deployment:
- [Take backup](site-operations.md#backup-sites)
- [Push backup to cloud](site-operations.md#push-backup-to-s3-compatible-storage)
- Create new deployment type anywhere
- [Restore backup from cloud](site-operations.md#restore-backups)
- [Restore `site_config.json` from cloud](site-operations.md#edit-configs)

View File

@@ -1,25 +0,0 @@
# Accessing ERPNext through https on local deployment
- ERPNext container deployment can be accessed through https easily using Caddy web server, Caddy will be used as reverse proxy and forward traffics to the frontend container.
### Prerequisites
- Caddy
- Adding a domain name to hosts file
#### Installation of caddy webserver
- Follow the official Caddy website for the installation guide https://caddyserver.com/docs/install
After completing the installation open the configuration file of Caddy ( You find the config file in ` /etc/caddy/Caddyfile`), add the following configuration to forward traffics to the ERPNext frontend container
```js
erp.localdev.net {
tls internal
reverse_proxy localhost:8085 {
}
}
```
- Caddy's root certificate must be added to other computers if computers from different networks access the ERPNext through https.

View File

@@ -1,79 +0,0 @@
1. [Fixing MariaDB issues after rebuilding the container](#fixing-mariadb-issues-after-rebuilding-the-container)
1. [docker-compose does not recognize variables from `.env` file](#docker-compose-does-not-recognize-variables-from-env-file)
1. [Windows Based Installation](#windows-based-installation)
### Fixing MariaDB issues after rebuilding the container
For any reason after rebuilding the container if you are not be able to access MariaDB correctly (i.e. `Access denied for user [...]`) with the previous configuration. Follow these instructions.
First test for network issues. Manually connect to the database through the `backend` container:
```
docker exec -it frappe_docker-backend-1 bash
mysql -uroot -padmin -hdb
```
Replace `root` with the database root user name, `admin` with the root password, and `db` with the service name specified in the docker-compose `.yml` configuration file. If the connection to the database is successful, then the network configuration is correct and you can proceed to the next step. Otherwise, modify the docker-compose `.yml` configuration file, in the `configurator` service's `environment` section, to use the container names (`frappe_docker-db-1`, `frappe_docker-redis-cache-1`, `frappe_docker-redis-queue-1` or as otherwise shown with `docker ps`) instead of the service names and rebuild the containers.
Then, the parameter `'db_name'@'%'` needs to be set in MariaDB and permission to the site database suitably assigned to the user.
This step has to be repeated for all sites available under the current bench.
Example shows the queries to be executed for site `localhost`
Open sites/localhost/site_config.json:
```shell
code sites/localhost/site_config.json
```
and take note of the parameters `db_name` and `db_password`.
Enter MariaDB Interactive shell:
```shell
mysql -uroot -padmin -hdb
```
The parameter `'db_name'@'%'` must not be duplicated. Verify that it is unique with the command:
```
SELECT User, Host FROM mysql.user;
```
Delete duplicated entries, if found, with the following:
```
DROP USER 'db_name'@'host';
```
Modify permissions by executing following queries replacing `db_name` and `db_password` with the values found in site_config.json.
```sql
-- if there is no user created already first try to created it using the next command
-- CREATE USER 'db_name'@'%' IDENTIFIED BY 'your_password';
-- skip the upgrade command below if you use the create command above
UPDATE mysql.global_priv SET Host = '%' where User = 'db_name'; FLUSH PRIVILEGES;
SET PASSWORD FOR 'db_name'@'%' = PASSWORD('db_password'); FLUSH PRIVILEGES;
GRANT ALL PRIVILEGES ON `db_name`.* TO 'db_name'@'%' IDENTIFIED BY 'db_password' WITH GRANT OPTION; FLUSH PRIVILEGES;
EXIT;
```
Note: For MariaDB 10.3 and older use `mysql.user` instead of `mysql.global_priv`.
### docker-compose does not recognize variables from `.env` file
If you are using old version of `docker-compose` the .env file needs to be located in directory from where the docker-compose command is executed. There may also be difference in official `docker-compose` and the one packaged by distro. Use `--env-file=.env` if available to explicitly specify the path to file.
### Windows Based Installation
- Set environment variable `COMPOSE_CONVERT_WINDOWS_PATHS` e.g. `set COMPOSE_CONVERT_WINDOWS_PATHS=1`
- While using docker machine, port-forward the ports of VM to ports of host machine. (ports 8080/8000/9000)
- Name all the sites ending with `.localhost`. and access it via browser locally. e.g. `http://site1.localhost`
### Redo installation
- If you have made changes and just want to start over again (abandoning all changes), remove all docker
- containers
- images
- volumes
- Install a fresh

View File

@@ -1 +0,0 @@
WwogICAgewogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9naXRodWIuY29tL2ZyYXBwZS9lcnBuZXh0IiwKICAgICAgICAiYnJhbmNoIjogInZlcnNpb24tMTUiCiAgICB9LAogICAgewogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9naXRodWIuY29tL2ZyYXBwZS9ocm1zIiwKICAgICAgICAiYnJhbmNoIjogInZlcnNpb24tMTUiCiAgICB9LAogICAgewogICAgICAgICJ1cmwiOiAiaHR0cHM6Ly9naXRodWIuY29tL2ZyYXBwZS9oZWxwZGVzayIsCiAgICAgICAgImJyYW5jaCI6ICJtYWluIgogICAgfSwKICAgIHsKICAgICAgICAidXJsIjogImh0dHBzOi8vZ2l0aHViLmNvbS9mcmFwcGUvcGF5bWVudHMiLAogICAgICAgICJicmFuY2giOiAidmVyc2lvbi0xNSIKICAgIH0KXQ==

7
env-example Normal file
View File

@@ -0,0 +1,7 @@
ERPNEXT_VERSION=edge
FRAPPE_VERSION=edge
MARIADB_HOST=mariadb
MYSQL_ROOT_PASSWORD=admin
SITES=your.domain.com
LETSENCRYPT_EMAIL=your.email@your.domain.com
SKIP_NGINX_TEMPLATE_GENERATION=0

18
env-local Normal file
View File

@@ -0,0 +1,18 @@
LETSENCRYPT_EMAIL=email@example.com
ERPNEXT_VERSION=edge
FRAPPE_VERSION=edge
MARIADB_HOST=mariadb
MYSQL_ROOT_PASSWORD=admin
SITE_NAME=mysite.localhost
SITES=`mysite.localhost`
DB_ROOT_USER=root
ADMIN_PASSWORD=admin
INSTALL_APPS=erpnext
ENTRYPOINT_LABEL=traefik.http.routers.erpnext-nginx.entrypoints=web
CERT_RESOLVER_LABEL=erpnext.local.no-cert-resolver
HTTPS_REDIRECT_RULE_LABEL=erpnext.local.no-redirect-rule
HTTPS_REDIRECT_ENTRYPOINT_LABEL=erpnext.local.no-entrypoint
HTTPS_REDIRECT_MIDDLEWARE_LABEL=erpnext.local.no-middleware
HTTPS_USE_REDIRECT_MIDDLEWARE_LABEL=erpnext.local-no-redirect-middleware
SKIP_NGINX_TEMPLATE_GENERATION=0
WORKER_CLASS=gthread

18
env-production Normal file
View File

@@ -0,0 +1,18 @@
LETSENCRYPT_EMAIL=email@example.com
ERPNEXT_VERSION=edge
FRAPPE_VERSION=edge
MARIADB_HOST=mariadb
MYSQL_ROOT_PASSWORD=admin
SITE_NAME=erp.example.com
SITES=`erp.example.com`
DB_ROOT_USER=root
ADMIN_PASSWORD=admin
INSTALL_APPS=erpnext
ENTRYPOINT_LABEL=traefik.http.routers.erpnext-nginx.entrypoints=websecure
CERT_RESOLVER_LABEL=traefik.http.routers.erpnext-nginx.tls.certresolver=myresolver
HTTPS_REDIRECT_RULE_LABEL=traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)
HTTPS_REDIRECT_ENTRYPOINT_LABEL=traefik.http.routers.http-catchall.entrypoints=web
HTTPS_REDIRECT_MIDDLEWARE_LABEL=traefik.http.routers.http-catchall.middlewares=redirect-to-https
HTTPS_USE_REDIRECT_MIDDLEWARE_LABEL=traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
SKIP_NGINX_TEMPLATE_GENERATION=0
WORKER_CLASS=gthread

View File

@@ -1,55 +0,0 @@
# Reference: https://github.com/frappe/frappe_docker/blob/main/docs/environment-variables.md
ERPNEXT_VERSION=v15.83.0
DB_PASSWORD=123
#Only if you use docker secrets for the db password
DB_PASSWORD_SECRETS_FILE=
# Only if you use external database
DB_HOST=
DB_PORT=
# Only if you use external Redis
REDIS_CACHE=
REDIS_QUEUE=
# Only with HTTPS override
LETSENCRYPT_EMAIL=admin@khaeli.com
# These environment variables are not required.
# Default value is `$$host` which resolves site by host. For example, if your host is `example.com`,
# site's name should be `example.com`, or if host is `127.0.0.1` (local debugging), it should be `127.0.0.1`.
# This variable allows to override described behavior. Let's say you create site named `mysite`
# and do want to access it by `127.0.0.1` host. Than you would set this variable to `mysite`.
FRAPPE_SITE_NAME_HEADER=
# Default value is `8080`.
HTTP_PUBLISH_PORT=
# Default value is `127.0.0.1`. Set IP address as our trusted upstream address.
UPSTREAM_REAL_IP_ADDRESS=
# Default value is `X-Forwarded-For`. Set request header field whose value will be used to replace the client address
UPSTREAM_REAL_IP_HEADER=
# Allowed values are on|off. Default value is `off`. If recursive search is disabled,
# the original client address that matches one of the trusted addresses
# is replaced by the last address sent in the request header field defined by the real_ip_header directive.
# If recursive search is enabled, the original client address that matches one of the trusted addresses is replaced by the last non-trusted address sent in the request header field.
UPSTREAM_REAL_IP_RECURSIVE=
# All Values Allowed by nginx proxy_read_timeout are allowed, default value is 120s
# Useful if you have longrunning print formats or slow loading sites
PROXY_READ_TIMEOUT=
# All Values allowed by nginx client_max_body_size are allowed, default value is 50m
# Necessary if the upload limit in the frappe application is increased
CLIENT_MAX_BODY_SIZE=
# List of sites for letsencrypt certificates quoted with backtick (`) and separated by comma (,)
# More https://doc.traefik.io/traefik/routing/routers/#rule
# About acme https://doc.traefik.io/traefik/https/acme/#domain-definition
SITES=`erp.khaeli.com`,`hr.khaeli.com`,`helpdesk.khaeli.com`,`payments.khaeli.com`

View File

@@ -1,159 +0,0 @@
ARG PYTHON_VERSION=3.11.6
ARG DEBIAN_BASE=bookworm
FROM python:${PYTHON_VERSION}-slim-${DEBIAN_BASE} AS base
COPY resources/nginx-template.conf /templates/nginx/frappe.conf.template
COPY resources/nginx-entrypoint.sh /usr/local/bin/nginx-entrypoint.sh
ARG WKHTMLTOPDF_VERSION=0.12.6.1-3
ARG WKHTMLTOPDF_DISTRO=bookworm
ARG NODE_VERSION=20.19.2
ENV NVM_DIR=/home/frappe/.nvm
ENV PATH=${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}
RUN useradd -ms /bin/bash frappe \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
git \
vim \
nginx \
gettext-base \
file \
# weasyprint dependencies
libpango-1.0-0 \
libharfbuzz0b \
libpangoft2-1.0-0 \
libpangocairo-1.0-0 \
# For backups
restic \
gpg \
# MariaDB
mariadb-client \
less \
# Postgres
libpq-dev \
postgresql-client \
# For healthcheck
wait-for-it \
jq \
# NodeJS
&& mkdir -p ${NVM_DIR} \
&& curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \
&& . ${NVM_DIR}/nvm.sh \
&& nvm install ${NODE_VERSION} \
&& nvm use v${NODE_VERSION} \
&& npm install -g yarn \
&& nvm alias default v${NODE_VERSION} \
&& rm -rf ${NVM_DIR}/.cache \
&& echo 'export NVM_DIR="/home/frappe/.nvm"' >>/home/frappe/.bashrc \
&& echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >>/home/frappe/.bashrc \
&& echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion' >>/home/frappe/.bashrc \
# Install wkhtmltopdf with patched qt
&& if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \
&& if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \
&& downloaded_file=wkhtmltox_${WKHTMLTOPDF_VERSION}.${WKHTMLTOPDF_DISTRO}_${ARCH}.deb \
&& curl -sLO https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \
&& apt-get install -y ./$downloaded_file \
&& rm $downloaded_file \
# Clean up
&& rm -rf /var/lib/apt/lists/* \
&& rm -fr /etc/nginx/sites-enabled/default \
&& pip3 install frappe-bench \
# Fixes for non-root nginx and logs to stdout
&& sed -i '/user www-data/d' /etc/nginx/nginx.conf \
&& ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log \
&& touch /run/nginx.pid \
&& chown -R frappe:frappe /etc/nginx/conf.d \
&& chown -R frappe:frappe /etc/nginx/nginx.conf \
&& chown -R frappe:frappe /var/log/nginx \
&& chown -R frappe:frappe /var/lib/nginx \
&& chown -R frappe:frappe /run/nginx.pid \
&& chmod 755 /usr/local/bin/nginx-entrypoint.sh \
&& chmod 644 /templates/nginx/frappe.conf.template
FROM base AS builder
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
# For frappe framework
wget \
#for building arm64 binaries
libcairo2-dev \
libpango1.0-dev \
libjpeg-dev \
libgif-dev \
librsvg2-dev \
# For psycopg2
libpq-dev \
# Other
libffi-dev \
liblcms2-dev \
libldap2-dev \
libmariadb-dev \
libsasl2-dev \
libtiff5-dev \
libwebp-dev \
pkg-config \
redis-tools \
rlwrap \
tk8.6-dev \
cron \
# For pandas
gcc \
build-essential \
libbz2-dev \
&& rm -rf /var/lib/apt/lists/*
# apps.json includes
ARG APPS_JSON_BASE64
RUN if [ -n "${APPS_JSON_BASE64}" ]; then \
mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \
fi
USER frappe
ARG FRAPPE_BRANCH=version-15
ARG FRAPPE_PATH=https://github.com/frappe/frappe
RUN export APP_INSTALL_ARGS="" && \
if [ -n "${APPS_JSON_BASE64}" ]; 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 base AS backend
USER frappe
COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench
WORKDIR /home/frappe/frappe-bench
VOLUME [ \
"/home/frappe/frappe-bench/sites", \
"/home/frappe/frappe-bench/sites/assets", \
"/home/frappe/frappe-bench/logs" \
]
CMD [ \
"/home/frappe/frappe-bench/env/bin/gunicorn", \
"--chdir=/home/frappe/frappe-bench/sites", \
"--bind=0.0.0.0:8000", \
"--threads=4", \
"--workers=2", \
"--worker-class=gthread", \
"--worker-tmp-dir=/dev/shm", \
"--timeout=120", \
"--preload", \
"frappe.app:application" \
]

View File

@@ -1,58 +0,0 @@
ARG FRAPPE_BRANCH=version-15
FROM frappe/build:${FRAPPE_BRANCH} AS builder
ARG FRAPPE_BRANCH=version-15
ARG FRAPPE_PATH=https://github.com/frappe/frappe
ARG APPS_JSON_BASE64
USER root
RUN if [ -n "${APPS_JSON_BASE64}" ]; then \
mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \
fi
USER frappe
RUN export APP_INSTALL_ARGS="" && \
if [ -n "${APPS_JSON_BASE64}" ]; 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/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
VOLUME [ \
"/home/frappe/frappe-bench/sites", \
"/home/frappe/frappe-bench/sites/assets", \
"/home/frappe/frappe-bench/logs" \
]
CMD [ \
"/home/frappe/frappe-bench/env/bin/gunicorn", \
"--chdir=/home/frappe/frappe-bench/sites", \
"--bind=0.0.0.0:8000", \
"--threads=4", \
"--workers=2", \
"--worker-class=gthread", \
"--worker-tmp-dir=/dev/shm", \
"--timeout=120", \
"--preload", \
"frappe.app:application" \
]

View File

@@ -1,148 +0,0 @@
ARG PYTHON_VERSION=3.11.6
ARG DEBIAN_BASE=bookworm
FROM python:${PYTHON_VERSION}-slim-${DEBIAN_BASE} AS base
ARG WKHTMLTOPDF_VERSION=0.12.6.1-3
ARG WKHTMLTOPDF_DISTRO=bookworm
ARG NODE_VERSION=20.19.2
ENV NVM_DIR=/home/frappe/.nvm
ENV PATH=${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}
RUN useradd -ms /bin/bash frappe \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
git \
vim \
nginx \
gettext-base \
file \
# weasyprint dependencies
libpango-1.0-0 \
libharfbuzz0b \
libpangoft2-1.0-0 \
libpangocairo-1.0-0 \
# For backups
restic \
gpg \
# MariaDB
mariadb-client \
less \
# Postgres
libpq-dev \
postgresql-client \
# For healthcheck
wait-for-it \
jq \
# NodeJS
&& mkdir -p ${NVM_DIR} \
&& curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \
&& . ${NVM_DIR}/nvm.sh \
&& nvm install ${NODE_VERSION} \
&& nvm use v${NODE_VERSION} \
&& npm install -g yarn \
&& nvm alias default v${NODE_VERSION} \
&& rm -rf ${NVM_DIR}/.cache \
&& echo 'export NVM_DIR="/home/frappe/.nvm"' >>/home/frappe/.bashrc \
&& echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >>/home/frappe/.bashrc \
&& echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion' >>/home/frappe/.bashrc \
# Install wkhtmltopdf with patched qt
&& if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \
&& if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \
&& downloaded_file=wkhtmltox_${WKHTMLTOPDF_VERSION}.${WKHTMLTOPDF_DISTRO}_${ARCH}.deb \
&& curl -sLO https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \
&& apt-get install -y ./$downloaded_file \
&& rm $downloaded_file \
# Clean up
&& rm -rf /var/lib/apt/lists/* \
&& rm -fr /etc/nginx/sites-enabled/default \
&& pip3 install frappe-bench \
# Fixes for non-root nginx and logs to stdout
&& sed -i '/user www-data/d' /etc/nginx/nginx.conf \
&& ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log \
&& touch /run/nginx.pid \
&& chown -R frappe:frappe /etc/nginx/conf.d \
&& chown -R frappe:frappe /etc/nginx/nginx.conf \
&& chown -R frappe:frappe /var/log/nginx \
&& chown -R frappe:frappe /var/lib/nginx \
&& chown -R frappe:frappe /run/nginx.pid
COPY resources/nginx-template.conf /templates/nginx/frappe.conf.template
COPY resources/nginx-entrypoint.sh /usr/local/bin/nginx-entrypoint.sh
FROM base AS build
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
# For frappe framework
wget \
# For psycopg2
libpq-dev \
# Other
libffi-dev \
liblcms2-dev \
libldap2-dev \
libmariadb-dev \
libsasl2-dev \
libtiff5-dev \
libwebp-dev \
pkg-config \
redis-tools \
rlwrap \
tk8.6-dev \
cron \
# For pandas
gcc \
build-essential \
libbz2-dev \
&& rm -rf /var/lib/apt/lists/*
USER frappe
FROM build AS builder
ARG FRAPPE_BRANCH=version-15
ARG FRAPPE_PATH=https://github.com/frappe/frappe
ARG ERPNEXT_REPO=https://github.com/frappe/erpnext
ARG ERPNEXT_BRANCH=version-15
RUN bench init \
--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 && \
bench get-app --branch=${ERPNEXT_BRANCH} --resolve-deps erpnext ${ERPNEXT_REPO} && \
echo "{}" > sites/common_site_config.json && \
find apps -mindepth 1 -path "*/.git" | xargs rm -fr
FROM base AS erpnext
USER frappe
RUN echo "echo \"Commands restricted in prodution container, Read FAQ before you proceed: https://frappe.io/ctr-faq\"" >> ~/.bashrc
COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench
WORKDIR /home/frappe/frappe-bench
VOLUME [ \
"/home/frappe/frappe-bench/sites", \
"/home/frappe/frappe-bench/sites/assets", \
"/home/frappe/frappe-bench/logs" \
]
CMD [ \
"/home/frappe/frappe-bench/env/bin/gunicorn", \
"--chdir=/home/frappe/frappe-bench/sites", \
"--bind=0.0.0.0:8000", \
"--threads=4", \
"--workers=2", \
"--worker-class=gthread", \
"--worker-tmp-dir=/dev/shm", \
"--timeout=120", \
"--preload", \
"frappe.app:application" \
]

View File

@@ -1,104 +0,0 @@
#!/bin/bash
set -e
# This script configures X11 forwarding for Linux and macOS systems.
# It installs X11, Openbox (on Linux), and checks for XQuartz (on macOS).
# It also updates the sshd_config file to enable X11Forwarding and restarts the SSH service.
# Check if the script is running with root privileges
if [ "$EUID" -ne 0 ]; then
echo "Error: This script requires root privileges. Please run it as a superuser"
exit 1
fi
# Function to restart SSH service (Linux)
restart_ssh_linux() {
if command -v service >/dev/null 2>&1; then
sudo service ssh restart
else
sudo systemctl restart ssh
fi
}
# Function to restart SSH service (macOS)
restart_ssh_macos() {
launchctl stop com.openssh.sshd
launchctl start com.openssh.sshd
}
update_x11_forwarding() {
if grep -q "X11Forwarding yes" /etc/ssh/sshd_config; then
echo "X11Forwarding is already set to 'yes' in ssh_config."
else
if [[ "$OSTYPE" == "linux-gnu" ]]; then
# Linux: Use sed for Linux
sudo sed -i 's/#\?X11Forwarding.*/X11Forwarding yes/' /etc/ssh/sshd_config
elif [[ "$OSTYPE" == "darwin"* ]]; then
# macOS: Use sed for macOS
sudo sed -i -E 's/#X11Forwarding.*/X11Forwarding yes/' /etc/ssh/sshd_config
restart_ssh_macos
fi
if [[ "$OSTYPE" == "linux-gnu" ]]; then
restart_ssh_linux
fi
fi
}
# Determine the operating system
if [[ "$OSTYPE" == "linux-gnu" ]]; then
# Linux
if command -v startx >/dev/null 2>&1; then
echo "X11 is already installed."
else
# Check which package manager is available
if command -v apt-get >/dev/null 2>&1; then
install_command="sudo apt-get update && sudo apt-get install xorg openbox"
elif command -v dnf >/dev/null 2>&1; then
install_command="sudo dnf install xorg-x11-server-Xorg openbox"
else
echo "Error: Unable to determine the package manager. Manual installation required."
exit 1
fi
fi
# Check if the installation command is defined
if [ -n "$install_command" ]; then
# Execute the installation command
if $install_command; then
echo "X11 and Openbox have been successfully installed."
else
echo "Error: Failed to install X11 and Openbox."
exit 1
fi
else
echo "Error: Unsupported package manager."
exit 1
fi
# Call the function to update X11Forwarding
update_x11_forwarding
# Get the IP address of the host dynamically
host_ip=$(hostname -I | awk '{print $1}')
xhost + "$host_ip" && xhost + local:
# Set the DISPLAY variable to the host IP
export DISPLAY="$host_ip:0.0"
echo "DISPLAY variable set to $DISPLAY"
elif [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
if command -v xquartz >/dev/null 2>&1; then
echo "XQuartz is already installed."
else
echo "Error: XQuartz is required for X11 forwarding on macOS. Please install XQuartz manually."
exit 1
fi
# Call the function to update X11Forwarding
update_x11_forwarding
# Export the DISPLAY variable for macOS
export DISPLAY=:0
echo "DISPLAY variable set to $DISPLAY"
else
echo "Error: Unsupported operating system."
exit 1
fi

View File

@@ -0,0 +1,42 @@
version: "3"
services:
redis-cache:
image: redis:latest
restart: on-failure
volumes:
- redis-cache-vol:/data
redis-queue:
image: redis:latest
restart: on-failure
volumes:
- redis-queue-vol:/data
redis-socketio:
image: redis:latest
restart: on-failure
volumes:
- redis-socketio-vol:/data
mariadb:
image: mariadb:10.6
restart: on-failure
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
# Sometimes db initialization takes longer than 10 seconds and site-creator goes away.
# Frappe doesn't use CONVERT_TZ() function that requires time zone info, so we can just skip it.
- MYSQL_INITDB_SKIP_TZINFO=1
volumes:
- mariadb-vol:/var/lib/mysql
volumes:
mariadb-vol:
redis-cache-vol:
redis-queue-vol:
redis-socketio-vol:

View File

@@ -0,0 +1,123 @@
version: '3'
services:
[app]-assets:
image: [app]-assets
build:
context: ../build/[app]-nginx
restart: on-failure
environment:
- FRAPPE_PY=[app]-python
- FRAPPE_PY_PORT=8000
- FRAPPE_SOCKETIO=frappe-socketio
- SOCKETIO_PORT=9000
- LETSENCRYPT_HOST=${SITES}
- VIRTUAL_HOST=${SITES}
- LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
depends_on:
- [app]-python
- frappe-socketio
- frappe-worker-default
- frappe-worker-long
- frappe-worker-short
links:
- [app]-python
- frappe-socketio
- frappe-worker-default
- frappe-worker-long
- frappe-worker-short
volumes:
- sites-vol:/var/www/html/sites:rw
- assets-vol:/assets:rw
[app]-python:
image: [app]-worker
build:
context: ../build/[app]-worker
restart: on-failure
environment:
- MARIADB_HOST=${MARIADB_HOST}
- REDIS_CACHE=redis-cache:6379
- REDIS_QUEUE=redis-queue:6379
- REDIS_SOCKETIO=redis-socketio:6379
- SOCKETIO_PORT=9000
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
- assets-vol:/home/frappe/frappe-bench/sites/assets:rw
frappe-socketio:
image: frappe/frappe-socketio:${FRAPPE_VERSION}
restart: on-failure
depends_on:
- redis-socketio
links:
- redis-socketio
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-worker-default:
image: [app]-worker
restart: on-failure
command: worker
depends_on:
- redis-queue
- redis-cache
links:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-worker-short:
image: [app]-worker
restart: on-failure
command: worker
environment:
- WORKER_TYPE=short
depends_on:
- redis-queue
- redis-cache
links:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-worker-long:
image: [app]-worker
restart: on-failure
command: worker
environment:
- WORKER_TYPE=long
depends_on:
- redis-queue
- redis-cache
links:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-schedule:
image: [app]-worker
restart: on-failure
command: schedule
depends_on:
- redis-queue
- redis-cache
links:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
volumes:
assets-vol:
sites-vol:
logs-vol:

View File

@@ -0,0 +1,120 @@
version: "3"
services:
erpnext-nginx:
image: frappe/erpnext-nginx:${ERPNEXT_VERSION}
restart: on-failure
environment:
- FRAPPE_PY=erpnext-python
- FRAPPE_PY_PORT=8000
- FRAPPE_SOCKETIO=frappe-socketio
- SOCKETIO_PORT=9000
- LETSENCRYPT_HOST=${SITES}
- VIRTUAL_HOST=${SITES}
- LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
depends_on:
- erpnext-python
- frappe-socketio
- frappe-worker-default
- frappe-worker-long
- frappe-worker-short
links:
- erpnext-python
- frappe-socketio
- frappe-worker-default
- frappe-worker-long
- frappe-worker-short
volumes:
- sites-vol:/var/www/html/sites:rw
- assets-vol:/assets:rw
erpnext-python:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
environment:
- MARIADB_HOST=${MARIADB_HOST}
- REDIS_CACHE=redis-cache:6379
- REDIS_QUEUE=redis-queue:6379
- REDIS_SOCKETIO=redis-socketio:6379
- SOCKETIO_PORT=9000
- AUTO_MIGRATE=1
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- assets-vol:/home/frappe/frappe-bench/sites/assets:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-socketio:
image: frappe/frappe-socketio:${FRAPPE_VERSION}
restart: on-failure
depends_on:
- redis-socketio
links:
- redis-socketio
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-worker-default:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
command: worker
depends_on:
- redis-queue
- redis-cache
links:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-worker-short:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
command: worker
environment:
- WORKER_TYPE=short
depends_on:
- redis-queue
- redis-cache
links:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-worker-long:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
command: worker
environment:
- WORKER_TYPE=long
depends_on:
- redis-queue
- redis-cache
links:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
frappe-schedule:
image: frappe/erpnext-worker:${ERPNEXT_VERSION}
restart: on-failure
command: schedule
depends_on:
- redis-queue
- redis-cache
links:
- redis-queue
- redis-cache
volumes:
- sites-vol:/home/frappe/frappe-bench/sites:rw
- logs-vol:/home/frappe/frappe-bench/logs:rw
volumes:
assets-vol:
sites-vol:
logs-vol:

Some files were not shown because too many files have changed in this diff Show More