Raja's Exocortex

ERPNext with India Compliance

Install ERPNext v15 with India Compliance v15 app on docker.

Note: the same steps work for ERPNext v16 also, just replace 15 with 16 in all the below config files, sed -i 's/15/16/g' *.sh *.yaml Containerfile.

Steps

  1. Use the below build.sh to create a custom docker container with ERPNext and India Compliance app installed.
  2. After build.sh completes, check docker images and delete any temporary images created during the build.
  3. Use the below docker-compose.yaml and run.sh to start the custom docker container and dependencies. The docker-compose.yaml shown here changes all docker volumes to bind mounts (but docker-compose still creates volumes for some reason).
  4. Login to ERPNext using http://$IP:8080 with default username Administrator and password admin.

Filename: build.sh

#!/bin/bash
# Filename: build.sh
# Build a custom docker image with ERPNext and India Compliance v15 apps
# Run `docker images` and delete any temporary images created during the build process

set -eoux pipefail

export APPS_JSON='[
  {
    "url": "https://github.com/frappe/erpnext",
    "branch": "version-15"
  },
  {
    "url": "https://github.com/resilient-tech/india-compliance",
    "branch": "version-15"
  }
]'

export APPS_JSON_BASE64=$(echo ${APPS_JSON} | base64 -w 0)

wget https://raw.githubusercontent.com/frappe/frappe_docker/refs/heads/main/images/layered/Containerfile

docker build \
 --build-arg=FRAPPE_BRANCH=version-15 \
 --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
 --tag=erpnext-custom:15 \
 --file=Containerfile .

Filename: run.sh

#!/bin/bash
# Filename: run.sh

set -eoux pipefail

mkdir sites logs
chown 1000:1000 sites logs


cat << EOF > sites/common_site_config.json
{
  "db_type": "mariadb",
  "db_name": "frontend",
  "db_password": "$(openssl rand -base64 12)"
}
EOF

chown 1000:1000 sites/common_site_config.json


docker-compose up -d
echo Waiting for ERPNext installation to complete...
sleep 120

echo Installing india_compliance app
docker-compose exec frontend bench --site frontend install-app india_compliance

Filename: Containerfile

# Filename: Containerfile
# Adapted from: https://github.com/frappe/frappe_docker/blob/main/images/layered/Containerfile
ARG FRAPPE_BRANCH=version-15

FROM frappe/build:${FRAPPE_BRANCH} AS builder

ARG FRAPPE_BRANCH=version-15
ARG FRAPPE_PATH=https://github.com/frappe/frappe
ARG APPS_JSON_BASE64

USER root

RUN if [ -n "${APPS_JSON_BASE64}" ]; then \
    mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \
  fi

USER frappe

RUN export APP_INSTALL_ARGS="" && \
  if [ -n "${APPS_JSON_BASE64}" ]; then \
    export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \
  fi && \
  bench init ${APP_INSTALL_ARGS}\
    --frappe-branch=${FRAPPE_BRANCH} \
    --frappe-path=${FRAPPE_PATH} \
    --no-procfile \
    --no-backups \
    --skip-redis-config-generation \
    --verbose \
    /home/frappe/frappe-bench && \
  cd /home/frappe/frappe-bench && \
  echo "{}" > sites/common_site_config.json && \
  find apps -mindepth 1 -path "*/.git" | xargs rm -fr

FROM frappe/base:${FRAPPE_BRANCH} AS backend

USER frappe

COPY --from=builder --chown=frappe:frappe /home/frappe/frappe-bench /home/frappe/frappe-bench

WORKDIR /home/frappe/frappe-bench

VOLUME [ \
  "/home/frappe/frappe-bench/sites", \
  "/home/frappe/frappe-bench/sites/assets", \
  "/home/frappe/frappe-bench/logs" \
]

CMD [ \
  "/home/frappe/frappe-bench/env/bin/gunicorn", \
  "--chdir=/home/frappe/frappe-bench/sites", \
  "--bind=0.0.0.0:8000", \
  "--threads=4", \
  "--workers=2", \
  "--worker-class=gthread", \
  "--worker-tmp-dir=/dev/shm", \
  "--timeout=120", \
  "--preload", \
  "frappe.app:application" \
]

Filename: docker-compose.yaml

# Filename: docker-compose.yaml
# Adapted from: https://github.com/frappe/frappe_docker/blob/main/pwd.yml
# Replaced docker volumes with bind mounts
# Replaced original erpnext image with custom image that includes India Compliance app

