This commit is contained in:
Minh 2025-11-17 13:22:53 +00:00
parent 1a799ee4da
commit 8a558f4a16
89 changed files with 6616 additions and 6614 deletions

View file

@ -1,4 +1,4 @@
README.md README.md
LICENSE LICENSE
.gitignore .gitignore
compose*.yaml compose*.yaml

View file

@ -1,19 +1,19 @@
# http://editorconfig.org # http://editorconfig.org
root = true root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.{py, pyi}] [*.{py, pyi}]
indent_size = 4 indent_size = 4
[*Dockerfile*] [*Dockerfile*]
indent_size = 4 indent_size = 4
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false

View file

@ -1,34 +1,34 @@
--- ---
name: Bug report name: Bug report
about: Report a bug encountered while using the Frappe_docker about: Report a bug encountered while using the Frappe_docker
labels: bug labels: bug
--- ---
<!-- <!--
Welcome to the frappe_docker issue tracker! Before creating an issue, please heed the following: Welcome to the frappe_docker issue tracker! Before creating an issue, please heed the following:
1. Is your issue relevant to the frappe_docker or the main Frappe framework? https://github.com/frappe/frappe . if It's the latter, publish the issue there. 1. Is your issue relevant to the frappe_docker or the main Frappe framework? https://github.com/frappe/frappe . if It's the latter, publish the issue there.
2. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion. 2. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion.
3. When making a bug report, make sure you provide all the required information. The easier it is for maintainers to reproduce, the faster it'll be fixed. 3. When making a bug report, make sure you provide all the required information. The easier it is for maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉 4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
--> -->
## Description of the issue ## Description of the issue
## Context information (for bug reports) ## Context information (for bug reports)
## Steps to reproduce the issue ## Steps to reproduce the issue
1. 1.
2. 2.
3. 3.
### Observed result ### Observed result
### Expected result ### Expected result
### Stacktrace / full error message if available ### Stacktrace / full error message if available
``` ```
(paste here) (paste here)
``` ```

View file

@ -1,23 +1,23 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea to improve frappe_docker about: Suggest an idea to improve frappe_docker
labels: enhancement labels: enhancement
--- ---
<!-- <!--
Welcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following: Welcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:
1. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion. 1. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion.
2. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen. 2. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
--> -->
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like** **Describe the solution you'd like**
A clear and concise description of what you want to happen. A clear and concise description of what you want to happen.
**Describe alternatives you've considered** **Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered. A clear and concise description of any alternative solutions or features you've considered.
**Additional context** **Additional context**
Add any other context or screenshots about the feature request here. Add any other context or screenshots about the feature request here.

View file

@ -1,12 +1,12 @@
--- ---
name: Question about using Frappe/Frappe Apps name: Question about using Frappe/Frappe Apps
about: Ask how to do something about: Ask how to do something
labels: question labels: question
--- ---
<!-- <!--
Welcome to the frappe_docker issue tracker! Before creating an issue, please heed the following: Welcome to the frappe_docker issue tracker! Before creating an issue, please heed the following:
1. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion. 1. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion.
2. Please write extensively, clearly and in detail. 2. Please write extensively, clearly and in detail.
--> -->

View file

@ -1,7 +1,7 @@
> Please provide enough information so that others can review your pull request: > Please provide enough information so that others can review your pull request:
<!-- You can skip this if you're fixing a typo or updating existing documentation --> <!-- You can skip this if you're fixing a typo or updating existing documentation -->
> Explain the **details** for making this change. What existing problem does the pull request solve? > Explain the **details** for making this change. What existing problem does the pull request solve?
<!-- Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. --> <!-- Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. -->

View file

@ -1,26 +1,26 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
interval: daily interval: daily
- package-ecosystem: docker - package-ecosystem: docker
directory: images/bench directory: images/bench
schedule: schedule:
interval: daily interval: daily
- package-ecosystem: docker - package-ecosystem: docker
directory: images/production directory: images/production
schedule: schedule:
interval: daily interval: daily
- package-ecosystem: docker - package-ecosystem: docker
directory: images/custom directory: images/custom
schedule: schedule:
interval: daily interval: daily
- package-ecosystem: pip - package-ecosystem: pip
directory: / directory: /
schedule: schedule:
interval: daily interval: daily

View file

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

View file

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

View file

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

View file

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

View file

