My favourite place to deploy simple hobby and experimental web applications is heroku. It has a free plan, and it’s pretty easy to get started.

Heroku can detect the type of your application, if it is in the root of your repository, but if you need some custom options, you can define a Procfile.

In the free plan as of today you can have 1000 hour of free hosting on spare instances. When your application is not used, it goes to sleep mode, and stays awake for a short period of time, or until it’s used. It means that the first reach of your application might take a bit longer, and if you don’t persist your application state it can be lost.

Get started

I usually use the heroku CLI to manage my applications.

  1. register in heroku, create a git repo for the project, navigate to it
  2. heroku login - opens the browser and authenticates for the CLI session
  3. heroku create project-name - creates a new project, and sets a new remote called heroku for the it up for the current git repository
  4. magic happens here (create the app)
  5. git push heroku master - start the deployment
  6. heroku logs -t - shows the logs of the current heroku app

Serve create-react-app application

Sometimes I just write a simpe react web application without any backend, that I just want to host somewhere for a quick demonstration.

I haven’t found a description how to host create-react-app for this simple use case in the docs.

The simplest way is to put the app in the root of the repository. If heroku finds package.json in the root, it assumes that the project is a node webapp. At deployment it will install both dependencies and devDependencies, build the application with npm run build, remove devDependencies, then start the application with npm start.

After create-react-app prod build, it shows a way to serve the application with serve. I added it as the start script in package.json, and build the app with react-scripts build.

For local development I kept react-scripts start as npm run cra-start.

Some gotchas

  • Heroku needs a README.md in the root, otherwise it won’t deploy.
  • In order to install the proper dependencies, package-lock.json or yarn.lock file must be pushed to the repo.
  • The engines property can set the necessary npm and node versions.
  • The port that the app needs to listen to is set in $PORT environment variable.

Sample package.json

{
  "name": "create-react-app",
  "version": "0.1.0",
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1",
    "serve": "^11.3.1"
  },
  "engines": {
    "npm": "6.11.3",
    "node": "10.17.0"
  },
  "scripts": {
    "start": "serve -s build",
    "cra-start": "react-scripts start",
    "build": "react-scripts build",
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Serve untracked non-generated files

I had a quick and small project where I couldn’t generate the static pages with a script, but I did not want to host the generated files in the repository.

I used a script to collect the data on every deploy, and push it to heroku. I wanted it to be simple, and reproducible in a new machine, without any manual steps.

Deploy script

I put the necessary files (package.json, package-lock.json, README.md) for heroku root into heroku-deploy folder.

For the sake of the example I collected the static files into untracked-files/export.

#!/bin/bash

echo "- Clean dist folder"
# Make sure that dist-folder does not exist
rm -rf ./dist-folder
echo "- Create dist branch"
git checkout --orphan dist
git reset --hard
git clean -fd -e untracked-files/export
git commit --allow-empty -m "Create Empty Dist Branch"
git checkout master
echo "- Add files and push new version"
git worktree add -f dist-folder dist
cp -r ./untracked-files/export/* dist-folder/
cp -r ./heroku-deploy/* dist-folder/
cd dist-folder || exit 1
git add .
git commit -m 'Deploy to Heroku'
git push --force heroku dist:master
# force push is needed, because a
# new local branch is created on every deploy.
cd ..
echo "- Cleanup"
git worktree remove dist-folder
git branch -D dist
echo "- Done"

Git commands to create a new empty parentless branch

git checkout --orphan dist
git reset --hard
git clean -fd -e untracked-files/export
git commit --allow-empty -m "Create Empty Dist Branch"
git checkout master

In order to create a new branch that does not have anything inside git checkout --orphan dist is a good start, it creates a new orphaned branch called dist.

After this script runs, we are taken to this new branch, and every file in the work tree is in staged state.

Some say that rm -rf . is a good next step, but for me it seems a bit dangerous.

I sticked with git reset --hard; git clean -fd -e untracked-files/export. It unstages all the files, remove the modified files, then clean all untracked files, except the ones in the excluded (-e) directories.

To create a branch I added a new empty commit, and headed back to the main branch.

Git worktree

Git worktree is a way to manage multiple branches in the same repository.

To put it simple, it creates a folder, and when you step into that folder, git acts like you’ve checked out that branch.

git worktree add -f dist-folder dist
cp -r anything/* dist-folder
cd dist-folder
# now git thinks we've checked out `dist`
git add .
git commit -m 'Deploy to Heroku'
git push --force heroku dist:master
cd ..
# back in master
git worktree remove dist-folder

For my usecase it’s perfect, I have an empty dist branch at this point,

  1. I check it out into a folder called dist-folder
  2. copy the necessary files
  3. step into the worktree, so it seems like I’ve checked out dist
  4. Stage the files, commit and deploy.
  5. step out, so git thinks I’m in master, and I can safely remove dist-folder.

Since I create a new branch on every deploy, I overwrite the contents of master branch in heroku remote. Since pushes to that branch is only to trigger deployment, I never check it out, I don’t yet see a problem with this approach.

Disclaimer

I was not asked to create this post, and did not get anything for it, I just wanted to share how simple it is to use and some of my experience with it.