heroku / heroku-buildpack-static

[DEPRECATED] Heroku buildpack for handling static sites and single page web apps
MIT License
674 stars 427 forks source link

Need help for migrating to nginx buildpack #245

Open kanadgodse-lh opened 2 years ago

kanadgodse-lh commented 2 years ago

Hello,

We have many production react apps and use this buildpack along with https://github.com/mars/create-react-app-buildpack

Since that buildpack and this one is also deprecated and the migration guide does not give details for the following step, I am stuck: Replace path logic that previously used mruby with static logic.

Can someone please help by elaborating how to replace path logic with static logic?

edmorley commented 2 years ago

@heroku/languages Could someone take a look at this? Making it easier for people to migrate from the static buildpack will help with Heroku-22 adoption, and thus also with Heroku-20 EOL (when that time comes) :-)

tylerjgarland commented 2 years ago

@kanadgodse-lh , if you cat the ruby files in question

~ $ cat /app/bin/config/lib/ngx_mruby/headers.rb
# ghetto require, since mruby doesn't have require
eval(File.read('/app/bin/config/lib/nginx_config_util.rb'))

USER_CONFIG = "/app/static.json"

config = {}
config = JSON.parse(File.read(USER_CONFIG)) if File.exist?(USER_CONFIG)
req    = Nginx::Request.new
uri    = req.var.uri

if config["headers"]
  config["headers"].to_a.reverse.each do |route, header_hash|
    if Regexp.compile("^#{NginxConfigUtil.to_regex(route)}$") =~ uri
      header_hash.each do |key, value|
        # value must be a string
        req.headers_out[key] = value.to_s
      end
      break
    end
  end
end
~ $ cat /app/bin/config/lib/ngx_mruby/routes_fallback.rb
# ghetto require, since mruby doesn't have require
eval(File.read('/app/bin/config/lib/nginx_config_util.rb'))

USER_CONFIG = "/app/static.json"

config    = {}
config    = JSON.parse(File.read(USER_CONFIG)) if File.exist?(USER_CONFIG)
req       = Nginx::Request.new
uri       = req.var.uri
proxies   = config["proxies"] || {}
redirects = config["redirects"] || {}

if proxy = NginxConfigUtil.match_proxies(proxies.keys, uri)
  "@#{proxy}"
elsif redirect = NginxConfigUtil.match_redirects(redirects.keys, uri)
  "@#{redirect}"
else
  "@404"
end

It seems to mostly be processing your cache-control headers you put in your static.json file. I just removed the mruby lines from the nginx config file and dropped the $fallback variable from the try_files call.

jamesmichiemo commented 2 years ago

@kanadgodse-lh , if you cat the ruby files in question

~ $ cat /app/bin/config/lib/ngx_mruby/headers.rb
# ghetto require, since mruby doesn't have require
eval(File.read('/app/bin/config/lib/nginx_config_util.rb'))

USER_CONFIG = "/app/static.json"

config = {}
config = JSON.parse(File.read(USER_CONFIG)) if File.exist?(USER_CONFIG)
req    = Nginx::Request.new
uri    = req.var.uri

if config["headers"]
  config["headers"].to_a.reverse.each do |route, header_hash|
    if Regexp.compile("^#{NginxConfigUtil.to_regex(route)}$") =~ uri
      header_hash.each do |key, value|
        # value must be a string
        req.headers_out[key] = value.to_s
      end
      break
    end
  end
end
~ $ cat /app/bin/config/lib/ngx_mruby/routes_fallback.rb
# ghetto require, since mruby doesn't have require
eval(File.read('/app/bin/config/lib/nginx_config_util.rb'))

USER_CONFIG = "/app/static.json"

config    = {}
config    = JSON.parse(File.read(USER_CONFIG)) if File.exist?(USER_CONFIG)
req       = Nginx::Request.new
uri       = req.var.uri
proxies   = config["proxies"] || {}
redirects = config["redirects"] || {}

if proxy = NginxConfigUtil.match_proxies(proxies.keys, uri)
  "@#{proxy}"
elsif redirect = NginxConfigUtil.match_redirects(redirects.keys, uri)
  "@#{redirect}"
else
  "@404"
end

It seems to mostly be processing your cache-control headers you put in your static.json file. I just removed the mruby lines from the nginx config file and dropped the $fallback variable from the try_files call.

So I removed every line with mruby and every $fallback variable from my config/nginx.conf.erb that was created from using

