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.