@ -1,33 +1,33 @@
name: Develop build name: Develop build
on: on:
pull_request: pull_request:
branches: branches:
- main - main
paths: paths:
- images/production/** - images/production/**
- overrides/** - overrides/**
- tests/** - tests/**
- compose.yaml - compose.yaml
- docker-bake.hcl - docker-bake.hcl
- example.env - example.env
- .github/workflows/build_develop.yml - .github/workflows/build_develop.yml
schedule: schedule:
# Every day at 12:00 pm # Every day at 12:00 pm
- cron: 0 0 * * * - cron: 0 0 * * *
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build:
uses: ./.github/workflows/docker-build-push.yml uses: ./.github/workflows/docker-build-push.yml
with: with:
repo: erpnext repo: erpnext
version: develop version: develop
push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
python_version: 3.11.6 python_version: 3.11.6
node_version: 20.19.2 node_version: 20.19.2
secrets: secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

View file

@ -1,116 +1,116 @@
name: Stable build name: Stable build
on: on:
pull_request: pull_request:
branches: branches:
- main - main
paths: paths:
- images/production/** - images/production/**
- overrides/** - overrides/**
- tests/** - tests/**
- compose.yaml - compose.yaml
- docker-bake.hcl - docker-bake.hcl
- example.env - example.env
- .github/workflows/build_stable.yml - .github/workflows/build_stable.yml
push: push:
branches: branches:
- main - main
paths: paths:
- images/production/** - images/production/**
- overrides/** - overrides/**
- tests/** - tests/**
- compose.yaml - compose.yaml
- docker-bake.hcl - docker-bake.hcl
- example.env - example.env
# Triggered from frappe/frappe and frappe/erpnext on releases # Triggered from frappe/frappe and frappe/erpnext on releases
repository_dispatch: repository_dispatch:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
v14: v14:
uses: ./.github/workflows/docker-build-push.yml uses: ./.github/workflows/docker-build-push.yml
with: with:
repo: erpnext repo: erpnext
version: "14" version: "14"
push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
python_version: 3.10.13 python_version: 3.10.13
node_version: 16.20.2 node_version: 16.20.2
secrets: secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
v15: v15:
uses: ./.github/workflows/docker-build-push.yml uses: ./.github/workflows/docker-build-push.yml
with: with:
repo: erpnext repo: erpnext
version: "15" version: "15"
push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} push: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
python_version: 3.11.6 python_version: 3.11.6
node_version: 20.19.2 node_version: 20.19.2
secrets: secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
update_versions: update_versions:
name: Update example.env and pwd.yml name: Update example.env and pwd.yml
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
needs: v15 needs: v15
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.10" python-version: "3.10"
- name: Get latest versions - name: Get latest versions
run: python3 ./.github/scripts/get_latest_tags.py --repo erpnext --version 15 run: python3 ./.github/scripts/get_latest_tags.py --repo erpnext --version 15
- name: Update - name: Update
run: | run: |
python3 ./.github/scripts/update_example_env.py python3 ./.github/scripts/update_example_env.py
python3 ./.github/scripts/update_pwd.py python3 ./.github/scripts/update_pwd.py
- name: Push - name: Push
run: | run: |
git config --global user.name github-actions git config --global user.name github-actions
git config --global user.email github-actions@github.com git config --global user.email github-actions@github.com
git add example.env pwd.yml git add example.env pwd.yml
if [ -z "$(git status --porcelain)" ]; then if [ -z "$(git status --porcelain)" ]; then
echo "versions did not change, exiting." echo "versions did not change, exiting."
exit 0 exit 0
else else
echo "version changed, pushing changes..." echo "version changed, pushing changes..."
git commit -m "chore: Update example.env" git commit -m "chore: Update example.env"
git pull --rebase git pull --rebase
git push origin main git push origin main
fi fi
release_helm: release_helm:
name: Release Helm name: Release Helm
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }} if: ${{ github.repository == 'frappe/frappe_docker' && github.event_name != 'pull_request' }}
needs: v15 needs: v15
steps: steps:
- name: Setup deploy key - name: Setup deploy key
uses: webfactory/ssh-agent@v0.9.1 uses: webfactory/ssh-agent@v0.9.1
with: with:
ssh-private-key: ${{ secrets.HELM_DEPLOY_KEY }} ssh-private-key: ${{ secrets.HELM_DEPLOY_KEY }}
- name: Setup Git Credentials - name: Setup Git Credentials
run: | run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]" git config --global user.name "github-actions[bot]"
- name: Release - name: Release
run: | run: |
git clone git@github.com:frappe/helm.git && cd helm git clone git@github.com:frappe/helm.git && cd helm
pip install -r release_wizard/requirements.txt pip install -r release_wizard/requirements.txt
./release_wizard/wizard 15 patch --remote origin --ci ./release_wizard/wizard 15 patch --remote origin --ci

View file

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

View file

@ -1,35 +1,35 @@
name: Lint name: Lint
on: on:
push: push:
branches: branches:
- main - main
pull_request: pull_request:
branches: branches:
- main - main
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.10.6" python-version: "3.10.6"
# For shfmt pre-commit hook # For shfmt pre-commit hook
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version: "^1.14" go-version: "^1.14"
- name: Install pre-commit - name: Install pre-commit
run: pip install -U pre-commit run: pip install -U pre-commit
- name: Lint - name: Lint
run: pre-commit run --color=always --all-files run: pre-commit run --color=always --all-files
env: env:
GO111MODULE: on GO111MODULE: on

View file

@ -1,26 +1,26 @@
name: Autoupdate pre-commit hooks name: Autoupdate pre-commit hooks
on: on:
schedule: schedule:
# Every day at 7 am # Every day at 7 am
- cron: 0 7 * * * - cron: 0 7 * * *
jobs: jobs:
pre-commit-autoupdate: pre-commit-autoupdate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Update pre-commit hooks - name: Update pre-commit hooks
uses: vrslev/pre-commit-autoupdate@v1.0.0 uses: vrslev/pre-commit-autoupdate@v1.0.0
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7
with: with:
branch: pre-commit-autoupdate branch: pre-commit-autoupdate
title: "chore(deps): Update pre-commit hooks" title: "chore(deps): Update pre-commit hooks"
commit-message: "chore(deps): Update pre-commit hooks" commit-message: "chore(deps): Update pre-commit hooks"
body: Update pre-commit hooks body: Update pre-commit hooks
labels: dependencies,development labels: dependencies,development
delete-branch: True delete-branch: True

View file

@ -1,18 +1,18 @@
name: Mark stale issues and pull requests name: Mark stale issues and pull requests
on: on:
schedule: schedule:
# Every day at 12:00 pm # Every day at 12:00 pm
- cron: 0 0 * * * - cron: 0 0 * * *
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v10 - uses: actions/stale@v10
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: This issue has been automatically marked as stale. You have a week to explain why you believe this is an error. stale-issue-message: This issue has been automatically marked as stale. You have a week to explain why you believe this is an error.
stale-pr-message: This PR has been automatically marked as stale. You have a week to explain why you believe this is an error. stale-pr-message: This PR has been automatically marked as stale. You have a week to explain why you believe this is an error.
stale-issue-label: no-issue-activity stale-issue-label: no-issue-activity
stale-pr-label: no-pr-activity stale-pr-label: no-pr-activity

60
.gitignore vendored
View file

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

View file

@ -1,55 +1,55 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v5.0.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.19.1 rev: v3.19.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 25.1.0 rev: 25.1.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: 6.0.1 rev: 6.0.1
hooks: hooks:
- id: isort - id: isort
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8 rev: v4.0.0-alpha.8
hooks: hooks:
- id: prettier - id: prettier
additional_dependencies: additional_dependencies:
- prettier@3.5.2 - prettier@3.5.2
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.4.1 rev: v2.4.1
hooks: hooks:
- id: codespell - id: codespell
args: args:
- -L - -L
- "ro" - "ro"
- repo: local - repo: local
hooks: hooks:
- id: shfmt - id: shfmt
name: shfmt name: shfmt
language: golang language: golang
additional_dependencies: [mvdan.cc/sh/v3/cmd/shfmt@latest] additional_dependencies: [mvdan.cc/sh/v3/cmd/shfmt@latest]
entry: shfmt entry: shfmt
args: [-w] args: [-w]
types: [shell] types: [shell]
- repo: https://github.com/shellcheck-py/shellcheck-py - repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1 rev: v0.10.0.1
hooks: hooks:
- id: shellcheck - id: shellcheck
args: [-x] args: [-x]

View file

@ -1 +1 @@
external-sources=true external-sources=true

View file

@ -1,9 +1,9 @@
{ {
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace. // List of extensions which should be recommended for users of this workspace.
"recommendations": ["ms-vscode-remote.remote-containers"], "recommendations": ["ms-vscode-remote.remote-containers"],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace. // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [] "unwantedRecommendations": []
} }

View file

@ -1,76 +1,76 @@
# Contributor Covenant Code of Conduct # Contributor Covenant Code of Conduct
## Our Pledge ## Our Pledge
In the interest of fostering an open and welcoming environment, we as In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression, size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socioeconomic status, nationality, personal level of experience, education, socioeconomic status, nationality, personal
appearance, race, religion, or sexual identity and orientation. appearance, race, religion, or sexual identity and orientation.
## Our Standards ## Our Standards
Examples of behavior that contributes to creating a positive environment Examples of behavior that contributes to creating a positive environment
include: include:
- Using welcoming and inclusive language - Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences - Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism - Gracefully accepting constructive criticism
- Focusing on what is best for the community - Focusing on what is best for the community
- Showing empathy towards other community members - Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or - The use of sexualized language or imagery and unwelcome sexual attention or
advances advances
- Trolling, insulting/derogatory comments, and personal or political attacks - Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment - Public or private harassment
- Publishing others' private information, such as a physical or electronic - Publishing others' private information, such as a physical or electronic
address, without explicit permission address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Our Responsibilities ## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior. response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate, permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful. threatening, offensive, or harmful.
## Scope ## Scope
This Code of Conduct applies both within project spaces and in public spaces This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers. further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at hello@frappe.io. All reported by contacting the project team at hello@frappe.io. All
complaints will be reviewed and investigated and will result in a response that complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident. obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other faith may face temporary or permanent repercussions as determined by other
members of the project's leadership. members of the project's leadership.
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org [homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq https://www.contributor-covenant.org/faq

View file

@ -1,81 +1,81 @@
# Contribution Guidelines # Contribution Guidelines
Before publishing a PR, please test builds locally. Before publishing a PR, please test builds locally.
On each PR that contains changes relevant to Docker builds, images are being built and tested in our CI (GitHub Actions). On each PR that contains changes relevant to Docker builds, images are being built and tested in our CI (GitHub Actions).
> :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. > :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.
## Lint ## Lint
We use `pre-commit` framework to lint the codebase before committing. We use `pre-commit` framework to lint the codebase before committing.
First, you need to install pre-commit with pip: First, you need to install pre-commit with pip:
```shell ```shell
pip install pre-commit pip install pre-commit
``` ```
Also you can use brew if you're on Mac: Also you can use brew if you're on Mac:
```shell ```shell
brew install pre-commit brew install pre-commit
``` ```
To setup _pre-commit_ hook, run: To setup _pre-commit_ hook, run:
```shell ```shell
pre-commit install pre-commit install
``` ```
To run all the files in repository, run: To run all the files in repository, run:
```shell ```shell
pre-commit run --all-files pre-commit run --all-files
``` ```
## Build ## Build
We use [Docker Buildx Bake](https://docs.docker.com/engine/reference/commandline/buildx_bake/). To build the images, run command below: We use [Docker Buildx Bake](https://docs.docker.com/engine/reference/commandline/buildx_bake/). To build the images, run command below:
```shell ```shell
FRAPPE_VERSION=... ERPNEXT_VERSION=... docker buildx bake <targets> FRAPPE_VERSION=... ERPNEXT_VERSION=... docker buildx bake <targets>
``` ```
Available targets can be found in `docker-bake.hcl`. Available targets can be found in `docker-bake.hcl`.
## Test ## Test
We use [pytest](https://pytest.org) for our integration tests. We use [pytest](https://pytest.org) for our integration tests.
Install Python test requirements: Install Python test requirements:
```shell ```shell
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
pip install -r requirements-test.txt pip install -r requirements-test.txt
``` ```
Run pytest: Run pytest:
```shell ```shell
pytest pytest
``` ```
# Documentation # Documentation
Place relevant markdown files in the `docs` directory and index them in README.md located at the root of repo. Place relevant markdown files in the `docs` directory and index them in README.md located at the root of repo.
# Frappe and ERPNext updates # Frappe and ERPNext updates
Each Frappe/ERPNext release triggers new stable images builds as well as bump to helm chart. Each Frappe/ERPNext release triggers new stable images builds as well as bump to helm chart.
# Maintenance # Maintenance
In case of new release of Debian. e.g. bullseye to bookworm. Change following files: In case of new release of Debian. e.g. bullseye to bookworm. Change following files:
- `images/erpnext/Containerfile` and `images/custom/Containerfile`: Change the files to use new debian release, make sure new python version tag that is available on new debian release image. e.g. 3.9.9 (bullseye) to 3.9.17 (bookworm) or 3.10.5 (bullseye) to 3.10.12 (bookworm). Make sure apt-get packages and wkhtmltopdf version are also upgraded accordingly. - `images/erpnext/Containerfile` and `images/custom/Containerfile`: Change the files to use new debian release, make sure new python version tag that is available on new debian release image. e.g. 3.9.9 (bullseye) to 3.9.17 (bookworm) or 3.10.5 (bullseye) to 3.10.12 (bookworm). Make sure apt-get packages and wkhtmltopdf version are also upgraded accordingly.
- `images/bench/Dockerfile`: Change the files to use new debian release. Make sure apt-get packages and wkhtmltopdf version are also upgraded accordingly. - `images/bench/Dockerfile`: Change the files to use new debian release. Make sure apt-get packages and wkhtmltopdf version are also upgraded accordingly.
Change following files on release of ERPNext Change following files on release of ERPNext
- `.github/workflows/build_stable.yml`: Add the new release step under `jobs` and remove the unmaintained one. e.g. In case v12, v13 available, v14 will be added and v12 will be removed on release of v14. Also change the `needs:` for later steps to `v14` from `v13`. - `.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`.

42
LICENSE
View file

@ -1,21 +1,21 @@
MIT License MIT License
Copyright (c) 2017 Frappe Technologies Pvt. Ltd. Copyright (c) 2017 Frappe Technologies Pvt. Ltd.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

186
README.md
View file

@ -1,93 +1,93 @@
[![Build Stable](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml) [![Build Stable](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_stable.yml)
[![Build Develop](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml) [![Build Develop](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml/badge.svg)](https://github.com/frappe/frappe_docker/actions/workflows/build_develop.yml)
Everything about [Frappe](https://github.com/frappe/frappe) and [ERPNext](https://github.com/frappe/erpnext) in containers. Everything about [Frappe](https://github.com/frappe/frappe) and [ERPNext](https://github.com/frappe/erpnext) in containers.
# Getting Started # Getting Started
**New to Frappe Docker?** Read the [Getting Started Guide](docs/getting-started.md) for a comprehensive overview of repository structure, development workflow, custom apps, Docker concepts, and quick start examples. **New to Frappe Docker?** Read the [Getting Started Guide](docs/getting-started.md) for a comprehensive overview of repository structure, development workflow, custom apps, Docker concepts, and quick start examples.
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). To get started you need [Docker](https://docs.docker.com/get-docker/), [docker-compose](https://docs.docker.com/compose/), and [git](https://docs.github.com/en/get-started/getting-started-with-git/set-up-git) setup on your machine. For Docker basics and best practices refer to Docker's [documentation](http://docs.docker.com).
Once completed, chose one of the following two sections for next steps. Once completed, chose one of the following two sections for next steps.
### Try in Play With Docker ### Try in Play With Docker
To play in an already set up sandbox, in your browser, click the button below: To play in an already set up sandbox, in your browser, click the button below:
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml"> <a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD"/> <img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD"/>
</a> </a>
### Try on your Dev environment ### Try on your Dev environment
First clone the repo: First clone the repo:
```sh ```sh
git clone https://github.com/frappe/frappe_docker git clone https://github.com/frappe/frappe_docker
cd frappe_docker cd frappe_docker
``` ```
Then run: `docker compose -f pwd.yml up -d` Then run: `docker compose -f pwd.yml up -d`
### To run on ARM64 architecture follow this instructions ### To run on ARM64 architecture follow this instructions
After you clone the repo and `cd frappe_docker`, run this command to build multi-architecture images specifically for ARM64. After you clone the repo and `cd frappe_docker`, run this command to build multi-architecture images specifically for ARM64.
`docker buildx bake --no-cache --set "*.platform=linux/arm64"` `docker buildx bake --no-cache --set "*.platform=linux/arm64"`
and then and then
- add `platform: linux/arm64` to all services in the `pwd.yml` - add `platform: linux/arm64` to all services in the `pwd.yml`
- replace the current specified versions of erpnext image on `pwd.yml` with `:latest` - replace the current specified versions of erpnext image on `pwd.yml` with `:latest`
Then run: `docker compose -f pwd.yml up -d` Then run: `docker compose -f pwd.yml up -d`
## Final steps ## Final steps
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`) 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`)
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. 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.
# Documentation # Documentation
### [Getting Started Guide](docs/getting-started.md) ### [Getting Started Guide](docs/getting-started.md)
### [Frequently Asked Questions](https://github.com/frappe/frappe_docker/wiki/Frequently-Asked-Questions) ### [Frequently Asked Questions](https://github.com/frappe/frappe_docker/wiki/Frequently-Asked-Questions)
### [Production](#production) ### [Production](#production)
- [List of containers](docs/container-setup/01-overview.md) - [List of containers](docs/container-setup/01-overview.md)
- [Single Compose Setup](docs/single-compose-setup.md) - [Single Compose Setup](docs/single-compose-setup.md)
- [Environment Variables](docs/container-setup/env-variables.md) - [Environment Variables](docs/container-setup/env-variables.md)
- [Single Server Example](docs/single-server-example.md) - [Single Server Example](docs/single-server-example.md)
- [Setup Options](docs/setup-options.md) - [Setup Options](docs/setup-options.md)
- [Site Operations](docs/site-operations.md) - [Site Operations](docs/site-operations.md)
- [Backup and Push Cron Job](docs/backup-and-push-cronjob.md) - [Backup and Push Cron Job](docs/backup-and-push-cronjob.md)
- [Port Based Multi Tenancy](docs/port-based-multi-tenancy.md) - [Port Based Multi Tenancy](docs/port-based-multi-tenancy.md)
- [Migrate from multi-image setup](docs/migrate-from-multi-image-setup.md) - [Migrate from multi-image setup](docs/migrate-from-multi-image-setup.md)
- [running on linux/mac](docs/setup_for_linux_mac.md) - [running on linux/mac](docs/setup_for_linux_mac.md)
- [TLS for local deployment](docs/tls-for-local-deployment.md) - [TLS for local deployment](docs/tls-for-local-deployment.md)
### [Custom Images](#custom-images) ### [Custom Images](#custom-images)
- [Custom Apps](docs/container-setup/02-build-setup.md) - [Custom Apps](docs/container-setup/02-build-setup.md)
- [Build Version 10 Images](docs/build-version-10-images.md) - [Build Version 10 Images](docs/build-version-10-images.md)
### [Development](#development) ### [Development](#development)
- [Development using containers](docs/development.md) - [Development using containers](docs/development.md)
- [Bench Console and VSCode Debugger](docs/bench-console-and-vscode-debugger.md) - [Bench Console and VSCode Debugger](docs/bench-console-and-vscode-debugger.md)
- [Connect to localhost services](docs/connect-to-localhost-services-from-containers-for-local-app-development.md) - [Connect to localhost services](docs/connect-to-localhost-services-from-containers-for-local-app-development.md)
### [Troubleshoot](docs/troubleshoot.md) ### [Troubleshoot](docs/troubleshoot.md)
# Contributing # Contributing
If you want to contribute to this repo refer to [CONTRIBUTING.md](CONTRIBUTING.md) If you want to contribute to this repo refer to [CONTRIBUTING.md](CONTRIBUTING.md)
This repository is only for container related stuff. You also might want to contribute to: This repository is only for container related stuff. You also might want to contribute to:
- [Frappe framework](https://github.com/frappe/frappe#contributing), - [Frappe framework](https://github.com/frappe/frappe#contributing),
- [ERPNext](https://github.com/frappe/erpnext#contributing), - [ERPNext](https://github.com/frappe/erpnext#contributing),
- [Frappe Bench](https://github.com/frappe/bench). - [Frappe Bench](https://github.com/frappe/bench).

View file

@ -1,95 +1,95 @@
x-customizable-image: &customizable_image x-customizable-image: &customizable_image
# By default the image used only contains the `frappe` and `erpnext` apps. # By default the image used only contains the `frappe` and `erpnext` apps.
# See https://github.com/frappe/frappe_docker/blob/main/docs/container-setup/02-build-setup.md#define-custom-apps # See https://github.com/frappe/frappe_docker/blob/main/docs/container-setup/02-build-setup.md#define-custom-apps
# about using custom images. # about using custom images.
image: ${CUSTOM_IMAGE:-frappe/erpnext}:${CUSTOM_TAG:-$ERPNEXT_VERSION} image: ${CUSTOM_IMAGE:-frappe/erpnext}:${CUSTOM_TAG:-$ERPNEXT_VERSION}
pull_policy: ${PULL_POLICY:-always} pull_policy: ${PULL_POLICY:-always}
restart: ${RESTART_POLICY:-unless-stopped} restart: ${RESTART_POLICY:-unless-stopped}
x-depends-on-configurator: &depends_on_configurator x-depends-on-configurator: &depends_on_configurator
depends_on: depends_on:
configurator: configurator:
condition: service_completed_successfully condition: service_completed_successfully
x-backend-defaults: &backend_defaults x-backend-defaults: &backend_defaults
<<: [*depends_on_configurator, *customizable_image] <<: [*depends_on_configurator, *customizable_image]
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
services: services:
configurator: configurator:
<<: *backend_defaults <<: *backend_defaults
platform: linux/amd64 platform: linux/amd64
entrypoint: entrypoint:
- bash - bash
- -c - -c
# add redis_socketio for backward compatibility # add redis_socketio for backward compatibility
command: command:
- > - >
ls -1 apps > sites/apps.txt; ls -1 apps > sites/apps.txt;
bench set-config -g db_host $$DB_HOST; bench set-config -g db_host $$DB_HOST;
bench set-config -gp db_port $$DB_PORT; bench set-config -gp db_port $$DB_PORT;
bench set-config -g redis_cache "redis://$$REDIS_CACHE"; bench set-config -g redis_cache "redis://$$REDIS_CACHE";
bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; bench set-config -g redis_queue "redis://$$REDIS_QUEUE";
bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; bench set-config -g redis_socketio "redis://$$REDIS_QUEUE";
bench set-config -gp socketio_port $$SOCKETIO_PORT; bench set-config -gp socketio_port $$SOCKETIO_PORT;
environment: environment:
DB_HOST: ${DB_HOST:-} DB_HOST: ${DB_HOST:-}
DB_PORT: ${DB_PORT:-} DB_PORT: ${DB_PORT:-}
REDIS_CACHE: ${REDIS_CACHE:-} REDIS_CACHE: ${REDIS_CACHE:-}
REDIS_QUEUE: ${REDIS_QUEUE:-} REDIS_QUEUE: ${REDIS_QUEUE:-}
SOCKETIO_PORT: 9000 SOCKETIO_PORT: 9000
depends_on: {} depends_on: {}
restart: on-failure restart: on-failure
backend: backend:
<<: *backend_defaults <<: *backend_defaults
platform: linux/amd64 platform: linux/amd64
frontend: frontend:
<<: *customizable_image <<: *customizable_image
platform: linux/amd64 platform: linux/amd64
command: command:
- nginx-entrypoint.sh - nginx-entrypoint.sh
environment: environment:
BACKEND: backend:8000 BACKEND: backend:8000
SOCKETIO: websocket:9000 SOCKETIO: websocket:9000
FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host} FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host}
UPSTREAM_REAL_IP_ADDRESS: ${UPSTREAM_REAL_IP_ADDRESS:-127.0.0.1} UPSTREAM_REAL_IP_ADDRESS: ${UPSTREAM_REAL_IP_ADDRESS:-127.0.0.1}
UPSTREAM_REAL_IP_HEADER: ${UPSTREAM_REAL_IP_HEADER:-X-Forwarded-For} UPSTREAM_REAL_IP_HEADER: ${UPSTREAM_REAL_IP_HEADER:-X-Forwarded-For}
UPSTREAM_REAL_IP_RECURSIVE: ${UPSTREAM_REAL_IP_RECURSIVE:-off} UPSTREAM_REAL_IP_RECURSIVE: ${UPSTREAM_REAL_IP_RECURSIVE:-off}
PROXY_READ_TIMEOUT: ${PROXY_READ_TIMEOUT:-120} PROXY_READ_TIMEOUT: ${PROXY_READ_TIMEOUT:-120}
CLIENT_MAX_BODY_SIZE: ${CLIENT_MAX_BODY_SIZE:-50m} CLIENT_MAX_BODY_SIZE: ${CLIENT_MAX_BODY_SIZE:-50m}
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
depends_on: depends_on:
- backend - backend
- websocket - websocket
websocket: websocket:
<<: [*depends_on_configurator, *customizable_image] <<: [*depends_on_configurator, *customizable_image]
platform: linux/amd64 platform: linux/amd64
command: command:
- node - node
- /home/frappe/frappe-bench/apps/frappe/socketio.js - /home/frappe/frappe-bench/apps/frappe/socketio.js
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
queue-short: queue-short:
<<: *backend_defaults <<: *backend_defaults
platform: linux/amd64 platform: linux/amd64
command: bench worker --queue short,default command: bench worker --queue short,default
queue-long: queue-long:
<<: *backend_defaults <<: *backend_defaults
platform: linux/amd64 platform: linux/amd64
command: bench worker --queue long,default,short command: bench worker --queue long,default,short
scheduler: scheduler:
<<: *backend_defaults <<: *backend_defaults
platform: linux/amd64 platform: linux/amd64
command: bench schedule command: bench schedule
# ERPNext requires local assets access (Frappe does not) # ERPNext requires local assets access (Frappe does not)
volumes: volumes:
sites: sites:

View file

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

View file

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

View file

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

View file

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

View file

@ -1,77 +1,77 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Bench Web", "name": "Bench Web",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": [ "args": [
"frappe", "frappe",
"serve", "serve",
"--port", "--port",
"8000", "8000",
"--noreload", "--noreload",
"--nothreading" "--nothreading"
], ],
"cwd": "${workspaceFolder}/frappe-bench/sites", "cwd": "${workspaceFolder}/frappe-bench/sites",
"env": { "env": {
"DEV_SERVER": "1" "DEV_SERVER": "1"
} }
}, },
{ {
"name": "Bench Short Worker", "name": "Bench Short Worker",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "worker", "--queue", "short"], "args": ["frappe", "worker", "--queue", "short"],
"cwd": "${workspaceFolder}/frappe-bench/sites", "cwd": "${workspaceFolder}/frappe-bench/sites",
"env": { "env": {
"DEV_SERVER": "1" "DEV_SERVER": "1"
} }
}, },
{ {
"name": "Bench Default Worker", "name": "Bench Default Worker",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "worker", "--queue", "default"], "args": ["frappe", "worker", "--queue", "default"],
"cwd": "${workspaceFolder}/frappe-bench/sites", "cwd": "${workspaceFolder}/frappe-bench/sites",
"env": { "env": {
"DEV_SERVER": "1" "DEV_SERVER": "1"
} }
}, },
{ {
"name": "Bench Long Worker", "name": "Bench Long Worker",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
"args": ["frappe", "worker", "--queue", "long"], "args": ["frappe", "worker", "--queue", "long"],
"cwd": "${workspaceFolder}/frappe-bench/sites", "cwd": "${workspaceFolder}/frappe-bench/sites",
"env": { "env": {
"DEV_SERVER": "1" "DEV_SERVER": "1"
} }
}, },
{ {
"name": "Honcho SocketIO Watch Schedule Worker", "name": "Honcho SocketIO Watch Schedule Worker",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"python": "/home/frappe/.pyenv/shims/python", "python": "/home/frappe/.pyenv/shims/python",
"program": "/home/frappe/.local/bin/honcho", "program": "/home/frappe/.local/bin/honcho",
"cwd": "${workspaceFolder}/frappe-bench", "cwd": "${workspaceFolder}/frappe-bench",
"console": "internalConsole", "console": "internalConsole",
"args": ["start", "socketio", "watch", "schedule", "worker"], "args": ["start", "socketio", "watch", "schedule", "worker"],
"postDebugTask": "Clean Honcho SocketIO Watch Schedule Worker" "postDebugTask": "Clean Honcho SocketIO Watch Schedule Worker"
} }
], ],
"compounds": [ "compounds": [
{ {
"name": "Honcho + Web debug", "name": "Honcho + Web debug",
"configurations": ["Bench Web", "Honcho SocketIO Watch Schedule Worker"], "configurations": ["Bench Web", "Honcho SocketIO Watch Schedule Worker"],
"stopAll": true "stopAll": true
} }
] ]
} }

View file

@ -1,3 +1,3 @@
{ {
"python.defaultInterpreterPath": "${workspaceFolder}/frappe-bench/env/bin/python" "python.defaultInterpreterPath": "${workspaceFolder}/frappe-bench/env/bin/python"
} }

View file

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

View file

@ -1,113 +1,113 @@
# Docker Buildx Bake build definition file # Docker Buildx Bake build definition file
# Reference: https://github.com/docker/buildx/blob/master/docs/reference/buildx_bake.md # Reference: https://github.com/docker/buildx/blob/master/docs/reference/buildx_bake.md
variable "REGISTRY_USER" { variable "REGISTRY_USER" {
default = "frappe" default = "frappe"
} }
variable PYTHON_VERSION { variable PYTHON_VERSION {
default = "3.11.6" default = "3.11.6"
} }
variable NODE_VERSION { variable NODE_VERSION {
default = "20.19.2" default = "20.19.2"
} }
variable "FRAPPE_VERSION" { variable "FRAPPE_VERSION" {
default = "develop" default = "develop"
} }
variable "ERPNEXT_VERSION" { variable "ERPNEXT_VERSION" {
default = "develop" default = "develop"
} }
variable "FRAPPE_REPO" { variable "FRAPPE_REPO" {
default = "https://github.com/frappe/frappe" default = "https://github.com/frappe/frappe"
} }
variable "ERPNEXT_REPO" { variable "ERPNEXT_REPO" {
default = "https://github.com/frappe/erpnext" default = "https://github.com/frappe/erpnext"
} }
variable "BENCH_REPO" { variable "BENCH_REPO" {
default = "https://github.com/frappe/bench" default = "https://github.com/frappe/bench"
} }
variable "LATEST_BENCH_RELEASE" { variable "LATEST_BENCH_RELEASE" {
default = "latest" default = "latest"
} }
# Bench image # Bench image
target "bench" { target "bench" {
args = { args = {
GIT_REPO = "${BENCH_REPO}" GIT_REPO = "${BENCH_REPO}"
} }
context = "images/bench" context = "images/bench"
target = "bench" target = "bench"
tags = [ tags = [
"frappe/bench:${LATEST_BENCH_RELEASE}", "frappe/bench:${LATEST_BENCH_RELEASE}",
"frappe/bench:latest", "frappe/bench:latest",
] ]
} }
target "bench-test" { target "bench-test" {
inherits = ["bench"] inherits = ["bench"]
target = "bench-test" target = "bench-test"
} }
# Main images # Main images
# Base for all other targets # Base for all other targets
group "default" { group "default" {
targets = ["erpnext", "base", "build"] targets = ["erpnext", "base", "build"]
} }
function "tag" { function "tag" {
params = [repo, version] params = [repo, version]
result = [ result = [
# Push frappe or erpnext branch as tag # Push frappe or erpnext branch as tag
"${REGISTRY_USER}/${repo}:${version}", "${REGISTRY_USER}/${repo}:${version}",
# If `version` param is develop (development build) then use tag `latest` # If `version` param is develop (development build) then use tag `latest`
"${version}" == "develop" ? "${REGISTRY_USER}/${repo}:latest" : "${REGISTRY_USER}/${repo}:${version}", "${version}" == "develop" ? "${REGISTRY_USER}/${repo}:latest" : "${REGISTRY_USER}/${repo}:${version}",
# Make short tag for major version if possible. For example, from v13.16.0 make v13. # Make short tag for major version if possible. For example, from v13.16.0 make v13.
can(regex("(v[0-9]+)[.]", "${version}")) ? "${REGISTRY_USER}/${repo}:${regex("(v[0-9]+)[.]", "${version}")[0]}" : "", can(regex("(v[0-9]+)[.]", "${version}")) ? "${REGISTRY_USER}/${repo}:${regex("(v[0-9]+)[.]", "${version}")[0]}" : "",
# Make short tag for major version if possible. For example, from v13.16.0 make version-13. # Make short tag for major version if possible. For example, from v13.16.0 make version-13.
can(regex("(v[0-9]+)[.]", "${version}")) ? "${REGISTRY_USER}/${repo}:version-${regex("([0-9]+)[.]", "${version}")[0]}" : "", can(regex("(v[0-9]+)[.]", "${version}")) ? "${REGISTRY_USER}/${repo}:version-${regex("([0-9]+)[.]", "${version}")[0]}" : "",
] ]
} }
target "default-args" { target "default-args" {
args = { args = {
FRAPPE_PATH = "${FRAPPE_REPO}" FRAPPE_PATH = "${FRAPPE_REPO}"
ERPNEXT_PATH = "${ERPNEXT_REPO}" ERPNEXT_PATH = "${ERPNEXT_REPO}"
BENCH_REPO = "${BENCH_REPO}" BENCH_REPO = "${BENCH_REPO}"
FRAPPE_BRANCH = "${FRAPPE_VERSION}" FRAPPE_BRANCH = "${FRAPPE_VERSION}"
ERPNEXT_BRANCH = "${ERPNEXT_VERSION}" ERPNEXT_BRANCH = "${ERPNEXT_VERSION}"
PYTHON_VERSION = "${PYTHON_VERSION}" PYTHON_VERSION = "${PYTHON_VERSION}"
NODE_VERSION = "${NODE_VERSION}" NODE_VERSION = "${NODE_VERSION}"
} }
} }
target "erpnext" { target "erpnext" {
inherits = ["default-args"] inherits = ["default-args"]
context = "." context = "."
dockerfile = "images/production/Containerfile" dockerfile = "images/production/Containerfile"
target = "erpnext" target = "erpnext"
tags = tag("erpnext", "${ERPNEXT_VERSION}") tags = tag("erpnext", "${ERPNEXT_VERSION}")
} }
target "base" { target "base" {
inherits = ["default-args"] inherits = ["default-args"]
context = "." context = "."
dockerfile = "images/production/Containerfile" dockerfile = "images/production/Containerfile"
target = "base" target = "base"
tags = tag("base", "${FRAPPE_VERSION}") tags = tag("base", "${FRAPPE_VERSION}")
} }
target "build" { target "build" {
inherits = ["default-args"] inherits = ["default-args"]
context = "." context = "."
dockerfile = "images/production/Containerfile" dockerfile = "images/production/Containerfile"
target = "build" target = "build"
tags = tag("build", "${ERPNEXT_VERSION}") tags = tag("build", "${ERPNEXT_VERSION}")
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,86 +1,86 @@
# Site operations # Site operations
> 💡 You should setup `--project-name` option in `docker-compose` commands if you have non-standard project name. > 💡 You should setup `--project-name` option in `docker-compose` commands if you have non-standard project name.
## Setup new site ## Setup new site
Note: Note:
- Wait for the `db` service to start and `configurator` to exit before trying to create a new site. Usually this takes up to 10 seconds. - Wait for the `db` service to start and `configurator` to exit before trying to create a new site. Usually this takes up to 10 seconds.
- Also you have to pass `-p <project_name>` if `-p` passed previously eg. `docker-compose -p <project_name> exec (rest of the command)`. - Also you have to pass `-p <project_name>` if `-p` passed previously eg. `docker-compose -p <project_name> exec (rest of the command)`.
```sh ```sh
docker-compose exec backend bench new-site --mariadb-user-host-login-scope=% --db-root-password <db-password> --admin-password <admin-password> <site-name> docker-compose exec backend bench new-site --mariadb-user-host-login-scope=% --db-root-password <db-password> --admin-password <admin-password> <site-name>
``` ```
If you need to install some app, specify `--install-app`. To see all options, just run `bench new-site --help`. If you need to install some app, specify `--install-app`. To see all options, just run `bench new-site --help`.
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: 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:
```sh ```sh
docker-compose exec backend bench set-config -g root_login <root-login> docker-compose exec backend bench set-config -g root_login <root-login>
docker-compose exec backend bench set-config -g root_password <root-password> docker-compose exec backend bench set-config -g root_password <root-password>
``` ```
Also command is slightly different: Also command is slightly different:
```sh ```sh
docker-compose exec backend bench new-site --mariadb-user-host-login-scope=% --db-type postgres --admin-password <admin-password> <site-name> docker-compose exec backend bench new-site --mariadb-user-host-login-scope=% --db-type postgres --admin-password <admin-password> <site-name>
``` ```
## Push backup to S3 storage ## Push backup to S3 storage
We have the script that helps to push latest backup to S3. We have the script that helps to push latest backup to S3.
```sh ```sh
docker-compose exec backend push_backup.py --site-name <site-name> --bucket <bucket> --region-name <region> --endpoint-url <endpoint-url> --aws-access-key-id <access-key> --aws-secret-access-key <secret-key> docker-compose exec backend push_backup.py --site-name <site-name> --bucket <bucket> --region-name <region> --endpoint-url <endpoint-url> --aws-access-key-id <access-key> --aws-secret-access-key <secret-key>
``` ```
Note that you can restore backup only manually. Note that you can restore backup only manually.
## Edit configs ## Edit configs
Editing config manually might be required in some cases, Editing config manually might be required in some cases,
one such case is to use Amazon RDS (or any other DBaaS). one such case is to use Amazon RDS (or any other DBaaS).
For full instructions, refer to the [wiki](<https://github.com/frappe/frappe/wiki/Using-Frappe-with-Amazon-RDS-(or-any-other-DBaaS)>). Common question can be found in Issues and on forum. For full instructions, refer to the [wiki](<https://github.com/frappe/frappe/wiki/Using-Frappe-with-Amazon-RDS-(or-any-other-DBaaS)>). Common question can be found in Issues and on forum.
`common_site_config.json` or `site_config.json` from `sites` volume has to be edited using following command: `common_site_config.json` or `site_config.json` from `sites` volume has to be edited using following command:
```sh ```sh
docker run --rm -it \ docker run --rm -it \
-v <project-name>_sites:/sites \ -v <project-name>_sites:/sites \
alpine vi /sites/common_site_config.json alpine vi /sites/common_site_config.json
``` ```
Instead of `alpine` use any image of your choice. Instead of `alpine` use any image of your choice.
## Health check ## Health check
For socketio and gunicorn service ping the hostname:port and that will be sufficient. For workers and scheduler, there is a command that needs to be executed. For socketio and gunicorn service ping the hostname:port and that will be sufficient. For workers and scheduler, there is a command that needs to be executed.
```shell ```shell
docker-compose exec backend healthcheck.sh --ping-service mongodb:27017 docker-compose exec backend healthcheck.sh --ping-service mongodb:27017
``` ```
Additional services can be pinged as part of health check with option `-p` or `--ping-service`. Additional services can be pinged as part of health check with option `-p` or `--ping-service`.
This check ensures that given service should be connected along with services in common_site_config.json. This check ensures that given service should be connected along with services in common_site_config.json.
If connection to service(s) fails, the command fails with exit code 1. If connection to service(s) fails, the command fails with exit code 1.
--- ---
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: 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:
```sh ```sh
docker-compose exec backend bench --help docker-compose exec backend bench --help
``` ```
## Migrate site ## Migrate site
Note: Note:
- Wait for the `db` service to start and `configurator` to exit before trying to migrate a site. Usually this takes up to 10 seconds. - Wait for the `db` service to start and `configurator` to exit before trying to migrate a site. Usually this takes up to 10 seconds.
```sh ```sh
docker-compose exec backend bench --site <site-name> migrate docker-compose exec backend bench --site <site-name> migrate
``` ```

View file

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

View file

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

View file

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

View file

@ -1,164 +1,164 @@
FROM debian:bookworm-slim AS bench FROM debian:bookworm-slim AS bench
LABEL author=frappé LABEL author=frappé
ARG GIT_REPO=https://github.com/frappe/bench.git ARG GIT_REPO=https://github.com/frappe/bench.git
ARG GIT_BRANCH=v5.x ARG GIT_BRANCH=v5.x
RUN apt-get update \ RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
# For frappe framework # For frappe framework
git \ git \
mariadb-client \ mariadb-client \
postgresql-client \ postgresql-client \
gettext-base \ gettext-base \
wget \ wget \
# for PDF # for PDF
libssl-dev \ libssl-dev \
fonts-cantarell \ fonts-cantarell \
xfonts-75dpi \ xfonts-75dpi \
xfonts-base \ xfonts-base \
# weasyprint dependencies # weasyprint dependencies
libpango-1.0-0 \ libpango-1.0-0 \
libharfbuzz0b \ libharfbuzz0b \
libpangoft2-1.0-0 \ libpangoft2-1.0-0 \
libpangocairo-1.0-0 \ libpangocairo-1.0-0 \
# to work inside the container # to work inside the container
locales \ locales \
build-essential \ build-essential \
cron \ cron \
curl \ curl \
vim \ vim \
sudo \ sudo \
iputils-ping \ iputils-ping \
watch \ watch \
tree \ tree \
nano \ nano \
less \ less \
software-properties-common \ software-properties-common \
bash-completion \ bash-completion \
# For psycopg2 # For psycopg2
libpq-dev \ libpq-dev \
# Other # Other
libffi-dev \ libffi-dev \
liblcms2-dev \ liblcms2-dev \
libldap2-dev \ libldap2-dev \
libmariadb-dev \ libmariadb-dev \
libsasl2-dev \ libsasl2-dev \
libtiff5-dev \ libtiff5-dev \
libwebp-dev \ libwebp-dev \
pkg-config \ pkg-config \
redis-tools \ redis-tools \
rlwrap \ rlwrap \
tk8.6-dev \ tk8.6-dev \
ssh-client \ ssh-client \
# VSCode container requirements # VSCode container requirements
net-tools \ net-tools \
# For pyenv build dependencies # For pyenv build dependencies
# https://github.com/frappe/frappe_docker/issues/840#issuecomment-1185206895 # https://github.com/frappe/frappe_docker/issues/840#issuecomment-1185206895
make \ make \
# For pandas # For pandas
libbz2-dev \ libbz2-dev \
# For bench execute # For bench execute
libsqlite3-dev \ libsqlite3-dev \
# For other dependencies # For other dependencies
zlib1g-dev \ zlib1g-dev \
libreadline-dev \ libreadline-dev \
llvm \ llvm \
libncurses5-dev \ libncurses5-dev \
libncursesw5-dev \ libncursesw5-dev \
xz-utils \ xz-utils \
tk-dev \ tk-dev \
liblzma-dev \ liblzma-dev \
file \ file \
# For MIME type detection # For MIME type detection
media-types \ media-types \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
&& dpkg-reconfigure --frontend=noninteractive locales && dpkg-reconfigure --frontend=noninteractive locales
# Detect arch and install wkhtmltopdf # Detect arch and install wkhtmltopdf
ARG WKHTMLTOPDF_VERSION=0.12.6.1-3 ARG WKHTMLTOPDF_VERSION=0.12.6.1-3
ARG WKHTMLTOPDF_DISTRO=bookworm ARG WKHTMLTOPDF_DISTRO=bookworm
RUN if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \ RUN if [ "$(uname -m)" = "aarch64" ]; then export ARCH=arm64; fi \
&& if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \ && if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; fi \
&& downloaded_file=wkhtmltox_${WKHTMLTOPDF_VERSION}.${WKHTMLTOPDF_DISTRO}_${ARCH}.deb \ && downloaded_file=wkhtmltox_${WKHTMLTOPDF_VERSION}.${WKHTMLTOPDF_DISTRO}_${ARCH}.deb \
&& wget -q https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \ && wget -q https://github.com/wkhtmltopdf/packaging/releases/download/$WKHTMLTOPDF_VERSION/$downloaded_file \
&& dpkg -i $downloaded_file \ && dpkg -i $downloaded_file \
&& rm $downloaded_file && rm $downloaded_file
# Create new user with home directory, improve docker compatibility with UID/GID 1000, # Create new user with home directory, improve docker compatibility with UID/GID 1000,
# add user to sudo group, allow passwordless sudo, switch to that user # add user to sudo group, allow passwordless sudo, switch to that user
# and change directory to user home directory # and change directory to user home directory
RUN groupadd -g 1000 frappe \ RUN groupadd -g 1000 frappe \
&& useradd --no-log-init -r -m -u 1000 -g 1000 -G sudo frappe \ && useradd --no-log-init -r -m -u 1000 -g 1000 -G sudo frappe \
&& echo "frappe ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && echo "frappe ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
USER frappe USER frappe
WORKDIR /home/frappe WORKDIR /home/frappe
# Install Python via pyenv # Install Python via pyenv
ENV PYTHON_VERSION_V14=3.10.13 ENV PYTHON_VERSION_V14=3.10.13
ENV PYTHON_VERSION=3.11.6 ENV PYTHON_VERSION=3.11.6
ENV PYENV_ROOT=/home/frappe/.pyenv ENV PYENV_ROOT=/home/frappe/.pyenv
ENV PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH ENV PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
# From https://github.com/pyenv/pyenv#basic-github-checkout # From https://github.com/pyenv/pyenv#basic-github-checkout
RUN git clone --depth 1 https://github.com/pyenv/pyenv.git .pyenv \ RUN git clone --depth 1 https://github.com/pyenv/pyenv.git .pyenv \
&& pyenv install $PYTHON_VERSION_V14 \ && pyenv install $PYTHON_VERSION_V14 \
&& pyenv install $PYTHON_VERSION \ && pyenv install $PYTHON_VERSION \
&& PYENV_VERSION=$PYTHON_VERSION_V14 pip install --no-cache-dir virtualenv \ && PYENV_VERSION=$PYTHON_VERSION_V14 pip install --no-cache-dir virtualenv \
&& PYENV_VERSION=$PYTHON_VERSION pip install --no-cache-dir virtualenv \ && PYENV_VERSION=$PYTHON_VERSION pip install --no-cache-dir virtualenv \
&& pyenv global $PYTHON_VERSION $PYTHON_VERSION_v14 \ && pyenv global $PYTHON_VERSION $PYTHON_VERSION_v14 \
&& sed -Ei -e '/^([^#]|$)/ {a export PYENV_ROOT="/home/frappe/.pyenv" a export PATH="$PYENV_ROOT/bin:$PATH" a ' -e ':a' -e '$!{n;ba};}' ~/.profile \ && sed -Ei -e '/^([^#]|$)/ {a export PYENV_ROOT="/home/frappe/.pyenv" a export PATH="$PYENV_ROOT/bin:$PATH" a ' -e ':a' -e '$!{n;ba};}' ~/.profile \
&& echo 'eval "$(pyenv init --path)"' >>~/.profile \ && echo 'eval "$(pyenv init --path)"' >>~/.profile \
&& echo 'eval "$(pyenv init -)"' >>~/.bashrc && echo 'eval "$(pyenv init -)"' >>~/.bashrc
# Clone and install bench in the local user home directory # Clone and install bench in the local user home directory
# For development, bench source is located in ~/.bench # For development, bench source is located in ~/.bench
ENV PATH=/home/frappe/.local/bin:$PATH ENV PATH=/home/frappe/.local/bin:$PATH
# Skip editable-bench warning # Skip editable-bench warning
# https://github.com/frappe/bench/commit/20560c97c4246b2480d7358c722bc9ad13606138 # https://github.com/frappe/bench/commit/20560c97c4246b2480d7358c722bc9ad13606138
RUN git clone ${GIT_REPO} --depth 1 -b ${GIT_BRANCH} .bench \ RUN git clone ${GIT_REPO} --depth 1 -b ${GIT_BRANCH} .bench \
&& pip install --no-cache-dir --user -e .bench \ && pip install --no-cache-dir --user -e .bench \
&& echo "export PATH=/home/frappe/.local/bin:\$PATH" >>/home/frappe/.bashrc \ && echo "export PATH=/home/frappe/.local/bin:\$PATH" >>/home/frappe/.bashrc \
&& echo "export BENCH_DEVELOPER=1" >>/home/frappe/.bashrc && echo "export BENCH_DEVELOPER=1" >>/home/frappe/.bashrc
# Install Node via nvm # Install Node via nvm
ENV NODE_VERSION_14=16.20.2 ENV NODE_VERSION_14=16.20.2
ENV NODE_VERSION=20.19.2 ENV NODE_VERSION=20.19.2
ENV NVM_DIR=/home/frappe/.nvm ENV NVM_DIR=/home/frappe/.nvm
ENV PATH=${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH} ENV PATH=${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}
RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \ RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \
&& . ${NVM_DIR}/nvm.sh \ && . ${NVM_DIR}/nvm.sh \
&& nvm install ${NODE_VERSION_14} \ && nvm install ${NODE_VERSION_14} \
&& nvm use v${NODE_VERSION_14} \ && nvm use v${NODE_VERSION_14} \
&& npm install -g yarn \ && npm install -g yarn \
&& nvm install ${NODE_VERSION} \ && nvm install ${NODE_VERSION} \
&& nvm use v${NODE_VERSION} \ && nvm use v${NODE_VERSION} \
&& npm install -g yarn \ && npm install -g yarn \
&& nvm alias default v${NODE_VERSION} \ && nvm alias default v${NODE_VERSION} \
&& rm -rf ${NVM_DIR}/.cache \ && rm -rf ${NVM_DIR}/.cache \
&& echo 'export NVM_DIR="/home/frappe/.nvm"' >>~/.bashrc \ && echo 'export NVM_DIR="/home/frappe/.nvm"' >>~/.bashrc \
&& echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >> ~/.bashrc \ && echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >> ~/.bashrc \
&& echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion' >> ~/.bashrc && echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion' >> ~/.bashrc
EXPOSE 8000-8005 9000-9005 6787 EXPOSE 8000-8005 9000-9005 6787
FROM bench AS bench-test FROM bench AS bench-test
# Print version and verify bashrc is properly sourced so that everything works # Print version and verify bashrc is properly sourced so that everything works
# in the interactive shell and Dockerfile # in the interactive shell and Dockerfile
RUN node --version \ RUN node --version \
&& npm --version \ && npm --version \
&& yarn --version \ && yarn --version \
&& bench --help && bench --help
RUN bash -c "node --version" \ RUN bash -c "node --version" \
&& bash -c "npm --version" \ && bash -c "npm --version" \
&& bash -c "yarn --version" \ && bash -c "yarn --version" \
&& bash -c "bench --help" && bash -c "bench --help"

View file

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

View file

@ -1,58 +1,58 @@
ARG FRAPPE_BRANCH=version-15 ARG FRAPPE_BRANCH=version-15
FROM frappe/build:${FRAPPE_BRANCH} AS builder FROM frappe/build:${FRAPPE_BRANCH} AS builder
ARG FRAPPE_BRANCH=version-15 ARG FRAPPE_BRANCH=version-15
ARG FRAPPE_PATH=https://github.com/frappe/frappe ARG FRAPPE_PATH=https://github.com/frappe/frappe
ARG APPS_JSON_BASE64 ARG APPS_JSON_BASE64
USER root USER root
RUN if [ -n "${APPS_JSON_BASE64}" ]; then \ RUN if [ -n "${APPS_JSON_BASE64}" ]; then \
mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \ mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \
fi fi
USER frappe USER frappe
RUN export APP_INSTALL_ARGS="" && \ RUN export APP_INSTALL_ARGS="" && \
if [ -n "${APPS_JSON_BASE64}" ]; then \ if [ -n "${APPS_JSON_BASE64}" ]; then \
export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \ export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \
fi && \ fi && \
bench init ${APP_INSTALL_ARGS}\ bench init ${APP_INSTALL_ARGS}\
--frappe-branch=${FRAPPE_BRANCH} \ --frappe-branch=${FRAPPE_BRANCH} \
--frappe-path=${FRAPPE_PATH} \ --frappe-path=${FRAPPE_PATH} \
--no-procfile \ --no-procfile \
--no-backups \ --no-backups \
--skip-redis-config-generation \ --skip-redis-config-generation \
--verbose \ --verbose \
/home/frappe/frappe-bench && \ /home/frappe/frappe-bench && \
cd /home/frappe/frappe-bench && \ cd /home/frappe/frappe-bench && \
echo "{}" > sites/common_site_config.json && \ echo "{}" > sites/common_site_config.json && \
find apps -mindepth 1 -path "*/.git" | xargs rm -fr find apps -mindepth 1 -path "*/.git" | xargs rm -fr
FROM frappe/base:${FRAPPE_BRANCH} AS backend FROM frappe/base:${FRAPPE_BRANCH} AS backend
USER frappe USER frappe
COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench
WORKDIR /home/frappe/frappe-bench WORKDIR /home/frappe/frappe-bench
VOLUME [ \ VOLUME [ \
"/home/frappe/frappe-bench/sites", \ "/home/frappe/frappe-bench/sites", \
"/home/frappe/frappe-bench/sites/assets", \ "/home/frappe/frappe-bench/sites/assets", \
"/home/frappe/frappe-bench/logs" \ "/home/frappe/frappe-bench/logs" \
] ]
CMD [ \ CMD [ \
"/home/frappe/frappe-bench/env/bin/gunicorn", \ "/home/frappe/frappe-bench/env/bin/gunicorn", \
"--chdir=/home/frappe/frappe-bench/sites", \ "--chdir=/home/frappe/frappe-bench/sites", \
"--bind=0.0.0.0:8000", \ "--bind=0.0.0.0:8000", \
"--threads=4", \ "--threads=4", \
"--workers=2", \ "--workers=2", \
"--worker-class=gthread", \ "--worker-class=gthread", \
"--worker-tmp-dir=/dev/shm", \ "--worker-tmp-dir=/dev/shm", \
"--timeout=120", \ "--timeout=120", \
"--preload", \ "--preload", \
"frappe.app:application" \ "frappe.app:application" \
] ]

