diff --git a/compose.yaml b/compose.yaml index 7c8e64f2..7ecceff1 100644 --- a/compose.yaml +++ b/compose.yaml @@ -15,24 +15,32 @@ x-backend-defaults: &backend_defaults <<: [*depends_on_configurator, *customizable_image] volumes: - sites:/home/frappe/frappe-bench/sites + deploy: + resources: + limits: + cpus: '0.5' + memory: '512M' + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000"] + interval: 30s + timeout: 10s + retries: 3 services: configurator: <<: *backend_defaults platform: linux/amd64 - entrypoint: - - bash - - -c - # add redis_socketio for backward compatibility + entrypoint: ["/bin/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; + - | + set -e + 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_HOST:-} DB_PORT: ${DB_PORT:-} @@ -49,8 +57,7 @@ services: frontend: <<: *customizable_image platform: linux/amd64 - command: - - nginx-entrypoint.sh + command: ["nginx-entrypoint.sh"] environment: BACKEND: backend:8000 SOCKETIO: websocket:9000 @@ -65,15 +72,33 @@ services: depends_on: - backend - websocket + deploy: + resources: + limits: + cpus: '0.5' + memory: '256M' + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 10s + retries: 3 websocket: <<: [*depends_on_configurator, *customizable_image] platform: linux/amd64 - command: - - node - - /home/frappe/frappe-bench/apps/frappe/socketio.js + command: ["node", "/home/frappe/frappe-bench/apps/frappe/socketio.js"] volumes: - sites:/home/frappe/frappe-bench/sites + deploy: + resources: + limits: + cpus: '0.3' + memory: '256M' + healthcheck: + test: ["CMD", "nc", "-z", "localhost", "9000"] + interval: 30s + timeout: 10s + retries: 3 queue-short: <<: *backend_defaults diff --git a/overrides/compose.https.yaml b/overrides/compose.https.yaml index 9096e560..ab805ffe 100644 --- a/overrides/compose.https.yaml +++ b/overrides/compose.https.yaml @@ -6,9 +6,12 @@ services: - traefik.http.routers.frontend-http.entrypoints=websecure - 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.middlewares=security-headers + - traefik.http.middlewares.security-headers.headers.sslRedirect=true + - traefik.http.middlewares.security-headers.headers.stsSeconds=31536000 proxy: - image: traefik:v2.11 + image: traefik:v3.3.4 restart: unless-stopped command: - --providers.docker=true @@ -17,10 +20,14 @@ services: - --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.websecure.address=:443 - - --certificatesResolvers.main-resolver.acme.httpChallenge=true - - --certificatesResolvers.main-resolver.acme.httpChallenge.entrypoint=web - - --certificatesResolvers.main-resolver.acme.email=${LETSENCRYPT_EMAIL:?No Let's Encrypt email set} - - --certificatesResolvers.main-resolver.acme.storage=/letsencrypt/acme.json + - --entrypoints.websecure.http.tls.options=modern + - --entrypoints.websecure.http.tls.minVersion=VersionTLS13 + - --accesslog=true + - --accesslog.fields.headers.names.Content-Type=keep + - --certificatesresolvers.main-resolver.acme.httpchallenge=true + - --certificatesresolvers.main-resolver.acme.httpchallenge.entrypoint=web + - --certificatesresolvers.main-resolver.acme.email=${LETSENCRYPT_EMAIL:?No Let's Encrypt email set} + - --certificatesresolvers.main-resolver.acme.storage=/letsencrypt/acme.json ports: - ${HTTP_PUBLISH_PORT:-80}:80 - ${HTTPS_PUBLISH_PORT:-443}:443 diff --git a/overrides/compose.mailhog.yaml b/overrides/compose.mailhog.yaml new file mode 100644 index 00000000..35f9800a --- /dev/null +++ b/overrides/compose.mailhog.yaml @@ -0,0 +1,43 @@ +services: + mailhog: + image: mailhog/mailhog:latest + restart: unless-stopped + ports: + - mode: ingress + target: 8025 + published: "${MAILHOG_PUBLISHED_PORT:-8025}" + protocol: tcp + labels: + traefik.docker.network: traefik-public + traefik.port: "{$MAILHOG_PUBLISHED_PORT:-8025}" + traefik.enable: "true" + traefik.http.routers.mailhog-http.entrypoints: http + traefik.http.routers.mailhog-http.middlewares: https-redirect + traefik.http.routers.mailhog-http.rule: Host(`${MAILHOG_HOSTNAME}`) + traefik.http.routers.mailhog-http.service: mailhog + traefik.http.routers.mailhog-https.entrypoints: https + traefik.http.routers.mailhog-https.rule: Host(`${MAILHOG_HOSTNAME}`) + traefik.http.routers.mailhog-https.service: mailhog + traefik.http.routers.mailhog-https.tls: "true" + traefik.http.routers.mailhog-https.tls.certresolver: le + traefik.http.services.mailhog.loadbalancer.server.port: "8025" + networks: + - mailhog-network + - traefik-public + volumes: + - type: volume + source: mailhog-data + target: /sessions + volume: {} + +networks: + mailhog-network: + name: mailhog-network + external: false + traefik-public: + name: traefik-public + external: true + +volumes: + mailhog-data: + name: mailhog-data diff --git a/overrides/compose.phpmyadmin.yaml b/overrides/compose.phpmyadmin.yaml new file mode 100644 index 00000000..51bb8a62 --- /dev/null +++ b/overrides/compose.phpmyadmin.yaml @@ -0,0 +1,52 @@ +version: "3.3" + +services: + phpmyadmin: + image: phpmyadmin/phpmyadmin:5.2.1 + restart: unless-stopped + ports: + - mode: ingress + target: 80 + published: "${PUBLISHED_PORT:-8081}" + protocol: tcp + environment: + - PMA_HOSTS=${PMA_HOSTS} + - PMA_PORTS=${PMA_PORTS} + - UPLOAD_LIMIT=2000M + labels: + traefik.docker.network: traefik-public + traefik.port: "{$PUBLISHED_PORT:-8081}" + traefik.enable: "true" + traefik.http.routers.phpmyadmin-http.entrypoints: http + traefik.http.routers.phpmyadmin-http.middlewares: https-redirect + traefik.http.routers.phpmyadmin-http.rule: Host(`${PMA_HOSTNAME}`) + traefik.http.routers.phpmyadmin-http.service: phpmyadmin + traefik.http.routers.phpmyadmin-https.entrypoints: https + traefik.http.routers.phpmyadmin-https.rule: Host(`${PMA_HOSTNAME}`) + traefik.http.routers.phpmyadmin-https.service: phpmyadmin + traefik.http.routers.phpmyadmin-https.tls: "true" + traefik.http.routers.phpmyadmin-https.tls.certresolver: le + traefik.http.services.phpmyadmin.loadbalancer.server.port: "80" + networks: + - phpmyadmin-network + - mariadb-network + - traefik-public + volumes: + - type: volume + source: phpmyadmin-data + target: /sessions + volume: {} + +networks: + phpmyadmin-network: + external: false + mariadb-network: + name: mariadb-network + external: true + traefik-public: + name: traefik-public + external: true + +volumes: + phpmyadmin-data: + name: phpmyadmin-data diff --git a/overrides/compose.proxy.yaml b/overrides/compose.proxy.yaml index 32ce9fab..0b8f629b 100644 --- a/overrides/compose.proxy.yaml +++ b/overrides/compose.proxy.yaml @@ -7,7 +7,7 @@ services: - traefik.http.routers.frontend-http.rule=HostRegexp(`{any:.+}`) proxy: - image: traefik:v2.11 + image: traefik:v3.3.4 command: - --providers.docker - --providers.docker.exposedbydefault=false diff --git a/overrides/compose.traefik-ssl.yaml b/overrides/compose.traefik-ssl.yaml index b83cb8e4..9144e566 100644 --- a/overrides/compose.traefik-ssl.yaml +++ b/overrides/compose.traefik-ssl.yaml @@ -15,7 +15,7 @@ services: # Use the special Traefik service api@internal with the web UI/Dashboard - traefik.http.routers.traefik-public-https.service=api@internal # 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=letsencrypt # Enable HTTP Basic auth, using the middleware created above - traefik.http.routers.traefik-public-https.middlewares=admin-auth command: diff --git a/overrides/compose.traefik.yaml b/overrides/compose.traefik.yaml index 25d362af..5cc1d30f 100644 --- a/overrides/compose.traefik.yaml +++ b/overrides/compose.traefik.yaml @@ -2,7 +2,7 @@ version: "3.3" services: traefik: - image: "traefik:v2.11" + image: "traefik:v3.3.4" restart: unless-stopped labels: # Enable Traefik for this service, to make it available in the public network diff --git a/pwd.yml b/pwd.yml index fb7c16d0..b19d9538 100644 --- a/pwd.yml +++ b/pwd.yml @@ -79,12 +79,30 @@ services: 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; + phpmyadmin: + image: phpmyadmin/phpmyadmin:latest + restart: unless-stopped + ports: + - mode: ingress + target: 80 + published: 8082 + protocol: tcp + environment: + - PMA_HOSTS=db + - PMA_PORTS=3306 + - UPLOAD_LIMIT=2000M + volumes: + - type: volume + source: phpmyadmin-data + target: /sessions + volume: {} + db: image: mariadb:10.6 networks: - frappe_network healthcheck: - test: mysqladmin ping -h localhost --password=admin + test: mysqladmin ping -h localhost --password=${MYSQL_ROOT_PASSWORD} interval: 1s retries: 20 deploy: @@ -96,44 +114,40 @@ services: - --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 + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-admin} + MARIADB_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-admin} volumes: - db-data:/var/lib/mysql frontend: image: frappe/erpnext:v15.55.2 + restart: unless-stopped 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 + FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host} 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 + 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_RECURSIVE: ${UPSTREAM_REAL_IP_RECURSIVE:-off} + PROXY_READ_TIMEOUT: ${PROXY_READ_TIMEOUT:-120} + CLIENT_MAX_BODY_SIZE: ${CLIENT_MAX_BODY_SIZE:-50m} volumes: - sites:/home/frappe/frappe-bench/sites - logs:/home/frappe/frappe-bench/logs ports: - - "8080:8080" + - "${HTTP_PUBLISH_PORT:-8080}:8080" queue-long: image: frappe/erpnext:v15.55.2 + restart: unless-stopped networks: - frappe_network - deploy: - restart_policy: - condition: on-failure command: - bench - worker @@ -145,11 +159,9 @@ services: queue-short: image: frappe/erpnext:v15.55.2 + restart: unless-stopped networks: - frappe_network - deploy: - restart_policy: - condition: on-failure command: - bench - worker @@ -161,29 +173,23 @@ services: redis-queue: image: redis:6.2-alpine + restart: unless-stopped networks: - frappe_network - deploy: - restart_policy: - condition: on-failure volumes: - redis-queue-data:/data redis-cache: image: redis:6.2-alpine + restart: unless-stopped networks: - frappe_network - deploy: - restart_policy: - condition: on-failure scheduler: image: frappe/erpnext:v15.55.2 + restart: unless-stopped networks: - frappe_network - deploy: - restart_policy: - condition: on-failure command: - bench - schedule @@ -193,11 +199,9 @@ services: websocket: image: frappe/erpnext:v15.55.2 + restart: unless-stopped networks: - frappe_network - deploy: - restart_policy: - condition: on-failure command: - node - /home/frappe/frappe-bench/apps/frappe/socketio.js diff --git a/resources/nginx-entrypoint.sh b/resources/nginx-entrypoint.sh index 50408e56..6d7b6f00 100755 --- a/resources/nginx-entrypoint.sh +++ b/resources/nginx-entrypoint.sh @@ -1,44 +1,17 @@ #!/bin/bash +set -euo pipefail -# Set variables that do not exist -if [[ -z "$BACKEND" ]]; then - echo "BACKEND defaulting to 0.0.0.0:8000" - export BACKEND=0.0.0.0:8000 -fi -if [[ -z "$SOCKETIO" ]]; then - echo "SOCKETIO defaulting to 0.0.0.0:9000" - export SOCKETIO=0.0.0.0:9000 -fi -if [[ -z "$UPSTREAM_REAL_IP_ADDRESS" ]]; then - echo "UPSTREAM_REAL_IP_ADDRESS defaulting to 127.0.0.1" - export UPSTREAM_REAL_IP_ADDRESS=127.0.0.1 -fi -if [[ -z "$UPSTREAM_REAL_IP_HEADER" ]]; then - echo "UPSTREAM_REAL_IP_HEADER defaulting to X-Forwarded-For" - export UPSTREAM_REAL_IP_HEADER=X-Forwarded-For -fi -if [[ -z "$UPSTREAM_REAL_IP_RECURSIVE" ]]; then - echo "UPSTREAM_REAL_IP_RECURSIVE defaulting to off" - export UPSTREAM_REAL_IP_RECURSIVE=off -fi -if [[ -z "$FRAPPE_SITE_NAME_HEADER" ]]; then - # shellcheck disable=SC2016 - echo 'FRAPPE_SITE_NAME_HEADER defaulting to $host' - # shellcheck disable=SC2016 - export FRAPPE_SITE_NAME_HEADER='$host' -fi +# Default values +BACKEND=${BACKEND:-0.0.0.0:8000} +SOCKETIO=${SOCKETIO:-0.0.0.0:9000} +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_RECURSIVE=${UPSTREAM_REAL_IP_RECURSIVE:-off} +FRAPPE_SITE_NAME_HEADER=${FRAPPE_SITE_NAME_HEADER:-$host} +PROXY_READ_TIMEOUT=${PROXY_READ_TIMEOUT:-120} +CLIENT_MAX_BODY_SIZE=${CLIENT_MAX_BODY_SIZE:-50m} -if [[ -z "$PROXY_READ_TIMEOUT" ]]; then - echo "PROXY_READ_TIMEOUT defaulting to 120" - export PROXY_READ_TIMEOUT=120 -fi - -if [[ -z "$CLIENT_MAX_BODY_SIZE" ]]; then - echo "CLIENT_MAX_BODY_SIZE defaulting to 50m" - export CLIENT_MAX_BODY_SIZE=50m -fi - -# shellcheck disable=SC2016 +# Generate nginx configuration envsubst '${BACKEND} ${SOCKETIO} ${UPSTREAM_REAL_IP_ADDRESS} @@ -46,7 +19,8 @@ envsubst '${BACKEND} ${UPSTREAM_REAL_IP_RECURSIVE} ${FRAPPE_SITE_NAME_HEADER} ${PROXY_READ_TIMEOUT} - ${CLIENT_MAX_BODY_SIZE}' \ - /etc/nginx/conf.d/frappe.conf + ${CLIENT_MAX_BODY_SIZE}' \ + < /templates/nginx/frappe.conf.template > /etc/nginx/conf.d/frappe.conf -nginx -g 'daemon off;' +# Start nginx +exec nginx -g 'daemon off;' diff --git a/resources/nginx-template.conf b/resources/nginx-template.conf index ded97c94..7448d81d 100644 --- a/resources/nginx-template.conf +++ b/resources/nginx-template.conf @@ -1,107 +1,105 @@ upstream backend-server { - server ${BACKEND} fail_timeout=0; + server ${BACKEND} fail_timeout=0; } upstream socketio-server { - server ${SOCKETIO} fail_timeout=0; + server ${SOCKETIO} fail_timeout=0; } server { - listen 8080; - server_name ${FRAPPE_SITE_NAME_HEADER}; - root /home/frappe/frappe-bench/sites; + listen 8080; + server_name ${FRAPPE_SITE_NAME_HEADER}; + root /home/frappe/frappe-bench/sites; - proxy_buffer_size 128k; - proxy_buffers 4 256k; - proxy_busy_buffers_size 256k; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; - 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"; + 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"; - set_real_ip_from ${UPSTREAM_REAL_IP_ADDRESS}; - real_ip_header ${UPSTREAM_REAL_IP_HEADER}; - real_ip_recursive ${UPSTREAM_REAL_IP_RECURSIVE}; + set_real_ip_from ${UPSTREAM_REAL_IP_ADDRESS}; + real_ip_header ${UPSTREAM_REAL_IP_HEADER}; + real_ip_recursive ${UPSTREAM_REAL_IP_RECURSIVE}; - location /assets { - try_files $uri =404; - } + location /assets { + try_files $uri =404; + } - location ~ ^/protected/(.*) { - internal; - try_files /${FRAPPE_SITE_NAME_HEADER}/$1 =404; - } + location ~ ^/protected/(.*) { + internal; + try_files /${FRAPPE_SITE_NAME_HEADER}/$1 =404; + } - location /socket.io { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER}; - proxy_set_header Origin $scheme://$http_host; - proxy_set_header Host $host; + location /socket.io { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER}; + proxy_set_header Origin $scheme://$http_host; + proxy_set_header Host $host; - proxy_pass http://socketio-server; - } + proxy_pass http://socketio-server; + } - location / { - rewrite ^(.+)/$ $1 permanent; - rewrite ^(.+)/index\.html$ $1 permanent; - rewrite ^(.+)\.html$ $1 permanent; + location / { + rewrite ^(.+)/$ $1 permanent; + rewrite ^(.+)/index\.html$ $1 permanent; + rewrite ^(.+)\.html$ $1 permanent; - location ~ ^/files/.*.(htm|html|svg|xml) { - add_header Content-disposition "attachment"; - try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver; - } + location ~ ^/files/.*.(htm|html|svg|xml) { + 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; + } - location @webserver { - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER}; - proxy_set_header Host $host; - proxy_set_header X-Use-X-Accel-Redirect True; - proxy_read_timeout ${PROXY_READ_TIMEOUT}; - proxy_redirect off; + location @webserver { + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frappe-Site-Name ${FRAPPE_SITE_NAME_HEADER}; + proxy_set_header Host $host; + proxy_set_header X-Use-X-Accel-Redirect True; + proxy_read_timeout ${PROXY_READ_TIMEOUT}; + proxy_redirect off; - proxy_pass http://backend-server; - } + proxy_pass http://backend-server; + } - # optimizations - sendfile on; - keepalive_timeout 15; - client_max_body_size ${CLIENT_MAX_BODY_SIZE}; - client_body_buffer_size 16K; - client_header_buffer_size 1k; + # Optimizations + sendfile on; + keepalive_timeout 15; + client_max_body_size ${CLIENT_MAX_BODY_SIZE}; + client_body_buffer_size 16K; + client_header_buffer_size 1k; - # enable gzip compression - # 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 + # Gzip compression + 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; }