$ heroku run bash ~ $ bin/config/make-config ~ $ cat config/nginx.conf

This was from inside the app that I removed the create-react-app-buildpack from and replaced with heroku-community/nginx. Somehow I got a build succeed but it's not rendering anything for me.

tylerjgarland commented 2 years ago

As a test @jamesmichiemo , can you slap a /index.html on the end of your URL?

jamesmichiemo commented 2 years ago

As a test @jamesmichiemo , can you slap a /index.html on the end of your URL?

Thank you @tylerjgarland for assisting! I'll try that change eventually but it feels like it would be worth more of my time if I tried the other alternative hosting providers listed on the create-react-app deployment docs and maybe telling them to remove Heroku from their documentation for now until the chaos dies.

xiaopow commented 2 years ago

Hi. I tried to follow the instructions regarding the mruby static logic.

I added node.js and nginx buildpack.

Screenshot 2022-07-15 at 2 47 33 PM

Deployed. The node build succeeded. The nginx didn't give any errors.

But when I visited the app https://invulnerable-maison-61218.herokuapp.com/ there is an app error.

In the server log is this.

2022-07-15T06:45:42.939892+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/" host=invulnerable-maison-61218.herokuapp.com request_id=5ad9be69-bdfb-4670-b73b-77d4ed89506f fwd="221.124.121.128" dyno= connect= service= status=503 bytes= protocol=https
2022-07-15T06:45:43.797328+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/favicon.ico" host=invulnerable-maison-61218.herokuapp.com request_id=e85c496a-6723-4c4a-9149-8cd07ebde3b7 fwd="221.124.121.128" dyno= connect= service= status=503 bytes= protocol=https
2022-07-15T06:45:47.687092+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/index.html" host=invulnerable-maison-61218.herokuapp.com request_id=469f0537-7db3-43b0-93d3-09b5dedfc325 fwd="221.124.121.128" dyno= connect= service= status=503 bytes= protocol=https
2022-07-15T06:45:48.506074+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/favicon.ico" host=invulnerable-maison-61218.herokuapp.com request_id=9f9ed82a-0a53-4445-9b96-a57ba605ac45 fwd="221.124.121.128" dyno= connect= service= status=503 bytes= protocol=https

Here is my config/nginx.conf.erb file which I generated based on another react app deployed on Heroku using the mars buildpack.

daemon off;
worker_processes auto;

events {
  use epoll;
  accept_mutex on;
  worker_connections 512;
}

http {
  gzip on;
  gzip_comp_level 6;
  gzip_min_length 512;
  gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
  gzip_vary on;
  gzip_proxied any;

  server_tokens off;

  access_log logs/access.log;

  error_log stderr error;

  include mime.types;
  default_type application/octet-stream;
  sendfile on;

  #Must read the body in 5 seconds.
  client_body_timeout 5;

  server {
    listen 38169 reuseport;
    charset UTF-8;
    port_in_redirect off;
    keepalive_timeout 5;
    root build/;

    location / {

      try_files $uri $uri/;

    }

    location ~ ^/.*$ {
      set $route /.*;

      try_files $uri $path ;

    }

  # need this b/c setting $fallback to =404 will try #{root}=404 instead of returning a 404
  location @404 {
    return 404;
  }

  # fallback proxy named match

  # fallback redirects named match

  }
}
xiaopow commented 2 years ago

Okay, I swapped the order of the two buildpacks and the app is working now. Screenshot 2022-07-15 at 2 55 50 PM

andersonar12 commented 2 years ago

Hi. I tried to follow the instructions regarding the mruby static logic.

I added node.js and nginx buildpack.

Screenshot 2022-07-15 at 2 47 33 PM

Deployed. The node build succeeded. The nginx didn't give any errors.

But when I visited the app https://invulnerable-maison-61218.herokuapp.com/ there is an app error.

In the server log is this.

2022-07-15T06:45:42.939892+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/" host=invulnerable-maison-61218.herokuapp.com request_id=5ad9be69-bdfb-4670-b73b-77d4ed89506f fwd="221.124.121.128" dyno= connect= service= status=503 bytes= protocol=https
2022-07-15T06:45:43.797328+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/favicon.ico" host=invulnerable-maison-61218.herokuapp.com request_id=e85c496a-6723-4c4a-9149-8cd07ebde3b7 fwd="221.124.121.128" dyno= connect= service= status=503 bytes= protocol=https
2022-07-15T06:45:47.687092+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/index.html" host=invulnerable-maison-61218.herokuapp.com request_id=469f0537-7db3-43b0-93d3-09b5dedfc325 fwd="221.124.121.128" dyno= connect= service= status=503 bytes= protocol=https
2022-07-15T06:45:48.506074+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/favicon.ico" host=invulnerable-maison-61218.herokuapp.com request_id=9f9ed82a-0a53-4445-9b96-a57ba605ac45 fwd="221.124.121.128" dyno= connect= service= status=503 bytes= protocol=https