View file

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

View file

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

View file

@ -1,15 +1,15 @@
services: services:
cron: cron:
image: mcuadros/ofelia:latest image: mcuadros/ofelia:latest
depends_on: depends_on:
- scheduler - scheduler
command: daemon --docker command: daemon --docker
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
scheduler: scheduler:
labels: labels:
ofelia.enabled: "true" ofelia.enabled: "true"
ofelia.job-exec.datecron.schedule: "${BACKUP_CRONSTRING:-@every 6h}" ofelia.job-exec.datecron.schedule: "${BACKUP_CRONSTRING:-@every 6h}"
ofelia.job-exec.datecron.command: "bench --site all backup" ofelia.job-exec.datecron.command: "bench --site all backup"
ofelia.job-exec.datecron.user: "frappe" ofelia.job-exec.datecron.user: "frappe"

View file

@ -1,5 +1,5 @@
services: services:
custom-domain: custom-domain:
labels: labels:
- traefik.http.routers.${ROUTER}.entrypoints=http,https - traefik.http.routers.${ROUTER}.entrypoints=http,https
- traefik.http.routers.${ROUTER}.tls.certresolver=le - traefik.http.routers.${ROUTER}.tls.certresolver=le

View file

@ -1,31 +1,31 @@
version: "3.3" version: "3.3"
services: services:
custom-domain: custom-domain:
image: caddy:2 image: caddy:2
command: command:
- caddy - caddy
- reverse-proxy - reverse-proxy
- --to - --to
- frontend:8080 - frontend:8080
- --from - --from
- :2016 - :2016
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.docker.network=traefik-public - traefik.docker.network=traefik-public
- traefik.http.services.${ROUTER?ROUTER not set}.loadbalancer.server.port=2016 - traefik.http.services.${ROUTER?ROUTER not set}.loadbalancer.server.port=2016
- traefik.http.routers.${ROUTER}.service=${ROUTER} - traefik.http.routers.${ROUTER}.service=${ROUTER}
- traefik.http.routers.${ROUTER}.entrypoints=http - traefik.http.routers.${ROUTER}.entrypoints=http
- traefik.http.routers.${ROUTER}.rule=Host(${SITES?SITES not set}) - traefik.http.routers.${ROUTER}.rule=Host(${SITES?SITES not set})
- traefik.http.middlewares.${ROUTER}.headers.customrequestheaders.Host=${BASE_SITE?BASE_SITE not set} - traefik.http.middlewares.${ROUTER}.headers.customrequestheaders.Host=${BASE_SITE?BASE_SITE not set}
- traefik.http.routers.${ROUTER}.middlewares=${ROUTER} - traefik.http.routers.${ROUTER}.middlewares=${ROUTER}
networks: networks:
- traefik-public - traefik-public
- bench-network - bench-network
networks: networks:
traefik-public: traefik-public:
external: true external: true
bench-network: bench-network:
name: ${BENCH_NETWORK?BENCH_NETWORK not set} name: ${BENCH_NETWORK?BENCH_NETWORK not set}
external: true external: true

