NGINX on Host for No-proxy ERPNext Docker Setup

Hello,

For different reasons, we need to deploy ERPNext without reverse proxy and use an existing NGINX setup on the host server.

The used docker compose file is:

name: example
services:
  backend:
    depends_on:
      configurator:
        condition: service_completed_successfully
        required: true
    image: frappe/erpnext:v15.4.0
    networks:
      default: null
    volumes:
    - type: volume
      source: sites
      target: /home/frappe/frappe-bench/sites
      volume: {}
  configurator:
    command:
    - |
      ls -1 apps > sites/apps.txt; bench set-config -g db_host $$DB_HOST; bench set-config -gp db_port $$DB_PORT; bench set-config -g redis_cache "redis://$$REDIS_CACHE"; bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; bench set-config -gp socketio_port $$SOCKETIO_PORT;
    depends_on:
      db:
        condition: service_healthy
        required: true
      redis-cache:
        condition: service_started
        required: true
      redis-queue:
        condition: service_started
        required: true
    entrypoint:
    - bash
    - -c
    environment:
      DB_HOST: db
      DB_PORT: "3306"
      REDIS_CACHE: redis-cache:6379
      REDIS_QUEUE: redis-queue:6379
      SOCKETIO_PORT: "9000"
    image: frappe/erpnext:v15.4.0
    networks:
      default: null
    volumes:
    - type: volume
      source: sites
      target: /home/frappe/frappe-bench/sites
      volume: {}
  db:
    command:
    - --character-set-server=utf8mb4
    - --collation-server=utf8mb4_unicode_ci
    - --skip-character-set-client-handshake
    - --skip-innodb-read-only-compressed
    environment:
      MYSQL_ROOT_PASSWORD: thePassword
    healthcheck:
      test:
      - CMD-SHELL
      - mysqladmin ping -h localhost --password=thePassword
      interval: 1s
      retries: 15
    image: mariadb:10.6
    networks:
      default: null
    volumes:
    - type: volume
      source: db-data
      target: /var/lib/mysql
      volume: {}
  frontend:
    command:
    - nginx-entrypoint.sh
    depends_on:
      backend:
        condition: service_started
        required: true
      websocket:
        condition: service_started
        required: true
    environment:
      BACKEND: backend:8000
      CLIENT_MAX_BODY_SIZE: 50m
      FRAPPE_SITE_NAME_HEADER: $$host
      PROXY_READ_TIMOUT: "120"
      SOCKETIO: websocket:9000
      UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
      UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
      UPSTREAM_REAL_IP_RECURSIVE: "off"
    image: frappe/erpnext:v15.4.0
    networks:
      default: null
    ports:
    - mode: ingress
      host_ip: 127.0.0.1
      target: 8080
      published: "8080"
      protocol: tcp
    volumes:
    - type: volume
      source: sites
      target: /home/frappe/frappe-bench/sites
      volume: {}
  queue-long:
    command:
    - bench
    - worker
    - --queue
    - long,default,short
    depends_on:
      configurator:
        condition: service_completed_successfully
        required: true
    image: frappe/erpnext:v15.4.0
    networks:
      default: null
    volumes:
    - type: volume
      source: sites
      target: /home/frappe/frappe-bench/sites
      volume: {}
  queue-short:
    command:
    - bench
    - worker
    - --queue
    - short,default
    depends_on:
      configurator:
        condition: service_completed_successfully
        required: true
    image: frappe/erpnext:v15.4.0
    networks:
      default: null
    volumes:
    - type: volume
      source: sites
      target: /home/frappe/frappe-bench/sites
      volume: {}
  redis-cache:
    image: redis:6.2-alpine
    networks:
      default: null
    volumes:
    - type: volume
      source: redis-cache-data
      target: /data
      volume: {}
  redis-queue:
    image: redis:6.2-alpine
    networks:
      default: null
    volumes:
    - type: volume
      source: redis-queue-data
      target: /data
      volume: {}
  scheduler:
    command:
    - bench
    - schedule
    depends_on:
      configurator:
        condition: service_completed_successfully
        required: true
    image: frappe/erpnext:v15.4.0
    networks:
      default: null
    volumes:
    - type: volume
      source: sites
      target: /home/frappe/frappe-bench/sites
      volume: {}
  websocket:
    command:
    - node
    - /home/frappe/frappe-bench/apps/frappe/socketio.js
    depends_on:
      configurator:
        condition: service_completed_successfully
        required: true
    image: frappe/erpnext:v15.4.0
    networks:
      default: null
    volumes:
    - type: volume
      source: sites
      target: /home/frappe/frappe-bench/sites
      volume: {}
