# Rails/React Monorepo Heroku Deployment # Phase 4, Lecture 7 - Deployment - Postgres - Application environments and environment variables - Production ready configuration - Configuration of build steps for react application - Telling Heroku how to run our application - Setting up post release scripts that ensure our application will be able to respond to requests in production Curriculum Resources: - [Deploying a Rails API to Heroku](https://github.com/learn-co-curriculum/phase-4-deploying-rails-api-to-heroku) - [Deploying Rails/React to Heroku](https://github.com/learn-co-curriculum/phase-4-deploying-rails-react-to-heroku) I'm going to come at this in two phases. First, we're going to update our dependencies and configuration in preparation to deploy, then we're going to start working with the Heroku CLI to iterate on deployments. The goal will be to think about our production environment and make as many of the necessary adjustments as we can before we start attempting deployments. Because each deployment requires another commit, we want to try and anticipate as many of the required tasks as possible before we make the commit and do the push. ### Ruby Version You can have any of the following Ruby versions as of October 2021: 2.6.8, 2.7.4, or 3.0.2. We've recommended 2.7.4. Whichever version you've got, you'll need to run commands that look like these: ``` rvm install 2.7.4 --default ``` ``` gem install bundler gem install rails ``` If you don't have one of these versions installed at the moment, open up a terminal and start it now, it sometimes can take 10-15 minutes, but you'll be able to do some other stuff in the meantime. ## Switching to Postgres Checking for local installation: ``` which postgres ``` or ``` postgres --version ``` Sometimes I've seen students have [issues with Postgres on WSL](https://dakotaleemartinez.com/tutorials/postgresql-setup-on-ubuntu/) due to an older version that's installed but not active. In the lessons, the idea is presented that we should work with Postgres locally as well if we plan to use it in production. This is definitely a best practice, as it can help use prevent strange bugs from occuring down the line. But, if you are currently having issues with Postgres locally, you can most likely still deploy your application to production while using sqlite as the database to support your application in development. At scale, this is not practical as larger codebases are more likely taking advantage of features that postgres has that sqlite doesn't support, but at this point, you probably won't have any issues using Sqlite locally and Postgres in production. I'll go through how to configure your app for postgres locally and then also how to configure it to continue using the sqlite database we've used in development so you can see how to do that if you've been unable to resolve local issues with Postgres at this time. ### A note about Heroku Heroku will automatically provision a new Postgres database with all of the adapter and connection information required for supporting a rails application and store all of that info in an environment variable called `DATABASE_URL`. When you create an application on Heroku for your project using the `heroku create` command (provided of the heroku CLI), a `DATABASE_URL` environment variable will be created for you containing all of the following configuration options: - adapter - database - username - password - host - port This means that you won't need to set any of these values for the production environment within the `database.yml` file as rails will automatically assign those when it reads the value of the `DATABASE_URL` environment variable generated by Heroku. Here is the `config/database.yml` file from the demo deployment app in the curriculum: ```yml # PostgreSQL. Versions 9.3 and up are supported. # # Install the pg driver: # gem install pg # On macOS with Homebrew: # gem install pg -- --with-pg-config=/usr/local/bin/pg_config # On macOS with MacPorts: # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config # On Windows: # gem install pg # Choose the win32 build. # Install PostgreSQL and put its /bin directory on your path. # # Configure Using Gemfile # gem 'pg' # default: &default adapter: postgresql encoding: unicode # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default database: phase_4_deploying_demo_app_development # The specified database role being used to connect to postgres. # To create additional roles in postgres see `$ createuser --help`. # When left blank, postgres will use the default role. This is # the same name as the operating system user running Rails. #username: phase_4_deploying_demo_app # The password associated with the postgres role (username). #password: # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. #host: localhost # The TCP port the server listens on. Defaults to 5432. # If your server runs on a different port number, change accordingly. #port: 5432 # Schema search path. The server defaults to $user,public #schema_search_path: myapp,sharedapp,public # Minimum log levels, in increasing order: # debug5, debug4, debug3, debug2, debug1, # log, notice, warning, error, fatal, and panic # Defaults to warning. #min_messages: notice # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: phase_4_deploying_demo_app_test # As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # # Instead, provide the password or a full connection URL as an environment # variable when you boot the app. For example: # # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" # # If the connection URL is provided in the special DATABASE_URL environment # variable, Rails will automatically merge its configuration values on top of # the values provided in this file. Alternatively, you can specify a connection # URL environment variable explicitly: # # production: # url: <%= ENV['MY_APP_DATABASE_URL'] %> # # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full overview on how database connection configuration can be specified. # production: <<: *default database: phase_4_deploying_demo_app_production username: phase_4_deploying_demo_app password: <%= ENV['PHASE_4_DEPLOYING_DEMO_APP_DATABASE_PASSWORD'] %> ``` Note that there are some options set for the production environment here. They'll actually be ignored on Heroku because of what we discussed before. These values would only be used if we were to run a command like `RAILS_ENV=production rails db:create db:migrate` to create the production database on your local machine (where the DATABASE_URL configured by heroku won't be defined) ## Generating a New Application for Deployment ```bash rails new heroku_deploy --api --minimal -T --database=postgresql ``` This will generate a new application with a database.yml file that looks like this: ```yml # PostgreSQL. Versions 9.3 and up are supported. # # Install the pg driver: # gem install pg # On macOS with Homebrew: # gem install pg -- --with-pg-config=/usr/local/bin/pg_config # On macOS with MacPorts: # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config # On Windows: # gem install pg # Choose the win32 build. # Install PostgreSQL and put its /bin directory on your path. # # Configure Using Gemfile # gem 'pg' # default: &default adapter: postgresql encoding: unicode # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default database: heroku_deploy_development # The specified database role being used to connect to postgres. # To create additional roles in postgres see `$ createuser --help`. # When left blank, postgres will use the default role. This is # the same name as the operating system user running Rails. #username: heroku_deploy # The password associated with the postgres role (username). #password: # Connect on a TCP socket. Omitted by default since the client uses a # domain socket that doesn't need configuration. Windows does not have # domain sockets, so uncomment these lines. #host: localhost # The TCP port the server listens on. Defaults to 5432. # If your server runs on a different port number, change accordingly. #port: 5432 # Schema search path. The server defaults to $user,public #schema_search_path: myapp,sharedapp,public # Minimum log levels, in increasing order: # debug5, debug4, debug3, debug2, debug1, # log, notice, warning, error, fatal, and panic # Defaults to warning. #min_messages: notice # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: heroku_deploy_test # As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # # Instead, provide the password or a full connection URL as an environment # variable when you boot the app. For example: # # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" # # If the connection URL is provided in the special DATABASE_URL environment # variable, Rails will automatically merge its configuration values on top of # the values provided in this file. Alternatively, you can specify a connection # URL environment variable explicitly: # # production: # url: <%= ENV['MY_APP_DATABASE_URL'] %> # # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full overview on how database connection configuration can be specified. # production: <<: *default database: heroku_deploy_production username: heroku_deploy password: <%= ENV['HEROKU_DEPLOY_DATABASE_PASSWORD'] %> ``` If there are problems installing postgresql locally, you can still use sqlite3 in development. If you've had issues with Postgres locally and are planning on keeping sqlite for the dev environment, then you'll want to do the following 2 steps: 1. Add a production group and wrap this line in it: ```rb group :production do gem "pg", "~> 1.2" end ``` 2. Move the sqlite gem to the :development, :test group ```rb group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] # Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.4' end ``` > **NOTE**: In either case, it is important that the `sqlite3` gem not be in the default (ungrouped) part of the Gemfile as Heroku will complain if it is. (You can move `sqlite3` into the development, test group and Heroku won't mind as it only installs gems in the default and production groups) Example Gemfile ```rb source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.7.4' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' gem 'rails', '~> 6.1.4', '>= 6.1.4.1' # Use postgresql as the database for Active Record gem 'pg', '~> 1.1' # Use Puma as the app server gem 'puma', '~> 5.0' # Use Active Model has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible # gem 'rack-cors' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do gem 'listen', '~> 3.3' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] ``` You'll need to uncomment the `bcrypt` gem and run `bundle` so we can install it. If you're sticking with sqlite3 in development, your `config/database.yml` file could look something like this: ```yml default: &default adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default adapter: sqlite3 database: db/development.sqlite3 test: <<: *default database: meetup_clone_demo_app_test production: <<: *default ``` After you've got the configuration updated here, we'll also need to create a new development database using postgres. Using sqlite, all we had to do was `rails db:migrate`. With Postgres, we'll need to run `rails db:create` first. ```bash rails db:create ``` We can create a bit of code to test things out. ```bash rails g model User username password_digest rails g model Post title content:text user:belongs_to ``` ```rb # app/models/post.rb class Post < ApplicationRecord belongs_to :user end ``` ```rb # app/models/user.rb class User < ApplicationRecord has_many :posts has_secure_password validates :username, presence: true, uniqueness: true end ``` ```rb # db/seeds.rb user = User.create(username: 'test', password: 'password') user.posts.create(title: 'first post', content: 'this is full of awesome content') user.posts.create(title: 'second post', content: 'so much original amazing content') ``` ```bash rails db:migrate db:seed ``` We can add a [Postgres extension for VSCode](https://marketplace.visualstudio.com/items?itemName=ckolkman.vscode-postgres) to enable the same thing for Postgres. I recorded a quick demo of how to get started with the extension and make your first database connection. After you're set up initially, it's pretty similar to the SQLite Explorer. <iframe width="560" height="315" src="https://www.youtube.com/embed/Cc9d2c8UuKA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> ## Adding Controllers/Routes ```bash rails g controller api/users rails g controller api/sessions rails g controller api/posts ``` We'll add an index route for posts and the 4 auth related routes to begin. ```rb # config/routes.rb namespace :api do resources :posts, only: [:index] get "/me", to: "users#show" post "/signup", to: "users#create" post "/login", to: "sessions#create" delete "/logout", to: "sessions#destroy" end ``` Next, we'll fill in our application controller with the cookie configuration, auth related private methods and our 404 error handling. ```rb # app/controllers/application_controller.rb class ApplicationController < ActionController::API include ActionController::Cookies rescue_from ActiveRecord::RecordNotFound, with: :not_found before_action :confirm_authentication private def current_user @current_user ||= User.find_by(id: session[:user_id]) end def logged_in? !!current_user end def confirm_authentication render json: { error: "You must be logged in to do that." }, status: :unauthorized unless logged_in? end def not_found(e) render json: { error: e.message }, status: :not_found end end ``` Then we'll add in the 2 actions to our users controller. ```rb # app/controllers/api/users_controller.rb class Api::UsersController < ApplicationController skip_before_action :confirm_authentication # get '/api/me' def show if current_user render json: current_user, status: :ok else render json: { error: 'No active session' }, status: :unauthorized end end # post '/api/signup' def create user = User.create(user_params) if user.valid? session[:user_id] = user.id render json: user, status: :ok else render json: { error: user.errors }, status: :unprocessable_entity end end private def user_params params.permit(:username, :password, :password_confirmation) end end ``` And next, we'll add in the two actions in our sessions controller. ```rb # app/controllers/api/sessions_controller.rb class Api::SessionsController < ApplicationController skip_before_action :confirm_authentication # post '/login' def create user = User.find_by_username(params[:username]) if user&.authenticate(params[:password]) session[:user_id] = user.id render json: user, status: :ok else render json: { error: 'invalid credentials' }, status: :unauthorized end end # delete '/logout' def destroy session.delete(:user_id) end end ``` Finally, let's put an index action in the posts controller so that we can get a list of all the posts after we're logged in. ```rb # app/controllers/api/posts_controller.rb class Api::PostsController < ApplicationController def index render json: Post.all end end ``` We'll need to add the following configuration to add support for Cookies. ```rb # config/application.rb class Application < Rails::Application # ... config.middleware.use ActionDispatch::Cookies config.middleware.use ActionDispatch::Session::CookieStore # Use SameSite=Strict for all cookies to help protect against CSRF config.action_dispatch.cookies_same_site_protection = :strict # ... end ``` ## Server vs Client Side Routing We also need to handle our routing configuration. Requests coming from our react client to the API should be routed to our API endpoints, but when we click react router links, we want the app to still work upon page refresh. For this, we need to set up a fallback route that will catch 404s and render the `public/index.html` file ```rb get "*path", to: "fallback#index", constraints: ->(req) { !req.xhr? && req.format.html? } ``` We're adding a couple of constraints to this route. This route will apply if we're not sending a fetch request and we're sending a request for html. This will mainly apply to the first time we visit the application and if we refresh the page any time thereafter. We also need to create the fallback controller and its index action. ``` rails g controller fallback ``` Replace the contents of the file with the following (this controller class **must** inherit from `ActionController::Base` not `ApplicationController` or it won't properly render the html for the react app): ```rb class FallbackController < ActionController::Base def index render file: 'public/index.html' end end ``` To configure our client side application for building and deployment, we'll need to create a `package.json` file at the root of the project. ```bash touch package.json ``` ```json { "name": "heroku_demo_client", "version": "1.0.0", "description": "Build and Deployment Configuration for Rails/React Deployment", "engines": { "node": "15.10.0" }, "scripts": { "clean": "rm -rf public", "build": "npm install --prefix client && npm run build --prefix client", "deploy": "cp -a client/build/. public/", "heroku-postbuild": "npm run clean && npm run build && npm run deploy" }, "author": "DakotaLMartinez" } ``` When we get into our heroku configuration, we'll tell Heroku that there is a node component to our application. This will tell heroku to run the heroku-postbuild script after the application is deployed. When this happens, the public directory will be removed, we'll get an npm install and a build in the client directory and all of the contents of the build directory in client will be copied into the public directory of our rails application. The fallback route that we defined in the previous step will render the public/index.html file that is created by this step. ## Creating the React Application Before we can test this out, we need to actually add a React client to the rails application: ```bash npx create-react-app client --use-npm ``` Once it's done, we'll add in react router: ```bash npm install react-router@5.2.1 react-router-dom@5.3.0 --save --prefix client ``` We also want to update the `client/package.json` file so that our dev server will run on port 4000 and proxy fetch requests sent to `'/api` to `http://localhost:3000/api` where our rails server will be running. Note that this proxy only goes into effect in development mode. In production, the requests will be treated as same origin requests–no proxy necessary–as the rails server will actually be serving up the react application. When we're done the `client/package.json` file should look something like this: ```json { "name": "client", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router": "^5.2.1", "react-router-dom": "^5.3.0", "react-scripts": "4.0.3", "web-vitals": "^1.1.2" }, "scripts": { "start": "PORT=4000 react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "proxy": "http://localhost:3000" } ``` Let's just set up a basic structure with the two components for `AuthenticatedApp` and `UnauthenticatedApp` in `App.js`. We'll also want to add the `Login` and `Signup` components ```bash mkdir client/src/components touch client/src/AuthenticatedApp.js client/src/UnauthenticatedApp.js client/src/components/Signup.js client/src/components/Login.js ``` Replace the contents of App.js with the following ```js // client/src/App.js import React, { useState, useEffect } from 'react' import AuthenticatedApp from './AuthenticatedApp' import UnauthenticatedApp from './UnauthenticatedApp' import { BrowserRouter as Router } from 'react-router-dom' function App() { const [currentUser, setCurrentUser] = useState(null) const [authChecked, setAuthChecked] = useState(false) useEffect(() => { fetch('/api/me', { credentials: 'include' }) .then(res => { if (res.ok) { res.json().then((user) => { setCurrentUser(user) setAuthChecked(true) }) } else { setAuthChecked(true) } }) }, []) if(!authChecked) { return <div></div>} return ( <Router> {currentUser ? ( <AuthenticatedApp setCurrentUser={setCurrentUser} currentUser={currentUser} /> ) : ( <UnauthenticatedApp setCurrentUser={setCurrentUser} /> ) } </Router> ) } export default App ``` Next, AuthenticatedApp.js with the following: ```js // client/src/AuthenticatedApp.js import './App.css'; import { useState, useEffect } from 'React'; import { useHistory } from 'react-router-dom'; function AuthenticatedApp({ currentUser, setCurrentUser }) { const history = useHistory() const [posts, setPosts] = useState([]) useEffect(() => { fetch('/api/posts') .then(res => res.json()) .then(posts => setPosts(posts)) }, []) const handleLogout = () => { fetch('/api/logout', { method: 'DELETE', credentials: 'include' }) .then(res => { if (res.ok) { setCurrentUser(null) history.push('/') } }) } return ( <div> <p><button onClick={handleLogout}>Logout</button></p> {posts.map(post => ( <div key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> </div> )) } </div> ); } export default AuthenticatedApp; ``` Then, `UnauthenticatedApp.js` with the following: ```js // client/src/UnauthenticatedApp.js import React from 'react' import { Switch, Route, Redirect } from 'react-router-dom' import Login from './components/Login' import Signup from './components/Signup' function UnauthenticatedApp({ setCurrentUser }) { return ( <Switch> <Route exact path="/"> <Login setCurrentUser={setCurrentUser} /> </Route> <Route exact path="/signup"> <Signup setCurrentUser={setCurrentUser}/> </Route> <Redirect to="/" /> </Switch> ) } export default UnauthenticatedApp ``` Finally, the `Signup.js` and `Login.js` files with the code below: ```js // client/src/components/Signup.js import React, { useState } from 'react' import { useHistory, Link } from 'react-router-dom' function Signup({ setCurrentUser }) { const history = useHistory() const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [passwordConfirmation, setPasswordConfirmation] = useState('') const handleSubmit = (event) => { event.preventDefault() fetch('/api/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, password_confirmation: passwordConfirmation }) }) .then(res => { if (res.ok) { res.json().then(user => { setCurrentUser(user) }) } else { res.json().then(errors => { console.error(errors) }) } }) } return ( <div> <form onSubmit={handleSubmit}> <h1>Sign Up</h1> <p> <label htmlFor="username" > Username </label> <input type="text" name="username" value={username} onChange={(e) => setUsername(e.target.value)} /> </p> <p> <label htmlFor="password" > Password </label> <input type="password" name="" value={password} onChange={(e) => setPassword(e.target.value)} /> </p> <p> <label htmlFor="password_confirmation" > Password Confirmation </label> <input type="password" name="password_confirmation" value={passwordConfirmation} onChange={(e) => setPasswordConfirmation(e.target.value)} /> </p> <p><button type="submit">Sign Up</button></p> <p>-- or --</p> <p><Link to="/login">Log In</Link></p> </form> </div> ) } export default Signup ``` ```js // client/src/components/Login.js import React, { useState } from 'react' import { Redirect, useHistory, Link } from 'react-router-dom' function Login({ setCurrentUser }) { const history = useHistory() const [username, setUsername] = useState('') const [password, setPassword] = useState('') const handleSubmit = (event) => { event.preventDefault() fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({username, password}) }) .then(res => { if (res.ok) { res.json().then(user => { setCurrentUser(user) }) } else { res.json().then(errors => { console.error(errors) }) } }) } return ( <div> <Redirect to="/" /> <form onSubmit={handleSubmit}> <h1>Log In</h1> <p> <label htmlFor="username"> Username </label> <input type="text" name="username" value={username} onChange={(e) => setUsername(e.target.value)} /> </p> <p> <label htmlFor="password" > Password </label> <input type="password" name="" value={password} onChange={(e) => setPassword(e.target.value)} /> </p> <p><button type="submit">Log In</button></p> <p>-- or --</p> <p><Link to="/signup">Sign Up</Link></p> </form> </div> ) } export default Login ``` At this point, we can test out this configuration by running: ``` npm run heroku-postbuild ``` from our rails directory and then ``` rails s ``` If we visit our react app in the browser and navigate around and then we're able to log in and log out, then everything works then we're on the right track. ## Configuring Heroku via the Heroku CLI First, we want to create a Procfile in the root of the project that describes how to run our application. ``` touch Procfile ``` and add this to it: ``` web: bundle exec rails s release: bin/rake db:migrate ``` We'll also need to ensure that our project supports the environment that Heroku will build it in. To do that, we need to specify a platform within our Gemfile.lock file. ``` bundle lock --add-platform x86_64-linux --add-platform ruby ``` Second, we need to make sure Next, you'll want to make sure you're connected to your Heroku account ``` heroku login ``` And then you'll be redirected to the browser. Once you sign into your account and click the button, you can return to the terminal and you should be logged in. Once you're logged in, you can create an application on heroku that you can deploy to. ``` heroku create ``` You'll see something like this if all is well: ``` Creating app... done, ⬢ stormy-basin-85902 https://stormy-basin-85902.herokuapp.com/ | https://git.heroku.com/stormy-basin-85902.git ``` And this adds in a remote that you can push to that will trigger a deployment. The syntax looks like this `git push heroku main` if you want to push up the main branch. Before we actually run this, we'll want to tell heroku that we have a node and ruby application and that we want to build the nodejs part first (this will build the app and copy it to the public directory). We can configure this via the CLI using the following commands. ``` heroku buildpacks:add heroku/nodejs --index 1 heroku buildpacks:add heroku/ruby --index 2 ``` When you've done both of these things, you should see something like this: ``` Buildpack added. Next release on stormy-basin-85902 will use: 1. heroku/nodejs 2. heroku/ruby ``` Since we're going to deploy via a git push, we need to commit all of our changes before we can do the deployment. ``` git add . git commit -m "configure for heroku" ``` Heroku will only deploy from main or master, so we're going to push the main branch up: ``` git push heroku main ``` This should take quite a while to build, deploy, install and migrate the first time, but once we're done, we can run ``` heroku open ``` from the CLI to check out the app in the browser. ## Gotchas ### Successful deployment but then errors when visiting the site in the browser The first time you deploy, you may get an application error when you visit your application in the browser. It notifies you to check: ``` heroku logs --tail ``` to see what the problem was. After scrolling through the logs, you'll be looking for something like this: 2021-10-05T05:57:12.258256+00:00 app[web.1]: Exiting 2021-10-05T05:57:12.258461+00:00 app[web.1]: /app/vendor/bundle/ruby/2.7.0/gems/zeitwerk-2.4.2/lib/zeitwerk/loader/callbacks.rb:18:in `on_file_autoloaded': expected file /app/app/serializers/user_serializer.rb to define constant UserSerializer, but didn't (Zeitwerk::NameError) So, I checked out that file and found this: ```rb class ChangedUserSerializer < ActiveModel::Serializer attributes :id, :username, :email end ``` Sure enough! We need to rename the class. ```rb class UserSerializer < ActiveModel::Serializer attributes :id, :username, :email end ``` then we need to commit the change ``` git add . git commit -m "rename serializer class" ``` Then we can try the deploy again: ``` git push heroku master ``` That should do it!!! ### React App Won't build successfully Make sure that the node version you're using locally is the same one that you've instructed Heroku to use in the `package.json` file **at the root of the project** (not the one in client). Put your local node version in the spot where it says engines: ```json "engines": { "node": "15.10.0" } ``` Also, be sure to check the terminal output carefully for error messages that occur right between the successful logs and the beginning of the stack trace. The errors will give you a good jumping off point for debugging. # Heroku Deployment Cheat Sheet ## Dependencies (Gems/packages) Add ```rb gem "pg", "~> 1.2" ``` Remove ```rb gem 'sqlite3', '~> 1.4' ``` Add support for Ubuntu Linux ``` bundle lock --add-platform x86_64-linux --add-platform ruby ``` Get supported version of ruby (2.6.8 and 3.0.2 also work as of October 2021) ``` rvm install 2.7.4 --default gem install bundler gem install rails ``` ## Configuration (environment variables/other stuff in config folder) config/database.yml ```yml default: &default adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default database: meetup_clone_demo_app_development test: <<: *default database: meetup_clone_demo_app_test production: <<: *default ``` Procfile ``` web: bundle exec rails s release: bin/rake db:migrate ``` package.json ```json { "name": "meetup_clone_client", "version": "1.0.0", "description": "Build and Deployment Configuration for Meetup Clone's React client", "engines": { "node": "your.actual.version" }, "scripts": { "clean": "rm -rf public", "build": "npm install --prefix client && npm run build --prefix client", "deploy": "cp -a client/build/. public/", "heroku-postbuild": "npm run clean && npm run build && npm run deploy" }, "author": "DakotaLMartinez" } ``` commands ``` heroku create ``` ``` heroku buildpacks:add heroku/nodejs --index 1 heroku buildpacks:add heroku/ruby --index 2 ``` Add your master key to Heroku as an environment variable so that any credential variables defined within `config/credentials.yml.enc` will be accessible to your application when it runs. ``` heroku config:set RAILS_MASTER_KEY=`cat config/master.key` ``` You can also set individual environment variables using this syntax: ``` heroku config:set GOOGLE_API_KEY=apikeygoeshere ``` ## Database No changes other than the switch to Postgres. ## Models No changes ## Views No changes (heroku postbuild will copy the react app into the rails public directory) ## Controllers Fallback controller ```rb class FallbackController < ActionController::Base def index render file: 'public/index.html' end end ``` Ensure api endpoints are namespaced under Api and stored in a directory called app/controllers/api ## Routes ```rb get "*path", to: "fallback#index", constraints: ->(req) { !req.xhr? && req.format.html? } ``` Ensure that api endpoints are namespaced under api so that they don't conflict with client side routes. ```rb # config/routes.rb namespace :api do resources :posts end # app/controllers/api/posts_controller.rb class Api::PostsController < ApplicationController # ... end ``` ## Usage To deploy the application you can push the main or master branch to the heroku remote using a command like this: ``` git push heroku main ``` or ``` git push heroku master ```