View file

@ -1,32 +1,32 @@
services: services:
frontend: frontend:
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.services.frontend.loadbalancer.server.port=8080 - traefik.http.services.frontend.loadbalancer.server.port=8080
- traefik.http.routers.frontend-http.entrypoints=websecure - traefik.http.routers.frontend-http.entrypoints=websecure
- traefik.http.routers.frontend-http.tls.certresolver=main-resolver - traefik.http.routers.frontend-http.tls.certresolver=main-resolver
- traefik.http.routers.frontend-http.rule=Host(${SITES:?List of sites not set}) - traefik.http.routers.frontend-http.rule=Host(${SITES:?List of sites not set})
proxy: proxy:
image: traefik:v2.11 image: traefik:v2.11
restart: unless-stopped restart: unless-stopped
command: command:
- --providers.docker=true - --providers.docker=true
- --providers.docker.exposedbydefault=false - --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80 - --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443 - --entrypoints.websecure.address=:443
- --certificatesResolvers.main-resolver.acme.httpChallenge=true - --certificatesResolvers.main-resolver.acme.httpChallenge=true
- --certificatesResolvers.main-resolver.acme.httpChallenge.entrypoint=web - --certificatesResolvers.main-resolver.acme.httpChallenge.entrypoint=web
- --certificatesResolvers.main-resolver.acme.email=${LETSENCRYPT_EMAIL:?No Let's Encrypt email set} - --certificatesResolvers.main-resolver.acme.email=${LETSENCRYPT_EMAIL:?No Let's Encrypt email set}
- --certificatesResolvers.main-resolver.acme.storage=/letsencrypt/acme.json - --certificatesResolvers.main-resolver.acme.storage=/letsencrypt/acme.json
ports: ports:
- ${HTTP_PUBLISH_PORT:-80}:80 - ${HTTP_PUBLISH_PORT:-80}:80
- ${HTTPS_PUBLISH_PORT:-443}:443 - ${HTTPS_PUBLISH_PORT:-443}:443
volumes: volumes:
- cert-data:/letsencrypt - cert-data:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
volumes: volumes:
cert-data: cert-data:

View file

@ -1,11 +1,11 @@
services: services:
db: db:
environment: environment:
MYSQL_ROOT_PASSWORD: !reset null MYSQL_ROOT_PASSWORD: !reset null
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_password MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_password
secrets: secrets:
- db_password - db_password
secrets: secrets:
db_password: db_password:
file: ${DB_PASSWORD_SECRETS_FILE:?No db secret file set} file: ${DB_PASSWORD_SECRETS_FILE:?No db secret file set}

View file

@ -1,33 +1,33 @@
version: "3.3" version: "3.3"
services: services:
database: database:
container_name: mariadb-database container_name: mariadb-database
image: mariadb:11.8 image: mariadb:11.8
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 5s start_period: 5s
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
command: command:
- --character-set-server=utf8mb4 - --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci - --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake - --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed - --skip-innodb-read-only-compressed
environment: environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-changeit} MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-changeit}
MARIADB_AUTO_UPGRADE: 1 MARIADB_AUTO_UPGRADE: 1
volumes: volumes:
- db-data:/var/lib/mysql - db-data:/var/lib/mysql
networks: networks:
- mariadb-network - mariadb-network
networks: networks:
mariadb-network: mariadb-network:
name: mariadb-network name: mariadb-network
external: false external: false
volumes: volumes:
db-data: db-data:

View file

@ -1,31 +1,31 @@
services: services:
configurator: configurator:
environment: environment:
DB_HOST: db DB_HOST: db
DB_PORT: 3306 DB_PORT: 3306
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
db: db:
image: mariadb:11.8 image: mariadb:11.8
healthcheck: healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 5s start_period: 5s
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
restart: unless-stopped restart: unless-stopped
command: command:
- --character-set-server=utf8mb4 - --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci - --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake - --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
environment: environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-123} MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-123}
MARIADB_AUTO_UPGRADE: 1 MARIADB_AUTO_UPGRADE: 1
volumes: volumes:
- db-data:/var/lib/mysql - db-data:/var/lib/mysql
volumes: volumes:
db-data: db-data:

View file

@ -1,14 +1,14 @@
services: services:
frontend: frontend:
labels: labels:
# ${ROUTER}-http to use the middleware to redirect to https # ${ROUTER}-http to use the middleware to redirect to https
- traefik.http.routers.${ROUTER}-http.middlewares=https-redirect - traefik.http.routers.${ROUTER}-http.middlewares=https-redirect
# ${ROUTER}-https the actual router using HTTPS # ${ROUTER}-https the actual router using HTTPS
# Uses the environment variable SITES # Uses the environment variable SITES
- traefik.http.routers.${ROUTER}-https.rule=Host(${SITES?SITES not set}) - traefik.http.routers.${ROUTER}-https.rule=Host(${SITES?SITES not set})
- traefik.http.routers.${ROUTER}-https.entrypoints=https - traefik.http.routers.${ROUTER}-https.entrypoints=https
- traefik.http.routers.${ROUTER}-https.tls=true - traefik.http.routers.${ROUTER}-https.tls=true
# Use the service ${ROUTER} with the frontend # Use the service ${ROUTER} with the frontend
- traefik.http.routers.${ROUTER}-https.service=${ROUTER} - traefik.http.routers.${ROUTER}-https.service=${ROUTER}
# Use the "le" (Let's Encrypt) resolver created below # Use the "le" (Let's Encrypt) resolver created below
- traefik.http.routers.${ROUTER}-https.tls.certresolver=le - traefik.http.routers.${ROUTER}-https.tls.certresolver=le

View file

@ -1,54 +1,54 @@
services: services:
frontend: frontend:
networks: networks:
- traefik-public - traefik-public
- bench-network - bench-network
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.docker.network=traefik-public - traefik.docker.network=traefik-public
- traefik.http.services.${ROUTER?ROUTER not set}.loadbalancer.server.port=8080 - traefik.http.services.${ROUTER?ROUTER not set}.loadbalancer.server.port=8080
- traefik.http.routers.${ROUTER}-http.service=${ROUTER} - traefik.http.routers.${ROUTER}-http.service=${ROUTER}
- traefik.http.routers.${ROUTER}-http.entrypoints=http - traefik.http.routers.${ROUTER}-http.entrypoints=http
- traefik.http.routers.${ROUTER}-http.rule=Host(${SITES?SITES not set}) - traefik.http.routers.${ROUTER}-http.rule=Host(${SITES?SITES not set})
configurator: configurator:
networks: networks:
- bench-network - bench-network
- mariadb-network - mariadb-network
backend: backend:
networks: networks:
- mariadb-network - mariadb-network
- bench-network - bench-network
websocket: websocket:
networks: networks:
- bench-network - bench-network
- mariadb-network - mariadb-network
scheduler: scheduler:
networks: networks:
- bench-network - bench-network
- mariadb-network - mariadb-network
queue-short: queue-short:
networks: networks:
- bench-network - bench-network
- mariadb-network - mariadb-network
queue-long: queue-long:
networks: networks:
- bench-network - bench-network
- mariadb-network - mariadb-network
redis-cache: redis-cache:
networks: networks:
- bench-network - bench-network
- mariadb-network - mariadb-network
redis-queue: redis-queue:
networks: networks:
- bench-network - bench-network
- mariadb-network - mariadb-network
networks: networks:
traefik-public: traefik-public:
external: true external: true
mariadb-network: mariadb-network:
external: true external: true
bench-network: bench-network:
name: ${ROUTER} name: ${ROUTER}
external: false external: false

View file

@ -1,4 +1,4 @@
services: services:
frontend: frontend:
ports: ports:
- ${HTTP_PUBLISH_PORT:-8080}:8080 - ${HTTP_PUBLISH_PORT:-8080}:8080

View file

@ -1,18 +1,18 @@
services: services:
configurator: configurator:
environment: environment:
DB_HOST: db DB_HOST: db
DB_PORT: 5432 DB_PORT: 5432
depends_on: depends_on:
- db - db
db: db:
image: postgres:13.5 image: postgres:13.5
command: [] command: []
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD:?No db password set} POSTGRES_PASSWORD: ${DB_PASSWORD:?No db password set}
volumes: volumes:
- db-data:/var/lib/postgresql/data - db-data:/var/lib/postgresql/data
volumes: volumes:
db-data: db-data:

View file

@ -1,19 +1,19 @@
services: services:
frontend: frontend:
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.services.frontend.loadbalancer.server.port=8080 - traefik.http.services.frontend.loadbalancer.server.port=8080
- traefik.http.routers.frontend-http.entrypoints=web - traefik.http.routers.frontend-http.entrypoints=web
- traefik.http.routers.frontend-http.rule=HostRegexp(`{any:.+}`) - traefik.http.routers.frontend-http.rule=HostRegexp(`{any:.+}`)
proxy: proxy:
image: traefik:v2.11 image: traefik:v2.11
command: command:
- --providers.docker - --providers.docker
- --providers.docker.exposedbydefault=false - --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80 - --entrypoints.web.address=:80
ports: ports:
- ${HTTP_PUBLISH_PORT:-80}:80 - ${HTTP_PUBLISH_PORT:-80}:80
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
userns_mode: host userns_mode: host

View file

@ -1,21 +1,21 @@
services: services:
configurator: configurator:
environment: environment:
REDIS_CACHE: redis-cache:6379 REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379 REDIS_QUEUE: redis-queue:6379
depends_on: depends_on:
- redis-cache - redis-cache
- redis-queue - redis-queue
redis-cache: redis-cache:
image: redis:6.2-alpine image: redis:6.2-alpine
restart: unless-stopped restart: unless-stopped
redis-queue: redis-queue:
image: redis:6.2-alpine image: redis:6.2-alpine
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- redis-queue-data:/data - redis-queue-data:/data
volumes: volumes:
redis-queue-data: redis-queue-data:

View file

@ -1,48 +1,48 @@
services: services:
traefik: traefik:
labels: labels:
# https-redirect middleware to redirect HTTP to HTTPS # https-redirect middleware to redirect HTTP to HTTPS
# It can be reused by other stacks in other Docker Compose files # It can be reused by other stacks in other Docker Compose files
- traefik.http.middlewares.https-redirect.redirectscheme.scheme=https - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.https-redirect.redirectscheme.permanent=true - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
# traefik-http to use the middleware to redirect to https # traefik-http to use the middleware to redirect to https
- traefik.http.routers.traefik-public-http.middlewares=https-redirect - traefik.http.routers.traefik-public-http.middlewares=https-redirect
# traefik-https the actual router using HTTPS # traefik-https the actual router using HTTPS
# Uses the environment variable DOMAIN # Uses the environment variable DOMAIN
- traefik.http.routers.traefik-public-https.rule=Host(`${TRAEFIK_DOMAIN}`) - traefik.http.routers.traefik-public-https.rule=Host(`${TRAEFIK_DOMAIN}`)
- traefik.http.routers.traefik-public-https.entrypoints=https - traefik.http.routers.traefik-public-https.entrypoints=https
- traefik.http.routers.traefik-public-https.tls=true - traefik.http.routers.traefik-public-https.tls=true
# Use the special Traefik service api@internal with the web UI/Dashboard # Use the special Traefik service api@internal with the web UI/Dashboard
- traefik.http.routers.traefik-public-https.service=api@internal - traefik.http.routers.traefik-public-https.service=api@internal
# Use the "le" (Let's Encrypt) resolver created below # Use the "le" (Let's Encrypt) resolver created below
- traefik.http.routers.traefik-public-https.tls.certresolver=le - traefik.http.routers.traefik-public-https.tls.certresolver=le
# Enable HTTP Basic auth, using the middleware created above # Enable HTTP Basic auth, using the middleware created above
- traefik.http.routers.traefik-public-https.middlewares=admin-auth - traefik.http.routers.traefik-public-https.middlewares=admin-auth
command: command:
# Enable Docker in Traefik, so that it reads labels from Docker services # Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker=true - --providers.docker=true
# Do not expose all Docker services, only the ones explicitly exposed # Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false - --providers.docker.exposedbydefault=false
# Create an entrypoint http listening on port 80 # Create an entrypoint http listening on port 80
- --entrypoints.http.address=:80 - --entrypoints.http.address=:80
# Create an entrypoint https listening on port 443 # Create an entrypoint https listening on port 443
- --entrypoints.https.address=:443 - --entrypoints.https.address=:443
# Create the certificate resolver le for Let's Encrypt, uses the environment variable EMAIL # Create the certificate resolver le for Let's Encrypt, uses the environment variable EMAIL
- --certificatesresolvers.le.acme.email=${EMAIL:?No EMAIL set} - --certificatesresolvers.le.acme.email=${EMAIL:?No EMAIL set}
# Store the Let's Encrypt certificates in the mounted volume # Store the Let's Encrypt certificates in the mounted volume
- --certificatesresolvers.le.acme.storage=/certificates/acme.json - --certificatesresolvers.le.acme.storage=/certificates/acme.json
# Use the TLS Challenge for Let's Encrypt # Use the TLS Challenge for Let's Encrypt
- --certificatesresolvers.le.acme.tlschallenge=true - --certificatesresolvers.le.acme.tlschallenge=true
# Enable the access log, with HTTP requests # Enable the access log, with HTTP requests
- --accesslog - --accesslog
# Enable the Traefik log, for configurations and errors # Enable the Traefik log, for configurations and errors
- --log - --log
# Enable the Dashboard and API # Enable the Dashboard and API
- --api - --api
ports: ports:
- ${HTTPS_PUBLISH_PORT:-443}:443 - ${HTTPS_PUBLISH_PORT:-443}:443
volumes: volumes:
- cert-data:/certificates - cert-data:/certificates
volumes: volumes:
cert-data: cert-data:

View file

@ -1,47 +1,47 @@
version: "3.3" version: "3.3"
services: services:
traefik: traefik:
image: "traefik:v2.11" image: "traefik:v2.11"
restart: unless-stopped restart: unless-stopped
labels: labels:
# Enable Traefik for this service, to make it available in the public network # Enable Traefik for this service, to make it available in the public network
- traefik.enable=true - traefik.enable=true
# Use the traefik-public network (declared below) # Use the traefik-public network (declared below)
- traefik.docker.network=traefik-public - traefik.docker.network=traefik-public
# admin-auth middleware with HTTP Basic auth # admin-auth middleware with HTTP Basic auth
# Using the environment variables USERNAME and HASHED_PASSWORD # Using the environment variables USERNAME and HASHED_PASSWORD
- traefik.http.middlewares.admin-auth.basicauth.users=admin:${HASHED_PASSWORD:?No HASHED_PASSWORD set} - traefik.http.middlewares.admin-auth.basicauth.users=admin:${HASHED_PASSWORD:?No HASHED_PASSWORD set}
# Uses the environment variable TRAEFIK_DOMAIN # Uses the environment variable TRAEFIK_DOMAIN
- traefik.http.routers.traefik-public-http.rule=Host(`${TRAEFIK_DOMAIN:?No TRAEFIK_DOMAIN set}`) - traefik.http.routers.traefik-public-http.rule=Host(`${TRAEFIK_DOMAIN:?No TRAEFIK_DOMAIN set}`)
- traefik.http.routers.traefik-public-http.entrypoints=http - traefik.http.routers.traefik-public-http.entrypoints=http
# Use the special Traefik service api@internal with the web UI/Dashboard # Use the special Traefik service api@internal with the web UI/Dashboard
- traefik.http.routers.traefik-public-http.service=api@internal - traefik.http.routers.traefik-public-http.service=api@internal
# Enable HTTP Basic auth, using the middleware created above # Enable HTTP Basic auth, using the middleware created above
- traefik.http.routers.traefik-public-http.middlewares=admin-auth - traefik.http.routers.traefik-public-http.middlewares=admin-auth
# Define the port inside of the Docker service to use # Define the port inside of the Docker service to use
- traefik.http.services.traefik-public.loadbalancer.server.port=8080 - traefik.http.services.traefik-public.loadbalancer.server.port=8080
command: command:
# Enable Docker in Traefik, so that it reads labels from Docker services # Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker=true - --providers.docker=true
# Do not expose all Docker services, only the ones explicitly exposed # Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false - --providers.docker.exposedbydefault=false
# Create an entrypoint http listening on port 80 # Create an entrypoint http listening on port 80
- --entrypoints.http.address=:80 - --entrypoints.http.address=:80
# Enable the access log, with HTTP requests # Enable the access log, with HTTP requests
- --accesslog - --accesslog
# Enable the Traefik log, for configurations and errors # Enable the Traefik log, for configurations and errors
- --log - --log
# Enable the Dashboard and API # Enable the Dashboard and API
- --api - --api
ports: ports:
- ${HTTP_PUBLISH_PORT:-80}:80 - ${HTTP_PUBLISH_PORT:-80}:80
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
networks: networks:
- traefik-public - traefik-public
networks: networks:
traefik-public: traefik-public:
name: traefik-public name: traefik-public
external: false external: false

450
pwd.yml
View file

@ -1,225 +1,225 @@
version: "3" version: "3"
services: services:
backend: backend:
image: frappe/erpnext:v15.88.1 image: frappe/erpnext:v15.88.1
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
environment: environment:
DB_HOST: db DB_HOST: db
DB_PORT: "3306" DB_PORT: "3306"
MYSQL_ROOT_PASSWORD: admin MYSQL_ROOT_PASSWORD: admin
MARIADB_ROOT_PASSWORD: admin MARIADB_ROOT_PASSWORD: admin
configurator: configurator:
image: frappe/erpnext:v15.88.1 image: frappe/erpnext:v15.88.1
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: none condition: none
entrypoint: entrypoint:
- bash - bash
- -c - -c
command: command:
- > - >
ls -1 apps > sites/apps.txt; ls -1 apps > sites/apps.txt;
bench set-config -g db_host $$DB_HOST; bench set-config -g db_host $$DB_HOST;
bench set-config -gp db_port $$DB_PORT; bench set-config -gp db_port $$DB_PORT;
bench set-config -g redis_cache "redis://$$REDIS_CACHE"; bench set-config -g redis_cache "redis://$$REDIS_CACHE";
bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; bench set-config -g redis_queue "redis://$$REDIS_QUEUE";
bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; bench set-config -g redis_socketio "redis://$$REDIS_QUEUE";
bench set-config -gp socketio_port $$SOCKETIO_PORT; bench set-config -gp socketio_port $$SOCKETIO_PORT;
environment: environment:
DB_HOST: db DB_HOST: db
DB_PORT: "3306" DB_PORT: "3306"
REDIS_CACHE: redis-cache:6379 REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379 REDIS_QUEUE: redis-queue:6379
SOCKETIO_PORT: "9000" SOCKETIO_PORT: "9000"
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
create-site: create-site:
image: frappe/erpnext:v15.88.1 image: frappe/erpnext:v15.88.1
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: none condition: none
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
entrypoint: entrypoint:
- bash - bash
- -c - -c
command: command:
- > - >
wait-for-it -t 120 db:3306; wait-for-it -t 120 db:3306;
wait-for-it -t 120 redis-cache:6379; wait-for-it -t 120 redis-cache:6379;
wait-for-it -t 120 redis-queue:6379; wait-for-it -t 120 redis-queue:6379;
export start=`date +%s`; export start=`date +%s`;
until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \ until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \ [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]]; [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]];
do do
echo "Waiting for sites/common_site_config.json to be created"; echo "Waiting for sites/common_site_config.json to be created";
sleep 5; sleep 5;
if (( `date +%s`-start > 120 )); then if (( `date +%s`-start > 120 )); then
echo "could not find sites/common_site_config.json with required keys"; echo "could not find sites/common_site_config.json with required keys";
exit 1 exit 1
fi fi
done; done;
echo "sites/common_site_config.json found"; echo "sites/common_site_config.json found";
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; 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;
db: db:
image: mariadb:10.6 image: mariadb:10.6
networks: networks:
- frappe_network - frappe_network
healthcheck: healthcheck:
test: mysqladmin ping -h localhost --password=admin test: mysqladmin ping -h localhost --password=admin
interval: 1s interval: 1s
retries: 20 retries: 20
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
command: command:
- --character-set-server=utf8mb4 - --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci - --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake - --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
environment: environment:
MYSQL_ROOT_PASSWORD: admin MYSQL_ROOT_PASSWORD: admin
MARIADB_ROOT_PASSWORD: admin MARIADB_ROOT_PASSWORD: admin
volumes: volumes:
- db-data:/var/lib/mysql - db-data:/var/lib/mysql
frontend: frontend:
image: frappe/erpnext:v15.88.1 image: frappe/erpnext:v15.88.1
networks: networks:
- frappe_network - frappe_network
depends_on: depends_on:
- websocket - websocket
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
command: command:
- nginx-entrypoint.sh - nginx-entrypoint.sh
environment: environment:
BACKEND: backend:8000 BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: frontend FRAPPE_SITE_NAME_HEADER: frontend
SOCKETIO: websocket:9000 SOCKETIO: websocket:9000
UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1 UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
UPSTREAM_REAL_IP_HEADER: X-Forwarded-For UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
UPSTREAM_REAL_IP_RECURSIVE: "off" UPSTREAM_REAL_IP_RECURSIVE: "off"
PROXY_READ_TIMEOUT: 120 PROXY_READ_TIMEOUT: 120
CLIENT_MAX_BODY_SIZE: 50m CLIENT_MAX_BODY_SIZE: 50m
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
ports: ports:
- "8080:8080" - "8080:8080"
queue-long: queue-long:
image: frappe/erpnext:v15.88.1 image: frappe/erpnext:v15.88.1
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
command: command:
- bench - bench
- worker - worker
- --queue - --queue
- long,default,short - long,default,short
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
environment: environment:
FRAPPE_REDIS_CACHE: redis://redis-cache:6379 FRAPPE_REDIS_CACHE: redis://redis-cache:6379
FRAPPE_REDIS_QUEUE: redis://redis-queue:6379 FRAPPE_REDIS_QUEUE: redis://redis-queue:6379
queue-short: queue-short:
image: frappe/erpnext:v15.88.1 image: frappe/erpnext:v15.88.1
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
command: command:
- bench - bench
- worker - worker
- --queue - --queue
- short,default - short,default
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
environment: environment:
FRAPPE_REDIS_CACHE: redis://redis-cache:6379 FRAPPE_REDIS_CACHE: redis://redis-cache:6379
FRAPPE_REDIS_QUEUE: redis://redis-queue:6379 FRAPPE_REDIS_QUEUE: redis://redis-queue:6379
redis-queue: redis-queue:
image: redis:6.2-alpine image: redis:6.2-alpine
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
volumes: volumes:
- redis-queue-data:/data - redis-queue-data:/data
redis-cache: redis-cache:
image: redis:6.2-alpine image: redis:6.2-alpine
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
scheduler: scheduler:
image: frappe/erpnext:v15.88.1 image: frappe/erpnext:v15.88.1
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
command: command:
- bench - bench
- schedule - schedule
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
websocket: websocket:
image: frappe/erpnext:v15.88.1 image: frappe/erpnext:v15.88.1
networks: networks:
- frappe_network - frappe_network
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
command: command:
- node - node
- /home/frappe/frappe-bench/apps/frappe/socketio.js - /home/frappe/frappe-bench/apps/frappe/socketio.js
environment: environment:
FRAPPE_REDIS_CACHE: redis://redis-cache:6379 FRAPPE_REDIS_CACHE: redis://redis-cache:6379
FRAPPE_REDIS_QUEUE: redis://redis-queue:6379 FRAPPE_REDIS_QUEUE: redis://redis-queue:6379
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
volumes: volumes:
db-data: db-data:
redis-queue-data: redis-queue-data:
sites: sites:
logs: logs:
networks: networks:
frappe_network: frappe_network:
driver: bridge driver: bridge

View file

@ -1 +1 @@
pytest==8.4.2 pytest==8.4.2

View file

@ -1,52 +1,52 @@
#!/bin/bash #!/bin/bash
# Set variables that do not exist # Set variables that do not exist
if [[ -z "$BACKEND" ]]; then if [[ -z "$BACKEND" ]]; then
echo "BACKEND defaulting to 0.0.0.0:8000" echo "BACKEND defaulting to 0.0.0.0:8000"
export BACKEND=0.0.0.0:8000 export BACKEND=0.0.0.0:8000
fi fi
if [[ -z "$SOCKETIO" ]]; then if [[ -z "$SOCKETIO" ]]; then
echo "SOCKETIO defaulting to 0.0.0.0:9000" echo "SOCKETIO defaulting to 0.0.0.0:9000"
export SOCKETIO=0.0.0.0:9000 export SOCKETIO=0.0.0.0:9000
fi fi
if [[ -z "$UPSTREAM_REAL_IP_ADDRESS" ]]; then if [[ -z "$UPSTREAM_REAL_IP_ADDRESS" ]]; then
echo "UPSTREAM_REAL_IP_ADDRESS defaulting to 127.0.0.1" echo "UPSTREAM_REAL_IP_ADDRESS defaulting to 127.0.0.1"
export UPSTREAM_REAL_IP_ADDRESS=127.0.0.1 export UPSTREAM_REAL_IP_ADDRESS=127.0.0.1
fi fi
if [[ -z "$UPSTREAM_REAL_IP_HEADER" ]]; then if [[ -z "$UPSTREAM_REAL_IP_HEADER" ]]; then
echo "UPSTREAM_REAL_IP_HEADER defaulting to X-Forwarded-For" echo "UPSTREAM_REAL_IP_HEADER defaulting to X-Forwarded-For"
export UPSTREAM_REAL_IP_HEADER=X-Forwarded-For export UPSTREAM_REAL_IP_HEADER=X-Forwarded-For
fi fi
if [[ -z "$UPSTREAM_REAL_IP_RECURSIVE" ]]; then if [[ -z "$UPSTREAM_REAL_IP_RECURSIVE" ]]; then
echo "UPSTREAM_REAL_IP_RECURSIVE defaulting to off" echo "UPSTREAM_REAL_IP_RECURSIVE defaulting to off"
export UPSTREAM_REAL_IP_RECURSIVE=off export UPSTREAM_REAL_IP_RECURSIVE=off
fi fi
if [[ -z "$FRAPPE_SITE_NAME_HEADER" ]]; then if [[ -z "$FRAPPE_SITE_NAME_HEADER" ]]; then
# shellcheck disable=SC2016 # shellcheck disable=SC2016
echo 'FRAPPE_SITE_NAME_HEADER defaulting to $host' echo 'FRAPPE_SITE_NAME_HEADER defaulting to $host'
# shellcheck disable=SC2016 # shellcheck disable=SC2016
export FRAPPE_SITE_NAME_HEADER='$host' export FRAPPE_SITE_NAME_HEADER='$host'
fi fi
if [[ -z "$PROXY_READ_TIMEOUT" ]]; then if [[ -z "$PROXY_READ_TIMEOUT" ]]; then
echo "PROXY_READ_TIMEOUT defaulting to 120" echo "PROXY_READ_TIMEOUT defaulting to 120"
export PROXY_READ_TIMEOUT=120 export PROXY_READ_TIMEOUT=120
fi fi
if [[ -z "$CLIENT_MAX_BODY_SIZE" ]]; then if [[ -z "$CLIENT_MAX_BODY_SIZE" ]]; then
echo "CLIENT_MAX_BODY_SIZE defaulting to 50m" echo "CLIENT_MAX_BODY_SIZE defaulting to 50m"
export CLIENT_MAX_BODY_SIZE=50m export CLIENT_MAX_BODY_SIZE=50m
fi fi
# shellcheck disable=SC2016 # shellcheck disable=SC2016
envsubst '${BACKEND} envsubst '${BACKEND}
${SOCKETIO} ${SOCKETIO}
${UPSTREAM_REAL_IP_ADDRESS} ${UPSTREAM_REAL_IP_ADDRESS}
${UPSTREAM_REAL_IP_HEADER} ${UPSTREAM_REAL_IP_HEADER}
${UPSTREAM_REAL_IP_RECURSIVE} ${UPSTREAM_REAL_IP_RECURSIVE}
${FRAPPE_SITE_NAME_HEADER} ${FRAPPE_SITE_NAME_HEADER}
${PROXY_READ_TIMEOUT} ${PROXY_READ_TIMEOUT}
${CLIENT_MAX_BODY_SIZE}' \ ${CLIENT_MAX_BODY_SIZE}' \
</templates/nginx/frappe.conf.template >/etc/nginx/conf.d/frappe.conf </templates/nginx/frappe.conf.template >/etc/nginx/conf.d/frappe.conf
nginx -g 'daemon off;' nginx -g 'daemon off;'