networks:
  default:
    name: openly_default
volumes:
  db-data:
    name: openly_db-data
  redis-cache-data:
    name: openly_redis-cache-data
  redis-queue-data:
    name: openly_redis-queue-data
  sites:
    name: openly_sites
x-backend-defaults:
  depends_on:
    configurator:
      condition: service_completed_successfully
  image: frappe/erpnext:v15.4.0
  volumes:
  - sites:/home/frappe/frappe-bench/sites
x-customizable-image:
  image: frappe/erpnext:v15.4.0
x-depends-on-configurator:
  depends_on:
    configurator:
      condition: service_completed_successfully

.
.
For No-proxy Override:

services:
  frontend:
    ports:
      - 127.0.0.1:8080:8080

.
.
We used easy-install.py script for setup as follows:

python3 easy-install.py -n example -p -s v15.example.com --email email@example.com

.
.
For NGINX Conf:

server {
	
	listen 1.2.3.4:443 ssl;
	listen [::]:443 ssl;

	server_name v15.example.com;

	proxy_buffer_size 128k;
	proxy_buffers 4 256k;
	proxy_busy_buffers_size 256k;

	
	ssl_certificate      /etc/letsencrypt/live/v15.example.com/fullchain.pem;
	ssl_certificate_key  /etc/letsencrypt/live/v15.example.com/privkey.pem;
	ssl_session_timeout  5m;
	ssl_session_cache shared:SSL:10m;
	ssl_session_tickets off;
	ssl_stapling on;
	ssl_stapling_verify on;
	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
	ssl_ecdh_curve secp384r1;
	ssl_prefer_server_ciphers on;
	

	add_header X-Frame-Options "SAMEORIGIN";
	add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
	add_header X-Content-Type-Options nosniff;
	add_header X-XSS-Protection "1; mode=block";
	add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin";

	location / {
		proxy_buffering off;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-Host v15.example.com;
		proxy_set_header X-Forwarded-Port 8080;
		proxy_pass http://127.0.0.1:8080;
	}

	access_log  /var/log/nginx/v15_access.log main;
	error_log  /var/log/nginx/v15_error.log;

	# enable gzip compresion
	# based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge
	gzip on;
	gzip_http_version 1.1;
	gzip_comp_level 5;
	gzip_min_length 256;
	gzip_proxied any;
	gzip_vary on;
	gzip_types
		application/atom+xml
		application/javascript
		application/json
		application/rss+xml
		application/vnd.ms-fontobject
		application/x-font-ttf
		application/font-woff
		application/x-web-app-manifest+json
		application/xhtml+xml
		application/xml
		font/opentype
		image/svg+xml
		image/x-icon
		text/css
		text/plain
		text/x-component
		;
		# text/html is always compressed by HttpGzipModule
}

# http to https redirect
server {
	listen 1.2.3.4:80;
	server_name v15.example.com;
	return 301 https://$host$request_uri;
	}

.
.

Will appreciate any hints or guidance.

Thanks

Hello @revant_one

As the expert in Docker/Frappe… can you take a look?
Will appreciate your help on this.

Thanks

What errors are you facing?

Hello @revant … really appreciate your reponse.

When I visit the domain root, I get:

Screenshot from 2023-12-07 08-43-10

When running:

docker logs --timestamps --follow example-frontend-1

I am getting:

2023-12-07T05:46:49.479209170Z 172.25.0.1 - - [07/Dec/2023:05:46:49 +0000] "GET / HTTP/1.0" 404 111 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
2023-12-07T05:46:49.613905831Z 172.25.0.1 - - [07/Dec/2023:05:46:49 +0000] "GET /favicon.ico HTTP/1.0" 404 111 "https://v15.openly.ae/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"

