├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question-about-using-frappe_docker.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── scripts │ ├── get_latest_tags.py │ ├── update_example_env.py │ └── update_pwd.py └── workflows │ ├── build_bench.yml │ ├── build_develop.yml │ ├── build_stable.yml │ ├── docker-build-push.yml │ ├── lint.yml │ ├── pre-commit-autoupdate.yml │ └── stale.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .shellcheckrc ├── .vscode └── extensions.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── compose.yaml ├── devcontainer-example ├── devcontainer.json └── docker-compose.yml ├── development ├── apps-example.json ├── installer.py └── vscode-example │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── docker-bake.hcl ├── docs ├── backup-and-push-cronjob.md ├── bench-console-and-vscode-debugger.md ├── build-version-10-images.md ├── connect-to-localhost-services-from-containers-for-local-app-development.md ├── custom-apps-podman.md ├── custom-apps.md ├── development.md ├── environment-variables.md ├── error-nginx-entrypoint-windows.md ├── images │ ├── Docker Desktop Screenshot - Resources section.png │ └── Docker Manual Screenshot - Resources section.png ├── list-of-containers.md ├── migrate-from-multi-image-setup.md ├── port-based-multi-tenancy.md ├── setup-options.md ├── setup_for_linux_mac.md ├── single-compose-setup.md ├── single-server-example.md ├── site-operations.md ├── tls-for-local-deployment.md └── troubleshoot.md ├── example.env ├── images ├── bench │ └── Dockerfile ├── custom │ └── Containerfile ├── layered │ └── Containerfile └── production │ └── Containerfile ├── install_x11_deps.sh ├── overrides ├── compose.backup-cron.yaml ├── compose.custom-domain-ssl.yaml ├── compose.custom-domain.yaml ├── compose.https.yaml ├── compose.mariadb-shared.yaml ├── compose.mariadb.yaml ├── compose.multi-bench-ssl.yaml ├── compose.multi-bench.yaml ├── compose.noproxy.yaml ├── compose.postgres.yaml ├── compose.proxy.yaml ├── compose.redis.yaml ├── compose.traefik-ssl.yaml └── compose.traefik.yaml ├── pwd.yml ├── requirements-test.txt ├── resources ├── nginx-entrypoint.sh └── nginx-template.conf ├── setup.cfg └── tests ├── __init__.py ├── _check_connections.py ├── _check_website_theme.py ├── _create_bucket.py ├── _ping_frappe_connections.py ├── compose.ci.yaml ├── conftest.py ├── test_frappe_docker.py └── utils.py /.dockerignore: -------------------------------------------------------------------------------- 1 | README.md 2 | LICENSE 3 | .gitignore 4 | compose*.yaml 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{py, pyi}] 13 | indent_size = 4 14 | 15 | [*Dockerfile*] 16 | indent_size = 4 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug encountered while using the Frappe_docker 4 | labels: bug 5 | --- 6 | 7 | 15 | 16 | ## Description of the issue 17 | 18 | ## Context information (for bug reports) 19 | 20 | ## Steps to reproduce the issue 21 | 22 | 1. 23 | 2. 24 | 3. 25 | 26 | ### Observed result 27 | 28 | ### Expected result 29 | 30 | ### Stacktrace / full error message if available 31 | 32 | ``` 33 | (paste here) 34 | ``` 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea to improve frappe_docker 4 | labels: enhancement 5 | --- 6 | 7 | 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-about-using-frappe_docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question about using Frappe/Frappe Apps 3 | about: Ask how to do something 4 | labels: question 5 | --- 6 | 7 | 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > Please provide enough information so that others can review your pull request: 2 | 3 | 4 | 5 | > Explain the **details** for making this change. What existing problem does the pull request solve? 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: docker 9 | directory: images/bench 10 | schedule: 11 | interval: daily 12 | 13 | - package-ecosystem: docker 14 | directory: images/production 15 | schedule: 16 | interval: daily 17 | 18 | - package-ecosystem: docker 19 | directory: images/custom 20 | schedule: 21 | interval: daily 22 | 23 | - package-ecosystem: pip 24 | directory: / 25 | schedule: 26 | interval: daily 27 | -------------------------------------------------------------------------------- /.github/scripts/get_latest_tags.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import json 5 | import os 6 | import re 7 | import subprocess 8 | import sys 9 | from typing import Literal 10 | 11 | Repo = Literal["frappe", "erpnext"] 12 | MajorVersion = Literal["12", "13", "14", "15", "develop"] 13 | 14 | 15 | def get_latest_tag(repo: Repo, version: MajorVersion) -> str: 16 | if version == "develop": 17 | return "develop" 18 | regex = rf"v{version}.*" 19 | refs = subprocess.check_output( 20 | ( 21 | "git", 22 | "-c", 23 | "versionsort.suffix=-", 24 | "ls-remote", 25 | "--refs", 26 | "--tags", 27 | "--sort=v:refname", 28 | f"https://github.com/frappe/{repo}", 29 | str(regex), 30 | ), 31 | encoding="UTF-8", 32 | ).split()[1::2] 33 | 34 | if not refs: 35 | raise RuntimeError(f'No tags found for version "{regex}"') 36 | ref = refs[-1] 37 | matches: list[str] = re.findall(regex, ref) 38 | if not matches: 39 | raise RuntimeError(f'Can\'t parse tag from ref "{ref}"') 40 | return matches[0] 41 | 42 | 43 | def update_env(file_name: str, frappe_tag: str, erpnext_tag: str | None = None): 44 | text = f"\nFRAPPE_VERSION={frappe_tag}" 45 | if erpnext_tag: 46 | text += f"\nERPNEXT_VERSION={erpnext_tag}" 47 | 48 | with open(file_name, "a") as f: 49 | f.write(text) 50 | 51 | 52 | def _print_resp(frappe_tag: str, erpnext_tag: str | None = None): 53 | print(json.dumps({"frappe": frappe_tag, "erpnext": erpnext_tag})) 54 | 55 | 56 | def main(_args: list[str]) -> int: 57 | parser = argparse.ArgumentParser() 58 | parser.add_argument("--repo", choices=["frappe", "erpnext"], required=True) 59 | parser.add_argument( 60 | "--version", choices=["12", "13", "14", "15", "develop"], required=True 61 | ) 62 | args = parser.parse_args(_args) 63 | 64 | frappe_tag = get_latest_tag("frappe", args.version) 65 | if args.repo == "erpnext": 66 | erpnext_tag = get_latest_tag("erpnext", args.version) 67 | else: 68 | erpnext_tag = None 69 | 70 | file_name = os.getenv("GITHUB_ENV") 71 | if file_name: 72 | update_env(file_name, frappe_tag, erpnext_tag) 73 | _print_resp(frappe_tag, erpnext_tag) 74 | return 0 75 | 76 | 77 | if __name__ == "__main__": 78 | raise SystemExit(main(sys.argv[1:])) 79 | -------------------------------------------------------------------------------- /.github/scripts/update_example_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | 5 | def get_erpnext_version(): 6 | erpnext_version = os.getenv("ERPNEXT_VERSION") 7 | assert erpnext_version, "No ERPNext version set" 8 | return erpnext_version 9 | 10 | 11 | def update_env(erpnext_version: str): 12 | with open("example.env", "r+") as f: 13 | content = f.read() 14 | content = re.sub( 15 | rf"ERPNEXT_VERSION=.*", f"ERPNEXT_VERSION={erpnext_version}", content 16 | ) 17 | f.seek(0) 18 | f.truncate() 19 | f.write(content) 20 | 21 | 22 | def main() -> int: 23 | update_env(get_erpnext_version()) 24 | return 0 25 | 26 | 27 | if __name__ == "__main__": 28 | raise SystemExit(main()) 29 | -------------------------------------------------------------------------------- /.github/scripts/update_pwd.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | 5 | def get_versions(): 6 | frappe_version = os.getenv("FRAPPE_VERSION") 7 | erpnext_version = os.getenv("ERPNEXT_VERSION") 8 | assert frappe_version, "No Frappe version set" 9 | assert erpnext_version, "No ERPNext version set" 10 | return frappe_version, erpnext_version 11 | 12 | 13 | def update_pwd(frappe_version: str, erpnext_version: str): 14 | with open("pwd.yml", "r+") as f: 15 | content = f.read() 16 | content = re.sub( 17 | rf"frappe/erpnext:.*", f"frappe/erpnext:{erpnext_version}", content 18 | ) 19 | f.seek(0) 20 | f.truncate() 21 | f.write(content) 22 | 23 | 24 | def main() -> int: 25 | update_pwd(*get_versions()) 26 | return 0 27 | 28 | 29 | if __name__ == "__main__": 30 | raise SystemExit(main()) 31 | -------------------------------------------------------------------------------- /.github/workflows/build_bench.yml: -------------------------------------------------------------------------------- 1 | name: Bench 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - images/bench/** 9 | - docker-bake.hcl 10 | - .github/workflows/build_bench.yml 11 | 12 | schedule: 13 | # Every day at 12:00 pm 14 | - cron: 0 0 * * * 15 | 16 | workflow_dispatch: 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup QEMU 26 | uses: docker/setup-qemu-action@v3 27 | with: 28 | image: tonistiigi/binfmt:latest 29 | platforms: all 30 | 31 | - name: Setup Buildx 32 | uses: docker/setup-buildx-action@v3 33 | 34 | - name: Set Environment Variables 35 | run: cat example.env | grep -o '^[^#]*' >> "$GITHUB_ENV" 36 | 37 | - name: Get Bench Latest Version 38 | run: echo "LATEST_BENCH_RELEASE=$(curl -s 'https://api.github.com/repos/frappe/bench/releases/latest' | jq -r '.tag_name')" >> "$GITHUB_ENV" 39 | 40 | - name: Build and test 41 | uses: docker/bake-action@v6.8.0 42 | with: 43 | source: . 44 | targets: bench-test 45 | 46 | - name: Login 47 | if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} 48 | uses: docker/login-action@v3 49 | with: 50 | username: ${{ secrets.DOCKERHUB_USERNAME }} 51 | password: ${{ secrets.DOCKERHUB_TOKEN }} 52 | 53 | - name: Push 54 | if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} 55 | uses: docker/bake-action@v6.8.0 56 | with: 57 | targets: bench 58 | push: true 59 | set: "*.platform=linux/amd64,linux/arm64" 60 | -------------------------------------------------------------------------------- /.github/workflows/build_develop.yml: -------------------------------------------------------------------------------- 1 | name: Develop build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - images/production/** 9 | - overrides/** 10 | - tests/** 11 | - compose.yaml 12 | - docker-bake.hcl 13 | - example.env 14 | - .github/workflows/build_develop.yml 15 | 16 | schedule: 17 | # Every day at 12:00 pm 18 | - cron: 0 0 * * * 19 | 20 | workflow_dispatch: 21 | 22 | jobs: 23 | build: 24 | uses: ./.github/workflows/docker-build-push.yml 25 | with: 26 | repo: erpnext 27 | version: develop 28 | push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} 29 | python_version: 3.11.6 30 | node_version: 18.18.2 31 | secrets: 32 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} 33 | DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/build_stable.yml: -------------------------------------------------------------------------------- 1 | name: Stable build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - images/production/** 9 | - overrides/** 10 | - tests/** 11 | - compose.yaml 12 | - docker-bake.hcl 13 | - example.env 14 | - .github/workflows/build_stable.yml 15 | 16 | push: 17 | branches: 18 | - main 19 | paths: 20 | - images/production/** 21 | - overrides/** 22 | - tests/** 23 | - compose.yaml 24 | - docker-bake.hcl 25 | - example.env 26 | 27 | # Triggered from frappe/frappe and frappe/erpnext on releases 28 | repository_dispatch: 29 | 30 | workflow_dispatch: 31 | 32 | jobs: 33 | v14: 34 | uses: ./.github/workflows/docker-build-push.yml 35 | with: 36 | repo: erpnext 37 | version: "14" 38 | push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} 39 | python_version: 3.10.13 40 | node_version: 16.20.2 41 | secrets: 42 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} 43 | DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} 44 | 45 | v15: 46 | uses: ./.github/workflows/docker-build-push.yml 47 | with: 48 | repo: erpnext 49 | version: "15" 50 | push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} 51 | python_version: 3.11.6 52 | node_version: 20.19.2 53 | secrets: 54 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} 55 | DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} 56 | 57 | update_versions: 58 | name: Update example.env and pwd.yml 59 | runs-on: ubuntu-latest 60 | if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} 61 | needs: v15 62 | 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v4 66 | 67 | - name: Setup Python 68 | uses: actions/setup-python@v5 69 | with: 70 | python-version: "3.10" 71 | 72 | - name: Get latest versions 73 | run: python3 ./.github/scripts/get_latest_tags.py --repo erpnext --version 15 74 | 75 | - name: Update 76 | run: | 77 | python3 ./.github/scripts/update_example_env.py 78 | python3 ./.github/scripts/update_pwd.py 79 | 80 | - name: Push 81 | run: | 82 | git config --global user.name github-actions 83 | git config --global user.email github-actions@github.com 84 | git add example.env pwd.yml 85 | if [ -z "$(git status --porcelain)" ]; then 86 | echo "versions did not change, exiting." 87 | exit 0 88 | else 89 | echo "version changed, pushing changes..." 90 | git commit -m "chore: Update example.env" 91 | git pull --rebase 92 | git push origin main 93 | fi 94 | 95 | release_helm: 96 | name: Release Helm 97 | runs-on: ubuntu-latest 98 | if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} 99 | needs: v15 100 | 101 | steps: 102 | - name: Setup deploy key 103 | uses: webfactory/ssh-agent@v0.9.1 104 | with: 105 | ssh-private-key: ${{ secrets.HELM_DEPLOY_KEY }} 106 | 107 | - name: Setup Git Credentials 108 | run: | 109 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 110 | git config --global user.name "github-actions[bot]" 111 | 112 | - name: Release 113 | run: | 114 | git clone git@github.com:frappe/helm.git && cd helm 115 | pip install -r release_wizard/requirements.txt 116 | ./release_wizard/wizard 15 patch --remote origin --ci 117 | -------------------------------------------------------------------------------- /.github/workflows/docker-build-push.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | repo: 7 | required: true 8 | type: string 9 | description: "'erpnext' or 'frappe'" 10 | version: 11 | required: true 12 | type: string 13 | description: "Major version, git tags should match 'v{version}.*'; or 'develop'" 14 | push: 15 | required: true 16 | type: boolean 17 | python_version: 18 | required: true 19 | type: string 20 | description: Python Version 21 | node_version: 22 | required: true 23 | type: string 24 | description: NodeJS Version 25 | secrets: 26 | DOCKERHUB_USERNAME: 27 | required: true 28 | DOCKERHUB_TOKEN: 29 | required: true 30 | 31 | jobs: 32 | build: 33 | name: Build 34 | runs-on: ubuntu-latest 35 | services: 36 | registry: 37 | image: docker.io/registry:2 38 | ports: 39 | - 5000:5000 40 | strategy: 41 | matrix: 42 | arch: [amd64, arm64] 43 | 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | 48 | - name: Setup QEMU 49 | uses: docker/setup-qemu-action@v3 50 | with: 51 | image: tonistiigi/binfmt:latest 52 | platforms: all 53 | 54 | - name: Setup Buildx 55 | uses: docker/setup-buildx-action@v3 56 | with: 57 | driver-opts: network=host 58 | platforms: linux/${{ matrix.arch }} 59 | 60 | - name: Get latest versions 61 | run: python3 ./.github/scripts/get_latest_tags.py --repo ${{ inputs.repo }} --version ${{ inputs.version }} 62 | 63 | - name: Set build args 64 | run: | 65 | echo "PYTHON_VERSION=${{ inputs.python_version }}" >> "$GITHUB_ENV" 66 | echo "NODE_VERSION=${{ inputs.node_version }}" >> "$GITHUB_ENV" 67 | 68 | - name: Build 69 | uses: docker/bake-action@v6.8.0 70 | with: 71 | source: . 72 | push: true 73 | env: 74 | REGISTRY_USER: localhost:5000/frappe 75 | 76 | - name: Setup Python 77 | uses: actions/setup-python@v5 78 | with: 79 | python-version: "3.10" 80 | 81 | - name: Install dependencies 82 | run: | 83 | python -m venv venv 84 | venv/bin/pip install -r requirements-test.txt 85 | 86 | - name: Test 87 | run: venv/bin/pytest --color=yes 88 | 89 | - name: Login 90 | if: ${{ inputs.push }} 91 | uses: docker/login-action@v3 92 | with: 93 | username: ${{ secrets.DOCKERHUB_USERNAME }} 94 | password: ${{ secrets.DOCKERHUB_TOKEN }} 95 | 96 | - name: Push 97 | if: ${{ inputs.push }} 98 | uses: docker/bake-action@v6.8.0 99 | with: 100 | push: true 101 | set: "*.platform=linux/amd64,linux/arm64" 102 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.10.6" 22 | 23 | # For shfmt pre-commit hook 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: "^1.14" 28 | 29 | - name: Install pre-commit 30 | run: pip install -U pre-commit 31 | 32 | - name: Lint 33 | run: pre-commit run --color=always --all-files 34 | env: 35 | GO111MODULE: on 36 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-autoupdate.yml: -------------------------------------------------------------------------------- 1 | name: Autoupdate pre-commit hooks 2 | 3 | on: 4 | schedule: 5 | # Every day at 7 am 6 | - cron: 0 7 * * * 7 | 8 | jobs: 9 | pre-commit-autoupdate: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Update pre-commit hooks 16 | uses: vrslev/pre-commit-autoupdate@v1.0.0 17 | 18 | - name: Create Pull Request 19 | uses: peter-evans/create-pull-request@v7 20 | with: 21 | branch: pre-commit-autoupdate 22 | title: "chore(deps): Update pre-commit hooks" 23 | commit-message: "chore(deps): Update pre-commit hooks" 24 | body: Update pre-commit hooks 25 | labels: dependencies,development 26 | delete-branch: True 27 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | # Every day at 12:00 pm 6 | - cron: 0 0 * * * 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/stale@v9 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: This issue has been automatically marked as stale. You have a week to explain why you believe this is an error. 16 | stale-pr-message: This PR has been automatically marked as stale. You have a week to explain why you believe this is an error. 17 | stale-issue-label: no-issue-activity 18 | stale-pr-label: no-pr-activity 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | .env 3 | 4 | # mounted volume 5 | sites 6 | 7 | development/* 8 | !development/README.md 9 | !development/installer.py 10 | !development/apps-example.json 11 | !development/vscode-example/ 12 | 13 | # Pycharm 14 | .idea 15 | 16 | # VS Code 17 | .vscode/** 18 | !.vscode/extensions.json 19 | 20 | # VS Code devcontainer 21 | .devcontainer 22 | *.code-workspace 23 | 24 | # Python 25 | *.pyc 26 | __pycache__ 27 | venv 28 | 29 | # NodeJS 30 | node_modules 31 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-executables-have-shebangs 6 | - id: check-shebang-scripts-are-executable 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | 10 | - repo: https://github.com/asottile/pyupgrade 11 | rev: v3.19.1 12 | hooks: 13 | - id: pyupgrade 14 | args: [--py37-plus] 15 | 16 | - repo: https://github.com/psf/black 17 | rev: 25.1.0 18 | hooks: 19 | - id: black 20 | 21 | - repo: https://github.com/pycqa/isort 22 | rev: 6.0.1 23 | hooks: 24 | - id: isort 25 | 26 | - repo: https://github.com/pre-commit/mirrors-prettier 27 | rev: v4.0.0-alpha.8 28 | hooks: 29 | - id: prettier 30 | additional_dependencies: 31 | - prettier@3.5.2 32 | 33 | - repo: https://github.com/codespell-project/codespell 34 | rev: v2.4.1 35 | hooks: 36 | - id: codespell 37 | args: 38 | - -L 39 | - "ro" 40 | 41 | - repo: local 42 | hooks: 43 | - id: shfmt 44 | name: shfmt 45 | language: golang 46 | additional_dependencies: [mvdan.cc/sh/v3/cmd/shfmt@latest] 47 | entry: shfmt 48 | args: [-w] 49 | types: [shell] 50 | 51 | - repo: https://github.com/shellcheck-py/shellcheck-py 52 | rev: v0.10.0.1 53 | hooks: 54 | - id: shellcheck 55 | args: [-x] 56 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | external-sources=true 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": ["ms-vscode-remote.remote-containers"], 7 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 8 | "unwantedRecommendations": [] 9 | } 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socioeconomic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@frappe.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Before publishing a PR, please test builds locally. 4 | 5 | On each PR that contains changes relevant to Docker builds, images are being built and tested in our CI (GitHub Actions). 6 | 7 | > :evergreen_tree: Please be considerate when pushing commits and opening PR for multiple branches, as the process of building images uses energy and contributes to global warming. 8 | 9 | ## Lint 10 | 11 | We use `pre-commit` framework to lint the codebase before committing. 12 | First, you need to install pre-commit with pip: 13 | 14 | ```shell 15 | pip install pre-commit 16 | ``` 17 | 18 | Also you can use brew if you're on Mac: 19 | 20 | ```shell 21 | brew install pre-commit 22 | ``` 23 | 24 | To setup _pre-commit_ hook, run: 25 | 26 | ```shell 27 | pre-commit install 28 | ``` 29 | 30 | To run all the files in repository, run: 31 | 32 | ```shell 33 | pre-commit run --all-files 34 | ``` 35 | 36 | ## Build 37 | 38 | We use [Docker Buildx Bake](https://docs.docker.com/engine/reference/commandline/buildx_bake/). To build the images, run command below: 39 | 40 | ```shell 41 | FRAPPE_VERSION=... ERPNEXT_VERSION=... docker buildx bake 42 | ``` 43 | 44 | Available targets can be found in `docker-bake.hcl`. 45 | 46 | ## Test 47 | 48 | We use [pytest](https://pytest.org) for our integration tests. 49 | 50 | Install Python test requirements: 51 | 52 | ```shell 53 | python3 -m venv venv 54 | source venv/bin/activate 55 | pip install -r requirements-test.txt 56 | ``` 57 | 58 | Run pytest: 59 | 60 | ```shell 61 | pytest 62 | ``` 63 | 64 | # Documentation 65 | 66 | Place relevant markdown files in the `docs` directory and index them in README.md located at the root of repo. 67 | 68 | # Frappe and ERPNext updates 69 | 70 | Each Frappe/ERPNext release triggers new stable images builds as well as bump to helm chart. 71 | 72 | # Maintenance 73 | 74 | In case of new release of Debian. e.g. bullseye to bookworm. Change following files: 75 | 76 | - `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. 77 | - `images/bench/Dockerfile`: Change the files to use new debian release. Make sure apt-get packages and wkhtmltopdf version are also upgraded accordingly. 78 | 79 | Change following files on release of ERPNext 80 | 81 | - `.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`. 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Frappe Technologies Pvt. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![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) 2 | [![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) 3 | 4 | Everything about [Frappe](https://github.com/frappe/frappe) and [ERPNext](https://github.com/frappe/erpnext) in containers. 5 | 6 | # Getting Started 7 | 8 | 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). 9 | 10 | Once completed, chose one of the following two sections for next steps. 11 | 12 | ### Try in Play With Docker 13 | 14 | To play in an already set up sandbox, in your browser, click the button below: 15 | 16 | 17 | Try in PWD 18 | 19 | 20 | ### Try on your Dev environment 21 | 22 | First clone the repo: 23 | 24 | ```sh 25 | git clone https://github.com/frappe/frappe_docker 26 | cd frappe_docker 27 | ``` 28 | 29 | Then run: `docker compose -f pwd.yml up -d` 30 | 31 | ### To run on ARM64 architecture follow this instructions 32 | 33 | After cloning the repo run this command to build multi-architecture images specifically for ARM64. 34 | 35 | `docker buildx bake --no-cache --set "*.platform=linux/arm64"` 36 | 37 | and then 38 | 39 | - add `platform: linux/arm64` to all services in the `pwd.yml` 40 | - replace the current specified versions of erpnext image on `pwd.yml` with `:latest` 41 | 42 | Then run: `docker compose -f pwd.yml up -d` 43 | 44 | ## Final steps 45 | 46 | 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`) 47 | 48 | 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. 49 | 50 | # Documentation 51 | 52 | ### [Frequently Asked Questions](https://github.com/frappe/frappe_docker/wiki/Frequently-Asked-Questions) 53 | 54 | ### [Production](#production) 55 | 56 | - [List of containers](docs/list-of-containers.md) 57 | - [Single Compose Setup](docs/single-compose-setup.md) 58 | - [Environment Variables](docs/environment-variables.md) 59 | - [Single Server Example](docs/single-server-example.md) 60 | - [Setup Options](docs/setup-options.md) 61 | - [Site Operations](docs/site-operations.md) 62 | - [Backup and Push Cron Job](docs/backup-and-push-cronjob.md) 63 | - [Port Based Multi Tenancy](docs/port-based-multi-tenancy.md) 64 | - [Migrate from multi-image setup](docs/migrate-from-multi-image-setup.md) 65 | - [running on linux/mac](docs/setup_for_linux_mac.md) 66 | - [TLS for local deployment](docs/tls-for-local-deployment.md) 67 | 68 | ### [Custom Images](#custom-images) 69 | 70 | - [Custom Apps](docs/custom-apps.md) 71 | - [Custom Apps with podman](docs/custom-apps-podman.md) 72 | - [Build Version 10 Images](docs/build-version-10-images.md) 73 | 74 | ### [Development](#development) 75 | 76 | - [Development using containers](docs/development.md) 77 | - [Bench Console and VSCode Debugger](docs/bench-console-and-vscode-debugger.md) 78 | - [Connect to localhost services](docs/connect-to-localhost-services-from-containers-for-local-app-development.md) 79 | 80 | ### [Troubleshoot](docs/troubleshoot.md) 81 | 82 | # Contributing 83 | 84 | If you want to contribute to this repo refer to [CONTRIBUTING.md](CONTRIBUTING.md) 85 | 86 | This repository is only for container related stuff. You also might want to contribute to: 87 | 88 | - [Frappe framework](https://github.com/frappe/frappe#contributing), 89 | - [ERPNext](https://github.com/frappe/erpnext#contributing), 90 | - [Frappe Bench](https://github.com/frappe/bench). 91 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | x-customizable-image: &customizable_image 2 | # By default the image used only contains the `frappe` and `erpnext` apps. 3 | # See https://github.com/frappe/frappe_docker/blob/main/docs/custom-apps.md 4 | # about using custom images. 5 | image: ${CUSTOM_IMAGE:-frappe/erpnext}:${CUSTOM_TAG:-$ERPNEXT_VERSION} 6 | pull_policy: ${PULL_POLICY:-always} 7 | restart: ${RESTART_POLICY:-unless-stopped} 8 | 9 | x-depends-on-configurator: &depends_on_configurator 10 | depends_on: 11 | configurator: 12 | condition: service_completed_successfully 13 | 14 | x-backend-defaults: &backend_defaults 15 | <<: [*depends_on_configurator, *customizable_image] 16 | volumes: 17 | - sites:/home/frappe/frappe-bench/sites 18 | 19 | services: 20 | configurator: 21 | <<: *backend_defaults 22 | platform: linux/amd64 23 | entrypoint: 24 | - bash 25 | - -c 26 | # add redis_socketio for backward compatibility 27 | command: 28 | - > 29 | ls -1 apps > sites/apps.txt; 30 | bench set-config -g db_host $$DB_HOST; 31 | bench set-config -gp db_port $$DB_PORT; 32 | bench set-config -g redis_cache "redis://$$REDIS_CACHE"; 33 | bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; 34 | bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; 35 | bench set-config -gp socketio_port $$SOCKETIO_PORT; 36 | environment: 37 | DB_HOST: ${DB_HOST:-} 38 | DB_PORT: ${DB_PORT:-} 39 | REDIS_CACHE: ${REDIS_CACHE:-} 40 | REDIS_QUEUE: ${REDIS_QUEUE:-} 41 | SOCKETIO_PORT: 9000 42 | depends_on: {} 43 | restart: on-failure 44 | 45 | backend: 46 | <<: *backend_defaults 47 | platform: linux/amd64 48 | 49 | frontend: 50 | <<: *customizable_image 51 | platform: linux/amd64 52 | command: 53 | - nginx-entrypoint.sh 54 | environment: 55 | BACKEND: backend:8000 56 | SOCKETIO: websocket:9000 57 | FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host} 58 | UPSTREAM_REAL_IP_ADDRESS: ${UPSTREAM_REAL_IP_ADDRESS:-127.0.0.1} 59 | UPSTREAM_REAL_IP_HEADER: ${UPSTREAM_REAL_IP_HEADER:-X-Forwarded-For} 60 | UPSTREAM_REAL_IP_RECURSIVE: ${UPSTREAM_REAL_IP_RECURSIVE:-off} 61 | PROXY_READ_TIMEOUT: ${PROXY_READ_TIMEOUT:-120} 62 | CLIENT_MAX_BODY_SIZE: ${CLIENT_MAX_BODY_SIZE:-50m} 63 | volumes: 64 | - sites:/home/frappe/frappe-bench/sites 65 | depends_on: 66 | - backend 67 | - websocket 68 | 69 | websocket: 70 | <<: [*depends_on_configurator, *customizable_image] 71 | platform: linux/amd64 72 | command: 73 | - node 74 | - /home/frappe/frappe-bench/apps/frappe/socketio.js 75 | volumes: 76 | - sites:/home/frappe/frappe-bench/sites 77 | 78 | queue-short: 79 | <<: *backend_defaults 80 | platform: linux/amd64 81 | command: bench worker --queue short,default 82 | 83 | queue-long: 84 | <<: *backend_defaults 85 | platform: linux/amd64 86 | command: bench worker --queue long,default,short 87 | 88 | scheduler: 89 | <<: *backend_defaults 90 | platform: linux/amd64 91 | command: bench schedule 92 | 93 | # ERPNext requires local assets access (Frappe does not) 94 | volumes: 95 | sites: 96 | -------------------------------------------------------------------------------- /devcontainer-example/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Frappe Bench", 3 | "forwardPorts": [8000, 9000, 6787], 4 | "remoteUser": "frappe", 5 | "customizations": { 6 | "vscode": { 7 | "extensions": [ 8 | "ms-python.python", 9 | "ms-vscode.live-server", 10 | "grapecity.gc-excelviewer", 11 | "mtxr.sqltools", 12 | "visualstudioexptteam.vscodeintellicode" 13 | ], 14 | "settings": { 15 | "terminal.integrated.profiles.linux": { 16 | "frappe bash": { 17 | "path": "/bin/bash" 18 | } 19 | }, 20 | "terminal.integrated.defaultProfile.linux": "frappe bash", 21 | "debug.node.autoAttach": "disabled" 22 | } 23 | } 24 | }, 25 | "dockerComposeFile": "./docker-compose.yml", 26 | "service": "frappe", 27 | "workspaceFolder": "/workspace/development", 28 | "shutdownAction": "stopCompose", 29 | "mounts": [ 30 | "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/frappe/.ssh,type=bind,consistency=cached" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /devcontainer-example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | mariadb: 4 | image: docker.io/mariadb:10.6 5 | platform: linux/amd64 6 | command: 7 | - --character-set-server=utf8mb4 8 | - --collation-server=utf8mb4_unicode_ci 9 | - --skip-character-set-client-handshake 10 | - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 11 | environment: 12 | MYSQL_ROOT_PASSWORD: 123 13 | volumes: 14 | - mariadb-data:/var/lib/mysql 15 | 16 | # Enable PostgreSQL only if you use it, see development/README.md for more information. 17 | # postgresql: 18 | # image: postgres:11.8 19 | # environment: 20 | # POSTGRES_PASSWORD: 123 21 | # volumes: 22 | # - postgresql-data:/var/lib/postgresql/data 23 | 24 | # Enable Mailpit if you need to test outgoing mail services 25 | # See https://mailpit.axllent.org/ 26 | # mailpit: 27 | # image: axllent/mailpit 28 | # volumes: 29 | # - mailpit-data:/data 30 | # ports: 31 | # - 8025:8025 32 | # - 1025:1025 33 | # environment: 34 | # MP_MAX_MESSAGES: 5000 35 | # MP_DATA_FILE: /data/mailpit.db 36 | # MP_SMTP_AUTH_ACCEPT_ANY: 1 37 | # MP_SMTP_AUTH_ALLOW_INSECURE: 1 38 | 39 | redis-cache: 40 | image: docker.io/redis:alpine 41 | platform: linux/amd64 42 | 43 | redis-queue: 44 | image: docker.io/redis:alpine 45 | platform: linux/amd64 46 | 47 | frappe: 48 | image: docker.io/frappe/bench:latest 49 | platform: linux/amd64 50 | command: sleep infinity 51 | environment: 52 | - SHELL=/bin/bash 53 | volumes: 54 | - ..:/workspace:cached 55 | # Enable if you require git cloning 56 | # - ${HOME}/.ssh:/home/frappe/.ssh 57 | working_dir: /workspace/development 58 | ports: 59 | - 8000-8005:8000-8005 60 | - 9000-9005:9000-9005 61 | # enable the below service if you need Cypress UI Tests to be executed 62 | # Before enabling ensure install_x11_deps.sh has been executed and display variable is exported. 63 | # Run install_x11_deps.sh again if DISPLAY is not set 64 | # ui-tester: 65 | # # pass custom command to start Cypress otherwise it will use the entrypoint 66 | # # specified in the Cypress Docker image. 67 | # # also pass "--project " so that when Cypress opens 68 | # # it can find file "cypress.json" and show integration specs 69 | # # https://on.cypress.io/command-line#cypress-open 70 | # entrypoint: 'sleep infinity' 71 | # image: "docker.io/cypress/included:latest" 72 | # environment: 73 | # - SHELL=/bin/bash 74 | # # get the IP address of the host machine and allow X11 to accept 75 | # # incoming connections from that IP address 76 | # # IP=$(ipconfig getifaddr en0) or mac or \ 77 | # # IP=$($(hostname -I | awk '{print $1}') ) for Ubuntu 78 | # # /usr/X11/bin/xhost + $IP 79 | # # then pass the environment variable DISPLAY to show Cypress GUI on the host system 80 | # # DISPLAY=$IP:0 81 | # - DISPLAY 82 | # volumes: 83 | # # for Cypress to communicate with the X11 server pass this socket file 84 | # # in addition to any other mapped volumes 85 | # - /tmp/.X11-unix:/tmp/.X11-unix 86 | # - ..:/workspace:z,cached 87 | # network_mode: "host" 88 | volumes: 89 | mariadb-data: 90 | #postgresql-data: 91 | #mailpit-data: 92 | -------------------------------------------------------------------------------- /development/apps-example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://github.com/frappe/erpnext.git", 4 | "branch": "version-15" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /development/installer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os 4 | import subprocess 5 | 6 | 7 | def cprint(*args, level: int = 1): 8 | """ 9 | logs colorful messages 10 | level = 1 : RED 11 | level = 2 : GREEN 12 | level = 3 : YELLOW 13 | 14 | default level = 1 15 | """ 16 | CRED = "\033[31m" 17 | CGRN = "\33[92m" 18 | CYLW = "\33[93m" 19 | reset = "\033[0m" 20 | message = " ".join(map(str, args)) 21 | if level == 1: 22 | print(CRED, message, reset) # noqa: T001, T201 23 | if level == 2: 24 | print(CGRN, message, reset) # noqa: T001, T201 25 | if level == 3: 26 | print(CYLW, message, reset) # noqa: T001, T201 27 | 28 | 29 | def main(): 30 | parser = get_args_parser() 31 | args = parser.parse_args() 32 | init_bench_if_not_exist(args) 33 | create_site_in_bench(args) 34 | 35 | 36 | def get_args_parser(): 37 | parser = argparse.ArgumentParser() 38 | parser.add_argument( 39 | "-j", 40 | "--apps-json", 41 | action="store", 42 | type=str, 43 | help="Path to apps.json, default: apps-example.json", 44 | default="apps-example.json", 45 | ) # noqa: E501 46 | parser.add_argument( 47 | "-b", 48 | "--bench-name", 49 | action="store", 50 | type=str, 51 | help="Bench directory name, default: frappe-bench", 52 | default="frappe-bench", 53 | ) # noqa: E501 54 | parser.add_argument( 55 | "-s", 56 | "--site-name", 57 | action="store", 58 | type=str, 59 | help="Site name, should end with .localhost, default: development.localhost", # noqa: E501 60 | default="development.localhost", 61 | ) 62 | parser.add_argument( 63 | "-r", 64 | "--frappe-repo", 65 | action="store", 66 | type=str, 67 | help="frappe repo to use, default: https://github.com/frappe/frappe", # noqa: E501 68 | default="https://github.com/frappe/frappe", 69 | ) 70 | parser.add_argument( 71 | "-t", 72 | "--frappe-branch", 73 | action="store", 74 | type=str, 75 | help="frappe repo to use, default: version-15", # noqa: E501 76 | default="version-15", 77 | ) 78 | parser.add_argument( 79 | "-p", 80 | "--py-version", 81 | action="store", 82 | type=str, 83 | help="python version, default: Not Set", # noqa: E501 84 | default=None, 85 | ) 86 | parser.add_argument( 87 | "-n", 88 | "--node-version", 89 | action="store", 90 | type=str, 91 | help="node version, default: Not Set", # noqa: E501 92 | default=None, 93 | ) 94 | parser.add_argument( 95 | "-v", 96 | "--verbose", 97 | action="store_true", 98 | help="verbose output", # noqa: E501 99 | ) 100 | parser.add_argument( 101 | "-a", 102 | "--admin-password", 103 | action="store", 104 | type=str, 105 | help="admin password for site, default: admin", # noqa: E501 106 | default="admin", 107 | ) 108 | parser.add_argument( 109 | "-d", 110 | "--db-type", 111 | action="store", 112 | type=str, 113 | help="Database type to use (e.g., mariadb or postgres)", 114 | default="mariadb", # Set your default database type here 115 | ) 116 | return parser 117 | 118 | 119 | def init_bench_if_not_exist(args): 120 | if os.path.exists(args.bench_name): 121 | cprint("Bench already exists. Only site will be created", level=3) 122 | return 123 | try: 124 | env = os.environ.copy() 125 | if args.py_version: 126 | env["PYENV_VERSION"] = args.py_version 127 | init_command = "" 128 | if args.node_version: 129 | init_command = f"nvm use {args.node_version};" 130 | if args.py_version: 131 | init_command += f"PYENV_VERSION={args.py_version} " 132 | init_command += "bench init " 133 | init_command += "--skip-redis-config-generation " 134 | init_command += "--verbose " if args.verbose else " " 135 | init_command += f"--frappe-path={args.frappe_repo} " 136 | init_command += f"--frappe-branch={args.frappe_branch} " 137 | init_command += f"--apps_path={args.apps_json} " 138 | init_command += args.bench_name 139 | command = [ 140 | "/bin/bash", 141 | "-i", 142 | "-c", 143 | init_command, 144 | ] 145 | subprocess.call(command, env=env, cwd=os.getcwd()) 146 | cprint("Configuring Bench ...", level=2) 147 | cprint("Set db_host", level=3) 148 | if args.db_type: 149 | cprint(f"Setting db_type to {args.db_type}", level=3) 150 | subprocess.call( 151 | ["bench", "set-config", "-g", "db_type", args.db_type], 152 | cwd=os.path.join(os.getcwd(), args.bench_name), 153 | ) 154 | 155 | cprint("Set redis_cache to redis://redis-cache:6379", level=3) 156 | subprocess.call( 157 | [ 158 | "bench", 159 | "set-config", 160 | "-g", 161 | "redis_cache", 162 | "redis://redis-cache:6379", 163 | ], 164 | cwd=os.getcwd() + "/" + args.bench_name, 165 | ) 166 | cprint("Set redis_queue to redis://redis-queue:6379", level=3) 167 | subprocess.call( 168 | [ 169 | "bench", 170 | "set-config", 171 | "-g", 172 | "redis_queue", 173 | "redis://redis-queue:6379", 174 | ], 175 | cwd=os.getcwd() + "/" + args.bench_name, 176 | ) 177 | cprint( 178 | "Set redis_socketio to redis://redis-queue:6379 for backward compatibility", # noqa: E501 179 | level=3, 180 | ) 181 | subprocess.call( 182 | [ 183 | "bench", 184 | "set-config", 185 | "-g", 186 | "redis_socketio", 187 | "redis://redis-queue:6379", 188 | ], 189 | cwd=os.getcwd() + "/" + args.bench_name, 190 | ) 191 | cprint("Set developer_mode", level=3) 192 | subprocess.call( 193 | ["bench", "set-config", "-gp", "developer_mode", "1"], 194 | cwd=os.getcwd() + "/" + args.bench_name, 195 | ) 196 | except subprocess.CalledProcessError as e: 197 | cprint(e.output, level=1) 198 | 199 | 200 | def create_site_in_bench(args): 201 | if "mariadb" == args.db_type: 202 | cprint("Set db_host", level=3) 203 | subprocess.call( 204 | ["bench", "set-config", "-g", "db_host", "mariadb"], 205 | cwd=os.getcwd() + "/" + args.bench_name, 206 | ) 207 | new_site_cmd = [ 208 | "bench", 209 | "new-site", 210 | f"--db-root-username=root", 211 | f"--db-host=mariadb", # Should match the compose service name 212 | f"--db-type={args.db_type}", # Add the selected database type 213 | f"--mariadb-user-host-login-scope=%", 214 | f"--db-root-password=123", # Replace with your MariaDB password 215 | f"--admin-password={args.admin_password}", 216 | ] 217 | else: 218 | cprint("Set db_host", level=3) 219 | subprocess.call( 220 | ["bench", "set-config", "-g", "db_host", "postgresql"], 221 | cwd=os.getcwd() + "/" + args.bench_name, 222 | ) 223 | new_site_cmd = [ 224 | "bench", 225 | "new-site", 226 | f"--db-root-username=root", 227 | f"--db-host=postgresql", # Should match the compose service name 228 | f"--db-type={args.db_type}", # Add the selected database type 229 | f"--db-root-password=123", # Replace with your PostgreSQL password 230 | f"--admin-password={args.admin_password}", 231 | ] 232 | apps = os.listdir(f"{os.getcwd()}/{args.bench_name}/apps") 233 | apps.remove("frappe") 234 | for app in apps: 235 | new_site_cmd.append(f"--install-app={app}") 236 | new_site_cmd.append(args.site_name) 237 | cprint(f"Creating Site {args.site_name} ...", level=2) 238 | subprocess.call( 239 | new_site_cmd, 240 | cwd=os.getcwd() + "/" + args.bench_name, 241 | ) 242 | 243 | 244 | if __name__ == "__main__": 245 | main() 246 | -------------------------------------------------------------------------------- /development/vscode-example/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Bench Web", 9 | "type": "debugpy", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", 12 | "args": [ 13 | "frappe", 14 | "serve", 15 | "--port", 16 | "8000", 17 | "--noreload", 18 | "--nothreading" 19 | ], 20 | "cwd": "${workspaceFolder}/frappe-bench/sites", 21 | "env": { 22 | "DEV_SERVER": "1" 23 | } 24 | }, 25 | { 26 | "name": "Bench Short Worker", 27 | "type": "debugpy", 28 | "request": "launch", 29 | "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", 30 | "args": ["frappe", "worker", "--queue", "short"], 31 | "cwd": "${workspaceFolder}/frappe-bench/sites", 32 | "env": { 33 | "DEV_SERVER": "1" 34 | } 35 | }, 36 | { 37 | "name": "Bench Long Worker", 38 | "type": "debugpy", 39 | "request": "launch", 40 | "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", 41 | "args": ["frappe", "worker", "--queue", "long"], 42 | "cwd": "${workspaceFolder}/frappe-bench/sites", 43 | "env": { 44 | "DEV_SERVER": "1" 45 | } 46 | }, 47 | { 48 | "name": "Honcho SocketIO Watch Schedule Worker", 49 | "type": "debugpy", 50 | "request": "launch", 51 | "python": "/home/frappe/.pyenv/shims/python", 52 | "program": "/home/frappe/.local/bin/honcho", 53 | "cwd": "${workspaceFolder}/frappe-bench", 54 | "console": "internalConsole", 55 | "args": ["start", "socketio", "watch", "schedule", "worker"], 56 | "postDebugTask": "Clean Honcho SocketIO Watch Schedule Worker" 57 | } 58 | ], 59 | "compounds": [ 60 | { 61 | "name": "Honcho + Web debug", 62 | "configurations": ["Bench Web", "Honcho SocketIO Watch Schedule Worker"], 63 | "stopAll": true 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /development/vscode-example/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.defaultInterpreterPath": "${workspaceFolder}/frappe-bench/env/bin/python" 3 | } 4 | -------------------------------------------------------------------------------- /development/vscode-example/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Clean Honcho SocketIO Watch Schedule Worker", 8 | "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.", 9 | "type": "shell", 10 | "command": "pkill -SIGINT -f bench; pkill -SIGINT -f socketio", 11 | "isBackground": false, 12 | "presentation": { 13 | "echo": true, 14 | "reveal": "silent", 15 | "focus": false, 16 | "panel": "shared", 17 | "showReuseMessage": false, 18 | "close": true 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | # Docker Buildx Bake build definition file 2 | # Reference: https://github.com/docker/buildx/blob/master/docs/reference/buildx_bake.md 3 | 4 | variable "REGISTRY_USER" { 5 | default = "frappe" 6 | } 7 | 8 | variable PYTHON_VERSION { 9 | default = "3.11.6" 10 | } 11 | variable NODE_VERSION { 12 | default = "18.18.2" 13 | } 14 | 15 | variable "FRAPPE_VERSION" { 16 | default = "develop" 17 | } 18 | 19 | variable "ERPNEXT_VERSION" { 20 | default = "develop" 21 | } 22 | 23 | variable "FRAPPE_REPO" { 24 | default = "https://github.com/frappe/frappe" 25 | } 26 | 27 | variable "ERPNEXT_REPO" { 28 | default = "https://github.com/frappe/erpnext" 29 | } 30 | 31 | variable "BENCH_REPO" { 32 | default = "https://github.com/frappe/bench" 33 | } 34 | 35 | variable "LATEST_BENCH_RELEASE" { 36 | default = "latest" 37 | } 38 | 39 | # Bench image 40 | 41 | target "bench" { 42 | args = { 43 | GIT_REPO = "${BENCH_REPO}" 44 | } 45 | context = "images/bench" 46 | target = "bench" 47 | tags = [ 48 | "frappe/bench:${LATEST_BENCH_RELEASE}", 49 | "frappe/bench:latest", 50 | ] 51 | } 52 | 53 | target "bench-test" { 54 | inherits = ["bench"] 55 | target = "bench-test" 56 | } 57 | 58 | # Main images 59 | # Base for all other targets 60 | 61 | group "default" { 62 | targets = ["erpnext", "base", "build"] 63 | } 64 | 65 | function "tag" { 66 | params = [repo, version] 67 | result = [ 68 | # Push frappe or erpnext branch as tag 69 | "${REGISTRY_USER}/${repo}:${version}", 70 | # If `version` param is develop (development build) then use tag `latest` 71 | "${version}" == "develop" ? "${REGISTRY_USER}/${repo}:latest" : "${REGISTRY_USER}/${repo}:${version}", 72 | # Make short tag for major version if possible. For example, from v13.16.0 make v13. 73 | can(regex("(v[0-9]+)[.]", "${version}")) ? "${REGISTRY_USER}/${repo}:${regex("(v[0-9]+)[.]", "${version}")[0]}" : "", 74 | # Make short tag for major version if possible. For example, from v13.16.0 make version-13. 75 | can(regex("(v[0-9]+)[.]", "${version}")) ? "${REGISTRY_USER}/${repo}:version-${regex("([0-9]+)[.]", "${version}")[0]}" : "", 76 | ] 77 | } 78 | 79 | target "default-args" { 80 | args = { 81 | FRAPPE_PATH = "${FRAPPE_REPO}" 82 | ERPNEXT_PATH = "${ERPNEXT_REPO}" 83 | BENCH_REPO = "${BENCH_REPO}" 84 | FRAPPE_BRANCH = "${FRAPPE_VERSION}" 85 | ERPNEXT_BRANCH = "${ERPNEXT_VERSION}" 86 | PYTHON_VERSION = "${PYTHON_VERSION}" 87 | NODE_VERSION = "${NODE_VERSION}" 88 | } 89 | } 90 | 91 | target "erpnext" { 92 | inherits = ["default-args"] 93 | context = "." 94 | dockerfile = "images/production/Containerfile" 95 | target = "erpnext" 96 | tags = tag("erpnext", "${ERPNEXT_VERSION}") 97 | } 98 | 99 | target "base" { 100 | inherits = ["default-args"] 101 | context = "." 102 | dockerfile = "images/production/Containerfile" 103 | target = "base" 104 | tags = tag("base", "${FRAPPE_VERSION}") 105 | } 106 | 107 | target "build" { 108 | inherits = ["default-args"] 109 | context = "." 110 | dockerfile = "images/production/Containerfile" 111 | target = "build" 112 | tags = tag("build", "${ERPNEXT_VERSION}") 113 | } 114 | -------------------------------------------------------------------------------- /docs/backup-and-push-cronjob.md: -------------------------------------------------------------------------------- 1 | Create backup service or stack. 2 | 3 | ```yaml 4 | # backup-job.yml 5 | version: "3.7" 6 | services: 7 | backup: 8 | image: frappe/erpnext:${VERSION} 9 | entrypoint: ["bash", "-c"] 10 | command: 11 | - | 12 | bench --site all backup 13 | ## Uncomment for restic snapshots. 14 | # restic snapshots || restic init 15 | # restic backup sites 16 | ## Uncomment to keep only last n=30 snapshots. 17 | # restic forget --group-by=paths --keep-last=30 --prune 18 | environment: 19 | # Set correct environment variables for restic 20 | - RESTIC_REPOSITORY=s3:https://s3.endpoint.com/restic 21 | - AWS_ACCESS_KEY_ID=access_key 22 | - AWS_SECRET_ACCESS_KEY=secret_access_key 23 | - RESTIC_PASSWORD=restic_password 24 | volumes: 25 | - "sites:/home/frappe/frappe-bench/sites" 26 | networks: 27 | - erpnext-network 28 | 29 | networks: 30 | erpnext-network: 31 | external: true 32 | name: ${PROJECT_NAME:-erpnext}_default 33 | 34 | volumes: 35 | sites: 36 | external: true 37 | name: ${PROJECT_NAME:-erpnext}_sites 38 | ``` 39 | 40 | In case of single docker host setup, add crontab entry for backup every 6 hours. 41 | 42 | ``` 43 | 0 */6 * * * /usr/local/bin/docker-compose -f /path/to/backup-job.yml up -d > /dev/null 44 | ``` 45 | 46 | Or 47 | 48 | ``` 49 | 0 */6 * * * docker compose -p erpnext exec backend bench --site all backup --with-files > /dev/null 50 | ``` 51 | 52 | Notes: 53 | 54 | - Make sure `docker-compose` or `docker compose` is available in path during execution. 55 | - Change the cron string as per need. 56 | - Set the correct project name in place of `erpnext`. 57 | - For Docker Swarm add it as a [swarm-cronjob](https://github.com/crazy-max/swarm-cronjob) 58 | - Add it as a `CronJob` in case of Kubernetes cluster. 59 | -------------------------------------------------------------------------------- /docs/bench-console-and-vscode-debugger.md: -------------------------------------------------------------------------------- 1 | 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. 2 | 3 | ```json 4 | { 5 | "name": "Bench Console", 6 | "type": "python", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", 9 | "args": ["frappe", "--site", "development.localhost", "console"], 10 | "pythonPath": "${workspaceFolder}/frappe-bench/env/bin/python", 11 | "cwd": "${workspaceFolder}/frappe-bench/sites", 12 | "env": { 13 | "DEV_SERVER": "1" 14 | } 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/build-version-10-images.md: -------------------------------------------------------------------------------- 1 | Clone the version-10 branch of this repo 2 | 3 | ```shell 4 | git clone https://github.com/frappe/frappe_docker.git -b version-10 && cd frappe_docker 5 | ``` 6 | 7 | Build the images 8 | 9 | ```shell 10 | export DOCKER_REGISTRY_PREFIX=frappe 11 | docker build -t ${DOCKER_REGISTRY_PREFIX}/frappe-socketio:v10 -f build/frappe-socketio/Dockerfile . 12 | docker build -t ${DOCKER_REGISTRY_PREFIX}/frappe-nginx:v10 -f build/frappe-nginx/Dockerfile . 13 | docker build -t ${DOCKER_REGISTRY_PREFIX}/erpnext-nginx:v10 -f build/erpnext-nginx/Dockerfile . 14 | docker build -t ${DOCKER_REGISTRY_PREFIX}/frappe-worker:v10 -f build/frappe-worker/Dockerfile . 15 | docker build -t ${DOCKER_REGISTRY_PREFIX}/erpnext-worker:v10 -f build/erpnext-worker/Dockerfile . 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/connect-to-localhost-services-from-containers-for-local-app-development.md: -------------------------------------------------------------------------------- 1 | Add following to frappe container from the `.devcontainer/docker-compose.yml`: 2 | 3 | ```yaml 4 | ... 5 | frappe: 6 | ... 7 | extra_hosts: 8 | app1.localhost: 172.17.0.1 9 | app2.localhost: 172.17.0.1 10 | ... 11 | ``` 12 | 13 | This is makes the domain names `app1.localhost` and `app2.localhost` connect to docker host and connect to services running on `localhost`. 14 | -------------------------------------------------------------------------------- /docs/custom-apps-podman.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | - podman 4 | - podman-compose 5 | - docker-compose 6 | 7 | Podman (the POD MANager) is a tool for managing containers and images, volumes mounted into those containers, and pods made from groups of containers. It is available on the official repositories of many Linux distributions. 8 | 9 | ## Step 1 10 | 11 | - Clone this repository and change the current directory to the downloaded folder 12 | ```cmd 13 | git clone https://github.com/frappe/frappe_docker 14 | cd frappe_docker 15 | ``` 16 | 17 | ## Step 2 18 | 19 | - Create `apps.json` file with custom apps listed in it 20 | ```json 21 | [ 22 | { 23 | "url": "https://github.com/frappe/erpnext", 24 | "branch": "version-15" 25 | }, 26 | { 27 | "url": "https://github.com/frappe/hrms", 28 | "branch": "version-15" 29 | }, 30 | { 31 | "url": "https://github.com/frappe/helpdesk", 32 | "branch": "main" 33 | } 34 | ] 35 | ``` 36 | Check the syntax of the file using `jq empty apps.json` 37 | ### Generate base64 string from JSON file: 38 | `cmd export APPS_JSON_BASE64=$(base64 -w 0 apps.json)` 39 | 40 | ## Step 3 41 | 42 | - Building the custom image using podman 43 | 44 | ```ruby 45 | podman build \ 46 | --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ 47 | --build-arg=FRAPPE_BRANCH=version-15 \ 48 | --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ 49 | --tag=custom:15 \ 50 | --file=images/layered/Containerfile . 51 | ``` 52 | 53 | ### Note 54 | 55 | - Make sure to use the same tag when you export a variable on the next step 56 | 57 | ## Step 4 58 | 59 | - Using the image 60 | - Export environment variables with image name, tag and pull_policy 61 | ```ruby 62 | export CUSTOM_IMAGE=custom 63 | export CUSTOM_TAG=15 64 | export PULL_POLICY=never 65 | ``` 66 | - Configuration of parameters used when starting the containers 67 | - create `.env` file copying from example.env (Read more on setting up environment variables [here](https://github.com/frappe/frappe_docker/blob/main/docs/environment-variables.md) 68 | 69 | ## Final step 70 | 71 | - Creating a compose file 72 | - ```ruby 73 | podman compose -f compose.yaml \ 74 | -f overrides/compose.mariadb.yaml \ 75 | -f overrides/compose.redis.yaml \ 76 | -f overrides/compose.noproxy.yaml \ 77 | config > ./docker-compose.yml 78 | ``` 79 | ### NOTE 80 | - 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. 81 | - Creating pod and starting the containers 82 | - `podman-compose --in-pod=1 --project-name erpnext -f ./docker-compose.yml up -d` 83 | 84 | ## Creating sites and installing apps 85 | 86 | - You can create sites from the backend container 87 | - `podman exec -ti erpnext_backend_1 /bin/bash` 88 | - `bench new-site myerp.net --mariadb-root-password 123456 --admin-password 123123` 89 | - `bench --site myerp.net install-app erpnext` 90 | 91 | ## Autostart pod 92 | 93 | - Systemd is the best option on autostart pods when the system boots. Create a unit file in either `/etc/systemd/system` [for root user] or `~/.config/systemd/user` [for non-root user] 94 | 95 | ```ruby 96 | [Unit] 97 | Description=Podman system daemon service 98 | After=network-online.target 99 | 100 | [Service] 101 | #User= 102 | #Group= 103 | Type=oneshot 104 | ExecStart=podman pod start POD_NAME 105 | 106 | 107 | [Install] 108 | WantedBy=default.target 109 | 110 | ``` 111 | 112 | **Note:** Replace POD_NAME with a created pod name while creating a pod. This is a basic systemd unit file to autostart pod, but multiple options can be used, refer to the man page for [systemd](https://man7.org/linux/man-pages/man1/init.1.html). For better management of containers, [Quadlet](https://docs.podman.io/en/v4.4/markdown/podman-systemd.unit.5.html) is the best option for ease of updating and tracing issues on each container. 113 | 114 | ## Troubleshoot 115 | 116 | - If there is a network issue while building the image, you need to remove caches and restart again 117 | 118 | - `podman system reset` 119 | - `sudo rm -rf ~/.local/share/containers/ /var/lib/container ~/.caches/containers` 120 | 121 | - Database issue when restarting the container 122 | - Execute the following commands from **backend** container 123 | - `mysql -uroot -padmin -hdb` (Note: put your db password in place of _admin_). 124 | - `SELECT User, Host FROM mysql.user;` 125 | - Change the IP address to %, e.g. `RENAME USER '_5e5899d8398b5f7b'@'172.18.0.7' TO '_5e5899d8398b5f7b'@'%'` 126 | -------------------------------------------------------------------------------- /docs/custom-apps.md: -------------------------------------------------------------------------------- 1 | ### Load custom apps through apps.json file 2 | 3 | Base64 encoded string of `apps.json` file needs to be passed in as build arg environment variable. 4 | 5 | Create the following `apps.json` file: 6 | 7 | ```json 8 | [ 9 | { 10 | "url": "https://github.com/frappe/erpnext", 11 | "branch": "version-15" 12 | }, 13 | { 14 | "url": "https://github.com/frappe/payments", 15 | "branch": "version-15" 16 | }, 17 | { 18 | "url": "https://{{ PAT }}@git.example.com/project/repository.git", 19 | "branch": "main" 20 | } 21 | ] 22 | ``` 23 | 24 | Note: 25 | 26 | - The `url` needs to be http(s) git url with personal access tokens without username eg:- `http://{{PAT}}@github.com/project/repository.git` in case of private repo. 27 | - Add dependencies manually in `apps.json` e.g. add `erpnext` if you are installing `hrms`. 28 | - Use fork repo or branch for ERPNext in case you need to use your fork or test a PR. 29 | 30 | Generate base64 string from json file: 31 | 32 | ```shell 33 | export APPS_JSON_BASE64=$(base64 -w 0 /path/to/apps.json) 34 | ``` 35 | 36 | Test the Previous Step: Decode the Base64-encoded Environment Variable 37 | 38 | To verify the previous step, decode the `APPS_JSON_BASE64` environment variable (which is Base64-encoded) into a JSON file. Follow the steps below: 39 | 40 | 1. Use the following command to decode and save the output into a JSON file named apps-test-output.json: 41 | 42 | ```shell 43 | echo -n ${APPS_JSON_BASE64} | base64 -d > apps-test-output.json 44 | ``` 45 | 46 | 2. Open the apps-test-output.json file to review the JSON output and ensure that the content is correct. 47 | 48 | ### Clone frappe_docker and switch directory 49 | 50 | ```shell 51 | git clone https://github.com/frappe/frappe_docker 52 | cd frappe_docker 53 | ``` 54 | 55 | ### Configure build 56 | 57 | Common build args. 58 | 59 | - `FRAPPE_PATH`, customize the source repo for frappe framework. Defaults to `https://github.com/frappe/frappe` 60 | - `FRAPPE_BRANCH`, customize the source repo branch for frappe framework. Defaults to `version-15`. 61 | - `APPS_JSON_BASE64`, correct base64 encoded JSON string generated from `apps.json` file. 62 | 63 | Notes 64 | 65 | - Use `buildah` or `docker` as per your setup. 66 | - Make sure `APPS_JSON_BASE64` variable has correct base64 encoded JSON string. It is consumed as build arg, base64 encoding ensures it to be friendly with environment variables. Use `jq empty apps.json` to validate `apps.json` file. 67 | - Make sure the `--tag` is valid image name that will be pushed to registry. See section [below](#use-images) for remarks about its use. 68 | - `.git` directories for all apps are removed from the image. 69 | 70 | ### Quick build image 71 | 72 | This method uses pre-built `frappe/base:${FRAPPE_BRANCH}` and `frappe/build:${FRAPPE_BRANCH}` image layers which come with required Python and NodeJS runtime. It speeds up the build time. 73 | 74 | It uses `images/layered/Containerfile`. 75 | 76 | ```shell 77 | docker build \ 78 | --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ 79 | --build-arg=FRAPPE_BRANCH=version-15 \ 80 | --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ 81 | --tag=ghcr.io/user/repo/custom:1.0.0 \ 82 | --file=images/layered/Containerfile . 83 | ``` 84 | 85 | ### Custom build image 86 | 87 | This method builds the base and build layer every time, it allows to customize Python and NodeJS runtime versions. It takes more time to build. 88 | 89 | It uses `images/custom/Containerfile`. 90 | 91 | ```shell 92 | docker build \ 93 | --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ 94 | --build-arg=FRAPPE_BRANCH=version-15 \ 95 | --build-arg=PYTHON_VERSION=3.11.9 \ 96 | --build-arg=NODE_VERSION=20.19.2 \ 97 | --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ 98 | --tag=ghcr.io/user/repo/custom:1.0.0 \ 99 | --file=images/custom/Containerfile . 100 | ``` 101 | 102 | Custom build args, 103 | 104 | - `PYTHON_VERSION`, use the specified python version for base image. Default is `3.11.6`. 105 | - `NODE_VERSION`, use the specified nodejs version, Default `20.19.2`. 106 | - `DEBIAN_BASE` use the base Debian version, defaults to `bookworm`. 107 | - `WKHTMLTOPDF_VERSION`, use the specified qt patched `wkhtmltopdf` version. Default is `0.12.6.1-3`. 108 | - `WKHTMLTOPDF_DISTRO`, use the specified distro for debian package. Default is `bookworm`. 109 | 110 | ### Push image to use in yaml files 111 | 112 | Login to `docker` or `buildah` 113 | 114 | ```shell 115 | docker login 116 | ``` 117 | 118 | Push image 119 | 120 | ```shell 121 | docker push ghcr.io/user/repo/custom:1.0.0 122 | ``` 123 | 124 | ### Use Images 125 | 126 | In the [compose.yaml](../compose.yaml), you can set the image name and tag through environment variables, making it easier to customize. 127 | 128 | ```yaml 129 | x-customizable-image: &customizable_image 130 | image: ${CUSTOM_IMAGE:-frappe/erpnext}:${CUSTOM_TAG:-${ERPNEXT_VERSION:?No ERPNext version or tag set}} 131 | pull_policy: ${PULL_POLICY:-always} 132 | ``` 133 | 134 | The environment variables can be set in the shell or in the .env file as [setup-options.md](setup-options.md) describes. 135 | 136 | - `CUSTOM_IMAGE`: The name of your custom image. Defaults to `frappe/erpnext` if not set. 137 | - `CUSTOM_TAG`: The tag for your custom image. Must be set if `CUSTOM_IMAGE` is used. Defaults to the value of `ERPNEXT_VERSION` if not set. 138 | - `PULL_POLICY`: The Docker pull policy. Defaults to `always`. Recommended set to `never` for local images, so prevent `docker` from trying to download the image when it has been built locally. 139 | - `HTTP_PUBLISH_PORT`: The port to publish through no SSL channel. Default depending on deployment, it may be `80` if SSL activated or `8080` if not. 140 | - `HTTPS_PUBLISH_PORT`: The secure port to publish using SSL. Default is `443`. 141 | 142 | Make sure the image name is correct before pushing to the registry. After the images are pushed, you can pull them to servers to be deployed. If the registry is private, additional auth is needed. 143 | 144 | #### Example 145 | 146 | If you built an image with the tag `ghcr.io/user/repo/custom:1.0.0`, you would set the environment variables as follows: 147 | 148 | ```bash 149 | export CUSTOM_IMAGE='ghcr.io/user/repo/custom' 150 | export CUSTOM_TAG='1.0.0' 151 | docker compose -f compose.yaml \ 152 | -f overrides/compose.mariadb.yaml \ 153 | -f overrides/compose.redis.yaml \ 154 | -f overrides/compose.https.yaml \ 155 | config > ~/gitops/docker-compose.yaml 156 | ``` 157 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Prerequisites 4 | 5 | In order to start developing you need to satisfy the following prerequisites: 6 | 7 | - Docker 8 | - docker-compose 9 | - user added to docker group 10 | 11 | It is recommended you allocate at least 4GB of RAM to docker: 12 | 13 | - [Instructions for Windows](https://docs.docker.com/docker-for-windows/#resources) 14 | - [Instructions for macOS](https://docs.docker.com/desktop/settings/mac/#advanced) 15 | 16 | Here is a screenshot showing the relevant setting in the Help Manual 17 | ![image](images/Docker%20Manual%20Screenshot%20-%20Resources%20section.png) 18 | Here is a screenshot showing the settings in Docker Desktop on Mac 19 | ![images](images/Docker%20Desktop%20Screenshot%20-%20Resources%20section.png) 20 | 21 | ## Bootstrap Containers for development 22 | 23 | Clone and change directory to frappe_docker directory 24 | 25 | ```shell 26 | git clone https://github.com/frappe/frappe_docker.git 27 | cd frappe_docker 28 | ``` 29 | 30 | Copy example devcontainer config from `devcontainer-example` to `.devcontainer` 31 | 32 | ```shell 33 | cp -R devcontainer-example .devcontainer 34 | ``` 35 | 36 | Copy example vscode config for devcontainer from `development/vscode-example` to `development/.vscode`. This will setup basic configuration for debugging. 37 | 38 | ```shell 39 | cp -R development/vscode-example development/.vscode 40 | ``` 41 | 42 | ## Use VSCode Remote Containers extension 43 | 44 | 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). 45 | 46 | Before opening the folder in container, determine the database that you want to use. The default is MariaDB. 47 | 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. 48 | 49 | VSCode should automatically inquire you to install the required extensions, that can also be installed manually as follows: 50 | 51 | - Install Dev Containers for VSCode 52 | - through command line `code --install-extension ms-vscode-remote.remote-containers` 53 | - clicking on the Install button in the Vistual Studio Marketplace: [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) 54 | - View: Extensions command in VSCode (Windows: Ctrl+Shift+X; macOS: Cmd+Shift+X) then search for extension `ms-vscode-remote.remote-containers` 55 | 56 | After the extensions are installed, you can: 57 | 58 | - Open frappe_docker folder in VS Code. 59 | - `code .` 60 | - 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. 61 | 62 | Notes: 63 | 64 | - 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. 65 | - Node v14 and v10 are installed. Check with `nvm ls`. Node v14 is used by default. 66 | 67 | ### Setup first bench 68 | 69 | > 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. 70 | 71 | Run the following commands in the terminal inside the container. You might need to create a new terminal in VSCode. 72 | 73 | NOTE: Prior to doing the following, make sure the user is **frappe**. 74 | 75 | ```shell 76 | bench init --skip-redis-config-generation frappe-bench 77 | cd frappe-bench 78 | ``` 79 | 80 | To setup frappe framework version 14 bench set `PYENV_VERSION` environment variable to `3.10.5` (default) and use NodeJS version 16 (default), 81 | 82 | ```shell 83 | # Use default environments 84 | bench init --skip-redis-config-generation --frappe-branch version-14 frappe-bench 85 | # Or set environment versions explicitly 86 | nvm use v16 87 | PYENV_VERSION=3.10.13 bench init --skip-redis-config-generation --frappe-branch version-14 frappe-bench 88 | # Switch directory 89 | cd frappe-bench 90 | ``` 91 | 92 | To setup frappe framework version 13 bench set `PYENV_VERSION` environment variable to `3.9.17` and use NodeJS version 14, 93 | 94 | ```shell 95 | nvm use v14 96 | PYENV_VERSION=3.9.17 bench init --skip-redis-config-generation --frappe-branch version-13 frappe-bench 97 | cd frappe-bench 98 | ``` 99 | 100 | ### Setup hosts 101 | 102 | We need to tell bench to use the right containers instead of localhost. Run the following commands inside the container: 103 | 104 | ```shell 105 | bench set-config -g db_host mariadb 106 | bench set-config -g redis_cache redis://redis-cache:6379 107 | bench set-config -g redis_queue redis://redis-queue:6379 108 | bench set-config -g redis_socketio redis://redis-queue:6379 109 | ``` 110 | 111 | For any reason the above commands fail, set the values in `common_site_config.json` manually. 112 | 113 | ```json 114 | { 115 | "db_host": "mariadb", 116 | "redis_cache": "redis://redis-cache:6379", 117 | "redis_queue": "redis://redis-queue:6379", 118 | "redis_socketio": "redis://redis-queue:6379" 119 | } 120 | ``` 121 | 122 | ### Edit Honcho's Procfile 123 | 124 | 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 125 | 126 | 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. 127 | 128 | 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. 129 | 130 | Open the Procfile file and remove the three lines containing the configuration from Redis, either by editing manually the file: 131 | 132 | ```shell 133 | code Procfile 134 | ``` 135 | 136 | Or running the following command: 137 | 138 | ```shell 139 | sed -i '/redis/d' ./Procfile 140 | ``` 141 | 142 | ### Create a new site with bench 143 | 144 | You can create a new site with the following command: 145 | 146 | ```shell 147 | bench new-site --mariadb-user-host-login-scope=% sitename 148 | ``` 149 | 150 | sitename MUST end with .localhost for trying deployments locally. 151 | 152 | for example: 153 | 154 | ```shell 155 | bench new-site --mariadb-user-host-login-scope=% development.localhost 156 | ``` 157 | 158 | The same command can be run non-interactively as well: 159 | 160 | ```shell 161 | bench new-site --db-root-password 123 --admin-password admin --mariadb-user-host-login-scope=% development.localhost 162 | ``` 163 | 164 | The command will ask the MariaDB root password. The default root password is `123`. 165 | This will create a new site and a `development.localhost` directory under `frappe-bench/sites`. 166 | The option `--mariadb-user-host-login-scope=%` will configure site's database credentials to work with docker. 167 | You may need to configure your system /etc/hosts if you're on Linux, Mac, or its Windows equivalent. 168 | 169 | 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). 170 | 171 | Example: 172 | 173 | ```shell 174 | bench new-site --db-type postgres --db-host postgresql mypgsql.localhost 175 | ``` 176 | 177 | To avoid entering postgresql username and root password, set it in `common_site_config.json`, 178 | 179 | ```shell 180 | bench config set-common-config -c root_login postgres 181 | bench config set-common-config -c root_password '"123"' 182 | ``` 183 | 184 | Note: If PostgreSQL is not required, the postgresql service / container can be stopped. 185 | 186 | ### Set bench developer mode on the new site 187 | 188 | 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). 189 | 190 | ```shell 191 | bench --site development.localhost set-config developer_mode 1 192 | bench --site development.localhost clear-cache 193 | ``` 194 | 195 | ### Install an app 196 | 197 | To install an app we need to fetch it from the appropriate git repo, then install in on the appropriate site: 198 | 199 | 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. 200 | 201 | To install custom app 202 | 203 | ```shell 204 | # --branch is optional, use it to point to branch on custom app repository 205 | bench get-app --branch version-12 https://github.com/myusername/myapp 206 | bench --site development.localhost install-app myapp 207 | ``` 208 | 209 | 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. 210 | 211 | ```shell 212 | bench get-app --branch version-14 --resolve-deps erpnext 213 | bench --site development.localhost install-app erpnext 214 | ``` 215 | 216 | To install ERPNext (from the version-13 branch): 217 | 218 | ```shell 219 | bench get-app --branch version-13 erpnext 220 | bench --site development.localhost install-app erpnext 221 | ``` 222 | 223 | Note: Both frappe and erpnext must be on branch with same name. e.g. version-14 224 | You can use the `switch-to-branch` command to align versions if you get an error about mismatching versions. 225 | 226 | ```shell 227 | bench switch-to-branch version-xx 228 | ``` 229 | 230 | ### Start Frappe without debugging 231 | 232 | Execute following command from the `frappe-bench` directory. 233 | 234 | ```shell 235 | bench start 236 | ``` 237 | 238 | You can now login with user `Administrator` and the password you choose when creating the site. 239 | Your website will now be accessible at location [development.localhost:8000](http://development.localhost:8000) 240 | Note: To start bench with debugger refer section for debugging. 241 | 242 | ### Setup bench / new site using script 243 | 244 | 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. 245 | 246 | 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`. 247 | 248 | 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. 249 | 250 | > 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). 251 | 252 | ```shell 253 | python installer.py #pass --db-type postgres for postgresdb 254 | ``` 255 | 256 | For command help 257 | 258 | ```shell 259 | python installer.py --help 260 | 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] 261 | 262 | options: 263 | -h, --help show this help message and exit 264 | -j APPS_JSON, --apps-json APPS_JSON 265 | Path to apps.json, default: apps-example.json 266 | -b BENCH_NAME, --bench-name BENCH_NAME 267 | Bench directory name, default: frappe-bench 268 | -s SITE_NAME, --site-name SITE_NAME 269 | Site name, should end with .localhost, default: development.localhost 270 | -r FRAPPE_REPO, --frappe-repo FRAPPE_REPO 271 | frappe repo to use, default: https://github.com/frappe/frappe 272 | -t FRAPPE_BRANCH, --frappe-branch FRAPPE_BRANCH 273 | frappe repo to use, default: version-15 274 | -p PY_VERSION, --py-version PY_VERSION 275 | python version, default: Not Set 276 | -n NODE_VERSION, --node-version NODE_VERSION 277 | node version, default: Not Set 278 | -v, --verbose verbose output 279 | -a ADMIN_PASSWORD, --admin-password ADMIN_PASSWORD 280 | admin password for site, default: admin 281 | -d DB_TYPE, --db-type DB_TYPE 282 | Database type to use (e.g., mariadb or postgres) 283 | ``` 284 | 285 | A new bench and / or site is created for the client with following defaults. 286 | 287 | - MariaDB root password: `123` 288 | - Admin password: `admin` 289 | 290 | > To use Postegres DB, comment the mariabdb service and uncomment postegres service. 291 | 292 | ### Start Frappe with Visual Studio Code Python Debugging 293 | 294 | 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: 295 | 296 | - Click on the extension icon inside VSCode 297 | - Search `ms-python.python` 298 | - Click on `Install on Dev Container: Frappe Bench` 299 | - Click on 'Reload' 300 | 301 | 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: 302 | 303 | ```shell 304 | honcho start \ 305 | socketio \ 306 | watch \ 307 | schedule \ 308 | worker_short \ 309 | worker_long 310 | ``` 311 | 312 | Alternatively you can use the VSCode launch configuration "Honcho SocketIO Watch Schedule Worker" which launches the same command as above. 313 | 314 | 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. 315 | 316 | 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`. 317 | 318 | To debug workers, skip starting worker with honcho and start it with VSCode debugger. 319 | 320 | For advance vscode configuration in the devcontainer, change the config files in `development/.vscode`. 321 | 322 | ## Developing using the interactive console 323 | 324 | You can launch a simple interactive shell console in the terminal with: 325 | 326 | ```shell 327 | bench --site development.localhost console 328 | ``` 329 | 330 | More likely, you may want to launch VSCode interactive console based on Jupyter kernel. 331 | 332 | 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`. 333 | 334 | 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. 335 | 336 | ```shell 337 | /workspace/development/frappe-bench/env/bin/python -m pip install --upgrade jupyter ipykernel ipython 338 | ``` 339 | 340 | Then, run the command `Python: Show Python interactive window` from the VSCode command palette. 341 | 342 | Replace `development.localhost` with your site and run the following code in a Jupyter cell: 343 | 344 | ```python 345 | import frappe 346 | 347 | frappe.init(site='development.localhost', sites_path='/workspace/development/frappe-bench/sites') 348 | frappe.connect() 349 | frappe.local.lang = frappe.db.get_default('lang') 350 | frappe.db.connect() 351 | ``` 352 | 353 | The first command can take a few seconds to be executed, this is to be expected. 354 | 355 | ## Manually start containers 356 | 357 | In case you don't use VSCode, you may start the containers manually with the following command: 358 | 359 | ### Running the containers 360 | 361 | ```shell 362 | docker-compose -f .devcontainer/docker-compose.yml up -d 363 | ``` 364 | 365 | And enter the interactive shell for the development container with the following command: 366 | 367 | ```shell 368 | docker exec -e "TERM=xterm-256color" -w /workspace/development -it devcontainer-frappe-1 bash 369 | ``` 370 | 371 | ## Use additional services during development 372 | 373 | Add any service that is needed for development in the `.devcontainer/docker-compose.yml` then rebuild and reopen in devcontainer. 374 | 375 | e.g. 376 | 377 | ```yaml 378 | ... 379 | services: 380 | ... 381 | postgresql: 382 | image: postgres:11.8 383 | environment: 384 | POSTGRES_PASSWORD: 123 385 | volumes: 386 | - postgresql-data:/var/lib/postgresql/data 387 | ports: 388 | - 5432:5432 389 | 390 | volumes: 391 | ... 392 | postgresql-data: 393 | ``` 394 | 395 | 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`. 396 | 397 | ## Using Cypress UI tests 398 | 399 | To run cypress based UI tests in a docker environment, follow the below steps: 400 | 401 | 1. Install and setup X11 tooling on VM using the script `install_x11_deps.sh` 402 | 403 | ```shell 404 | sudo bash ./install_x11_deps.sh 405 | ``` 406 | 407 | This script will install required deps, enable X11Forwarding and restart SSH daemon and export `DISPLAY` variable. 408 | 409 | 2. Run X11 service `startx` or `xquartz` 410 | 3. Start docker compose services. 411 | 4. SSH into ui-tester service using `docker exec..` command 412 | 5. Export CYPRESS_baseUrl and other required env variables 413 | 6. Start Cypress UI console by issuing `cypress run command` 414 | 415 | > More references : [Cypress Official Documentation](https://www.cypress.io/blog/2019/05/02/run-cypress-with-a-single-docker-command) 416 | 417 | > Ensure DISPLAY environment is always exported. 418 | 419 | ## Using Mailpit to test mail services 420 | 421 | To use Mailpit just uncomment the service in the docker-compose.yml file. 422 | The Interface is then available under port 8025 and the smtp service can be used as mailpit:1025. 423 | -------------------------------------------------------------------------------- /docs/environment-variables.md: -------------------------------------------------------------------------------- 1 | ## Environment Variables 2 | 3 | All of the commands are directly passed to container as per type of service. Only environment variables used in image are for `nginx-entrypoint.sh` command. They are as follows: 4 | 5 | - `BACKEND`: Set to `{host}:{port}`, defaults to `0.0.0.0:8000` 6 | - `SOCKETIO`: Set to `{host}:{port}`, defaults to `0.0.0.0:9000` 7 | - `UPSTREAM_REAL_IP_ADDRESS`: Set Nginx config for [ngx_http_realip_module#set_real_ip_from](http://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from), defaults to `127.0.0.1` 8 | - `UPSTREAM_REAL_IP_HEADER`: Set Nginx config for [ngx_http_realip_module#real_ip_header](http://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header), defaults to `X-Forwarded-For` 9 | - `UPSTREAM_REAL_IP_RECURSIVE`: Set Nginx config for [ngx_http_realip_module#real_ip_recursive](http://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_recursive) Set defaults to `off` 10 | - `FRAPPE_SITE_NAME_HEADER`: Set proxy header `X-Frappe-Site-Name` and serve site named in the header, defaults to `$host`, i.e. find site name from host header. More details [below](#frappe_site_name_header) 11 | - `PROXY_READ_TIMEOUT`: Upstream gunicorn service timeout, defaults to `120` 12 | - `CLIENT_MAX_BODY_SIZE`: Max body size for uploads, defaults to `50m` 13 | 14 | To bypass `nginx-entrypoint.sh`, mount desired `/etc/nginx/conf.d/default.conf` and run `nginx -g 'daemon off;'` as container command. 15 | 16 | ## Configuration 17 | 18 | We use environment variables to configure our setup. docker-compose uses variables from the `environment:` section of the services defined within and the`.env` file, if present. Variables defined in the `.env` file are referenced via `${VARIABLE_NAME}` within the docker-compose `.yml` file. `example.env` contains a non-exhaustive list of possible configuration variables. To get started, copy `example.env` to `.env`. 19 | 20 | ### `FRAPPE_VERSION` 21 | 22 | Frappe framework release. You can find all releases [here](https://github.com/frappe/frappe/releases). 23 | 24 | ### `DB_PASSWORD` 25 | 26 | Password for MariaDB (or Postgres) database. 27 | 28 | ### `DB_HOST` 29 | 30 | Hostname for MariaDB (or Postgres) database. Set only if external service for database is used or the container can not be reached by its service name (db) by other containers. 31 | 32 | ### `DB_PORT` 33 | 34 | Port for MariaDB (3306) or Postgres (5432) database. Set only if external service for database is used. 35 | 36 | ### `REDIS_CACHE` 37 | 38 | Hostname for redis server to store cache. Set only if external service for redis is used or the container can not be reached by its service name (redis-cache) by other containers. 39 | 40 | ### `REDIS_QUEUE` 41 | 42 | Hostname for redis server to store queue data and socketio. Set only if external service for redis is used or the container can not be reached by its service name (redis-queue) by other containers. 43 | 44 | ### `ERPNEXT_VERSION` 45 | 46 | ERPNext [release](https://github.com/frappe/erpnext/releases). This variable is required if you use ERPNext override. 47 | 48 | ### `LETSENCRYPT_EMAIL` 49 | 50 | Email that used to register https certificate. This one is required only if you use HTTPS override. 51 | 52 | ### `FRAPPE_SITE_NAME_HEADER` 53 | 54 | This environment variable is 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`. 55 | 56 | There is other variables not mentioned here. They're somewhat internal and you don't have to worry about them except you want to change main compose file. 57 | -------------------------------------------------------------------------------- /docs/error-nginx-entrypoint-windows.md: -------------------------------------------------------------------------------- 1 | # Resolving Docker `nginx-entrypoint.sh` Script Not Found Error on Windows 2 | 3 | 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. 4 | 5 | ## 1. Check Line Endings 6 | 7 | On Windows, files often have `CRLF` line endings, while Linux systems expect `LF`. This can cause issues when executing shell scripts in Linux containers. 8 | 9 | - **Convert Line Endings using `dos2unix`:** 10 | ```bash 11 | dos2unix resources/nginx-entrypoint.sh 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/images/Docker Desktop Screenshot - Resources section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/frappe_docker/ebd80217309e1d9230f09fa984c0f2d40e84704d/docs/images/Docker Desktop Screenshot - Resources section.png -------------------------------------------------------------------------------- /docs/images/Docker Manual Screenshot - Resources section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/frappe_docker/ebd80217309e1d9230f09fa984c0f2d40e84704d/docs/images/Docker Manual Screenshot - Resources section.png -------------------------------------------------------------------------------- /docs/list-of-containers.md: -------------------------------------------------------------------------------- 1 | # Images 2 | 3 | There are 3 images that you can find in `/images` directory: 4 | 5 | - `bench`. It is used for development. [Learn more how to start development](development.md). 6 | - `production`. 7 | - Multi-purpose Python backend. Runs [Werkzeug server](https://werkzeug.palletsprojects.com/en/2.0.x/) with [gunicorn](https://gunicorn.org), queues (via `bench worker`), or schedule (via `bench schedule`). 8 | - Contains JS and CSS assets and routes incoming requests using [nginx](https://www.nginx.com). 9 | - Processes realtime websocket requests using [Socket.IO](https://socket.io). 10 | - `custom`. It is used to build bench using `apps.json` file set with `--apps_path` during bench initialization. `apps.json` is a json array. e.g. `[{"url":"{{repo_url}}","branch":"{{repo_branch}}"}]` 11 | 12 | Image has everything we need to be able to run all processes that Frappe framework requires (take a look at [Bench Procfile reference](https://frappeframework.com/docs/v14/user/en/bench/resources/bench-procfile)). We follow [Docker best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#decouple-applications) and split these processes to different containers. 13 | 14 | > 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 reuse as much things as possible and make our builds more efficient. 15 | 16 | # Compose files 17 | 18 | After building the images we have to run the containers. The best and simplest way to do this is to use [compose files](https://docs.docker.com/compose/compose-file/). 19 | 20 | We have one main compose file, `compose.yaml`. Services described, networking, volumes are also handled there. 21 | 22 | ## Services 23 | 24 | All services are described in `compose.yaml` 25 | 26 | - `configurator`. 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. 27 | - `backend`. [Werkzeug server](https://werkzeug.palletsprojects.com/en/2.0.x/). 28 | - `db`. Optional service that runs [MariaDB](https://mariadb.com) if you also use `overrides/compose.mariadb.yaml` or [Postgres](https://www.postgresql.org) if you also use `overrides/compose.postgres.yaml`. 29 | - `redis`. Optional service that runs [Redis](https://redis.io) server with cache, [Socket.IO](https://socket.io) and queues data. 30 | - `frontend`. [nginx](https://www.nginx.com) server that serves JS/CSS assets and routes incoming requests. 31 | - `proxy`. [Traefik](https://traefik.io/traefik/) proxy. It is here for complicated setups or HTTPS override (with `overrides/compose.https.yaml`). 32 | - `websocket`. Node server that runs [Socket.IO](https://socket.io). 33 | - `queue-short`, `queue-long`. Python servers that run job queues using [rq](https://python-rq.org). 34 | - `scheduler`. Python server that runs tasks on schedule using [schedule](https://schedule.readthedocs.io/en/stable/). 35 | 36 | ## Overrides 37 | 38 | We have several [overrides](https://docs.docker.com/compose/extends/): 39 | 40 | - `overrides/compose.proxy.yaml`. Adds traefik proxy to setup. 41 | - `overrides/compose.noproxy.yaml`. Publishes `frontend` ports directly without any proxy. 42 | - `overrides/compose.https.yaml`. Automatically sets up Let's Encrypt certificate and redirects all requests to directed to http, to https. 43 | - `overrides/compose.mariadb.yaml`. Adds `db` service and sets its image to MariaDB. 44 | - `overrides/compose.postgres.yaml`. Adds `db` service and sets its image to Postgres. Note that ERPNext currently doesn't support Postgres. 45 | - `overrides/compose.redis.yaml`. Adds `redis` service and sets its image to `redis`. 46 | 47 | It is quite simple to run overrides. All we need to do is to specify compose files that should be used by docker-compose. For example, we want ERPNext: 48 | 49 | ```bash 50 | # Point to main compose file (compose.yaml) and add one more. 51 | docker-compose -f compose.yaml -f overrides/compose.redis.yaml config 52 | ``` 53 | 54 | ⚠ Make sure to use docker-compose v2 (run `docker-compose -v` to check). If you want to use v1 make sure the correct `$`-signs as they get duplicated by the `config` command! 55 | 56 | That's it! Of course, we also have to setup `.env` before all of that, but that's not the point. 57 | 58 | Check [environment variables](environment-variables.md) for more. 59 | -------------------------------------------------------------------------------- /docs/migrate-from-multi-image-setup.md: -------------------------------------------------------------------------------- 1 | ## Migrate from multi-image setup 2 | 3 | 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`. 4 | 5 | Now you need to specify command and environment variables for following containers: 6 | 7 | ### Frontend 8 | 9 | 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) 10 | 11 | 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. 12 | 13 | Example change: 14 | 15 | ```yaml 16 | # ... removed for brevity 17 | frontend: 18 | image: frappe/erpnext:${ERPNEXT_VERSION:?ERPNext version not set} 19 | command: 20 | - nginx-entrypoint.sh 21 | environment: 22 | BACKEND: backend:8000 23 | SOCKETIO: websocket:9000 24 | volumes: 25 | - sites:/home/frappe/frappe-bench/sites 26 | # ... removed for brevity 27 | ``` 28 | 29 | ### Websocket 30 | 31 | For `websocket` service to act as socketio backend, you need to pass `["node", "/home/frappe/frappe-bench/apps/frappe/socketio.js"]` as container `command` 32 | 33 | Example change: 34 | 35 | ```yaml 36 | # ... removed for brevity 37 | websocket: 38 | image: frappe/erpnext:${ERPNEXT_VERSION:?ERPNext version not set} 39 | command: 40 | - node 41 | - /home/frappe/frappe-bench/apps/frappe/socketio.js 42 | # ... removed for brevity 43 | ``` 44 | 45 | ### Configurator 46 | 47 | 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. 48 | 49 | Example change: 50 | 51 | ```yaml 52 | # ... removed for brevity 53 | configurator: 54 | image: frappe/erpnext:${ERPNEXT_VERSION:?ERPNext version not set} 55 | restart: "no" 56 | entrypoint: 57 | - bash 58 | - -c 59 | command: 60 | - > 61 | bench set-config -g db_host $$DB_HOST; 62 | bench set-config -gp db_port $$DB_PORT; 63 | bench set-config -g redis_cache "redis://$$REDIS_CACHE"; 64 | bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; 65 | bench set-config -gp socketio_port $$SOCKETIO_PORT; 66 | environment: 67 | DB_HOST: db 68 | DB_PORT: "3306" 69 | REDIS_CACHE: redis-cache:6379 70 | REDIS_QUEUE: redis-queue:6379 71 | SOCKETIO_PORT: "9000" 72 | # ... removed for brevity 73 | ``` 74 | 75 | ### Site Creation 76 | 77 | 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. 78 | 79 | 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`. 80 | 81 | Example change: 82 | 83 | ```yaml 84 | # ... removed for brevity 85 | create-site: 86 | image: frappe/erpnext:${ERPNEXT_VERSION:?ERPNext version not set} 87 | restart: "no" 88 | entrypoint: 89 | - bash 90 | - -c 91 | command: 92 | - > 93 | wait-for-it -t 120 db:3306; 94 | wait-for-it -t 120 redis-cache:6379; 95 | wait-for-it -t 120 redis-queue:6379; 96 | export start=`date +%s`; 97 | until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \ 98 | [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \ 99 | [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]]; 100 | do 101 | echo "Waiting for sites/common_site_config.json to be created"; 102 | sleep 5; 103 | if (( `date +%s`-start > 120 )); then 104 | echo "could not find sites/common_site_config.json with required keys"; 105 | exit 1 106 | fi 107 | done; 108 | echo "sites/common_site_config.json found"; 109 | bench new-site --mariadb-user-host-login-scope=% --admin-password=admin --db-root-password=admin --install-app erpnext --set-default frontend; 110 | 111 | # ... removed for brevity 112 | ``` 113 | -------------------------------------------------------------------------------- /docs/port-based-multi-tenancy.md: -------------------------------------------------------------------------------- 1 | WARNING: Do not use this in production if the site is going to be served over plain http. 2 | 3 | ### Step 1 4 | 5 | Remove the traefik service from docker-compose.yml 6 | 7 | ### Step 2 8 | 9 | Add service for each port that needs to be exposed. 10 | 11 | e.g. `port-site-1`, `port-site-2`, `port-site-3`. 12 | 13 | ```yaml 14 | # ... removed for brevity 15 | services: 16 | # ... removed for brevity 17 | port-site-1: 18 | image: frappe/erpnext:v14.11.1 19 | deploy: 20 | restart_policy: 21 | condition: on-failure 22 | command: 23 | - nginx-entrypoint.sh 24 | environment: 25 | BACKEND: backend:8000 26 | FRAPPE_SITE_NAME_HEADER: site1.local 27 | SOCKETIO: websocket:9000 28 | volumes: 29 | - sites:/home/frappe/frappe-bench/sites 30 | ports: 31 | - "8080:8080" 32 | port-site-2: 33 | image: frappe/erpnext:v14.11.1 34 | deploy: 35 | restart_policy: 36 | condition: on-failure 37 | command: 38 | - nginx-entrypoint.sh 39 | environment: 40 | BACKEND: backend:8000 41 | FRAPPE_SITE_NAME_HEADER: site2.local 42 | SOCKETIO: websocket:9000 43 | volumes: 44 | - sites:/home/frappe/frappe-bench/sites 45 | ports: 46 | - "8081:8080" 47 | port-site-3: 48 | image: frappe/erpnext:v14.11.1 49 | deploy: 50 | restart_policy: 51 | condition: on-failure 52 | command: 53 | - nginx-entrypoint.sh 54 | environment: 55 | BACKEND: backend:8000 56 | FRAPPE_SITE_NAME_HEADER: site3.local 57 | SOCKETIO: websocket:9000 58 | volumes: 59 | - sites:/home/frappe/frappe-bench/sites 60 | ports: 61 | - "8082:8080" 62 | ``` 63 | 64 | Notes: 65 | 66 | - Above setup will expose `site1.local`, `site2.local`, `site3.local` on port `8080`, `8081`, `8082` respectively. 67 | - Change `site1.local` to site name to serve from bench. 68 | - Change the `BACKEND` and `SOCKETIO` environment variables as per your service names. 69 | - Make sure `sites:` volume is available as part of yaml. 70 | -------------------------------------------------------------------------------- /docs/setup-options.md: -------------------------------------------------------------------------------- 1 | # Containerized Production Setup 2 | 3 | Make sure you've cloned this repository and switch to the directory before executing following commands. 4 | 5 | Commands will generate YAML as per the environment for setup. 6 | 7 | ## Prerequisites 8 | 9 | - [docker](https://docker.com/get-started) 10 | - [docker compose v2](https://docs.docker.com/compose/cli-command) 11 | 12 | ## Setup Environment Variables 13 | 14 | Copy the example docker environment file to `.env`: 15 | 16 | ```sh 17 | cp example.env .env 18 | ``` 19 | 20 | Note: To know more about environment variable [read here](./environment-variables.md). Set the necessary variables in the `.env` file. 21 | 22 | ## Generate docker-compose.yml for variety of setups 23 | 24 | Notes: 25 | 26 | - Make sure to replace `` with the desired name you wish to set for the project. 27 | - This setup is not to be used for development. A complete development environment is available [here](../development) 28 | 29 | ### Store the yaml files 30 | 31 | 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. 32 | 33 | ```shell 34 | mkdir ~/gitops 35 | ``` 36 | 37 | You can make the directory into a private git repo which stores the yaml and secrets. It can help in tracking changes. 38 | 39 | Instead of `docker compose config`, you can directly use `docker compose up` to start the containers and skip storing the yamls in `gitops` directory. 40 | 41 | ### Setup Frappe without proxy and external MariaDB and Redis 42 | 43 | In this case make sure you've set `DB_HOST`, `DB_PORT`, `REDIS_CACHE` and `REDIS_QUEUE` environment variables or the `configurator` will fail. 44 | 45 | ```sh 46 | # Generate YAML 47 | docker compose -f compose.yaml -f overrides/compose.noproxy.yaml config > ~/gitops/docker-compose.yml 48 | 49 | # Start containers 50 | docker compose --project-name -f ~/gitops/docker-compose.yml up -d 51 | ``` 52 | 53 | ### Setup ERPNext with proxy and external MariaDB and Redis 54 | 55 | In this case make sure you've set `DB_HOST`, `DB_PORT`, `REDIS_CACHE` and `REDIS_QUEUE` environment variables or the `configurator` will fail. 56 | 57 | ```sh 58 | # Generate YAML 59 | docker compose -f compose.yaml \ 60 | -f overrides/compose.proxy.yaml \ 61 | config > ~/gitops/docker-compose.yml 62 | 63 | # Start containers 64 | docker compose --project-name -f ~/gitops/docker-compose.yml up -d 65 | ``` 66 | 67 | ### Setup Frappe using containerized MariaDB and Redis with Letsencrypt certificates. 68 | 69 | In this case make sure you've set `LETSENCRYPT_EMAIL` and `SITES` environment variables are set or certificates won't work. 70 | 71 | ```sh 72 | # Generate YAML 73 | docker compose -f compose.yaml \ 74 | -f overrides/compose.mariadb.yaml \ 75 | -f overrides/compose.redis.yaml \ 76 | -f overrides/compose.https.yaml \ 77 | config > ~/gitops/docker-compose.yml 78 | 79 | # Start containers 80 | docker compose --project-name -f ~/gitops/docker-compose.yml up -d 81 | ``` 82 | 83 | ### Setup ERPNext using containerized MariaDB and Redis with Letsencrypt certificates. 84 | 85 | In this case make sure you've set `LETSENCRYPT_EMAIL` and `SITES` environment variables are set or certificates won't work. 86 | 87 | ```sh 88 | # Generate YAML 89 | docker compose -f compose.yaml \ 90 | -f overrides/compose.mariadb.yaml \ 91 | -f overrides/compose.redis.yaml \ 92 | -f overrides/compose.https.yaml \ 93 | config > ~/gitops/docker-compose.yml 94 | 95 | # Start containers 96 | docker compose --project-name -f ~/gitops/docker-compose.yml up -d 97 | ``` 98 | 99 | ## Create first site 100 | 101 | After starting containers, the first site needs to be created. Refer [site operations](./site-operations.md#setup-new-site). 102 | 103 | ## Updating Images 104 | 105 | Switch to the root of the `frappe_docker` directory before running the following commands: 106 | 107 | ```sh 108 | # Update environment variables ERPNEXT_VERSION and FRAPPE_VERSION 109 | nano .env 110 | 111 | # Pull new images 112 | docker compose -f compose.yaml \ 113 | # ... your other overrides 114 | config > ~/gitops/docker-compose.yml 115 | 116 | # Pull images 117 | docker compose --project-name -f ~/gitops/docker-compose.yml pull 118 | 119 | # Stop containers 120 | docker compose --project-name -f ~/gitops/docker-compose.yml down 121 | 122 | # Restart containers 123 | docker compose --project-name -f ~/gitops/docker-compose.yml up -d 124 | ``` 125 | 126 | Note: 127 | 128 | - pull and stop container commands can be skipped if immutable image tags are used 129 | - `docker compose up -d` will pull new immutable tags if not found. 130 | 131 | To migrate sites refer [site operations](./site-operations.md#migrate-site) 132 | -------------------------------------------------------------------------------- /docs/setup_for_linux_mac.md: -------------------------------------------------------------------------------- 1 | # How to install ERPNext on linux/mac using Frappe_docker ? 2 | 3 | step1: clone the repo 4 | 5 | ``` 6 | git clone https://github.com/frappe/frappe_docker 7 | ``` 8 | 9 | step2: add platform: linux/amd64 to all services in the /pwd.yaml 10 | 11 | here is the update pwd.yml file 12 | 13 | ```yml 14 | version: "3" 15 | 16 | services: 17 | backend: 18 | image: frappe/erpnext:v15 19 | platform: linux/amd64 20 | deploy: 21 | restart_policy: 22 | condition: on-failure 23 | volumes: 24 | - sites:/home/frappe/frappe-bench/sites 25 | - logs:/home/frappe/frappe-bench/logs 26 | 27 | configurator: 28 | image: frappe/erpnext:v15 29 | platform: linux/amd64 30 | deploy: 31 | restart_policy: 32 | condition: none 33 | entrypoint: 34 | - bash 35 | - -c 36 | # add redis_socketio for backward compatibility 37 | command: 38 | - > 39 | ls -1 apps > sites/apps.txt; 40 | bench set-config -g db_host $$DB_HOST; 41 | bench set-config -gp db_port $$DB_PORT; 42 | bench set-config -g redis_cache "redis://$$REDIS_CACHE"; 43 | bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; 44 | bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; 45 | bench set-config -gp socketio_port $$SOCKETIO_PORT; 46 | environment: 47 | DB_HOST: db 48 | DB_PORT: "3306" 49 | REDIS_CACHE: redis-cache:6379 50 | REDIS_QUEUE: redis-queue:6379 51 | SOCKETIO_PORT: "9000" 52 | volumes: 53 | - sites:/home/frappe/frappe-bench/sites 54 | - logs:/home/frappe/frappe-bench/logs 55 | 56 | create-site: 57 | image: frappe/erpnext:v15 58 | platform: linux/amd64 59 | deploy: 60 | restart_policy: 61 | condition: none 62 | volumes: 63 | - sites:/home/frappe/frappe-bench/sites 64 | - logs:/home/frappe/frappe-bench/logs 65 | entrypoint: 66 | - bash 67 | - -c 68 | command: 69 | - > 70 | wait-for-it -t 120 db:3306; 71 | wait-for-it -t 120 redis-cache:6379; 72 | wait-for-it -t 120 redis-queue:6379; 73 | export start=`date +%s`; 74 | until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \ 75 | [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \ 76 | [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]]; 77 | do 78 | echo "Waiting for sites/common_site_config.json to be created"; 79 | sleep 5; 80 | if (( `date +%s`-start > 120 )); then 81 | echo "could not find sites/common_site_config.json with required keys"; 82 | exit 1 83 | fi 84 | done; 85 | echo "sites/common_site_config.json found"; 86 | bench new-site --mariadb-user-host-login-scope=% --admin-password=admin --db-root-password=admin --install-app erpnext --set-default frontend; 87 | 88 | db: 89 | image: mariadb:10.6 90 | platform: linux/amd64 91 | healthcheck: 92 | test: mysqladmin ping -h localhost --password=admin 93 | interval: 1s 94 | retries: 20 95 | deploy: 96 | restart_policy: 97 | condition: on-failure 98 | command: 99 | - --character-set-server=utf8mb4 100 | - --collation-server=utf8mb4_unicode_ci 101 | - --skip-character-set-client-handshake 102 | - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 103 | environment: 104 | MYSQL_ROOT_PASSWORD: admin 105 | volumes: 106 | - db-data:/var/lib/mysql 107 | 108 | frontend: 109 | image: frappe/erpnext:v15 110 | platform: linux/amd64 111 | depends_on: 112 | - websocket 113 | deploy: 114 | restart_policy: 115 | condition: on-failure 116 | command: 117 | - nginx-entrypoint.sh 118 | environment: 119 | BACKEND: backend:8000 120 | FRAPPE_SITE_NAME_HEADER: frontend 121 | SOCKETIO: websocket:9000 122 | UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1 123 | UPSTREAM_REAL_IP_HEADER: X-Forwarded-For 124 | UPSTREAM_REAL_IP_RECURSIVE: "off" 125 | PROXY_READ_TIMEOUT: 120 126 | CLIENT_MAX_BODY_SIZE: 50m 127 | volumes: 128 | - sites:/home/frappe/frappe-bench/sites 129 | - logs:/home/frappe/frappe-bench/logs 130 | ports: 131 | - "8080:8080" 132 | 133 | queue-long: 134 | image: frappe/erpnext:v15 135 | platform: linux/amd64 136 | deploy: 137 | restart_policy: 138 | condition: on-failure 139 | command: 140 | - bench 141 | - worker 142 | - --queue 143 | - long,default,short 144 | volumes: 145 | - sites:/home/frappe/frappe-bench/sites 146 | - logs:/home/frappe/frappe-bench/logs 147 | 148 | queue-short: 149 | image: frappe/erpnext:v15 150 | platform: linux/amd64 151 | deploy: 152 | restart_policy: 153 | condition: on-failure 154 | command: 155 | - bench 156 | - worker 157 | - --queue 158 | - short,default 159 | volumes: 160 | - sites:/home/frappe/frappe-bench/sites 161 | - logs:/home/frappe/frappe-bench/logs 162 | 163 | redis-queue: 164 | image: redis:6.2-alpine 165 | platform: linux/amd64 166 | deploy: 167 | restart_policy: 168 | condition: on-failure 169 | volumes: 170 | - redis-queue-data:/data 171 | 172 | redis-cache: 173 | image: redis:6.2-alpine 174 | platform: linux/amd64 175 | deploy: 176 | restart_policy: 177 | condition: on-failure 178 | 179 | scheduler: 180 | image: frappe/erpnext:v15 181 | platform: linux/amd64 182 | deploy: 183 | restart_policy: 184 | condition: on-failure 185 | command: 186 | - bench 187 | - schedule 188 | volumes: 189 | - sites:/home/frappe/frappe-bench/sites 190 | - logs:/home/frappe/frappe-bench/logs 191 | 192 | websocket: 193 | image: frappe/erpnext:v15 194 | platform: linux/amd64 195 | deploy: 196 | restart_policy: 197 | condition: on-failure 198 | command: 199 | - node 200 | - /home/frappe/frappe-bench/apps/frappe/socketio.js 201 | volumes: 202 | - sites:/home/frappe/frappe-bench/sites 203 | - logs:/home/frappe/frappe-bench/logs 204 | 205 | volumes: 206 | db-data: 207 | redis-queue-data: 208 | sites: 209 | logs: 210 | ``` 211 | 212 | step3: run the docker 213 | 214 | ``` 215 | cd frappe_docker 216 | ``` 217 | 218 | ``` 219 | docker-compose -f ./pwd.yml up 220 | ``` 221 | 222 | --- 223 | 224 | Wait for couple of minutes. 225 | 226 | Open localhost:8080 227 | -------------------------------------------------------------------------------- /docs/single-compose-setup.md: -------------------------------------------------------------------------------- 1 | # Single Compose Setup 2 | 3 | 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`. 4 | 5 | ## Services 6 | 7 | ### frappe-bench components 8 | 9 | - backend, serves gunicorn backend 10 | - frontend, serves static assets through nginx frontend reverse proxies websocket and gunicorn. 11 | - queue-long, long default and short rq worker. 12 | - queue-short, default and short rq worker. 13 | - schedule, event scheduler. 14 | - websocket, socketio websocket for realtime communication. 15 | 16 | ### Run once configuration 17 | 18 | - configurator, configures `common_site_config.json` to set db and redis hosts. 19 | - create-site, creates one site to serve as default site for the frappe-bench. 20 | 21 | ### Service dependencies 22 | 23 | - db, mariadb, container with frappe specific configuration. 24 | - redis-cache, redis for cache data. 25 | - redis-queue, redis for rq data and pub/sub. 26 | 27 | ## Volumes 28 | 29 | - sites: Volume for bench data. Common config, all sites, all site configs and site files will be stored here. 30 | - 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. 31 | 32 | ## Adaptation 33 | 34 | 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. 35 | 36 | 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. 37 | 38 | 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. 39 | -------------------------------------------------------------------------------- /docs/single-server-example.md: -------------------------------------------------------------------------------- 1 | ### Single Server Example 2 | 3 | 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. 4 | 5 | We will setup the following: 6 | 7 | - Install docker and docker compose v2 on linux server. 8 | - Install traefik service for internal load balancer and letsencrypt. 9 | - Install MariaDB with containers. 10 | - Setup project called `erpnext-one` and create sites `one.example.com` and `two.example.com` in the project. 11 | - Setup project called `erpnext-two` and create sites `three.example.com` and `four.example.com` in the project. 12 | 13 | Explanation: 14 | 15 | 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. 16 | 17 | Single instance of **MariaDB** will be installed and act as database service for all the benches/projects installed on the server. 18 | 19 | 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. 20 | 21 | ### Install Docker 22 | 23 | Easiest way to install docker is to use the [convenience script](https://docs.docker.com/engine/install/ubuntu/#install-using-the-convenience-script). 24 | 25 | ```shell 26 | curl -fsSL https://get.docker.com | bash 27 | ``` 28 | 29 | 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. 30 | 31 | ### Install Compose V2 32 | 33 | Refer [original documentation](https://docs.docker.com/compose/cli-command/#install-on-linux) for updated version. 34 | 35 | ```shell 36 | DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker} 37 | mkdir -p $DOCKER_CONFIG/cli-plugins 38 | curl -SL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose 39 | chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose 40 | ``` 41 | 42 | ### Prepare 43 | 44 | Clone `frappe_docker` repo for the needed YAMLs and change the current working directory of your shell to the cloned repo. 45 | 46 | ```shell 47 | git clone https://github.com/frappe/frappe_docker 48 | cd frappe_docker 49 | ``` 50 | 51 | Create configuration and resources directory 52 | 53 | ```shell 54 | mkdir ~/gitops 55 | ``` 56 | 57 | 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. 58 | 59 | ### Install Traefik 60 | 61 | Basic Traefik setup using docker compose. 62 | 63 | Create a file called `traefik.env` in `~/gitops` 64 | 65 | ```shell 66 | echo 'TRAEFIK_DOMAIN=traefik.example.com' > ~/gitops/traefik.env 67 | echo 'EMAIL=admin@example.com' >> ~/gitops/traefik.env 68 | echo 'HASHED_PASSWORD='$(openssl passwd -apr1 changeit | sed -e s/\\$/\\$\\$/g) >> ~/gitops/traefik.env 69 | ``` 70 | 71 | Note: 72 | 73 | - Change the domain from `traefik.example.com` to the one used in production. DNS entry needs to point to the Server IP. 74 | - Change the letsencrypt notification email from `admin@example.com` to correct email. 75 | - Change the password from `changeit` to more secure. 76 | 77 | env file generated at location `~/gitops/traefik.env` will look like following: 78 | 79 | ```env 80 | TRAEFIK_DOMAIN=traefik.example.com 81 | EMAIL=admin@example.com 82 | HASHED_PASSWORD=$apr1$K.4gp7RT$tj9R2jHh0D4Gb5o5fIAzm/ 83 | ``` 84 | 85 | If Container does not deploy put the HASHED_PASSWORD in ''. 86 | 87 | Deploy the traefik container with letsencrypt SSL 88 | 89 | ```shell 90 | docker compose --project-name traefik \ 91 | --env-file ~/gitops/traefik.env \ 92 | -f overrides/compose.traefik.yaml \ 93 | -f overrides/compose.traefik-ssl.yaml up -d 94 | ``` 95 | 96 | This will make the traefik dashboard available on `traefik.example.com` and all certificates will reside in the Docker volume `cert-data`. 97 | 98 | For LAN setup deploy the traefik container without overriding `overrides/compose.traefik-ssl.yaml`. 99 | 100 | ### Install MariaDB 101 | 102 | Basic MariaDB setup using docker compose. 103 | 104 | Create a file called `mariadb.env` in `~/gitops` 105 | 106 | ```shell 107 | echo "DB_PASSWORD=changeit" > ~/gitops/mariadb.env 108 | ``` 109 | 110 | Note: 111 | 112 | - Change the password from `changeit` to more secure. 113 | 114 | env file generated at location `~/gitops/mariadb.env` will look like following: 115 | 116 | ```env 117 | DB_PASSWORD=changeit 118 | ``` 119 | 120 | Note: Change the password from `changeit` to more secure one. 121 | 122 | Deploy the mariadb container 123 | 124 | ```shell 125 | docker compose --project-name mariadb --env-file ~/gitops/mariadb.env -f overrides/compose.mariadb-shared.yaml up -d 126 | ``` 127 | 128 | This will make `mariadb-database` service available under `mariadb-network`. Data will reside in `/data/mariadb`. 129 | 130 | ### Install ERPNext 131 | 132 | #### Create first bench 133 | 134 | Create first bench called `erpnext-one` with `one.example.com` and `two.example.com` 135 | 136 | Create a file called `erpnext-one.env` in `~/gitops` 137 | 138 | ```shell 139 | cp example.env ~/gitops/erpnext-one.env 140 | sed -i 's/DB_PASSWORD=123/DB_PASSWORD=changeit/g' ~/gitops/erpnext-one.env 141 | sed -i 's/DB_HOST=/DB_HOST=mariadb-database/g' ~/gitops/erpnext-one.env 142 | sed -i 's/DB_PORT=/DB_PORT=3306/g' ~/gitops/erpnext-one.env 143 | sed -i 's/SITES=`erp.example.com`/SITES=\`one.example.com\`,\`two.example.com\`/g' ~/gitops/erpnext-one.env 144 | echo 'ROUTER=erpnext-one' >> ~/gitops/erpnext-one.env 145 | echo "BENCH_NETWORK=erpnext-one" >> ~/gitops/erpnext-one.env 146 | ``` 147 | 148 | Note: 149 | 150 | - Change the password from `changeit` to the one set for MariaDB compose in the previous step. 151 | 152 | env file is generated at location `~/gitops/erpnext-one.env`. 153 | 154 | Create a yaml file called `erpnext-one.yaml` in `~/gitops` directory: 155 | 156 | ```shell 157 | docker compose --project-name erpnext-one \ 158 | --env-file ~/gitops/erpnext-one.env \ 159 | -f compose.yaml \ 160 | -f overrides/compose.redis.yaml \ 161 | -f overrides/compose.multi-bench.yaml \ 162 | -f overrides/compose.multi-bench-ssl.yaml config > ~/gitops/erpnext-one.yaml 163 | ``` 164 | 165 | For LAN setup do not override `compose.multi-bench-ssl.yaml`. 166 | 167 | 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. 168 | 169 | Deploy `erpnext-one` containers: 170 | 171 | ```shell 172 | docker compose --project-name erpnext-one -f ~/gitops/erpnext-one.yaml up -d 173 | ``` 174 | 175 | Create sites `one.example.com` and `two.example.com`: 176 | 177 | ```shell 178 | # one.example.com 179 | docker compose --project-name erpnext-one exec backend \ 180 | bench new-site --mariadb-user-host-login-scope=% --db-root-password changeit --install-app erpnext --admin-password changeit one.example.com 181 | ``` 182 | 183 | You can stop here and have a single bench single site setup complete. Continue to add one more site to the current bench. 184 | 185 | ```shell 186 | # two.example.com 187 | docker compose --project-name erpnext-one exec backend \ 188 | bench new-site --mariadb-user-host-login-scope=% --db-root-password changeit --install-app erpnext --admin-password changeit two.example.com 189 | ``` 190 | 191 | #### Create second bench 192 | 193 | Setting up additional bench is optional. Continue only if you need multi bench setup. 194 | 195 | Create second bench called `erpnext-two` with `three.example.com` and `four.example.com` 196 | 197 | Create a file called `erpnext-two.env` in `~/gitops` 198 | 199 | ```shell 200 | curl -sL https://raw.githubusercontent.com/frappe/frappe_docker/main/example.env -o ~/gitops/erpnext-two.env 201 | sed -i 's/DB_PASSWORD=123/DB_PASSWORD=changeit/g' ~/gitops/erpnext-two.env 202 | sed -i 's/DB_HOST=/DB_HOST=mariadb-database/g' ~/gitops/erpnext-two.env 203 | sed -i 's/DB_PORT=/DB_PORT=3306/g' ~/gitops/erpnext-two.env 204 | echo "ROUTER=erpnext-two" >> ~/gitops/erpnext-two.env 205 | echo "SITES=\`three.example.com\`,\`four.example.com\`" >> ~/gitops/erpnext-two.env 206 | echo "BENCH_NETWORK=erpnext-two" >> ~/gitops/erpnext-two.env 207 | ``` 208 | 209 | Note: 210 | 211 | - Change the password from `changeit` to the one set for MariaDB compose in the previous step. 212 | 213 | env file is generated at location `~/gitops/erpnext-two.env`. 214 | 215 | Create a yaml file called `erpnext-two.yaml` in `~/gitops` directory: 216 | 217 | ```shell 218 | docker compose --project-name erpnext-two \ 219 | --env-file ~/gitops/erpnext-two.env \ 220 | -f compose.yaml \ 221 | -f overrides/compose.redis.yaml \ 222 | -f overrides/compose.multi-bench.yaml \ 223 | -f overrides/compose.multi-bench-ssl.yaml config > ~/gitops/erpnext-two.yaml 224 | ``` 225 | 226 | 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. 227 | 228 | Deploy `erpnext-two` containers: 229 | 230 | ```shell 231 | docker compose --project-name erpnext-two -f ~/gitops/erpnext-two.yaml up -d 232 | ``` 233 | 234 | Create sites `three.example.com` and `four.example.com`: 235 | 236 | ```shell 237 | # three.example.com 238 | docker compose --project-name erpnext-two exec backend \ 239 | bench new-site --mariadb-user-host-login-scope=% --db-root-password changeit --install-app erpnext --admin-password changeit three.example.com 240 | # four.example.com 241 | docker compose --project-name erpnext-two exec backend \ 242 | bench new-site --mariadb-user-host-login-scope=% --db-root-password changeit --install-app erpnext --admin-password changeit four.example.com 243 | ``` 244 | 245 | #### Create custom domain to existing site 246 | 247 | In case you need to point custom domain to existing site follow these steps. 248 | Also useful if custom domain is required for LAN based access. 249 | 250 | Create environment file 251 | 252 | ```shell 253 | echo "ROUTER=custom-one-example" > ~/gitops/custom-one-example.env 254 | echo "SITES=\`custom-one.example.com\`" >> ~/gitops/custom-one-example.env 255 | echo "BASE_SITE=one.example.com" >> ~/gitops/custom-one-example.env 256 | echo "BENCH_NETWORK=erpnext-one" >> ~/gitops/custom-one-example.env 257 | ``` 258 | 259 | Note: 260 | 261 | - Change the file name from `custom-one-example.env` to a logical one. 262 | - Change `ROUTER` variable from `custom-one.example.com` to the one being added. 263 | - 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. 264 | - Change `BASE_SITE` variable from `one.example.com` to the one which is being pointed to. 265 | - Change `BENCH_NETWORK` variable from `erpnext-one` to the one which was created with the bench. 266 | 267 | env file is generated at location mentioned in command. 268 | 269 | Generate yaml to reverse proxy: 270 | 271 | ```shell 272 | docker compose --project-name custom-one-example \ 273 | --env-file ~/gitops/custom-one-example.env \ 274 | -f overrides/compose.custom-domain.yaml \ 275 | -f overrides/compose.custom-domain-ssl.yaml config > ~/gitops/custom-one-example.yaml 276 | ``` 277 | 278 | For LAN setup do not override `compose.custom-domain-ssl.yaml`. 279 | 280 | Deploy `erpnext-two` containers: 281 | 282 | ```shell 283 | docker compose --project-name custom-one-example -f ~/gitops/custom-one-example.yaml up -d 284 | ``` 285 | 286 | ### Site operations 287 | 288 | Refer: [site operations](./site-operations.md) 289 | -------------------------------------------------------------------------------- /docs/site-operations.md: -------------------------------------------------------------------------------- 1 | # Site operations 2 | 3 | > 💡 You should setup `--project-name` option in `docker-compose` commands if you have non-standard project name. 4 | 5 | ## Setup new site 6 | 7 | Note: 8 | 9 | - 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. 10 | - Also you have to pass `-p ` if `-p` passed previously eg. `docker-compose -p exec (rest of the command)`. 11 | 12 | ```sh 13 | docker-compose exec backend bench new-site --mariadb-user-host-login-scope=% --db-root-password --admin-password 14 | ``` 15 | 16 | If you need to install some app, specify `--install-app`. To see all options, just run `bench new-site --help`. 17 | 18 | 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: 19 | 20 | ```sh 21 | docker-compose exec backend bench set-config -g root_login 22 | docker-compose exec backend bench set-config -g root_password 23 | ``` 24 | 25 | Also command is slightly different: 26 | 27 | ```sh 28 | docker-compose exec backend bench new-site --mariadb-user-host-login-scope=% --db-type postgres --admin-password 29 | ``` 30 | 31 | ## Push backup to S3 storage 32 | 33 | We have the script that helps to push latest backup to S3. 34 | 35 | ```sh 36 | docker-compose exec backend push_backup.py --site-name --bucket --region-name --endpoint-url --aws-access-key-id --aws-secret-access-key 37 | ``` 38 | 39 | Note that you can restore backup only manually. 40 | 41 | ## Edit configs 42 | 43 | Editing config manually might be required in some cases, 44 | one such case is to use Amazon RDS (or any other DBaaS). 45 | For full instructions, refer to the [wiki](). Common question can be found in Issues and on forum. 46 | 47 | `common_site_config.json` or `site_config.json` from `sites` volume has to be edited using following command: 48 | 49 | ```sh 50 | docker run --rm -it \ 51 | -v _sites:/sites \ 52 | alpine vi /sites/common_site_config.json 53 | ``` 54 | 55 | Instead of `alpine` use any image of your choice. 56 | 57 | ## Health check 58 | 59 | 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. 60 | 61 | ```shell 62 | docker-compose exec backend healthcheck.sh --ping-service mongodb:27017 63 | ``` 64 | 65 | Additional services can be pinged as part of health check with option `-p` or `--ping-service`. 66 | 67 | This check ensures that given service should be connected along with services in common_site_config.json. 68 | If connection to service(s) fails, the command fails with exit code 1. 69 | 70 | --- 71 | 72 | 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: 73 | 74 | ```sh 75 | docker-compose exec backend bench --help 76 | ``` 77 | 78 | ## Migrate site 79 | 80 | Note: 81 | 82 | - Wait for the `db` service to start and `configurator` to exit before trying to migrate a site. Usually this takes up to 10 seconds. 83 | 84 | ```sh 85 | docker-compose exec backend bench --site migrate 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/tls-for-local-deployment.md: -------------------------------------------------------------------------------- 1 | # Accessing ERPNext through https on local deployment 2 | 3 | - 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. 4 | 5 | ### Prerequisites 6 | 7 | - Caddy 8 | - Adding a domain name to hosts file 9 | 10 | #### Installation of caddy webserver 11 | 12 | - Follow the official Caddy website for the installation guide https://caddyserver.com/docs/install 13 | 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 14 | 15 | ```js 16 | erp.localdev.net { 17 | tls internal 18 | 19 | reverse_proxy localhost:8085 { 20 | 21 | } 22 | } 23 | ``` 24 | 25 | - Caddy's root certificate must be added to other computers if computers from different networks access the ERPNext through https. 26 | -------------------------------------------------------------------------------- /docs/troubleshoot.md: -------------------------------------------------------------------------------- 1 | 1. [Fixing MariaDB issues after rebuilding the container](#fixing-mariadb-issues-after-rebuilding-the-container) 2 | 1. [docker-compose does not recognize variables from `.env` file](#docker-compose-does-not-recognize-variables-from-env-file) 3 | 1. [Windows Based Installation](#windows-based-installation) 4 | 5 | ### Fixing MariaDB issues after rebuilding the container 6 | 7 | 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. 8 | 9 | First test for network issues. Manually connect to the database through the `backend` container: 10 | 11 | ``` 12 | docker exec -it frappe_docker-backend-1 bash 13 | mysql -uroot -padmin -hdb 14 | ``` 15 | 16 | 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. 17 | 18 | Then, the parameter `'db_name'@'%'` needs to be set in MariaDB and permission to the site database suitably assigned to the user. 19 | 20 | This step has to be repeated for all sites available under the current bench. 21 | Example shows the queries to be executed for site `localhost` 22 | 23 | Open sites/localhost/site_config.json: 24 | 25 | ```shell 26 | code sites/localhost/site_config.json 27 | ``` 28 | 29 | and take note of the parameters `db_name` and `db_password`. 30 | 31 | Enter MariaDB Interactive shell: 32 | 33 | ```shell 34 | mysql -uroot -padmin -hdb 35 | ``` 36 | 37 | The parameter `'db_name'@'%'` must not be duplicated. Verify that it is unique with the command: 38 | 39 | ``` 40 | SELECT User, Host FROM mysql.user; 41 | ``` 42 | 43 | Delete duplicated entries, if found, with the following: 44 | 45 | ``` 46 | DROP USER 'db_name'@'host'; 47 | ``` 48 | 49 | Modify permissions by executing following queries replacing `db_name` and `db_password` with the values found in site_config.json. 50 | 51 | ```sql 52 | -- if there is no user created already first try to created it using the next command 53 | -- CREATE USER 'db_name'@'%' IDENTIFIED BY 'your_password'; 54 | -- skip the upgrade command below if you use the create command above 55 | UPDATE mysql.global_priv SET Host = '%' where User = 'db_name'; FLUSH PRIVILEGES; 56 | SET PASSWORD FOR 'db_name'@'%' = PASSWORD('db_password'); FLUSH PRIVILEGES; 57 | GRANT ALL PRIVILEGES ON `db_name`.* TO 'db_name'@'%' IDENTIFIED BY 'db_password' WITH GRANT OPTION; FLUSH PRIVILEGES; 58 | EXIT; 59 | ``` 60 | 61 | Note: For MariaDB 10.3 and older use `mysql.user` instead of `mysql.global_priv`. 62 | 63 | ### docker-compose does not recognize variables from `.env` file 64 | 65 | 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. 66 | 67 | ### Windows Based Installation 68 | 69 | - Set environment variable `COMPOSE_CONVERT_WINDOWS_PATHS` e.g. `set COMPOSE_CONVERT_WINDOWS_PATHS=1` 70 | - While using docker machine, port-forward the ports of VM to ports of host machine. (ports 8080/8000/9000) 71 | - Name all the sites ending with `.localhost`. and access it via browser locally. e.g. `http://site1.localhost` 72 | 73 | ### Redo installation 74 | 75 | - If you have made changes and just want to start over again (abandoning all changes), remove all docker 76 | - containers 77 | - images 78 | - volumes 79 | - Install a fresh 80 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/frappe/frappe_docker/blob/main/docs/environment-variables.md 2 | 3 | ERPNEXT_VERSION=v15.63.0 4 | 5 | DB_PASSWORD=123 6 | 7 | # Only if you use external database 8 | DB_HOST= 9 | DB_PORT= 10 | 11 | # Only if you use external Redis 12 | REDIS_CACHE= 13 | REDIS_QUEUE= 14 | 15 | # Only with HTTPS override 16 | LETSENCRYPT_EMAIL=mail@example.com 17 | 18 | # These environment variables are not required. 19 | 20 | # Default value is `$$host` which resolves site by host. For example, if your host is `example.com`, 21 | # site's name should be `example.com`, or if host is `127.0.0.1` (local debugging), it should be `127.0.0.1`. 22 | # This variable allows to override described behavior. Let's say you create site named `mysite` 23 | # and do want to access it by `127.0.0.1` host. Than you would set this variable to `mysite`. 24 | FRAPPE_SITE_NAME_HEADER= 25 | 26 | # Default value is `8080`. 27 | HTTP_PUBLISH_PORT= 28 | 29 | # Default value is `127.0.0.1`. Set IP address as our trusted upstream address. 30 | UPSTREAM_REAL_IP_ADDRESS= 31 | 32 | # Default value is `X-Forwarded-For`. Set request header field whose value will be used to replace the client address 33 | UPSTREAM_REAL_IP_HEADER= 34 | 35 | # Allowed values are on|off. Default value is `off`. If recursive search is disabled, 36 | # the original client address that matches one of the trusted addresses 37 | # is replaced by the last address sent in the request header field defined by the real_ip_header directive. 38 | # 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. 39 | UPSTREAM_REAL_IP_RECURSIVE= 40 | 41 | # All Values Allowed by nginx proxy_read_timeout are allowed, default value is 120s 42 | # Useful if you have longrunning print formats or slow loading sites 43 | PROXY_READ_TIMEOUT= 44 | 45 | # All Values allowed by nginx client_max_body_size are allowed, default value is 50m 46 | # Necessary if the upload limit in the frappe application is increased 47 | CLIENT_MAX_BODY_SIZE= 48 | 49 | # List of sites for letsencrypt certificates quoted with backtick (`) and separated by comma (,) 50 | # More https://doc.traefik.io/traefik/routing/routers/#rule 51 | # About acme https://doc.traefik.io/traefik/https/acme/#domain-definition 52 | SITES=`erp.example.com` 53 | -------------------------------------------------------------------------------- /images/bench/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim AS bench 2 | 3 | LABEL author=frappé 4 | 5 | ARG GIT_REPO=https://github.com/frappe/bench.git 6 | ARG GIT_BRANCH=v5.x 7 | 8 | RUN apt-get update \ 9 | && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ 10 | # For frappe framework 11 | git \ 12 | mariadb-client \ 13 | postgresql-client \ 14 | gettext-base \ 15 | wget \ 16 | # for PDF 17 | libssl-dev \ 18 | fonts-cantarell \ 19 | xfonts-75dpi \ 20 | xfonts-base \ 21 | # weasyprint dependencies 22 | libpango-1.0-0 \ 23 | libharfbuzz0b \ 24 | libpangoft2-1.0-0 \ 25 | libpangocairo-1.0-0 \ 26 | # to work inside the container 27 | locales \ 28 | build-essential \ 29 | cron \ 30 | curl \ 31 | vim \ 32 | sudo \ 33 | iputils-ping \ 34 | watch \ 35 | tree \ 36 | nano \ 37 | less \ 38 | software-properties-common \ 39 | bash-completion \ 40 | # For psycopg2 41 | libpq-dev \ 42 | # Other 43 | libffi-dev \ 44 | liblcms2-dev \ 45 | libldap2-dev \ 46 | libmariadb-dev \ 47 | libsasl2-dev \ 48 | libtiff5-dev \ 49 | libwebp-dev \ 50 | pkg-config \ 51 | redis-tools \ 52 | rlwrap \ 53 | tk8.6-dev \ 54 | ssh-client \ 55 | # VSCode container requirements 56 | net-tools \ 57 | # For pyenv build dependencies 58 | # https://github.com/frappe/frappe_docker/issues/840#issuecomment-1185206895 59 | make \ 60 | # For pandas 61 | libbz2-dev \ 62 | # For bench execute 63 | libsqlite3-dev \ 64 | # For other dependencies 65 | zlib1g-dev \ 66 | libreadline-dev \ 67 | llvm \ 68 | libncurses5-dev \ 69 | libncursesw5-dev \ 70 | xz-utils \ 71 | tk-dev \ 72 | liblzma-dev \ 73 | file \ 74 | && rm -rf /var/lib/apt/lists/* 75 | 76 | RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ 77 | && dpkg-reconfigure --frontend=noninteractive locales 78 | 79 | # Detect arch and install wkhtmltopdf 80 | ARG WKHTMLTOPDF_VERSION=0.12.6.1-3 81 | ARG WKHTMLTOPDF_DISTRO=bookworm 82 | RUN if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \ 83 | && if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \ 84 | && downloaded_file=wkhtmltox_${WKHTMLTOPDF_VERSION}.${WKHTMLTOPDF_DISTRO}_${ARCH}.deb \ 85 | && wget -q https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \ 86 | && dpkg -i $downloaded_file \ 87 | && rm $downloaded_file 88 | 89 | # Create new user with home directory, improve docker compatibility with UID/GID 1000, 90 | # add user to sudo group, allow passwordless sudo, switch to that user 91 | # and change directory to user home directory 92 | RUN groupadd -g 1000 frappe \ 93 | && useradd --no-log-init -r -m -u 1000 -g 1000 -G sudo frappe \ 94 | && echo "frappe ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers 95 | 96 | USER frappe 97 | WORKDIR /home/frappe 98 | 99 | # Install Python via pyenv 100 | ENV PYTHON_VERSION_V14=3.10.13 101 | ENV PYTHON_VERSION=3.11.6 102 | ENV PYENV_ROOT=/home/frappe/.pyenv 103 | ENV PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH 104 | 105 | # From https://github.com/pyenv/pyenv#basic-github-checkout 106 | RUN git clone --depth 1 https://github.com/pyenv/pyenv.git .pyenv \ 107 | && pyenv install $PYTHON_VERSION_V14 \ 108 | && pyenv install $PYTHON_VERSION \ 109 | && PYENV_VERSION=$PYTHON_VERSION_V14 pip install --no-cache-dir virtualenv \ 110 | && PYENV_VERSION=$PYTHON_VERSION pip install --no-cache-dir virtualenv \ 111 | && pyenv global $PYTHON_VERSION $PYTHON_VERSION_v14 \ 112 | && sed -Ei -e '/^([^#]|$)/ {a export PYENV_ROOT="/home/frappe/.pyenv" a export PATH="$PYENV_ROOT/bin:$PATH" a ' -e ':a' -e '$!{n;ba};}' ~/.profile \ 113 | && echo 'eval "$(pyenv init --path)"' >>~/.profile \ 114 | && echo 'eval "$(pyenv init -)"' >>~/.bashrc 115 | 116 | # Clone and install bench in the local user home directory 117 | # For development, bench source is located in ~/.bench 118 | ENV PATH=/home/frappe/.local/bin:$PATH 119 | # Skip editable-bench warning 120 | # https://github.com/frappe/bench/commit/20560c97c4246b2480d7358c722bc9ad13606138 121 | RUN git clone ${GIT_REPO} --depth 1 -b ${GIT_BRANCH} .bench \ 122 | && pip install --no-cache-dir --user -e .bench \ 123 | && echo "export PATH=/home/frappe/.local/bin:\$PATH" >>/home/frappe/.bashrc \ 124 | && echo "export BENCH_DEVELOPER=1" >>/home/frappe/.bashrc 125 | 126 | # Install Node via nvm 127 | ENV NODE_VERSION_14=16.20.2 128 | ENV NODE_VERSION=20.19.2 129 | ENV NVM_DIR=/home/frappe/.nvm 130 | ENV PATH=${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH} 131 | 132 | RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \ 133 | && . ${NVM_DIR}/nvm.sh \ 134 | && nvm install ${NODE_VERSION_14} \ 135 | && nvm use v${NODE_VERSION_14} \ 136 | && npm install -g yarn \ 137 | && nvm install ${NODE_VERSION} \ 138 | && nvm use v${NODE_VERSION} \ 139 | && npm install -g yarn \ 140 | && nvm alias default v${NODE_VERSION} \ 141 | && rm -rf ${NVM_DIR}/.cache \ 142 | && echo 'export NVM_DIR="/home/frappe/.nvm"' >>~/.bashrc \ 143 | && echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >> ~/.bashrc \ 144 | && echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion' >> ~/.bashrc 145 | 146 | 147 | EXPOSE 8000-8005 9000-9005 6787 148 | 149 | FROM bench AS bench-test 150 | 151 | # Print version and verify bashrc is properly sourced so that everything works 152 | # in the interactive shell and Dockerfile 153 | 154 | RUN node --version \ 155 | && npm --version \ 156 | && yarn --version \ 157 | && bench --help 158 | 159 | RUN bash -c "node --version" \ 160 | && bash -c "npm --version" \ 161 | && bash -c "yarn --version" \ 162 | && bash -c "bench --help" 163 | -------------------------------------------------------------------------------- /images/custom/Containerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.11.6 2 | ARG DEBIAN_BASE=bookworm 3 | FROM python:${PYTHON_VERSION}-slim-${DEBIAN_BASE} AS base 4 | 5 | COPY resources/nginx-template.conf /templates/nginx/frappe.conf.template 6 | COPY resources/nginx-entrypoint.sh /usr/local/bin/nginx-entrypoint.sh 7 | 8 | ARG WKHTMLTOPDF_VERSION=0.12.6.1-3 9 | ARG WKHTMLTOPDF_DISTRO=bookworm 10 | ARG NODE_VERSION=20.19.2 11 | ENV NVM_DIR=/home/frappe/.nvm 12 | ENV PATH=${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH} 13 | 14 | RUN useradd -ms /bin/bash frappe \ 15 | && apt-get update \ 16 | && apt-get install --no-install-recommends -y \ 17 | curl \ 18 | git \ 19 | vim \ 20 | nginx \ 21 | gettext-base \ 22 | file \ 23 | # weasyprint dependencies 24 | libpango-1.0-0 \ 25 | libharfbuzz0b \ 26 | libpangoft2-1.0-0 \ 27 | libpangocairo-1.0-0 \ 28 | # For backups 29 | restic \ 30 | gpg \ 31 | # MariaDB 32 | mariadb-client \ 33 | less \ 34 | # Postgres 35 | libpq-dev \ 36 | postgresql-client \ 37 | # For healthcheck 38 | wait-for-it \ 39 | jq \ 40 | # NodeJS 41 | && mkdir -p ${NVM_DIR} \ 42 | && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \ 43 | && . ${NVM_DIR}/nvm.sh \ 44 | && nvm install ${NODE_VERSION} \ 45 | && nvm use v${NODE_VERSION} \ 46 | && npm install -g yarn \ 47 | && nvm alias default v${NODE_VERSION} \ 48 | && rm -rf ${NVM_DIR}/.cache \ 49 | && echo 'export NVM_DIR="/home/frappe/.nvm"' >>/home/frappe/.bashrc \ 50 | && echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >>/home/frappe/.bashrc \ 51 | && echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion' >>/home/frappe/.bashrc \ 52 | # Install wkhtmltopdf with patched qt 53 | && if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \ 54 | && if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \ 55 | && downloaded_file=wkhtmltox_${WKHTMLTOPDF_VERSION}.${WKHTMLTOPDF_DISTRO}_${ARCH}.deb \ 56 | && curl -sLO https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \ 57 | && apt-get install -y ./$downloaded_file \ 58 | && rm $downloaded_file \ 59 | # Clean up 60 | && rm -rf /var/lib/apt/lists/* \ 61 | && rm -fr /etc/nginx/sites-enabled/default \ 62 | && pip3 install frappe-bench \ 63 | # Fixes for non-root nginx and logs to stdout 64 | && sed -i '/user www-data/d' /etc/nginx/nginx.conf \ 65 | && ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log \ 66 | && touch /run/nginx.pid \ 67 | && chown -R frappe:frappe /etc/nginx/conf.d \ 68 | && chown -R frappe:frappe /etc/nginx/nginx.conf \ 69 | && chown -R frappe:frappe /var/log/nginx \ 70 | && chown -R frappe:frappe /var/lib/nginx \ 71 | && chown -R frappe:frappe /run/nginx.pid \ 72 | && chmod 755 /usr/local/bin/nginx-entrypoint.sh \ 73 | && chmod 644 /templates/nginx/frappe.conf.template 74 | 75 | FROM base AS builder 76 | 77 | RUN apt-get update \ 78 | && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ 79 | # For frappe framework 80 | wget \ 81 | #for building arm64 binaries 82 | libcairo2-dev \ 83 | libpango1.0-dev \ 84 | libjpeg-dev \ 85 | libgif-dev \ 86 | librsvg2-dev \ 87 | # For psycopg2 88 | libpq-dev \ 89 | # Other 90 | libffi-dev \ 91 | liblcms2-dev \ 92 | libldap2-dev \ 93 | libmariadb-dev \ 94 | libsasl2-dev \ 95 | libtiff5-dev \ 96 | libwebp-dev \ 97 | pkg-config \ 98 | redis-tools \ 99 | rlwrap \ 100 | tk8.6-dev \ 101 | cron \ 102 | # For pandas 103 | gcc \ 104 | build-essential \ 105 | libbz2-dev \ 106 | && rm -rf /var/lib/apt/lists/* 107 | 108 | # apps.json includes 109 | ARG APPS_JSON_BASE64 110 | RUN if [ -n "${APPS_JSON_BASE64}" ]; then \ 111 | mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \ 112 | fi 113 | 114 | USER frappe 115 | 116 | ARG FRAPPE_BRANCH=version-15 117 | ARG FRAPPE_PATH=https://github.com/frappe/frappe 118 | RUN export APP_INSTALL_ARGS="" && \ 119 | if [ -n "${APPS_JSON_BASE64}" ]; then \ 120 | export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \ 121 | fi && \ 122 | bench init ${APP_INSTALL_ARGS}\ 123 | --frappe-branch=${FRAPPE_BRANCH} \ 124 | --frappe-path=${FRAPPE_PATH} \ 125 | --no-procfile \ 126 | --no-backups \ 127 | --skip-redis-config-generation \ 128 | --verbose \ 129 | /home/frappe/frappe-bench && \ 130 | cd /home/frappe/frappe-bench && \ 131 | echo "{}" > sites/common_site_config.json && \ 132 | find apps -mindepth 1 -path "*/.git" | xargs rm -fr 133 | 134 | FROM base AS backend 135 | 136 | USER frappe 137 | 138 | COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench 139 | 140 | WORKDIR /home/frappe/frappe-bench 141 | 142 | VOLUME [ \ 143 | "/home/frappe/frappe-bench/sites", \ 144 | "/home/frappe/frappe-bench/sites/assets", \ 145 | "/home/frappe/frappe-bench/logs" \ 146 | ] 147 | 148 | CMD [ \ 149 | "/home/frappe/frappe-bench/env/bin/gunicorn", \ 150 | "--chdir=/home/frappe/frappe-bench/sites", \ 151 | "--bind=0.0.0.0:8000", \ 152 | "--threads=4", \ 153 | "--workers=2", \ 154 | "--worker-class=gthread", \ 155 | "--worker-tmp-dir=/dev/shm", \ 156 | "--timeout=120", \ 157 | "--preload", \ 158 | "frappe.app:application" \ 159 | ] 160 | -------------------------------------------------------------------------------- /images/layered/Containerfile: -------------------------------------------------------------------------------- 1 | ARG FRAPPE_BRANCH=version-15 2 | 3 | FROM frappe/build:${FRAPPE_BRANCH} AS builder 4 | 5 | ARG FRAPPE_BRANCH=version-15 6 | ARG FRAPPE_PATH=https://github.com/frappe/frappe 7 | ARG APPS_JSON_BASE64 8 | 9 | USER root 10 | 11 | RUN if [ -n "${APPS_JSON_BASE64}" ]; then \ 12 | mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \ 13 | fi 14 | 15 | USER frappe 16 | 17 | RUN export APP_INSTALL_ARGS="" && \ 18 | if [ -n "${APPS_JSON_BASE64}" ]; then \ 19 | export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \ 20 | fi && \ 21 | bench init ${APP_INSTALL_ARGS}\ 22 | --frappe-branch=${FRAPPE_BRANCH} \ 23 | --frappe-path=${FRAPPE_PATH} \ 24 | --no-procfile \ 25 | --no-backups \ 26 | --skip-redis-config-generation \ 27 | --verbose \ 28 | /home/frappe/frappe-bench && \ 29 | cd /home/frappe/frappe-bench && \ 30 | echo "{}" > sites/common_site_config.json && \ 31 | find apps -mindepth 1 -path "*/.git" | xargs rm -fr 32 | 33 | FROM frappe/base:${FRAPPE_BRANCH} AS backend 34 | 35 | USER frappe 36 | 37 | COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench 38 | 39 | WORKDIR /home/frappe/frappe-bench 40 | 41 | VOLUME [ \ 42 | "/home/frappe/frappe-bench/sites", \ 43 | "/home/frappe/frappe-bench/sites/assets", \ 44 | "/home/frappe/frappe-bench/logs" \ 45 | ] 46 | 47 | CMD [ \ 48 | "/home/frappe/frappe-bench/env/bin/gunicorn", \ 49 | "--chdir=/home/frappe/frappe-bench/sites", \ 50 | "--bind=0.0.0.0:8000", \ 51 | "--threads=4", \ 52 | "--workers=2", \ 53 | "--worker-class=gthread", \ 54 | "--worker-tmp-dir=/dev/shm", \ 55 | "--timeout=120", \ 56 | "--preload", \ 57 | "frappe.app:application" \ 58 | ] 59 | -------------------------------------------------------------------------------- /images/production/Containerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.11.6 2 | ARG DEBIAN_BASE=bookworm 3 | FROM python:${PYTHON_VERSION}-slim-${DEBIAN_BASE} AS base 4 | 5 | ARG WKHTMLTOPDF_VERSION=0.12.6.1-3 6 | ARG WKHTMLTOPDF_DISTRO=bookworm 7 | ARG NODE_VERSION=20.19.2 8 | ENV NVM_DIR=/home/frappe/.nvm 9 | ENV PATH=${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH} 10 | 11 | RUN useradd -ms /bin/bash frappe \ 12 | && apt-get update \ 13 | && apt-get install --no-install-recommends -y \ 14 | curl \ 15 | git \ 16 | vim \ 17 | nginx \ 18 | gettext-base \ 19 | file \ 20 | # weasyprint dependencies 21 | libpango-1.0-0 \ 22 | libharfbuzz0b \ 23 | libpangoft2-1.0-0 \ 24 | libpangocairo-1.0-0 \ 25 | # For backups 26 | restic \ 27 | gpg \ 28 | # MariaDB 29 | mariadb-client \ 30 | less \ 31 | # Postgres 32 | libpq-dev \ 33 | postgresql-client \ 34 | # For healthcheck 35 | wait-for-it \ 36 | jq \ 37 | # NodeJS 38 | && mkdir -p ${NVM_DIR} \ 39 | && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \ 40 | && . ${NVM_DIR}/nvm.sh \ 41 | && nvm install ${NODE_VERSION} \ 42 | && nvm use v${NODE_VERSION} \ 43 | && npm install -g yarn \ 44 | && nvm alias default v${NODE_VERSION} \ 45 | && rm -rf ${NVM_DIR}/.cache \ 46 | && echo 'export NVM_DIR="/home/frappe/.nvm"' >>/home/frappe/.bashrc \ 47 | && echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >>/home/frappe/.bashrc \ 48 | && echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion' >>/home/frappe/.bashrc \ 49 | # Install wkhtmltopdf with patched qt 50 | && if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \ 51 | && if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \ 52 | && downloaded_file=wkhtmltox_${WKHTMLTOPDF_VERSION}.${WKHTMLTOPDF_DISTRO}_${ARCH}.deb \ 53 | && curl -sLO https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \ 54 | && apt-get install -y ./$downloaded_file \ 55 | && rm $downloaded_file \ 56 | # Clean up 57 | && rm -rf /var/lib/apt/lists/* \ 58 | && rm -fr /etc/nginx/sites-enabled/default \ 59 | && pip3 install frappe-bench \ 60 | # Fixes for non-root nginx and logs to stdout 61 | && sed -i '/user www-data/d' /etc/nginx/nginx.conf \ 62 | && ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log \ 63 | && touch /run/nginx.pid \ 64 | && chown -R frappe:frappe /etc/nginx/conf.d \ 65 | && chown -R frappe:frappe /etc/nginx/nginx.conf \ 66 | && chown -R frappe:frappe /var/log/nginx \ 67 | && chown -R frappe:frappe /var/lib/nginx \ 68 | && chown -R frappe:frappe /run/nginx.pid 69 | 70 | COPY resources/nginx-template.conf /templates/nginx/frappe.conf.template 71 | COPY resources/nginx-entrypoint.sh /usr/local/bin/nginx-entrypoint.sh 72 | 73 | FROM base AS build 74 | 75 | RUN apt-get update \ 76 | && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ 77 | # For frappe framework 78 | wget \ 79 | # For psycopg2 80 | libpq-dev \ 81 | # Other 82 | libffi-dev \ 83 | liblcms2-dev \ 84 | libldap2-dev \ 85 | libmariadb-dev \ 86 | libsasl2-dev \ 87 | libtiff5-dev \ 88 | libwebp-dev \ 89 | pkg-config \ 90 | redis-tools \ 91 | rlwrap \ 92 | tk8.6-dev \ 93 | cron \ 94 | # For pandas 95 | gcc \ 96 | build-essential \ 97 | libbz2-dev \ 98 | && rm -rf /var/lib/apt/lists/* 99 | 100 | USER frappe 101 | 102 | FROM build AS builder 103 | 104 | ARG FRAPPE_BRANCH=version-15 105 | ARG FRAPPE_PATH=https://github.com/frappe/frappe 106 | ARG ERPNEXT_REPO=https://github.com/frappe/erpnext 107 | ARG ERPNEXT_BRANCH=version-15 108 | RUN bench init \ 109 | --frappe-branch=${FRAPPE_BRANCH} \ 110 | --frappe-path=${FRAPPE_PATH} \ 111 | --no-procfile \ 112 | --no-backups \ 113 | --skip-redis-config-generation \ 114 | --verbose \ 115 | /home/frappe/frappe-bench && \ 116 | cd /home/frappe/frappe-bench && \ 117 | bench get-app --branch=${ERPNEXT_BRANCH} --resolve-deps erpnext ${ERPNEXT_REPO} && \ 118 | echo "{}" > sites/common_site_config.json && \ 119 | find apps -mindepth 1 -path "*/.git" | xargs rm -fr 120 | 121 | FROM base AS erpnext 122 | 123 | USER frappe 124 | 125 | RUN echo "echo \"Commands restricted in prodution container, Read FAQ before you proceed: https://frappe.io/ctr-faq\"" >> ~/.bashrc 126 | 127 | COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench 128 | 129 | WORKDIR /home/frappe/frappe-bench 130 | 131 | VOLUME [ \ 132 | "/home/frappe/frappe-bench/sites", \ 133 | "/home/frappe/frappe-bench/sites/assets", \ 134 | "/home/frappe/frappe-bench/logs" \ 135 | ] 136 | 137 | CMD [ \ 138 | "/home/frappe/frappe-bench/env/bin/gunicorn", \ 139 | "--chdir=/home/frappe/frappe-bench/sites", \ 140 | "--bind=0.0.0.0:8000", \ 141 | "--threads=4", \ 142 | "--workers=2", \ 143 | "--worker-class=gthread", \ 144 | "--worker-tmp-dir=/dev/shm", \ 145 | "--timeout=120", \ 146 | "--preload", \ 147 | "frappe.app:application" \ 148 | ] 149 | -------------------------------------------------------------------------------- /install_x11_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # This script configures X11 forwarding for Linux and macOS systems. 4 | # It installs X11, Openbox (on Linux), and checks for XQuartz (on macOS). 5 | # It also updates the sshd_config file to enable X11Forwarding and restarts the SSH service. 6 | 7 | # Check if the script is running with root privileges 8 | if [ "$EUID" -ne 0 ]; then 9 | echo "Error: This script requires root privileges. Please run it as a superuser" 10 | exit 1 11 | fi 12 | 13 | # Function to restart SSH service (Linux) 14 | restart_ssh_linux() { 15 | if command -v service >/dev/null 2>&1; then 16 | sudo service ssh restart 17 | else 18 | sudo systemctl restart ssh 19 | fi 20 | } 21 | 22 | # Function to restart SSH service (macOS) 23 | restart_ssh_macos() { 24 | launchctl stop com.openssh.sshd 25 | launchctl start com.openssh.sshd 26 | } 27 | 28 | update_x11_forwarding() { 29 | if grep -q "X11Forwarding yes" /etc/ssh/sshd_config; then 30 | echo "X11Forwarding is already set to 'yes' in ssh_config." 31 | else 32 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 33 | # Linux: Use sed for Linux 34 | sudo sed -i 's/#\?X11Forwarding.*/X11Forwarding yes/' /etc/ssh/sshd_config 35 | elif [[ "$OSTYPE" == "darwin"* ]]; then 36 | # macOS: Use sed for macOS 37 | sudo sed -i -E 's/#X11Forwarding.*/X11Forwarding yes/' /etc/ssh/sshd_config 38 | restart_ssh_macos 39 | fi 40 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 41 | restart_ssh_linux 42 | fi 43 | fi 44 | } 45 | 46 | # Determine the operating system 47 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 48 | # Linux 49 | if command -v startx >/dev/null 2>&1; then 50 | echo "X11 is already installed." 51 | else 52 | # Check which package manager is available 53 | if command -v apt-get >/dev/null 2>&1; then 54 | install_command="sudo apt-get update && sudo apt-get install xorg openbox" 55 | elif command -v dnf >/dev/null 2>&1; then 56 | install_command="sudo dnf install xorg-x11-server-Xorg openbox" 57 | else 58 | echo "Error: Unable to determine the package manager. Manual installation required." 59 | exit 1 60 | fi 61 | fi 62 | # Check if the installation command is defined 63 | if [ -n "$install_command" ]; then 64 | # Execute the installation command 65 | if $install_command; then 66 | echo "X11 and Openbox have been successfully installed." 67 | else 68 | echo "Error: Failed to install X11 and Openbox." 69 | exit 1 70 | fi 71 | else 72 | echo "Error: Unsupported package manager." 73 | exit 1 74 | fi 75 | 76 | # Call the function to update X11Forwarding 77 | update_x11_forwarding 78 | 79 | # Get the IP address of the host dynamically 80 | host_ip=$(hostname -I | awk '{print $1}') 81 | xhost + "$host_ip" && xhost + local: 82 | # Set the DISPLAY variable to the host IP 83 | export DISPLAY="$host_ip:0.0" 84 | echo "DISPLAY variable set to $DISPLAY" 85 | 86 | elif [[ "$OSTYPE" == "darwin"* ]]; then 87 | # macOS 88 | if command -v xquartz >/dev/null 2>&1; then 89 | echo "XQuartz is already installed." 90 | else 91 | echo "Error: XQuartz is required for X11 forwarding on macOS. Please install XQuartz manually." 92 | exit 1 93 | fi 94 | 95 | # Call the function to update X11Forwarding 96 | update_x11_forwarding 97 | 98 | # Export the DISPLAY variable for macOS 99 | export DISPLAY=:0 100 | echo "DISPLAY variable set to $DISPLAY" 101 | else 102 | echo "Error: Unsupported operating system." 103 | exit 1 104 | fi 105 | -------------------------------------------------------------------------------- /overrides/compose.backup-cron.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | cron: 3 | image: mcuadros/ofelia:latest 4 | depends_on: 5 | - scheduler 6 | command: daemon --docker 7 | volumes: 8 | - /var/run/docker.sock:/var/run/docker.sock:ro 9 | 10 | scheduler: 11 | labels: 12 | ofelia.enabled: "true" 13 | ofelia.job-exec.datecron.schedule: "${BACKUP_CRONSTRING:-@every 6h}" 14 | ofelia.job-exec.datecron.command: "bench --site all backup" 15 | ofelia.job-exec.datecron.user: "frappe" 16 | -------------------------------------------------------------------------------- /overrides/compose.custom-domain-ssl.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | custom-domain: 3 | labels: 4 | - traefik.http.routers.${ROUTER}.entrypoints=http,https 5 | - traefik.http.routers.${ROUTER}.tls.certresolver=le 6 | -------------------------------------------------------------------------------- /overrides/compose.custom-domain.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | custom-domain: 5 | image: caddy:2 6 | command: 7 | - caddy 8 | - reverse-proxy 9 | - --to 10 | - frontend:8080 11 | - --from 12 | - :2016 13 | labels: 14 | - traefik.enable=true 15 | - traefik.docker.network=traefik-public 16 | - traefik.http.services.${ROUTER?ROUTER not set}.loadbalancer.server.port=2016 17 | - traefik.http.routers.${ROUTER}.service=${ROUTER} 18 | - traefik.http.routers.${ROUTER}.entrypoints=http 19 | - traefik.http.routers.${ROUTER}.rule=Host(${SITES?SITES not set}) 20 | - traefik.http.middlewares.${ROUTER}.headers.customrequestheaders.Host=${BASE_SITE?BASE_SITE not set} 21 | - traefik.http.routers.${ROUTER}.middlewares=${ROUTER} 22 | networks: 23 | - traefik-public 24 | - bench-network 25 | 26 | networks: 27 | traefik-public: 28 | external: true 29 | bench-network: 30 | name: ${BENCH_NETWORK?BENCH_NETWORK not set} 31 | external: true 32 | -------------------------------------------------------------------------------- /overrides/compose.https.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | labels: 4 | - traefik.enable=true 5 | - traefik.http.services.frontend.loadbalancer.server.port=8080 6 | - traefik.http.routers.frontend-http.entrypoints=websecure 7 | - traefik.http.routers.frontend-http.tls.certresolver=main-resolver 8 | - traefik.http.routers.frontend-http.rule=Host(${SITES:?List of sites not set}) 9 | 10 | proxy: 11 | image: traefik:v2.11 12 | restart: unless-stopped 13 | command: 14 | - --providers.docker=true 15 | - --providers.docker.exposedbydefault=false 16 | - --entrypoints.web.address=:80 17 | - --entrypoints.web.http.redirections.entrypoint.to=websecure 18 | - --entrypoints.web.http.redirections.entrypoint.scheme=https 19 | - --entrypoints.websecure.address=:443 20 | - --certificatesResolvers.main-resolver.acme.httpChallenge=true 21 | - --certificatesResolvers.main-resolver.acme.httpChallenge.entrypoint=web 22 | - --certificatesResolvers.main-resolver.acme.email=${LETSENCRYPT_EMAIL:?No Let's Encrypt email set} 23 | - --certificatesResolvers.main-resolver.acme.storage=/letsencrypt/acme.json 24 | ports: 25 | - ${HTTP_PUBLISH_PORT:-80}:80 26 | - ${HTTPS_PUBLISH_PORT:-443}:443 27 | volumes: 28 | - cert-data:/letsencrypt 29 | - /var/run/docker.sock:/var/run/docker.sock:ro 30 | 31 | volumes: 32 | cert-data: 33 | -------------------------------------------------------------------------------- /overrides/compose.mariadb-shared.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | database: 5 | container_name: mariadb-database 6 | image: mariadb:10.6 7 | restart: unless-stopped 8 | healthcheck: 9 | test: mysqladmin ping -h localhost --password=${DB_PASSWORD:-changeit} 10 | interval: 1s 11 | retries: 20 12 | command: 13 | - --character-set-server=utf8mb4 14 | - --collation-server=utf8mb4_unicode_ci 15 | - --skip-character-set-client-handshake 16 | - --skip-innodb-read-only-compressed 17 | environment: 18 | MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-changeit} 19 | volumes: 20 | - db-data:/var/lib/mysql 21 | networks: 22 | - mariadb-network 23 | 24 | networks: 25 | mariadb-network: 26 | name: mariadb-network 27 | external: false 28 | 29 | volumes: 30 | db-data: 31 | -------------------------------------------------------------------------------- /overrides/compose.mariadb.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | configurator: 3 | environment: 4 | DB_HOST: db 5 | DB_PORT: 3306 6 | depends_on: 7 | db: 8 | condition: service_healthy 9 | 10 | db: 11 | image: mariadb:10.6 12 | healthcheck: 13 | test: mysqladmin ping -h localhost --password=${DB_PASSWORD} 14 | interval: 1s 15 | retries: 20 16 | restart: unless-stopped 17 | command: 18 | - --character-set-server=utf8mb4 19 | - --collation-server=utf8mb4_unicode_ci 20 | - --skip-character-set-client-handshake 21 | - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 22 | environment: 23 | MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:?No db password set} 24 | volumes: 25 | - db-data:/var/lib/mysql 26 | 27 | volumes: 28 | db-data: 29 | -------------------------------------------------------------------------------- /overrides/compose.multi-bench-ssl.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | labels: 4 | # ${ROUTER}-http to use the middleware to redirect to https 5 | - traefik.http.routers.${ROUTER}-http.middlewares=https-redirect 6 | # ${ROUTER}-https the actual router using HTTPS 7 | # Uses the environment variable SITES 8 | - traefik.http.routers.${ROUTER}-https.rule=Host(${SITES?SITES not set}) 9 | - traefik.http.routers.${ROUTER}-https.entrypoints=https 10 | - traefik.http.routers.${ROUTER}-https.tls=true 11 | # Use the service ${ROUTER} with the frontend 12 | - traefik.http.routers.${ROUTER}-https.service=${ROUTER} 13 | # Use the "le" (Let's Encrypt) resolver created below 14 | - traefik.http.routers.${ROUTER}-https.tls.certresolver=le 15 | -------------------------------------------------------------------------------- /overrides/compose.multi-bench.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | networks: 4 | - traefik-public 5 | - bench-network 6 | labels: 7 | - traefik.enable=true 8 | - traefik.docker.network=traefik-public 9 | - traefik.http.services.${ROUTER?ROUTER not set}.loadbalancer.server.port=8080 10 | - traefik.http.routers.${ROUTER}-http.service=${ROUTER} 11 | - traefik.http.routers.${ROUTER}-http.entrypoints=http 12 | - traefik.http.routers.${ROUTER}-http.rule=Host(${SITES?SITES not set}) 13 | configurator: 14 | networks: 15 | - bench-network 16 | - mariadb-network 17 | backend: 18 | networks: 19 | - mariadb-network 20 | - bench-network 21 | websocket: 22 | networks: 23 | - bench-network 24 | - mariadb-network 25 | scheduler: 26 | networks: 27 | - bench-network 28 | - mariadb-network 29 | queue-short: 30 | networks: 31 | - bench-network 32 | - mariadb-network 33 | queue-long: 34 | networks: 35 | - bench-network 36 | - mariadb-network 37 | redis-cache: 38 | networks: 39 | - bench-network 40 | - mariadb-network 41 | 42 | redis-queue: 43 | networks: 44 | - bench-network 45 | - mariadb-network 46 | 47 | networks: 48 | traefik-public: 49 | external: true 50 | mariadb-network: 51 | external: true 52 | bench-network: 53 | name: ${ROUTER} 54 | external: false 55 | -------------------------------------------------------------------------------- /overrides/compose.noproxy.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | ports: 4 | - ${HTTP_PUBLISH_PORT:-8080}:8080 5 | -------------------------------------------------------------------------------- /overrides/compose.postgres.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | configurator: 3 | environment: 4 | DB_HOST: db 5 | DB_PORT: 5432 6 | depends_on: 7 | - db 8 | 9 | db: 10 | image: postgres:13.5 11 | command: [] 12 | environment: 13 | POSTGRES_PASSWORD: ${DB_PASSWORD:?No db password set} 14 | volumes: 15 | - db-data:/var/lib/postgresql/data 16 | 17 | volumes: 18 | db-data: 19 | -------------------------------------------------------------------------------- /overrides/compose.proxy.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | labels: 4 | - traefik.enable=true 5 | - traefik.http.services.frontend.loadbalancer.server.port=8080 6 | - traefik.http.routers.frontend-http.entrypoints=web 7 | - traefik.http.routers.frontend-http.rule=HostRegexp(`{any:.+}`) 8 | 9 | proxy: 10 | image: traefik:v2.11 11 | command: 12 | - --providers.docker 13 | - --providers.docker.exposedbydefault=false 14 | - --entrypoints.web.address=:80 15 | ports: 16 | - ${HTTP_PUBLISH_PORT:-80}:80 17 | volumes: 18 | - /var/run/docker.sock:/var/run/docker.sock:ro 19 | userns_mode: host 20 | -------------------------------------------------------------------------------- /overrides/compose.redis.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | configurator: 3 | environment: 4 | REDIS_CACHE: redis-cache:6379 5 | REDIS_QUEUE: redis-queue:6379 6 | depends_on: 7 | - redis-cache 8 | - redis-queue 9 | 10 | redis-cache: 11 | image: redis:6.2-alpine 12 | restart: unless-stopped 13 | 14 | redis-queue: 15 | image: redis:6.2-alpine 16 | restart: unless-stopped 17 | volumes: 18 | - redis-queue-data:/data 19 | 20 | volumes: 21 | redis-queue-data: 22 | -------------------------------------------------------------------------------- /overrides/compose.traefik-ssl.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | traefik: 3 | labels: 4 | # https-redirect middleware to redirect HTTP to HTTPS 5 | # It can be reused by other stacks in other Docker Compose files 6 | - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https 7 | - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true 8 | # traefik-http to use the middleware to redirect to https 9 | - traefik.http.routers.traefik-public-http.middlewares=https-redirect 10 | # traefik-https the actual router using HTTPS 11 | # Uses the environment variable DOMAIN 12 | - traefik.http.routers.traefik-public-https.rule=Host(`${TRAEFIK_DOMAIN}`) 13 | - traefik.http.routers.traefik-public-https.entrypoints=https 14 | - traefik.http.routers.traefik-public-https.tls=true 15 | # Use the special Traefik service api@internal with the web UI/Dashboard 16 | - traefik.http.routers.traefik-public-https.service=api@internal 17 | # Use the "le" (Let's Encrypt) resolver created below 18 | - traefik.http.routers.traefik-public-https.tls.certresolver=le 19 | # Enable HTTP Basic auth, using the middleware created above 20 | - traefik.http.routers.traefik-public-https.middlewares=admin-auth 21 | command: 22 | # Enable Docker in Traefik, so that it reads labels from Docker services 23 | - --providers.docker=true 24 | # Do not expose all Docker services, only the ones explicitly exposed 25 | - --providers.docker.exposedbydefault=false 26 | # Create an entrypoint http listening on port 80 27 | - --entrypoints.http.address=:80 28 | # Create an entrypoint https listening on port 443 29 | - --entrypoints.https.address=:443 30 | # Create the certificate resolver le for Let's Encrypt, uses the environment variable EMAIL 31 | - --certificatesresolvers.le.acme.email=${EMAIL:?No EMAIL set} 32 | # Store the Let's Encrypt certificates in the mounted volume 33 | - --certificatesresolvers.le.acme.storage=/certificates/acme.json 34 | # Use the TLS Challenge for Let's Encrypt 35 | - --certificatesresolvers.le.acme.tlschallenge=true 36 | # Enable the access log, with HTTP requests 37 | - --accesslog 38 | # Enable the Traefik log, for configurations and errors 39 | - --log 40 | # Enable the Dashboard and API 41 | - --api 42 | ports: 43 | - ${HTTPS_PUBLISH_PORT:-443}:443 44 | volumes: 45 | - cert-data:/certificates 46 | 47 | volumes: 48 | cert-data: 49 | -------------------------------------------------------------------------------- /overrides/compose.traefik.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | traefik: 5 | image: "traefik:v2.11" 6 | restart: unless-stopped 7 | labels: 8 | # Enable Traefik for this service, to make it available in the public network 9 | - traefik.enable=true 10 | # Use the traefik-public network (declared below) 11 | - traefik.docker.network=traefik-public 12 | # admin-auth middleware with HTTP Basic auth 13 | # Using the environment variables USERNAME and HASHED_PASSWORD 14 | - traefik.http.middlewares.admin-auth.basicauth.users=admin:${HASHED_PASSWORD:?No HASHED_PASSWORD set} 15 | # Uses the environment variable TRAEFIK_DOMAIN 16 | - traefik.http.routers.traefik-public-http.rule=Host(`${TRAEFIK_DOMAIN:?No TRAEFIK_DOMAIN set}`) 17 | - traefik.http.routers.traefik-public-http.entrypoints=http 18 | # Use the special Traefik service api@internal with the web UI/Dashboard 19 | - traefik.http.routers.traefik-public-http.service=api@internal 20 | # Enable HTTP Basic auth, using the middleware created above 21 | - traefik.http.routers.traefik-public-http.middlewares=admin-auth 22 | # Define the port inside of the Docker service to use 23 | - traefik.http.services.traefik-public.loadbalancer.server.port=8080 24 | command: 25 | # Enable Docker in Traefik, so that it reads labels from Docker services 26 | - --providers.docker=true 27 | # Do not expose all Docker services, only the ones explicitly exposed 28 | - --providers.docker.exposedbydefault=false 29 | # Create an entrypoint http listening on port 80 30 | - --entrypoints.http.address=:80 31 | # Enable the access log, with HTTP requests 32 | - --accesslog 33 | # Enable the Traefik log, for configurations and errors 34 | - --log 35 | # Enable the Dashboard and API 36 | - --api 37 | ports: 38 | - ${HTTP_PUBLISH_PORT:-80}:80 39 | volumes: 40 | - /var/run/docker.sock:/var/run/docker.sock:ro 41 | networks: 42 | - traefik-public 43 | 44 | networks: 45 | traefik-public: 46 | name: traefik-public 47 | external: false 48 | -------------------------------------------------------------------------------- /pwd.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | backend: 5 | image: frappe/erpnext:v15.63.0 6 | networks: 7 | - frappe_network 8 | deploy: 9 | restart_policy: 10 | condition: on-failure 11 | volumes: 12 | - sites:/home/frappe/frappe-bench/sites 13 | - logs:/home/frappe/frappe-bench/logs 14 | environment: 15 | DB_HOST: db 16 | DB_PORT: "3306" 17 | MYSQL_ROOT_PASSWORD: admin 18 | MARIADB_ROOT_PASSWORD: admin 19 | 20 | configurator: 21 | image: frappe/erpnext:v15.63.0 22 | networks: 23 | - frappe_network 24 | deploy: 25 | restart_policy: 26 | condition: none 27 | entrypoint: 28 | - bash 29 | - -c 30 | command: 31 | - > 32 | ls -1 apps > sites/apps.txt; 33 | bench set-config -g db_host $$DB_HOST; 34 | bench set-config -gp db_port $$DB_PORT; 35 | bench set-config -g redis_cache "redis://$$REDIS_CACHE"; 36 | bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; 37 | bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; 38 | bench set-config -gp socketio_port $$SOCKETIO_PORT; 39 | environment: 40 | DB_HOST: db 41 | DB_PORT: "3306" 42 | REDIS_CACHE: redis-cache:6379 43 | REDIS_QUEUE: redis-queue:6379 44 | SOCKETIO_PORT: "9000" 45 | volumes: 46 | - sites:/home/frappe/frappe-bench/sites 47 | - logs:/home/frappe/frappe-bench/logs 48 | 49 | create-site: 50 | image: frappe/erpnext:v15.63.0 51 | networks: 52 | - frappe_network 53 | deploy: 54 | restart_policy: 55 | condition: none 56 | volumes: 57 | - sites:/home/frappe/frappe-bench/sites 58 | - logs:/home/frappe/frappe-bench/logs 59 | entrypoint: 60 | - bash 61 | - -c 62 | command: 63 | - > 64 | wait-for-it -t 120 db:3306; 65 | wait-for-it -t 120 redis-cache:6379; 66 | wait-for-it -t 120 redis-queue:6379; 67 | export start=`date +%s`; 68 | until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \ 69 | [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \ 70 | [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]]; 71 | do 72 | echo "Waiting for sites/common_site_config.json to be created"; 73 | sleep 5; 74 | if (( `date +%s`-start > 120 )); then 75 | echo "could not find sites/common_site_config.json with required keys"; 76 | exit 1 77 | fi 78 | done; 79 | echo "sites/common_site_config.json found"; 80 | bench new-site --mariadb-user-host-login-scope='%' --admin-password=admin --db-root-username=root --db-root-password=admin --install-app erpnext --set-default frontend; 81 | 82 | db: 83 | image: mariadb:10.6 84 | networks: 85 | - frappe_network 86 | healthcheck: 87 | test: mysqladmin ping -h localhost --password=admin 88 | interval: 1s 89 | retries: 20 90 | deploy: 91 | restart_policy: 92 | condition: on-failure 93 | command: 94 | - --character-set-server=utf8mb4 95 | - --collation-server=utf8mb4_unicode_ci 96 | - --skip-character-set-client-handshake 97 | - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 98 | environment: 99 | MYSQL_ROOT_PASSWORD: admin 100 | MARIADB_ROOT_PASSWORD: admin 101 | volumes: 102 | - db-data:/var/lib/mysql 103 | 104 | frontend: 105 | image: frappe/erpnext:v15.63.0 106 | networks: 107 | - frappe_network 108 | depends_on: 109 | - websocket 110 | deploy: 111 | restart_policy: 112 | condition: on-failure 113 | command: 114 | - nginx-entrypoint.sh 115 | environment: 116 | BACKEND: backend:8000 117 | FRAPPE_SITE_NAME_HEADER: frontend 118 | SOCKETIO: websocket:9000 119 | UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1 120 | UPSTREAM_REAL_IP_HEADER: X-Forwarded-For 121 | UPSTREAM_REAL_IP_RECURSIVE: "off" 122 | PROXY_READ_TIMEOUT: 120 123 | CLIENT_MAX_BODY_SIZE: 50m 124 | volumes: 125 | - sites:/home/frappe/frappe-bench/sites 126 | - logs:/home/frappe/frappe-bench/logs 127 | ports: 128 | - "8080:8080" 129 | 130 | queue-long: 131 | image: frappe/erpnext:v15.63.0 132 | networks: 133 | - frappe_network 134 | deploy: 135 | restart_policy: 136 | condition: on-failure 137 | command: 138 | - bench 139 | - worker 140 | - --queue 141 | - long,default,short 142 | volumes: 143 | - sites:/home/frappe/frappe-bench/sites 144 | - logs:/home/frappe/frappe-bench/logs 145 | 146 | queue-short: 147 | image: frappe/erpnext:v15.63.0 148 | networks: 149 | - frappe_network 150 | deploy: 151 | restart_policy: 152 | condition: on-failure 153 | command: 154 | - bench 155 | - worker 156 | - --queue 157 | - short,default 158 | volumes: 159 | - sites:/home/frappe/frappe-bench/sites 160 | - logs:/home/frappe/frappe-bench/logs 161 | 162 | redis-queue: 163 | image: redis:6.2-alpine 164 | networks: 165 | - frappe_network 166 | deploy: 167 | restart_policy: 168 | condition: on-failure 169 | volumes: 170 | - redis-queue-data:/data 171 | 172 | redis-cache: 173 | image: redis:6.2-alpine 174 | networks: 175 | - frappe_network 176 | deploy: 177 | restart_policy: 178 | condition: on-failure 179 | 180 | scheduler: 181 | image: frappe/erpnext:v15.63.0 182 | networks: 183 | - frappe_network 184 | deploy: 185 | restart_policy: 186 | condition: on-failure 187 | command: 188 | - bench 189 | - schedule 190 | volumes: 191 | - sites:/home/frappe/frappe-bench/sites 192 | - logs:/home/frappe/frappe-bench/logs 193 | 194 | websocket: 195 | image: frappe/erpnext:v15.63.0 196 | networks: 197 | - frappe_network 198 | deploy: 199 | restart_policy: 200 | condition: on-failure 201 | command: 202 | - node 203 | - /home/frappe/frappe-bench/apps/frappe/socketio.js 204 | volumes: 205 | - sites:/home/frappe/frappe-bench/sites 206 | - logs:/home/frappe/frappe-bench/logs 207 | 208 | volumes: 209 | db-data: 210 | redis-queue-data: 211 | sites: 212 | logs: 213 | 214 | networks: 215 | frappe_network: 216 | driver: bridge 217 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest==8.3.5 2 | -------------------------------------------------------------------------------- /resources/nginx-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set variables that do not exist 4 | if [[ -z "$BACKEND" ]]; then 5 | echo "BACKEND defaulting to 0.0.0.0:8000" 6 | export BACKEND=0.0.0.0:8000 7 | fi 8 | if [[ -z "$SOCKETIO" ]]; then 9 | echo "SOCKETIO defaulting to 0.0.0.0:9000" 10 | export SOCKETIO=0.0.0.0:9000 11 | fi 12 | if [[ -z "$UPSTREAM_REAL_IP_ADDRESS" ]]; then 13 | echo "UPSTREAM_REAL_IP_ADDRESS defaulting to 127.0.0.1" 14 | export UPSTREAM_REAL_IP_ADDRESS=127.0.0.1 15 | fi 16 | if [[ -z "$UPSTREAM_REAL_IP_HEADER" ]]; then 17 | echo "UPSTREAM_REAL_IP_HEADER defaulting to X-Forwarded-For" 18 | export UPSTREAM_REAL_IP_HEADER=X-Forwarded-For 19 | fi 20 | if [[ -z "$UPSTREAM_REAL_IP_RECURSIVE" ]]; then 21 | echo "UPSTREAM_REAL_IP_RECURSIVE defaulting to off" 22 | export UPSTREAM_REAL_IP_RECURSIVE=off 23 | fi 24 | if [[ -z "$FRAPPE_SITE_NAME_HEADER" ]]; then 25 | # shellcheck disable=SC2016 26 | echo 'FRAPPE_SITE_NAME_HEADER defaulting to $host' 27 | # shellcheck disable=SC2016 28 | export FRAPPE_SITE_NAME_HEADER='$host' 29 | fi 30 | 31 | if [[ -z "$PROXY_READ_TIMEOUT" ]]; then 32 | echo "PROXY_READ_TIMEOUT defaulting to 120" 33 | export PROXY_READ_TIMEOUT=120 34 | fi 35 | 36 | if [[ -z "$CLIENT_MAX_BODY_SIZE" ]]; then 37 | echo "CLIENT_MAX_BODY_SIZE defaulting to 50m" 38 | export CLIENT_MAX_BODY_SIZE=50m 39 | fi 40 | 41 | # shellcheck disable=SC2016 42 | envsubst '${BACKEND} 43 | ${SOCKETIO} 44 | ${UPSTREAM_REAL_IP_ADDRESS} 45 | ${UPSTREAM_REAL_IP_HEADER} 46 | ${UPSTREAM_REAL_IP_RECURSIVE} 47 | ${FRAPPE_SITE_NAME_HEADER} 48 | ${PROXY_READ_TIMEOUT} 49 | ${CLIENT_MAX_BODY_SIZE}' \ 50 | /etc/nginx/conf.d/frappe.conf 51 | 52 | nginx -g 'daemon off;' 53 | -------------------------------------------------------------------------------- /resources/nginx-template.conf: -------------------------------------------------------------------------------- 1 | upstream backend-server { 2 | server ${BACKEND} fail_timeout=0; 3 | } 4 | 5 | upstream socketio-server { 6 | server ${SOCKETIO} fail_timeout=0; 7 | } 8 | 9 | server { 10 | listen 8080; 11 | server_name ${FRAPPE_SITE_NAME_HEADER}; 12 | root /home/frappe/frappe-bench/sites; 13 | 14 | proxy_buffer_size 128k; 15 | proxy_buffers 4 256k; 16 | proxy_busy_buffers_size 256k; 17 | 18 | add_header X-Frame-Options "SAMEORIGIN"; 19 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; 20 | add_header X-Content-Type-Options nosniff; 21 | add_header X-XSS-Protection "1; mode=block"; 22 | add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin"; 23 | 24 | set_real_ip_from ${UPSTREAM_REAL_IP_ADDRESS}; 25 | real_ip_header ${UPSTREAM_REAL_IP_HEADER}; 26 | real_ip_recursive ${UPSTREAM_REAL_IP_RECURSIVE}; 27 | 28 | location /assets { 29 | try_files $uri =404; 30 | } 31 | 32 | location ~ ^/protected/(.*) { 33 | internal; 34 | try_files /${FRAPPE_SITE_NAME_HEADER}/$1 =404; 35 | } 36 | 37 | location /socket.io { 38 | proxy_http_version 1.1; 39 | proxy_set_header Upgrade $http_upgrade; 40 | proxy_set_header Connection "upgrade"; 41 | proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER}; 42 | proxy_set_header Origin $scheme://$http_host; 43 | proxy_set_header Host $host; 44 | 45 | proxy_pass http://socketio-server; 46 | } 47 | 48 | location / { 49 | rewrite ^(.+)/$ $1 permanent; 50 | rewrite ^(.+)/index\.html$ $1 permanent; 51 | rewrite ^(.+)\.html$ $1 permanent; 52 | 53 | location ~ ^/files/.*.(htm|html|svg|xml) { 54 | add_header Content-disposition "attachment"; 55 | try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver; 56 | } 57 | 58 | try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver; 59 | } 60 | 61 | location @webserver { 62 | proxy_http_version 1.1; 63 | proxy_set_header X-Forwarded-For $remote_addr; 64 | proxy_set_header X-Forwarded-Proto $scheme; 65 | proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER}; 66 | proxy_set_header Host $host; 67 | proxy_set_header X-Use-X-Accel-Redirect True; 68 | proxy_read_timeout ${PROXY_READ_TIMEOUT}; 69 | proxy_redirect off; 70 | 71 | proxy_pass http://backend-server; 72 | } 73 | 74 | # optimizations 75 | sendfile on; 76 | keepalive_timeout 15; 77 | client_max_body_size ${CLIENT_MAX_BODY_SIZE}; 78 | client_body_buffer_size 16K; 79 | client_header_buffer_size 1k; 80 | 81 | # enable gzip compression 82 | # based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge 83 | gzip on; 84 | gzip_http_version 1.1; 85 | gzip_comp_level 5; 86 | gzip_min_length 256; 87 | gzip_proxied any; 88 | gzip_vary on; 89 | gzip_types 90 | application/atom+xml 91 | application/javascript 92 | application/json 93 | application/rss+xml 94 | application/vnd.ms-fontobject 95 | application/x-font-ttf 96 | application/font-woff 97 | application/x-web-app-manifest+json 98 | application/xhtml+xml 99 | application/xml 100 | font/opentype 101 | image/svg+xml 102 | image/x-icon 103 | text/css 104 | text/plain 105 | text/x-component; 106 | # text/html is always compressed by HttpGzipModule 107 | } 108 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Config file for isort, codespell and other Python projects. 2 | # In this case it is not used for packaging. 3 | 4 | [isort] 5 | profile = black 6 | known_third_party = frappe 7 | 8 | [codespell] 9 | skip = images/bench/Dockerfile 10 | 11 | [tool:pytest] 12 | addopts = -s --exitfirst 13 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/frappe_docker/ebd80217309e1d9230f09fa984c0f2d40e84704d/tests/__init__.py -------------------------------------------------------------------------------- /tests/_check_connections.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import json 5 | import socket 6 | from typing import Any, Iterable, Tuple 7 | 8 | Address = Tuple[str, int] 9 | 10 | 11 | async def wait_for_port(address: Address) -> None: 12 | # From https://github.com/clarketm/wait-for-it 13 | while True: 14 | try: 15 | _, writer = await asyncio.open_connection(*address) 16 | writer.close() 17 | await writer.wait_closed() 18 | break 19 | except (socket.gaierror, ConnectionError, OSError, TypeError): 20 | pass 21 | await asyncio.sleep(0.1) 22 | 23 | 24 | def get_redis_url(addr: str) -> Address: 25 | result = addr.replace("redis://", "") 26 | result = result.split("/")[0] 27 | parts = result.split(":") 28 | assert len(parts) == 2 29 | return parts[0], int(parts[1]) 30 | 31 | 32 | def get_addresses(config: dict[str, Any]) -> Iterable[Address]: 33 | yield (config["db_host"], config["db_port"]) 34 | for key in ("redis_cache", "redis_queue"): 35 | yield get_redis_url(config[key]) 36 | 37 | 38 | async def async_main(addresses: set[Address]) -> None: 39 | tasks = [asyncio.wait_for(wait_for_port(addr), timeout=5) for addr in addresses] 40 | await asyncio.gather(*tasks) 41 | 42 | 43 | def main() -> int: 44 | with open("/home/frappe/frappe-bench/sites/common_site_config.json") as f: 45 | config = json.load(f) 46 | addresses = set(get_addresses(config)) 47 | asyncio.run(async_main(addresses)) 48 | return 0 49 | 50 | 51 | if __name__ == "__main__": 52 | raise SystemExit(main()) 53 | -------------------------------------------------------------------------------- /tests/_check_website_theme.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def check_website_theme(): 5 | doc = frappe.new_doc("Website Theme") 6 | doc.theme = "test theme" 7 | doc.insert() 8 | 9 | 10 | def main() -> int: 11 | frappe.connect(site="tests") 12 | check_website_theme() 13 | return 0 14 | 15 | 16 | if __name__ == "__main__": 17 | raise SystemExit(main()) 18 | -------------------------------------------------------------------------------- /tests/_create_bucket.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import boto3 4 | 5 | 6 | def main() -> int: 7 | resource = boto3.resource( 8 | service_name="s3", 9 | endpoint_url="http://minio:9000", 10 | region_name="us-east-1", 11 | aws_access_key_id=os.getenv("S3_ACCESS_KEY"), 12 | aws_secret_access_key=os.getenv("S3_SECRET_KEY"), 13 | ) 14 | resource.create_bucket(Bucket="frappe") 15 | return 0 16 | 17 | 18 | if __name__ == "__main__": 19 | raise SystemExit(main()) 20 | -------------------------------------------------------------------------------- /tests/_ping_frappe_connections.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def check_db(): 5 | doc = frappe.get_single("System Settings") 6 | assert any(v is None for v in doc.as_dict().values()), "Database test didn't pass" 7 | print("Database works!") 8 | 9 | 10 | def check_cache(): 11 | key_and_name = "mytestkey", "mytestname" 12 | frappe.cache().hset(*key_and_name, "mytestvalue") 13 | assert frappe.cache().hget(*key_and_name) == "mytestvalue", "Cache test didn't pass" 14 | frappe.cache().hdel(*key_and_name) 15 | print("Cache works!") 16 | 17 | 18 | def main() -> int: 19 | frappe.connect(site="tests.localhost") 20 | check_db() 21 | check_cache() 22 | return 0 23 | 24 | 25 | if __name__ == "__main__": 26 | raise SystemExit(main()) 27 | -------------------------------------------------------------------------------- /tests/compose.ci.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | configurator: 3 | image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} 4 | 5 | backend: 6 | image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} 7 | 8 | frontend: 9 | image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} 10 | 11 | websocket: 12 | image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} 13 | 14 | queue-short: 15 | image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} 16 | 17 | queue-long: 18 | image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} 19 | 20 | scheduler: 21 | image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} 22 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import subprocess 5 | from dataclasses import dataclass 6 | from pathlib import Path 7 | 8 | import pytest 9 | 10 | from tests.utils import CI, Compose 11 | 12 | 13 | def _add_version_var(name: str, env_path: Path): 14 | value = os.getenv(name) 15 | 16 | if not value: 17 | return 18 | 19 | if value == "develop": 20 | os.environ[name] = "latest" 21 | 22 | with open(env_path, "a") as f: 23 | f.write(f"\n{name}={os.environ[name]}") 24 | 25 | 26 | def _add_sites_var(env_path: Path): 27 | with open(env_path, "r+") as f: 28 | content = f.read() 29 | content = re.sub( 30 | rf"SITES=.*", 31 | f"SITES=`tests.localhost`,`test-erpnext-site.localhost`,`test-pg-site.localhost`", 32 | content, 33 | ) 34 | f.seek(0) 35 | f.truncate() 36 | f.write(content) 37 | 38 | 39 | @pytest.fixture(scope="session") 40 | def env_file(tmp_path_factory: pytest.TempPathFactory): 41 | tmp_path = tmp_path_factory.mktemp("frappe-docker") 42 | file_path = tmp_path / ".env" 43 | shutil.copy("example.env", file_path) 44 | 45 | _add_sites_var(file_path) 46 | 47 | for var in ("FRAPPE_VERSION", "ERPNEXT_VERSION"): 48 | _add_version_var(name=var, env_path=file_path) 49 | 50 | yield str(file_path) 51 | os.remove(file_path) 52 | 53 | 54 | @pytest.fixture(scope="session") 55 | def compose(env_file: str): 56 | return Compose(project_name="test", env_file=env_file) 57 | 58 | 59 | @pytest.fixture(autouse=True, scope="session") 60 | def frappe_setup(compose: Compose): 61 | compose.stop() 62 | 63 | compose("up", "-d", "--quiet-pull") 64 | yield 65 | 66 | compose.stop() 67 | 68 | 69 | @pytest.fixture(scope="session") 70 | def frappe_site(compose: Compose): 71 | site_name = "tests.localhost" 72 | compose.bench( 73 | "new-site", 74 | # TODO: change to --mariadb-user-host-login-scope=% 75 | "--no-mariadb-socket", 76 | "--db-root-password=123", 77 | "--admin-password=admin", 78 | site_name, 79 | ) 80 | compose("restart", "backend") 81 | yield site_name 82 | 83 | 84 | @pytest.fixture(scope="class") 85 | def erpnext_setup(compose: Compose): 86 | compose.stop() 87 | compose("up", "-d", "--quiet-pull") 88 | 89 | yield 90 | compose.stop() 91 | 92 | 93 | @pytest.fixture(scope="class") 94 | def erpnext_site(compose: Compose): 95 | site_name = "test-erpnext-site.localhost" 96 | args = [ 97 | "new-site", 98 | # TODO: change to --mariadb-user-host-login-scope=% 99 | "--no-mariadb-socket", 100 | "--db-root-password=123", 101 | "--admin-password=admin", 102 | "--install-app=erpnext", 103 | site_name, 104 | ] 105 | compose.bench(*args) 106 | compose("restart", "backend") 107 | yield site_name 108 | 109 | 110 | @pytest.fixture 111 | def postgres_setup(compose: Compose): 112 | compose.stop() 113 | compose("-f", "overrides/compose.postgres.yaml", "up", "-d", "--quiet-pull") 114 | compose.bench("set-config", "-g", "root_login", "postgres") 115 | compose.bench("set-config", "-g", "root_password", "123") 116 | yield 117 | compose.stop() 118 | 119 | 120 | @pytest.fixture 121 | def python_path(): 122 | return "/home/frappe/frappe-bench/env/bin/python" 123 | 124 | 125 | @dataclass 126 | class S3ServiceResult: 127 | access_key: str 128 | secret_key: str 129 | 130 | 131 | @pytest.fixture 132 | def s3_service(python_path: str, compose: Compose): 133 | access_key = "AKIAIOSFODNN7EXAMPLE" 134 | secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 135 | cmd = ( 136 | "docker", 137 | "run", 138 | "--name", 139 | "minio", 140 | "-d", 141 | "-e", 142 | f"MINIO_ACCESS_KEY={access_key}", 143 | "-e", 144 | f"MINIO_SECRET_KEY={secret_key}", 145 | "--network", 146 | f"{compose.project_name}_default", 147 | "minio/minio", 148 | "server", 149 | "/data", 150 | ) 151 | subprocess.check_call(cmd) 152 | 153 | compose("cp", "tests/_create_bucket.py", "backend:/tmp") 154 | compose.exec( 155 | "-e", 156 | f"S3_ACCESS_KEY={access_key}", 157 | "-e", 158 | f"S3_SECRET_KEY={secret_key}", 159 | "backend", 160 | python_path, 161 | "/tmp/_create_bucket.py", 162 | ) 163 | 164 | yield S3ServiceResult(access_key=access_key, secret_key=secret_key) 165 | subprocess.call(("docker", "rm", "minio", "-f")) 166 | -------------------------------------------------------------------------------- /tests/test_frappe_docker.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | import pytest 6 | 7 | from tests.conftest import S3ServiceResult 8 | from tests.utils import Compose, check_url_content 9 | 10 | BACKEND_SERVICES = ( 11 | "backend", 12 | "queue-short", 13 | "queue-long", 14 | "scheduler", 15 | ) 16 | 17 | 18 | @pytest.mark.parametrize("service", BACKEND_SERVICES) 19 | def test_links_in_backends(service: str, compose: Compose, python_path: str): 20 | filename = "_check_connections.py" 21 | compose("cp", f"tests/{filename}", f"{service}:/tmp/") 22 | compose.exec(service, python_path, f"/tmp/{filename}") 23 | 24 | 25 | def index_cb(text: str): 26 | if "404 page not found" not in text: 27 | return text[:200] 28 | 29 | 30 | def api_cb(text: str): 31 | if '"message"' in text: 32 | return text 33 | 34 | 35 | def assets_cb(text: str): 36 | if text: 37 | return text[:200] 38 | 39 | 40 | @pytest.mark.parametrize( 41 | ("url", "callback"), (("/", index_cb), ("/api/method/ping", api_cb)) 42 | ) 43 | def test_endpoints(url: str, callback: Any, frappe_site: str): 44 | check_url_content( 45 | url=f"http://127.0.0.1{url}", callback=callback, site_name=frappe_site 46 | ) 47 | 48 | 49 | @pytest.mark.skipif( 50 | os.environ["FRAPPE_VERSION"][0:3] == "v12", reason="v12 doesn't have the asset" 51 | ) 52 | def test_assets_endpoint(frappe_site: str): 53 | check_url_content( 54 | url=f"http://127.0.0.1/assets/frappe/images/frappe-framework-logo.svg", 55 | callback=assets_cb, 56 | site_name=frappe_site, 57 | ) 58 | 59 | 60 | def test_files_reachable(frappe_site: str, tmp_path: Path, compose: Compose): 61 | content = "lalala\n" 62 | file_path = tmp_path / "testfile.txt" 63 | 64 | with file_path.open("w") as f: 65 | f.write(content) 66 | 67 | compose( 68 | "cp", 69 | str(file_path), 70 | f"backend:/home/frappe/frappe-bench/sites/{frappe_site}/public/files/", 71 | ) 72 | 73 | def callback(text: str): 74 | if text == content: 75 | return text 76 | 77 | check_url_content( 78 | url=f"http://127.0.0.1/files/{file_path.name}", 79 | callback=callback, 80 | site_name=frappe_site, 81 | ) 82 | 83 | 84 | @pytest.mark.parametrize("service", BACKEND_SERVICES) 85 | @pytest.mark.usefixtures("frappe_site") 86 | def test_frappe_connections_in_backends( 87 | service: str, python_path: str, compose: Compose 88 | ): 89 | filename = "_ping_frappe_connections.py" 90 | compose("cp", f"tests/{filename}", f"{service}:/tmp/") 91 | compose.exec( 92 | "-w", 93 | "/home/frappe/frappe-bench/sites", 94 | service, 95 | python_path, 96 | f"/tmp/{filename}", 97 | ) 98 | 99 | 100 | def test_push_backup( 101 | frappe_site: str, 102 | s3_service: S3ServiceResult, 103 | compose: Compose, 104 | ): 105 | restic_password = "secret" 106 | compose.bench("--site", frappe_site, "backup", "--with-files") 107 | restic_args = [ 108 | "--env=RESTIC_REPOSITORY=s3:http://minio:9000/frappe", 109 | f"--env=AWS_ACCESS_KEY_ID={s3_service.access_key}", 110 | f"--env=AWS_SECRET_ACCESS_KEY={s3_service.secret_key}", 111 | f"--env=RESTIC_PASSWORD={restic_password}", 112 | ] 113 | compose.exec(*restic_args, "backend", "restic", "init") 114 | compose.exec(*restic_args, "backend", "restic", "backup", "sites") 115 | compose.exec(*restic_args, "backend", "restic", "snapshots") 116 | 117 | 118 | def test_https(frappe_site: str, compose: Compose): 119 | compose("-f", "overrides/compose.https.yaml", "up", "-d") 120 | check_url_content(url="https://127.0.0.1", callback=index_cb, site_name=frappe_site) 121 | 122 | 123 | @pytest.mark.usefixtures("erpnext_setup") 124 | class TestErpnext: 125 | @pytest.mark.parametrize( 126 | ("url", "callback"), 127 | ( 128 | ( 129 | "/api/method/erpnext.templates.pages.search_help.get_help_results_sections?text=help", 130 | api_cb, 131 | ), 132 | ("/assets/erpnext/js/setup_wizard.js", assets_cb), 133 | ), 134 | ) 135 | def test_endpoints(self, url: str, callback: Any, erpnext_site: str): 136 | check_url_content( 137 | url=f"http://127.0.0.1{url}", callback=callback, site_name=erpnext_site 138 | ) 139 | 140 | 141 | @pytest.mark.usefixtures("postgres_setup") 142 | class TestPostgres: 143 | def test_site_creation(self, compose: Compose): 144 | compose.bench( 145 | "new-site", 146 | "test-pg-site.localhost", 147 | "--db-type", 148 | "postgres", 149 | "--admin-password", 150 | "admin", 151 | ) 152 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ssl 3 | import subprocess 4 | import sys 5 | import time 6 | from contextlib import suppress 7 | from typing import Callable, Optional 8 | from urllib.error import HTTPError, URLError 9 | from urllib.request import Request, urlopen 10 | 11 | CI = os.getenv("CI") 12 | 13 | 14 | class Compose: 15 | def __init__(self, project_name: str, env_file: str): 16 | self.project_name = project_name 17 | self.base_cmd = ( 18 | "docker", 19 | "compose", 20 | "-p", 21 | project_name, 22 | "--env-file", 23 | env_file, 24 | ) 25 | 26 | def __call__(self, *cmd: str) -> None: 27 | file_args = [ 28 | "-f", 29 | "compose.yaml", 30 | "-f", 31 | "overrides/compose.proxy.yaml", 32 | "-f", 33 | "overrides/compose.mariadb.yaml", 34 | "-f", 35 | "overrides/compose.redis.yaml", 36 | ] 37 | if CI: 38 | file_args += ("-f", "tests/compose.ci.yaml") 39 | 40 | args = self.base_cmd + tuple(file_args) + cmd 41 | subprocess.check_call(args) 42 | 43 | def exec(self, *cmd: str) -> None: 44 | if sys.stdout.isatty(): 45 | self("exec", *cmd) 46 | else: 47 | self("exec", "-T", *cmd) 48 | 49 | def stop(self) -> None: 50 | # Stop all containers in `test` project if they are running. 51 | # We don't care if it fails. 52 | with suppress(subprocess.CalledProcessError): 53 | subprocess.check_call(self.base_cmd + ("down", "-v", "--remove-orphans")) 54 | 55 | def bench(self, *cmd: str) -> None: 56 | self.exec("backend", "bench", *cmd) 57 | 58 | 59 | def check_url_content( 60 | url: str, callback: Callable[[str], Optional[str]], site_name: str 61 | ): 62 | request = Request(url, headers={"Host": site_name}) 63 | 64 | # This is needed to check https override 65 | ctx = ssl.create_default_context() 66 | ctx.check_hostname = False 67 | ctx.verify_mode = ssl.CERT_NONE 68 | 69 | for _ in range(100): 70 | try: 71 | response = urlopen(request, context=ctx) 72 | 73 | except HTTPError as exc: 74 | if exc.code not in (404, 502): 75 | raise 76 | 77 | except URLError: 78 | pass 79 | 80 | else: 81 | text: str = response.read().decode() 82 | ret = callback(text) 83 | if ret: 84 | print(ret) 85 | return 86 | 87 | time.sleep(0.1) 88 | 89 | raise RuntimeError(f"Couldn't ping {url}") 90 | --------------------------------------------------------------------------------