Here is my config/nginx.conf.erb file which I generated based on another react app deployed on Heroku using the mars buildpack.


daemon off;
worker_processes auto;

events {
  use epoll;
  accept_mutex on;
  worker_connections 512;
}

http {
  gzip on;
  gzip_comp_level 6;
  gzip_min_length 512;
  gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
  gzip_vary on;
  gzip_proxied any;

  server_tokens off;

  access_log logs/access.log;

  error_log stderr error;

  include mime.types;
  default_type application/octet-stream;
  sendfile on;

  #Must read the body in 5 seconds.
  client_body_timeout 5;

  server {
    listen 38169 reuseport;
    charset UTF-8;
    port_in_redirect off;
    keepalive_timeout 5;
    root build/;

    location / {

      try_files $uri $uri/;

    }

    location ~ ^/.*$ {
      set $route /.*;

      try_files $uri $path ;

    }

  # need this b/c setting $fallback to =404 will try #{root}=404 instead of returning a 404
  location @404 {
    return 404;
  }

  # fallback proxy named match

  # fallback redirects named match

  }
}

@xiaopow  I need to deploy a Vue application on heroku with the new buildpack, I tried this configuration and it didn't work.
tylerjgarland commented 2 years ago

@andersonar12 , did you create a web procfile?

loicplaire commented 2 years ago

Unfortunately, none of these examples works for me either, Heroku complains about the missing start script. We use heroku-buildpack-static to deploy a storybook build folder and have no node server running.

What is unclear to me is whether we now need a web Procfile to start a (node) server? Did this buildback handle it before?

Cheers!

DanAndreasson commented 2 years ago

After much back and forth I finally got the migration working for us. React + vite. Two things to take notice of

  1. you need to create a Procfile with
    web: bin/start-nginx-solo
  2. Need to remove all mentions of mruby in the config file and have to use environment variable for the port. This is what I ended up with
    
    daemon off;
    worker_processes auto;

events { use epoll; accept_mutex on; worker_connections 512; }

http { gzip on; gzip_comp_level 6; gzip_min_length 512; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; gzip_vary on; gzip_proxied any;

server_tokens off; access_log logs/access.log; error_log stderr error; include mime.types; default_type application/octet-stream; sendfile on;

Must read the body in 5 seconds.

client_body_timeout 5;

server { listen <%= ENV["PORT"] %>; charset UTF-8; port_in_redirect off; keepalive_timeout 5; root /app/dist;

resolver 10.1.0.2 8.8.8.8;

# Allow payloads of up to 10mb
client_max_body_size 10M;

# Redirect all non-https traffic to https
if ($http_x_forwarded_proto != "https") {
  return 301 https://$host$request_uri;
}

location = /index.html {
  add_header Cache-Control "no-store, no-cache";
  add_header Strict-Transport-Security "max-age=31536002;";

  try_files $uri $uri/ =404;
}

location / {
  add_header 'Cache-Control' "public, max-age=3600";

  try_files $uri $uri/ /index.html;
}

location /manifest.json {
  add_header Cache-Control "no-store, no-cache";
  add_header Access-Control-Allow-Origin "*";

  try_files $uri $uri/ =404;
}

# Cache assets for a long time, since all of them get unique names on each build
location ~ ^/assets/[^/]*$ {
  add_header Cache-Control "public, max-age=31536000";
  add_header Access-Control-Allow-Origin "*";

  try_files $uri $uri/ =404;
}

# Need this b/c setting $fallback to =404 will try #{root}=404 instead of returning a 404
location @404 {
  return 404;
}

# Redirect all request on /api/* to backend server
set $api_endpoint '<%= "#{ENV['VITE_API_PROTOCOL']}://#{ENV['VITE_API_ENDPOINT']}" %>';
location /api/ {
  rewrite ^/api//?(.*)$ /$1 break;
  proxy_pass $api_endpoint;
  proxy_ssl_server_name on;
  proxy_redirect $api_endpoint/ /api/;
}

} }