.
.

So, proxying looks to be working and traffic is reaching the container, yet it is replying with 404!

Thanks again.

Host header and site name must match.

Or override it like this

Thank you @revant_one I knew you will give the right advice :rocket:

So, the page is now loading, yet it does not load JS and CSS files.
For each of the below files, I got a 404 error:

https://example.domain/assets/erpnext/dist/css/erpnext-web.bundle.IXSTIIWU.css
https://example.domain/assets/frappe/dist/css/login.bundle.BPARWUMP.css
https://example.domain/assets/frappe/dist/js/frappe-web.bundle.VKJCYSFT.js

While for those JS files, I get 200 success:

https://example.domain/assets/erpnext/dist/js/erpnext-web.bundle.J6G3BWUP.js
https://example.domain/website_script.js

→ Worth mentioning that I am now having the ERPNext version as: 15.5.0

More Information…

When did a:

docker exec -it example-frontend-1 /bin/bash
cd ~/frappe-bench/sites/assets/frappe/dist/css
ls -la

Got the file name but with a different “random” part. Please note screen shot:

So, for example, the browser is requesting:

https://example.domain/assets/frappe/dist/css/website.bundle.2I2FHKLZ.css

While the file on the filesystem in the container is:

~/frappe-bench/sites/assets/frappe/dist/css/website.bundle.DDSTCXJW.css

Hope this adds more insight.
Does it make sense to consider that there is an issue in the creation of Frappe assets in the container?

Please help.

By any chance you did bench build in the production container?

the files mentioned in “sites/assets/assets.json” and files present in directory should match.

Clean up your assets so that assets.json is correct.

You can start a fresh container and copy assets.json from it so it is in sync with actual files in image.

I will stop and delete all docker containers, volumes, networks. Do the setup from scratch again and confirm here.

I guess, this is an old volume being used for a new container.

Hello,

I did a:

docker compose down

Also, removed all related docker volumes.

Did a fresh:

python3 easy-install.py -n example -p -s v15.example.com --email email@example.com

again and all worked fine.

Hello @revant_one

I am facing the same issue. Yet, I cannot delete the docker volumes as I need the files there.

I checked the assets.json and it includes the exact file names of the files in the filesystem.

I tried from inside the backend container to:

  • bench build
  • bench migrate
  • bench clear-cache

All of which finished successfully.

Also, restarted NGINX and Docker.service.

Still when loading the /app/home the css file names that are called are wrong (not matching assets.json nor filesystem files)!

Can you please help with this?

Thanks
K

Hello @revant_one or anyone else… any advice on this?

DO NOT execute bench build in production containers.

Try deleting the sites/assets directory and restart containers. Backup the dir before deleting if you wish.

Hello @revant_one

Did that - deleted the directory.

I got “Internal Server Error”, when running docker logs, I get the following:

[2024-05-25 18:28:10 +0000] [7] [ERROR] Error handling request /
Traceback (most recent call last):
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/serve.py", line 19, in get_response
    endpoint, renderer_instance = path_resolver.resolve()
                                  ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/path_resolver.py", line 38, in resolve
    resolve_redirect(self.path, request.query_string)
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/path_resolver.py", line 118, in resolve_redirect
    redirects += frappe.get_all("Website Route Redirect", ["source", "target"], order_by=None)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/__init__.py", line 2057, in get_all
    return get_list(doctype, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/__init__.py", line 2032, in get_list
    return frappe.model.db_query.DatabaseQuery(doctype).execute(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/model/db_query.py", line 167, in execute
    if is_virtual_doctype(self.doctype):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/utils/caching.py", line 119, in site_cache_wrapper
    _SITE_CACHE[func_key][frappe.local.site][func_call_key] = func(*args, **kwargs)
                                                              ^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/model/utils/__init__.py", line 133, in is_virtual_doctype
    return frappe.db.get_value("DocType", doctype, "is_virtual")
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 519, in get_value
    result = self.get_values(
             ^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 623, in get_values
    out = self._get_values_from_table(
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 896, in _get_values_from_table
    return query.run(as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/query_builder/utils.py", line 87, in execute_query
    result = frappe.db.sql(query, params, *args, **kwargs)  # nosemgrep
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 211, in sql
    self.connect()
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 117, in connect
    self._conn: "MariadbConnection" | "PostgresConnection" = self.get_connection()
                                                             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/mariadb/database.py", line 107, in get_connection
    conn = self._get_connection()
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/mariadb/database.py", line 113, in _get_connection
    return self.create_connection()
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/mariadb/database.py", line 116, in create_connection
    return pymysql.connect(**self.get_connection_settings())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/connections.py", line 358, in __init__
    self.connect()
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/connections.py", line 664, in connect
    self._request_authentication()
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/connections.py", line 954, in _request_authentication
    auth_packet = self._read_packet()
                  ^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/connections.py", line 772, in _read_packet
    packet.raise_for_error()
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/protocol.py", line 221, in raise_for_error
    err.raise_mysql_exception(self._data)
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/err.py", line 143, in raise_mysql_exception
    raise errorclass(errno, errval)
pymysql.err.OperationalError: (1045, "Access denied for user '_03d69a0af8a8feab'@'172.20.0.5' (using password: YES)")

####################### REMOVING SIMILAR ERRORS #####################

Traceback (most recent call last):
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/gunicorn/workers/gthread.py", line 282, in handle
    keepalive = self.handle_request(req, conn)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/gunicorn/workers/gthread.py", line 334, in handle_request
    respiter = self.wsgi(environ, resp.start_response)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 74, in application
    app(environ, start_response),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/werkzeug/wrappers/request.py", line 190, in application
    resp = f(*args[:-2] + (request,))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 128, in application
    response = handle_exception(e)
               ^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/app.py", line 391, in handle_exception
    response = get_response("message", http_status_code=http_status_code)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/serve.py", line 28, in get_response
    response = ErrorPage(exception=e).render()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/page_renderers/template_page.py", line 84, in render
    html = self.get_html()
           ^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/utils.py", line 523, in cache_html_decorator
    html = func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/page_renderers/template_page.py", line 92, in get_html
    self.init_context()
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/page_renderers/error_page.py", line 14, in init_context
    super().init_context()
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/page_renderers/base_template_page.py", line 15, in init_context
    self.context.update(get_website_settings())
                        ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/doctype/website_settings/website_settings.py", line 263, in get_website_settings
    context.boot = get_boot_data()
                   ^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/website/utils.py", line 179, in get_boot_data
    "user": frappe.db.get_value("User", frappe.session.user, "time_zone") or get_system_timezone(),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 519, in get_value
    result = self.get_values(
             ^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 623, in get_values
    out = self._get_values_from_table(
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 896, in _get_values_from_table
    return query.run(as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/query_builder/utils.py", line 87, in execute_query
    result = frappe.db.sql(query, params, *args, **kwargs)  # nosemgrep
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 211, in sql
    self.connect()
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/database.py", line 117, in connect
    self._conn: "MariadbConnection" | "PostgresConnection" = self.get_connection()
                                                             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/mariadb/database.py", line 107, in get_connection
    conn = self._get_connection()
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/mariadb/database.py", line 113, in _get_connection
    return self.create_connection()
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/apps/frappe/frappe/database/mariadb/database.py", line 116, in create_connection
    return pymysql.connect(**self.get_connection_settings())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/connections.py", line 358, in __init__
    self.connect()
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/connections.py", line 664, in connect
    self._request_authentication()
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/connections.py", line 954, in _request_authentication
    auth_packet = self._read_packet()
                  ^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/connections.py", line 772, in _read_packet
    packet.raise_for_error()
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/protocol.py", line 221, in raise_for_error
    err.raise_mysql_exception(self._data)
  File "/home/frappe/frappe-bench/env/lib/python3.11/site-packages/pymysql/err.py", line 143, in raise_mysql_exception
    raise errorclass(errno, errval)
pymysql.err.OperationalError: (1045, "Access denied for user '_03d69a0af8a8feab'@'172.20.0.5' (using password: YES)")

The assets directory?

  • Try deleting and creating containers again
  • recreate assets directory and try recreation of containers