services:
  backend:
    image: erpnext-custom:15
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: on-failure
    volumes:
      - ./sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs
    environment:
      DB_HOST: db
      DB_PORT: "3306"
      MYSQL_ROOT_PASSWORD: admin
      MARIADB_ROOT_PASSWORD: admin

  configurator:
    image: erpnext-custom:15
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: none
    entrypoint:
      - bash
      - -c
    command:
      - >
        ls -1 apps > sites/apps.txt;
        bench set-config -g db_host $DB_HOST;
        bench set-config -gp db_port $DB_PORT;
        bench set-config -g redis_cache "redis://$REDIS_CACHE";
        bench set-config -g redis_queue "redis://$REDIS_QUEUE";
        bench set-config -g redis_socketio "redis://$REDIS_QUEUE";
        bench set-config -gp socketio_port $SOCKETIO_PORT;
    environment:
      DB_HOST: db
      DB_PORT: "3306"
      REDIS_CACHE: redis-cache:6379
      REDIS_QUEUE: redis-queue:6379
      SOCKETIO_PORT: "9000"
    volumes:
      - ./sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs

  create-site:
    image: erpnext-custom:15
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: none
    volumes:
      - ./sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs
    entrypoint:
      - bash
      - -c
    command:
      - >
        wait-for-it -t 120 db:3306;
        wait-for-it -t 120 redis-cache:6379;
        wait-for-it -t 120 redis-queue:6379;
        export start=`date +%s`;
        until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \
          [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \
          [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]];
        do
          echo "Waiting for sites/common_site_config.json to be created";
          sleep 5;
          if (( `date +%s`-start > 120 )); then
            echo "could not find sites/common_site_config.json with required keys";
            exit 1
          fi
        done;
        echo "sites/common_site_config.json found";
        bench new-site --mariadb-user-host-login-scope='%' --admin-password=admin --db-root-username=root --db-root-password=admin --install-app erpnext --set-default frontend;

  db:
    image: mariadb:10.6
    networks:
      - frappe_network
    healthcheck:
      test: mysqladmin ping -h localhost --password=admin
      interval: 1s
      retries: 20
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --skip-character-set-client-handshake
      - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
    environment:
      MYSQL_ROOT_PASSWORD: admin
      MARIADB_ROOT_PASSWORD: admin
    volumes:
      - ./db-data:/var/lib/mysql

  frontend:
    image: erpnext-custom:15
    networks:
      - frappe_network
    depends_on:
      - websocket
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - nginx-entrypoint.sh
    environment:
      BACKEND: backend:8000
      FRAPPE_SITE_NAME_HEADER: frontend
      SOCKETIO: websocket:9000
      UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
      UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
      UPSTREAM_REAL_IP_RECURSIVE: "off"
      PROXY_READ_TIMEOUT: 120
      CLIENT_MAX_BODY_SIZE: 50m
    volumes:
      - ./sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs
    ports:
      - "8080:8080"

  queue-long:
    image: erpnext-custom:15
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - bench
      - worker
      - --queue
      - long,default,short
    volumes:
      - ./sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs
    environment:
      FRAPPE_REDIS_CACHE: redis://redis-cache:6379
      FRAPPE_REDIS_QUEUE: redis://redis-queue:6379

  queue-short:
    image: erpnext-custom:15
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - bench
      - worker
      - --queue
      - short,default
    volumes:
      - ./sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs
    environment:
      FRAPPE_REDIS_CACHE: redis://redis-cache:6379
      FRAPPE_REDIS_QUEUE: redis://redis-queue:6379

  redis-queue:
    image: redis:6.2-alpine
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: on-failure
    volumes:
      - ./redis-queue-data:/data

  redis-cache:
    image: redis:6.2-alpine
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: on-failure

  scheduler:
    image: erpnext-custom:15
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - bench
      - schedule
    volumes:
      - ./sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs

  websocket:
    image: erpnext-custom:15
    networks:
      - frappe_network
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - node
      - /home/frappe/frappe-bench/apps/frappe/socketio.js
    environment:
      FRAPPE_REDIS_CACHE: redis://redis-cache:6379
      FRAPPE_REDIS_QUEUE: redis://redis-queue:6379
    volumes:
      - ./sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs

networks:
  frappe_network:
    driver: bridge

References

  1. India Compliance app docker installation: https://docs.indiacompliance.app/docs/getting-started/installation#docker
  2. ERPNext single compose setup: https://github.com/frappe/frappe_docker/blob/main/docs/01-getting-started/02-single-compose-setup.md

Bugs

  1. docker volume ls shows volumes created for sites, logs etc. even though bind mounts are used in docker-compose.yaml.
  2. Removing VOLUME section from Containerfile still docker volumes.