This was migrated from `static.json`
```json
{
  "root": "./dist",
  "https_only": true,

  "headers": {
    "/**": {
      "Strict-Transport-Security": "max-age=7776001"
    },

    "/": {
      "Cache-Control": "no-store, no-cache",
      "Strict-Transport-Security": "max-age=7776002"
    },

    "/assets/**": {
      "Cache-Control": "public, max-age=31536000",
      "Access-Control-Allow-Origin": "*"
    },

    "/manifest.json": {
      "Cache-Control": "no-store, no-cache",
      "Access-Control-Allow-Origin": "*"
    }
  },

  "routes": {
    "/assets/*": "/assets/",
    "/**": "index.html"
  },

  "proxies": {
    "/api/": {
      "origin": "${VITE_API_PROTOCOL}://${VITE_API_ENDPOINT}"
    }
  }
}
adamalfredsson commented 2 years ago

After much back and forth I finally got the migration working for us. React + vite. Two things to take notice of

  1. you need to create a Procfile with
web: bin/start-nginx-solo
  1. Need to remove all mentions of mruby in the config file and have to use environment variable for the port. This is what I ended up with
daemon off;
worker_processes auto;

events {
  use epoll;
  accept_mutex on;
  worker_connections 512;
}

http {
  gzip on;
  gzip_comp_level 6;
  gzip_min_length 512;
  gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
  gzip_vary on;
  gzip_proxied any;

  server_tokens off;
  access_log logs/access.log;
  error_log stderr error;
  include mime.types;
  default_type application/octet-stream;
  sendfile on;

  # Must read the body in 5 seconds.
  client_body_timeout 5;

  server {
    listen <%= ENV["PORT"] %>;
    charset UTF-8;
    port_in_redirect off;
    keepalive_timeout 5;
    root /app/dist;

    resolver 10.1.0.2 8.8.8.8;

    # Allow payloads of up to 10mb
    client_max_body_size 10M;

    # Redirect all non-https traffic to https
    if ($http_x_forwarded_proto != "https") {
      return 301 https://$host$request_uri;
    }

    location = /index.html {
      add_header Cache-Control "no-store, no-cache";
      add_header Strict-Transport-Security "max-age=31536002;";

      try_files $uri $uri/ =404;
    }

    location / {
      add_header 'Cache-Control' "public, max-age=3600";

      try_files $uri $uri/ /index.html;
    }

    location /manifest.json {
      add_header Cache-Control "no-store, no-cache";
      add_header Access-Control-Allow-Origin "*";

      try_files $uri $uri/ =404;
    }

    # Cache assets for a long time, since all of them get unique names on each build
    location ~ ^/assets/[^/]*$ {
      add_header Cache-Control "public, max-age=31536000";
      add_header Access-Control-Allow-Origin "*";

      try_files $uri $uri/ =404;
    }

    # Need this b/c setting $fallback to =404 will try #{root}=404 instead of returning a 404
    location @404 {
      return 404;
    }

    # Redirect all request on /api/* to backend server
    set $api_endpoint '<%= "#{ENV['VITE_API_PROTOCOL']}://#{ENV['VITE_API_ENDPOINT']}" %>';
    location /api/ {
      rewrite ^/api//?(.*)$ /$1 break;
      proxy_pass $api_endpoint;
      proxy_ssl_server_name on;
      proxy_redirect $api_endpoint/ /api/;
    }
  }
}

This was migrated from static.json

{
  "root": "./dist",
  "https_only": true,

  "headers": {
    "/**": {
      "Strict-Transport-Security": "max-age=7776001"
    },

    "/": {
      "Cache-Control": "no-store, no-cache",
      "Strict-Transport-Security": "max-age=7776002"
    },

    "/assets/**": {
      "Cache-Control": "public, max-age=31536000",
      "Access-Control-Allow-Origin": "*"
    },

    "/manifest.json": {
      "Cache-Control": "no-store, no-cache",
      "Access-Control-Allow-Origin": "*"
    }
  },

  "routes": {
    "/assets/*": "/assets/",
    "/**": "index.html"
  },

  "proxies": {
    "/api/": {
      "origin": "${VITE_API_PROTOCOL}://${VITE_API_ENDPOINT}"
    }
  }
}

This is gold! Couldn't have done this without your help!

midoh7 commented 1 year ago
Screenshot 2023-04-28 at 11 29 15

@adamalfredsson I run into the problem recently and after following all the steps here I got 502 bad gateway, don't know what to do next