View file

@ -1,107 +1,107 @@
upstream backend-server { upstream backend-server {
server ${BACKEND} fail_timeout=0; server ${BACKEND} fail_timeout=0;
} }
upstream socketio-server { upstream socketio-server {
server ${SOCKETIO} fail_timeout=0; server ${SOCKETIO} fail_timeout=0;
} }
server { server {
listen 8080; listen 8080;
server_name ${FRAPPE_SITE_NAME_HEADER}; server_name ${FRAPPE_SITE_NAME_HEADER};
root /home/frappe/frappe-bench/sites; root /home/frappe/frappe-bench/sites;
proxy_buffer_size 128k; proxy_buffer_size 128k;
proxy_buffers 4 256k; proxy_buffers 4 256k;
proxy_busy_buffers_size 256k; proxy_busy_buffers_size 256k;
add_header X-Frame-Options "SAMEORIGIN"; add_header X-Frame-Options "SAMEORIGIN";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin"; add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin";
set_real_ip_from ${UPSTREAM_REAL_IP_ADDRESS}; set_real_ip_from ${UPSTREAM_REAL_IP_ADDRESS};
real_ip_header ${UPSTREAM_REAL_IP_HEADER}; real_ip_header ${UPSTREAM_REAL_IP_HEADER};
real_ip_recursive ${UPSTREAM_REAL_IP_RECURSIVE}; real_ip_recursive ${UPSTREAM_REAL_IP_RECURSIVE};
location /assets { location /assets {
try_files $uri =404; try_files $uri =404;
} }
location ~ ^/protected/(.*) { location ~ ^/protected/(.*) {
internal; internal;
try_files /${FRAPPE_SITE_NAME_HEADER}/$1 =404; try_files /${FRAPPE_SITE_NAME_HEADER}/$1 =404;
} }
location /socket.io { location /socket.io {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER}; proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER};
proxy_set_header Origin $scheme://$http_host; proxy_set_header Origin $scheme://$http_host;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_pass http://socketio-server; proxy_pass http://socketio-server;
} }
location / { location / {
rewrite ^(.+)/$ $1 permanent; rewrite ^(.+)/$ $1 permanent;
rewrite ^(.+)/index\.html$ $1 permanent; rewrite ^(.+)/index\.html$ $1 permanent;
rewrite ^(.+)\.html$ $1 permanent; rewrite ^(.+)\.html$ $1 permanent;
location ~ ^/files/.*.(htm|html|svg|xml) { location ~ ^/files/.*.(htm|html|svg|xml) {
add_header Content-disposition "attachment"; add_header Content-disposition "attachment";
try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver; try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver;
} }
try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver; try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver;
} }
location @webserver { location @webserver {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER}; proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER};
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Use-X-Accel-Redirect True; proxy_set_header X-Use-X-Accel-Redirect True;
proxy_read_timeout ${PROXY_READ_TIMEOUT}; proxy_read_timeout ${PROXY_READ_TIMEOUT};
proxy_redirect off; proxy_redirect off;
proxy_pass http://backend-server; proxy_pass http://backend-server;
} }
# optimizations # optimizations
sendfile on; sendfile on;
keepalive_timeout 15; keepalive_timeout 15;
client_max_body_size ${CLIENT_MAX_BODY_SIZE}; client_max_body_size ${CLIENT_MAX_BODY_SIZE};
client_body_buffer_size 16K; client_body_buffer_size 16K;
client_header_buffer_size 1k; client_header_buffer_size 1k;
# enable gzip compression # enable gzip compression
# based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge # based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge
gzip on; gzip on;
gzip_http_version 1.1; gzip_http_version 1.1;
gzip_comp_level 5; gzip_comp_level 5;
gzip_min_length 256; gzip_min_length 256;
gzip_proxied any; gzip_proxied any;
gzip_vary on; gzip_vary on;
gzip_types gzip_types
application/atom+xml application/atom+xml
application/javascript application/javascript
application/json application/json
application/rss+xml application/rss+xml
application/vnd.ms-fontobject application/vnd.ms-fontobject
application/x-font-ttf application/x-font-ttf
application/font-woff application/font-woff
application/x-web-app-manifest+json application/x-web-app-manifest+json
application/xhtml+xml application/xhtml+xml
application/xml application/xml
font/opentype font/opentype
image/svg+xml image/svg+xml
image/x-icon image/x-icon
text/css text/css
text/plain text/plain
text/x-component; text/x-component;
# text/html is always compressed by HttpGzipModule # text/html is always compressed by HttpGzipModule
} }

View file

@ -1,12 +1,12 @@
# Config file for isort, codespell and other Python projects. # Config file for isort, codespell and other Python projects.
# In this case it is not used for packaging. # In this case it is not used for packaging.
[isort] [isort]
profile = black profile = black
known_third_party = frappe known_third_party = frappe
[codespell] [codespell]
skip = images/bench/Dockerfile skip = images/bench/Dockerfile
[tool:pytest] [tool:pytest]
addopts = -s --exitfirst addopts = -s --exitfirst

View file

@ -1,52 +1,52 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import json import json
import socket import socket
from typing import Any, Iterable, Tuple from typing import Any, Iterable, Tuple
Address = Tuple[str, int] Address = Tuple[str, int]
async def wait_for_port(address: Address) -> None: async def wait_for_port(address: Address) -> None:
# From https://github.com/clarketm/wait-for-it # From https://github.com/clarketm/wait-for-it
while True: while True:
try: try:
_, writer = await asyncio.open_connection(*address) _, writer = await asyncio.open_connection(*address)
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
break break
except (socket.gaierror, ConnectionError, OSError, TypeError): except (socket.gaierror, ConnectionError, OSError, TypeError):
pass pass
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
def get_redis_url(addr: str) -> Address: def get_redis_url(addr: str) -> Address:
result = addr.replace("redis://", "") result = addr.replace("redis://", "")
result = result.split("/")[0] result = result.split("/")[0]
parts = result.split(":") parts = result.split(":")
assert len(parts) == 2 assert len(parts) == 2
return parts[0], int(parts[1]) return parts[0], int(parts[1])
def get_addresses(config: dict[str, Any]) -> Iterable[Address]: def get_addresses(config: dict[str, Any]) -> Iterable[Address]:
yield (config["db_host"], config["db_port"]) yield (config["db_host"], config["db_port"])
for key in ("redis_cache", "redis_queue"): for key in ("redis_cache", "redis_queue"):
yield get_redis_url(config[key]) yield get_redis_url(config[key])
async def async_main(addresses: set[Address]) -> None: async def async_main(addresses: set[Address]) -> None:
tasks = [asyncio.wait_for(wait_for_port(addr), timeout=5) for addr in addresses] tasks = [asyncio.wait_for(wait_for_port(addr), timeout=5) for addr in addresses]
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
def main() -> int: def main() -> int:
with open("/home/frappe/frappe-bench/sites/common_site_config.json") as f: with open("/home/frappe/frappe-bench/sites/common_site_config.json") as f:
config = json.load(f) config = json.load(f)
addresses = set(get_addresses(config)) addresses = set(get_addresses(config))
asyncio.run(async_main(addresses)) asyncio.run(async_main(addresses))
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())

View file

@ -1,17 +1,17 @@
import frappe import frappe
def check_website_theme(): def check_website_theme():
doc = frappe.new_doc("Website Theme") doc = frappe.new_doc("Website Theme")
doc.theme = "test theme" doc.theme = "test theme"
doc.insert() doc.insert()
def main() -> int: def main() -> int:
frappe.connect(site="tests") frappe.connect(site="tests")
check_website_theme() check_website_theme()
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())

View file

@ -1,19 +1,19 @@
import os import os
import boto3 import boto3
def main() -> int: def main() -> int:
resource = boto3.resource( resource = boto3.resource(
service_name="s3", service_name="s3",
endpoint_url="http://minio:9000", endpoint_url="http://minio:9000",
region_name="us-east-1", region_name="us-east-1",
aws_access_key_id=os.getenv("S3_ACCESS_KEY"), aws_access_key_id=os.getenv("S3_ACCESS_KEY"),
aws_secret_access_key=os.getenv("S3_SECRET_KEY"), aws_secret_access_key=os.getenv("S3_SECRET_KEY"),
) )
resource.create_bucket(Bucket="frappe") resource.create_bucket(Bucket="frappe")
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())

View file

@ -1,26 +1,26 @@
import frappe import frappe
def check_db(): def check_db():
doc = frappe.get_single("System Settings") doc = frappe.get_single("System Settings")
assert any(v is None for v in doc.as_dict().values()), "Database test didn't pass" assert any(v is None for v in doc.as_dict().values()), "Database test didn't pass"
print("Database works!") print("Database works!")
def check_cache(): def check_cache():
key_and_name = "mytestkey", "mytestname" key_and_name = "mytestkey", "mytestname"
frappe.cache().hset(*key_and_name, "mytestvalue") frappe.cache().hset(*key_and_name, "mytestvalue")
assert frappe.cache().hget(*key_and_name) == "mytestvalue", "Cache test didn't pass" assert frappe.cache().hget(*key_and_name) == "mytestvalue", "Cache test didn't pass"
frappe.cache().hdel(*key_and_name) frappe.cache().hdel(*key_and_name)
print("Cache works!") print("Cache works!")
def main() -> int: def main() -> int:
frappe.connect(site="tests.localhost") frappe.connect(site="tests.localhost")
check_db() check_db()
check_cache() check_cache()
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())

View file

@ -1,21 +1,21 @@
services: services:
configurator: configurator:
image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION}
backend: backend:
image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION}
frontend: frontend:
image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION}
websocket: websocket:
image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION}
queue-short: queue-short:
image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION}
queue-long: queue-long:
image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION}
scheduler: scheduler:
image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION} image: localhost:5000/frappe/erpnext:${ERPNEXT_VERSION}

View file

