Intermittent 404 Errors for Static Assets After Custom Apps Build on macOS (Even with Single App)

Hi everyone,

I’m experiencing an intermittent issue when deploying a Docker image built using the official frappe_docker setup with custom apps building, even when installing only one app like erpnext. I share the most relevants parts.

Dev environment

macOS (Apple M4-pro, Docker Desktop ~3 months old)

custom_apps.json

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

build_image.sh

#!/bin/bash

#Ruta a file
RUTA_FILE="../frappe_docker/"

# Verifica si se proporcionó un argumento
if [ -z "$1" ]; then
  echo "Use: $0 <image_name>"
  exit 1
fi

# Asigna el tag proporcionado como parámetro
IMAGE_TAG=$1

# Verifica si la variable APPS_JSON_BASE64 está definida
if [ -z "$APPS_JSON_BASE64" ]; then
  echo "Error: La variable de entorno APPS_JSON_BASE64 no está definida."
  echo "Ejecuta primero el script para configurarla."
  exit 1
fi

# Construcción de la imagen Docker
docker buildx build --no-cache --platform=linux/amd64 \
  --build-arg FRAPPE_BRANCH=version-15 \
  --build-arg APPS_JSON_BASE64=$APPS_JSON_BASE64 \
  --tag=$IMAGE_TAG \
  --file="${RUTA_FILE}images/layered/Containerfile" .

# Verifica si la construcción fue exitosa
if [ $? -eq 0 ]; then
  echo "✅ Imagen construida con éxito: $IMAGE_TAG"
else
  echo "❌ Error en la construcción de la imagen."
  exit 1
fi

Production environment

Linux ubuntu-8gb-nbg1-1 6.8.0-55-generic #57-Ubuntu SMP PREEMPT_DYNAMIC Wed Feb 12 23:42:21 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

Everything seems to work fine. After that, I push the image in the repo, and deploy in production.

# Reference: https://github.com/frappe/frappe_docker/blob/main/docs/environment-variables.md

ERPNEXT_VERSION=v15.63.0

DB_PASSWORD=pwd

# Only if you use external database
DB_HOST=mariadb-database
DB_PORT=3306

# Only if you use external Redis
REDIS_CACHE=
REDIS_QUEUE=

# Only with HTTPS override
LETSENCRYPT_EMAIL=admin@mycompany.net

# These environment variables are not required.

# 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`.
# 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`.
FRAPPE_SITE_NAME_HEADER=test04.mycompany.net

# Default value is `8080`.
HTTP_PUBLISH_PORT=

# Default value is `127.0.0.1`. Set IP address as our trusted upstream 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
UPSTREAM_REAL_IP_HEADER=

# 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
# 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.
UPSTREAM_REAL_IP_RECURSIVE=

# 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
PROXY_READ_TIMEOUT=

# 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
CLIENT_MAX_BODY_SIZE=

# List of sites for letsencrypt certificates quoted with backtick (`) and separated by comma (,)
# More https://doc.traefik.io/traefik/routing/routers/#rule
# About acme https://doc.traefik.io/traefik/https/acme/#domain-definition
SITES=`test04.mycompany.net`
ROUTER=test04
BENCH_NETWORK=test04

CUSTOM_IMAGE='app_imagen_name'
CUSTOM_TAG='v0.0.1'

I then use:

docker compose --project-name "$PROJECT_NAME" \
  --env-file "$ENV_FILE" \
  -f ../frappe_docker/compose.yaml \
  -f ../frappe_docker/overrides/compose.redis.yaml \
  -f ../frappe_docker/overrides/compose.multi-bench.yaml \
  -f ../frappe_docker/overrides/compose.multi-bench-ssl.yaml \
  config > "$OUTPUT_YAML"

To generate the yaml in production, deploy with:

docker compose --project-name "$PROJECT_NAME" -f "$YAML_FILE" up -d

and init site with something like:

INSTALL_APPS_ARGS=()
for APP in "${APPS[@]}"; do
  INSTALL_APPS_ARGS+=(--install-app "$APP")
done

# Ejecuta el comando de creación del sitio con todas las apps
docker compose --project-name "$PROJECT_NAME" exec backend \
  bench new-site --mariadb-user-host-login-scope=% \
  --db-root-password "$DB_ROOT_PASSWORD" \
  "${INSTALL_APPS_ARGS[@]}" \
  --admin-password "$ADMIN_PASSWORD" \
  "$SITE_NAME"

Key behaviors:

  • The first load of the site fails to load some assets (CSS/JS).
  • A second reload sometimes loads assets correctly, but other times reverts to missing files again.
  • Hashes in the asset filenames change unexpectedly between reloads.
  • Happens even with only erpnext in the custom build.
  • No issue when using the default frappe/erpnext image, only when using a custom image via APPS_JSON_BASE64.
  • There are other benches/sites beeing served in the same production host.
  • When I was using the previous docker for frappe library, and I deployed a new bench, the old ones got broken. Now, under this new version, the rest of the benches keep on working properly.

If you read all of this — thank you so much! Let me know if you need any more info or want me to test something. I’ll continue debugging until I find the root cause.

Thanks again :pray:

Updates:

  • Removed all container in host, built an image in dev environment with develop branch in frappe, erpnext and builder, and deployed in production. It works like a charm. Now I can add as many benches as I like in production host.
  • When I added a “no custom image container” it fails. It’s ERPNEXT_VERSION=v15.63.0

Thereby I suspect problem has nothing to do with the building process, but with versions and branches in apps/frappe.

--file is located in ../frappe_docker then context needs to be ../frappe_docker instead of .

1 Like

Sorry for the confusion, @revant_one. Let me clarify.

I’m running that command from a custom shell script located at same level as frappe_docker (that is, they share the same parent directory). So my RUTA_FILE var is “…/frappe_docker/”. Images seems to build properly. It seems the issue is related with versioning.

In the begining of this video you mention there has to be some kind of match between FRAPPE_BRANCH and branches in custom_apps, so you can’t use develop branch in erpnext apps.json , for example, unless you use FRAPPE_BRANCH=develop as well when building. I suspect the error comes from there, but I need to confirm yet. Could you elaborate on that?

Thanks for your time.

Updates:

  • I tried different combinations of version branches in apps.json. Sometimes assets are built properly when deployed, sometimes they aren’t. At first, I thought the issue was caused by using 'develop' in any app branch, but that doesn’t seem to be the case. Also at first, benches working properly seemed to be isolated from the benches with errors. But that was not the case at the end, when they started to fail as well on building assets.
  • You can find here the images used until now.

In summary: Asset building is unstable when multiple benches run on the same host. Any ideas on what could be causing this?

Updates:

  • After some research, I’ve found some ‘sleeping’ connections in Mariadb process list (after running show processlist command in the failing site) in a site of a bench. The connection appears after internal server error, on first site visit.
  • It’s the fourth bench in host. Is there any limit to this?