@ -1,166 +1,166 @@
import os import os
import re import re
import shutil import shutil
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import pytest import pytest
from tests.utils import CI, Compose from tests.utils import CI, Compose
def _add_version_var(name: str, env_path: Path): def _add_version_var(name: str, env_path: Path):
value = os.getenv(name) value = os.getenv(name)
if not value: if not value:
return return
if value == "develop": if value == "develop":
os.environ[name] = "latest" os.environ[name] = "latest"
with open(env_path, "a") as f: with open(env_path, "a") as f:
f.write(f"\n{name}={os.environ[name]}") f.write(f"\n{name}={os.environ[name]}")
def _add_sites_var(env_path: Path): def _add_sites_var(env_path: Path):
with open(env_path, "r+") as f: with open(env_path, "r+") as f:
content = f.read() content = f.read()
content = re.sub( content = re.sub(
rf"SITES=.*", rf"SITES=.*",
f"SITES=`tests.localhost`,`test-erpnext-site.localhost`,`test-pg-site.localhost`", f"SITES=`tests.localhost`,`test-erpnext-site.localhost`,`test-pg-site.localhost`",
content, content,
) )
f.seek(0) f.seek(0)
f.truncate() f.truncate()
f.write(content) f.write(content)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def env_file(tmp_path_factory: pytest.TempPathFactory): def env_file(tmp_path_factory: pytest.TempPathFactory):
tmp_path = tmp_path_factory.mktemp("frappe-docker") tmp_path = tmp_path_factory.mktemp("frappe-docker")
file_path = tmp_path / ".env" file_path = tmp_path / ".env"
shutil.copy("example.env", file_path) shutil.copy("example.env", file_path)
_add_sites_var(file_path) _add_sites_var(file_path)
for var in ("FRAPPE_VERSION", "ERPNEXT_VERSION"): for var in ("FRAPPE_VERSION", "ERPNEXT_VERSION"):
_add_version_var(name=var, env_path=file_path) _add_version_var(name=var, env_path=file_path)
yield str(file_path) yield str(file_path)
os.remove(file_path) os.remove(file_path)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def compose(env_file: str): def compose(env_file: str):
return Compose(project_name="test", env_file=env_file) return Compose(project_name="test", env_file=env_file)
@pytest.fixture(autouse=True, scope="session") @pytest.fixture(autouse=True, scope="session")
def frappe_setup(compose: Compose): def frappe_setup(compose: Compose):
compose.stop() compose.stop()
compose("up", "-d", "--quiet-pull") compose("up", "-d", "--quiet-pull")
yield yield
compose.stop() compose.stop()
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def frappe_site(compose: Compose): def frappe_site(compose: Compose):
site_name = "tests.localhost" site_name = "tests.localhost"
compose.bench( compose.bench(
"new-site", "new-site",
# TODO: change to --mariadb-user-host-login-scope=% # TODO: change to --mariadb-user-host-login-scope=%
"--no-mariadb-socket", "--no-mariadb-socket",
"--db-root-password=123", "--db-root-password=123",
"--admin-password=admin", "--admin-password=admin",
site_name, site_name,
) )
compose("restart", "backend") compose("restart", "backend")
yield site_name yield site_name
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def erpnext_setup(compose: Compose): def erpnext_setup(compose: Compose):
compose.stop() compose.stop()
compose("up", "-d", "--quiet-pull") compose("up", "-d", "--quiet-pull")
yield yield
compose.stop() compose.stop()
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def erpnext_site(compose: Compose): def erpnext_site(compose: Compose):
site_name = "test-erpnext-site.localhost" site_name = "test-erpnext-site.localhost"
args = [ args = [
"new-site", "new-site",
# TODO: change to --mariadb-user-host-login-scope=% # TODO: change to --mariadb-user-host-login-scope=%
"--no-mariadb-socket", "--no-mariadb-socket",
"--db-root-password=123", "--db-root-password=123",
"--admin-password=admin", "--admin-password=admin",
"--install-app=erpnext", "--install-app=erpnext",
site_name, site_name,
] ]
compose.bench(*args) compose.bench(*args)
compose("restart", "backend") compose("restart", "backend")
yield site_name yield site_name
@pytest.fixture @pytest.fixture
def postgres_setup(compose: Compose): def postgres_setup(compose: Compose):
compose.stop() compose.stop()
compose("-f", "overrides/compose.postgres.yaml", "up", "-d", "--quiet-pull") compose("-f", "overrides/compose.postgres.yaml", "up", "-d", "--quiet-pull")
compose.bench("set-config", "-g", "root_login", "postgres") compose.bench("set-config", "-g", "root_login", "postgres")
compose.bench("set-config", "-g", "root_password", "123") compose.bench("set-config", "-g", "root_password", "123")
yield yield
compose.stop() compose.stop()
@pytest.fixture @pytest.fixture
def python_path(): def python_path():
return "/home/frappe/frappe-bench/env/bin/python" return "/home/frappe/frappe-bench/env/bin/python"
@dataclass @dataclass
class S3ServiceResult: class S3ServiceResult:
access_key: str access_key: str
secret_key: str secret_key: str
@pytest.fixture @pytest.fixture
def s3_service(python_path: str, compose: Compose): def s3_service(python_path: str, compose: Compose):
access_key = "AKIAIOSFODNN7EXAMPLE" access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
cmd = ( cmd = (
"docker", "docker",
"run", "run",
"--name", "--name",
"minio", "minio",
"-d", "-d",
"-e", "-e",
f"MINIO_ACCESS_KEY={access_key}", f"MINIO_ACCESS_KEY={access_key}",
"-e", "-e",
f"MINIO_SECRET_KEY={secret_key}", f"MINIO_SECRET_KEY={secret_key}",
"--network", "--network",
f"{compose.project_name}_default", f"{compose.project_name}_default",
"minio/minio", "minio/minio",
"server", "server",
"/data", "/data",
) )
subprocess.check_call(cmd) subprocess.check_call(cmd)
compose("cp", "tests/_create_bucket.py", "backend:/tmp") compose("cp", "tests/_create_bucket.py", "backend:/tmp")
compose.exec("backend", "bench", "pip", "install", "boto3~=1.34.143") compose.exec("backend", "bench", "pip", "install", "boto3~=1.34.143")
compose.exec( compose.exec(
"-e", "-e",
f"S3_ACCESS_KEY={access_key}", f"S3_ACCESS_KEY={access_key}",
"-e", "-e",
f"S3_SECRET_KEY={secret_key}", f"S3_SECRET_KEY={secret_key}",
"backend", "backend",
python_path, python_path,
"/tmp/_create_bucket.py", "/tmp/_create_bucket.py",
) )
yield S3ServiceResult(access_key=access_key, secret_key=secret_key) yield S3ServiceResult(access_key=access_key, secret_key=secret_key)
subprocess.call(("docker", "rm", "minio", "-f")) subprocess.call(("docker", "rm", "minio", "-f"))

View file

@ -1,151 +1,151 @@
import os import os
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import pytest import pytest
from tests.conftest import S3ServiceResult from tests.conftest import S3ServiceResult
from tests.utils import Compose, check_url_content from tests.utils import Compose, check_url_content
BACKEND_SERVICES = ( BACKEND_SERVICES = (
"backend", "backend",
"queue-short", "queue-short",
"queue-long", "queue-long",
"scheduler", "scheduler",
) )
@pytest.mark.parametrize("service", BACKEND_SERVICES) @pytest.mark.parametrize("service", BACKEND_SERVICES)
def test_links_in_backends(service: str, compose: Compose, python_path: str): def test_links_in_backends(service: str, compose: Compose, python_path: str):
filename = "_check_connections.py" filename = "_check_connections.py"
compose("cp", f"tests/{filename}", f"{service}:/tmp/") compose("cp", f"tests/{filename}", f"{service}:/tmp/")
compose.exec(service, python_path, f"/tmp/{filename}") compose.exec(service, python_path, f"/tmp/{filename}")
def index_cb(text: str): def index_cb(text: str):
if "404 page not found" not in text: if "404 page not found" not in text:
return text[:200] return text[:200]
def api_cb(text: str): def api_cb(text: str):
if '"message"' in text: if '"message"' in text:
return text return text
def assets_cb(text: str): def assets_cb(text: str):
if text: if text:
return text[:200] return text[:200]
@pytest.mark.parametrize( @pytest.mark.parametrize(
("url", "callback"), (("/", index_cb), ("/api/method/ping", api_cb)) ("url", "callback"), (("/", index_cb), ("/api/method/ping", api_cb))
) )
def test_endpoints(url: str, callback: Any, frappe_site: str): def test_endpoints(url: str, callback: Any, frappe_site: str):
check_url_content( check_url_content(
url=f"http://127.0.0.1{url}", callback=callback, site_name=frappe_site url=f"http://127.0.0.1{url}", callback=callback, site_name=frappe_site
) )
@pytest.mark.skipif( @pytest.mark.skipif(
os.environ["FRAPPE_VERSION"][0:3] == "v12", reason="v12 doesn't have the asset" os.environ["FRAPPE_VERSION"][0:3] == "v12", reason="v12 doesn't have the asset"
) )
def test_assets_endpoint(frappe_site: str): def test_assets_endpoint(frappe_site: str):
check_url_content( check_url_content(
url=f"http://127.0.0.1/assets/frappe/images/frappe-framework-logo.svg", url=f"http://127.0.0.1/assets/frappe/images/frappe-framework-logo.svg",
callback=assets_cb, callback=assets_cb,
site_name=frappe_site, site_name=frappe_site,
) )
def test_files_reachable(frappe_site: str, tmp_path: Path, compose: Compose): def test_files_reachable(frappe_site: str, tmp_path: Path, compose: Compose):
content = "lalala\n" content = "lalala\n"
file_path = tmp_path / "testfile.txt" file_path = tmp_path / "testfile.txt"
with file_path.open("w") as f: with file_path.open("w") as f:
f.write(content) f.write(content)
compose( compose(
"cp", "cp",
str(file_path), str(file_path),
f"backend:/home/frappe/frappe-bench/sites/{frappe_site}/public/files/", f"backend:/home/frappe/frappe-bench/sites/{frappe_site}/public/files/",
) )
def callback(text: str): def callback(text: str):
if text == content: if text == content:
return text return text
check_url_content( check_url_content(
url=f"http://127.0.0.1/files/{file_path.name}", url=f"http://127.0.0.1/files/{file_path.name}",
callback=callback, callback=callback,
site_name=frappe_site, site_name=frappe_site,
) )
@pytest.mark.parametrize("service", BACKEND_SERVICES) @pytest.mark.parametrize("service", BACKEND_SERVICES)
@pytest.mark.usefixtures("frappe_site") @pytest.mark.usefixtures("frappe_site")
def test_frappe_connections_in_backends( def test_frappe_connections_in_backends(
service: str, python_path: str, compose: Compose service: str, python_path: str, compose: Compose
): ):
filename = "_ping_frappe_connections.py" filename = "_ping_frappe_connections.py"
compose("cp", f"tests/{filename}", f"{service}:/tmp/") compose("cp", f"tests/{filename}", f"{service}:/tmp/")
compose.exec( compose.exec(
"-w", "-w",
"/home/frappe/frappe-bench/sites", "/home/frappe/frappe-bench/sites",
service, service,
python_path, python_path,
f"/tmp/{filename}", f"/tmp/{filename}",
) )
def test_push_backup( def test_push_backup(
frappe_site: str, frappe_site: str,
s3_service: S3ServiceResult, s3_service: S3ServiceResult,
compose: Compose, compose: Compose,
): ):
restic_password = "secret" restic_password = "secret"
compose.bench("--site", frappe_site, "backup", "--with-files") compose.bench("--site", frappe_site, "backup", "--with-files")
restic_args = [ restic_args = [
"--env=RESTIC_REPOSITORY=s3:http://minio:9000/frappe", "--env=RESTIC_REPOSITORY=s3:http://minio:9000/frappe",
f"--env=AWS_ACCESS_KEY_ID={s3_service.access_key}", f"--env=AWS_ACCESS_KEY_ID={s3_service.access_key}",
f"--env=AWS_SECRET_ACCESS_KEY={s3_service.secret_key}", f"--env=AWS_SECRET_ACCESS_KEY={s3_service.secret_key}",
f"--env=RESTIC_PASSWORD={restic_password}", f"--env=RESTIC_PASSWORD={restic_password}",
] ]
compose.exec(*restic_args, "backend", "restic", "init") compose.exec(*restic_args, "backend", "restic", "init")
compose.exec(*restic_args, "backend", "restic", "backup", "sites") compose.exec(*restic_args, "backend", "restic", "backup", "sites")
compose.exec(*restic_args, "backend", "restic", "snapshots") compose.exec(*restic_args, "backend", "restic", "snapshots")
def test_https(frappe_site: str, compose: Compose): def test_https(frappe_site: str, compose: Compose):
compose("-f", "overrides/compose.https.yaml", "up", "-d") compose("-f", "overrides/compose.https.yaml", "up", "-d")
check_url_content(url="https://127.0.0.1", callback=index_cb, site_name=frappe_site) check_url_content(url="https://127.0.0.1", callback=index_cb, site_name=frappe_site)
@pytest.mark.usefixtures("erpnext_setup") @pytest.mark.usefixtures("erpnext_setup")
class TestErpnext: class TestErpnext:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("url", "callback"), ("url", "callback"),
( (
( (
"/api/method/erpnext.templates.pages.search_help.get_help_results_sections?text=help", "/api/method/erpnext.templates.pages.search_help.get_help_results_sections?text=help",
api_cb, api_cb,
), ),
("/assets/erpnext/js/setup_wizard.js", assets_cb), ("/assets/erpnext/js/setup_wizard.js", assets_cb),
), ),
) )
def test_endpoints(self, url: str, callback: Any, erpnext_site: str): def test_endpoints(self, url: str, callback: Any, erpnext_site: str):
check_url_content( check_url_content(
url=f"http://127.0.0.1{url}", callback=callback, site_name=erpnext_site url=f"http://127.0.0.1{url}", callback=callback, site_name=erpnext_site
) )
@pytest.mark.usefixtures("postgres_setup") @pytest.mark.usefixtures("postgres_setup")
class TestPostgres: class TestPostgres:
def test_site_creation(self, compose: Compose): def test_site_creation(self, compose: Compose):
compose.bench( compose.bench(
"new-site", "new-site",
"test-pg-site.localhost", "test-pg-site.localhost",
"--db-type", "--db-type",
"postgres", "postgres",
"--admin-password", "--admin-password",
"admin", "admin",
) )

View file

@ -1,89 +1,89 @@
import os import os
import ssl import ssl
import subprocess import subprocess
import sys import sys
import time import time
from contextlib import suppress from contextlib import suppress
from typing import Callable, Optional from typing import Callable, Optional
from urllib.error import HTTPError, URLError from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
CI = os.getenv("CI") CI = os.getenv("CI")
class Compose: class Compose:
def __init__(self, project_name: str, env_file: str): def __init__(self, project_name: str, env_file: str):
self.project_name = project_name self.project_name = project_name
self.base_cmd = ( self.base_cmd = (
"docker", "docker",
"compose", "compose",
"-p", "-p",
project_name, project_name,
"--env-file", "--env-file",
env_file, env_file,
) )
def __call__(self, *cmd: str) -> None: def __call__(self, *cmd: str) -> None:
file_args = [ file_args = [
"-f", "-f",
"compose.yaml", "compose.yaml",
"-f", "-f",
"overrides/compose.proxy.yaml", "overrides/compose.proxy.yaml",
"-f", "-f",
"overrides/compose.mariadb.yaml", "overrides/compose.mariadb.yaml",
"-f", "-f",
"overrides/compose.redis.yaml", "overrides/compose.redis.yaml",
] ]
if CI: if CI:
file_args += ("-f", "tests/compose.ci.yaml") file_args += ("-f", "tests/compose.ci.yaml")
args = self.base_cmd + tuple(file_args) + cmd args = self.base_cmd + tuple(file_args) + cmd
subprocess.check_call(args) subprocess.check_call(args)
def exec(self, *cmd: str) -> None: def exec(self, *cmd: str) -> None:
if sys.stdout.isatty(): if sys.stdout.isatty():
self("exec", *cmd) self("exec", *cmd)
else: else:
self("exec", "-T", *cmd) self("exec", "-T", *cmd)
def stop(self) -> None: def stop(self) -> None:
# Stop all containers in `test` project if they are running. # Stop all containers in `test` project if they are running.
# We don't care if it fails. # We don't care if it fails.
with suppress(subprocess.CalledProcessError): with suppress(subprocess.CalledProcessError):
subprocess.check_call(self.base_cmd + ("down", "-v", "--remove-orphans")) subprocess.check_call(self.base_cmd + ("down", "-v", "--remove-orphans"))
def bench(self, *cmd: str) -> None: def bench(self, *cmd: str) -> None:
self.exec("backend", "bench", *cmd) self.exec("backend", "bench", *cmd)
def check_url_content( def check_url_content(
url: str, callback: Callable[[str], Optional[str]], site_name: str url: str, callback: Callable[[str], Optional[str]], site_name: str
): ):
request = Request(url, headers={"Host": site_name}) request = Request(url, headers={"Host": site_name})
# This is needed to check https override # This is needed to check https override
ctx = ssl.create_default_context() ctx = ssl.create_default_context()
ctx.check_hostname = False ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE ctx.verify_mode = ssl.CERT_NONE
for _ in range(100): for _ in range(100):
try: try:
response = urlopen(request, context=ctx) response = urlopen(request, context=ctx)
except HTTPError as exc: except HTTPError as exc:
if exc.code not in (404, 502): if exc.code not in (404, 502):
raise raise
except URLError: except URLError:
pass pass
else: else:
text: str = response.read().decode() text: str = response.read().decode()
ret = callback(text) ret = callback(text)
if ret: if ret:
print(ret) print(ret)
return return
time.sleep(0.1) time.sleep(0.1)
raise RuntimeError(f"Couldn't ping {url}") raise RuntimeError(f"Couldn't ping {url}")