Ruby: Advanced usages

Going further with your Ruby deployments

👋 Welcome to the Stackhero documentation!

Stackhero offers a ready-to-use Ruby cloud solution that provides a host of benefits, including:

  • Deploy your application in seconds with a simple git push.
  • Use your own domain name and benefit from the automatic configuration of HTTPS certificates for enhanced security.
  • Enjoy peace of mind with automatic backups, one-click updates, and straightforward, transparent, and predictable pricing.
  • Get optimal performance and robust security thanks to a private and dedicated VM.

Save time and simplify your life: it only takes 5 minutes to try Stackhero's Ruby cloud hosting solution!

Until now, we have deployed our Ruby application by pushing the main branch using:

git push stackhero main

If you wish to deploy a different branch, you can use this command. Replace <BRANCH> with the branch name you want to deploy:

git push stackhero <BRANCH>:main

For example, to deploy a branch named production, run:

git push stackhero production:main

In some cases you may wish to deploy a tag rather than a branch. To do so, run the following command. Replace <TAG> with the tag you want to deploy:

git push stackhero '<TAG>^{}:main'

For instance, to deploy the tag v1.0.0, run:

git push stackhero 'v1.0.0^{}:main'

The ^{} syntax is used to reference the commit that the tag points to.

In addition to branches or tags, you can deploy a specific commit. Replace <COMMIT_HASH> in the command below with the hash of the desired commit:

git push -f stackhero <COMMIT_HASH>:main

For example, to deploy a commit with the hash abcde, run:

git push -f stackhero abcde:main

If your production deployment is not working as expected, you can roll back by deploying an older commit. First, use the command below to view your commit history:

git log

This command displays the date, commit hash, and description for every commit in your repository. For example, you might see output like:

commit cccc8b3ebdccb9abc1926ef49ee589dae5c5fe06 (HEAD -> main, stackhero/main)
Author: Developer
Date:   Fri Apr 28 09:36:18 +0000

    Break the code

commit bbbb622301772072c3d82f3cc0d91e29e6e84901
Author: Developer
Date:   Wed Apr 26 12:49:28 +0000

    Update the code

commit aaaa1d8b06535b413e0df8298ccf52339dfef3ff
Author: Developer
Date:   Wed Apr 26 12:44:50 +0000

    Improve the code

If the commit with the message "Break the code" (hash cccc...) is running in production, and you decide to roll back to the previous commit "Update the code" (hash bbbb...), run:

git push -f stackhero bbbb622301772072c3d82f3cc0d91e29e6e84901:main

To prevent deploying broken code and increase the stability of your production, it is highly recommended to have a "staging" environment.

Situated between "development" environments and the "production" environment, the "staging" environment provides a near-exact replica of the production environment. This allows you to test your code and ensure its quality before deploying it to production.

By using a staging environment, you can be more confident in your code's functionality and performance, ensuring a more reliable and robust production deployment.

This type of environment will be discussed later in the documentation.

A staging environment is a best practice to use alongside your development and production environments. It replicates your production environment so you can test updates and changes before they go live.

A staging environment must closely mirror the production environment.

However, ensure that the staging environment uses a clone of the production database rather than the actual production database.

If your Ruby service is linked to a database or other services, recreate them in the new <Project> - Staging stack.

To set up a staging environment on Stackhero, follow these steps:

  1. On the Stackhero dashboard, rename your existing stack from <Project> to <Project> - Production. For example, if your project is called Chat Bot, rename the stack to Chat Bot - Production.
  2. Create a new stack called <Project> - Staging. Using the previous example, this would be Chat Bot - Staging.
  3. Start a Ruby service within the staging stack.
  4. Retrieve the value of the git remote command and follow the instructions in the Deploying to staging environment section.

Following these steps will give you a properly configured staging environment to test and verify updates before they reach production.

Managing separate environments such as staging and production is highly recommended. As explained in Setting up a staging environment, you can deploy to each environment with different Git remotes.

Begin by renaming the current remote repository. For example, rename the remote "stackhero" to "stackhero-production" with this command:

git remote rename stackhero stackhero-production

Next, create a new Ruby service for the staging environment. Use the provided "git remote add" command and modify it as follows (replace <XXXXXX> with your service's domain):

  • Original command:

    git remote add stackhero ssh://stackhero@<XXXXXX>.stackhero-network.com:222/project.git
    
  • Modified command:

    git remote add stackhero-staging ssh://stackhero@<XXXXXX>.stackhero-network.com:222/project.git
    

You can now deploy to staging using:

git push stackhero-staging main

Or deploy to production with:

git push stackhero-production main

To streamline the deployment process further, consider using the improved Makefile version.

With this improved Makefile, deploying to production or staging can be done easily using make deploy-production or make deploy-staging.

Below is an improved Makefile that accommodates multiple rules for common tasks:

  • make dev (or simply make): Start the application in development mode.
  • make deploy: Deploy the application to the remote named stackhero (ideal when you have a single Stackhero instance).
  • make deploy-production: Deploy the application to the remote named stackhero-production.
  • make deploy-staging: Deploy the application to the remote named stackhero-staging.

This Makefile is designed to handle cases where code has already been deployed, avoiding the "Everything up-to-date" error.

Copy and paste the following content into your new Makefile:

# Rule to execute by default when invoking "make" without an argument
.DEFAULT_GOAL := dev


# Stackhero for Ruby will execute the "run" rule on your instance.
# This is the command to run on both production and staging platforms.
run:
  rake assets:precompile
  rake db:migrate RAILS_ENV=production
  RAILS_ENV=production bundle exec puma -C config/puma.rb


# Command to run in the development environment
dev:
  RAILS_ENV=development rails server -b 0.0.0.0


# Rule "deploy" deploys to the instance "stackhero".
# Suitable when you have only one instance.
deploy:
  @$(MAKE) -s deploy-script DEPLOY_REMOTE=stackhero DEPLOY_BRANCH=main


# Rule "deploy-*" deploys to the instance "stackhero-*".
# For example, run "make deploy-production" to deploy to "stackhero-production",
# or "make deploy-staging" to deploy to "stackhero-staging".
deploy-%:
  @$(MAKE) -s deploy-script DEPLOY_REMOTE=stackhero-$* DEPLOY_BRANCH=main


# Internal deployment rule. Do not modify.
deploy-script:
  @echo "Deploying branch \"${DEPLOY_BRANCH}\" to \"${DEPLOY_REMOTE}\"..."
  @echo

  @if [ -n "$$(git status --porcelain)" ]; then \
    echo "Can't deploy because there are uncommitted changes:"; \
    echo "\e[0m"; \
    git status -s; \
    echo ""; \
    echo "\e[0;31m"; \
    echo "You can use this command to commit the changes:"; \
    echo "git add -A . && git commit -m \"Your message\""; \
    echo "\e[0m"; \
    exit 1; \
  fi

  @git push --dry-run ${DEPLOY_REMOTE} ${DEPLOY_BRANCH} 2>&1 | grep -q -F "Everything up-to-date"; \
  EXIT_CODE=$$?; \
  if [ $$EXIT_CODE -eq 0 ]; then \
    echo -n "Nothing new to deploy... Force deploy (this will create a new commit)? (y/N) "; \
    read answer && \
    case $$answer in \
      y|Y|yes|YES) \
      git commit --allow-empty -m "Force update for deploy purpose to \"${DEPLOY_REMOTE}\"" ; \
      ;; \
      *) \
      echo "Nothing to deploy!"; \
      exit 1; \
      ;; \
    esac \
  fi

  git push ${DEPLOY_REMOTE} ${DEPLOY_BRANCH}

At some point, you will need to manage secrets such as tokens or passwords for databases and third-party services. It is essential to store these secrets securely. Avoid embedding secrets directly in your repository or code because this poses a serious security risk.

Environment variables offer two significant benefits:

  1. Your secrets will not be stored in your Git repository, reducing the risk if someone gains access to your source code.
  2. You can use different credentials for different environments. For example, connect to your production database in production while using a development database during development.

For development, create a .env file in the root of your project. This file will be excluded from Git so that it is never committed. Use the dotenv gem to automatically load the .env file.

First, add the dotenv-rails gem to your Gemfile:

# Gemfile
gem 'dotenv-rails', groups: [:development, :test]

Then install the gem:

bundle install

Next, create a .env file at the root of your project and add your variables:

RAILS_ENV="development"
DATABASE_PASSWORD="secretPassword"
THIRD_API_PRIVATE_KEY="secretKey"
# ...

Finally, ensure the .env file is ignored by Git:

echo '.env*' >> .gitignore

For staging and production, the .env file is not secure or practical because it cannot be stored in a Git repository. Instead, Stackhero provides a secure solution for managing environment variables directly in your Ruby service configuration.

You can set these variables via the Stackhero dashboard by selecting your Ruby service and clicking the "Configure" button.

In Ruby, you can easily access environment variables using ENV. For example, to retrieve DATABASE_PASSWORD, use:

ENV['DATABASE_PASSWORD'] # => 'secretPassword'

Here is an example of how to connect to a RabbitMQ server using environment variables:

require 'bunny'

class RabbitMQClient
  def initialize
    @connection = Bunny.new(hostname: ENV['RABBITMQ_HOST'],
                            username: ENV['RABBITMQ_USERNAME'],
                            password: ENV['RABBITMQ_PASSWORD'])
    @connection.start
  end

  def publish(queue_name, message)
    channel = @connection.create_channel
    queue = channel.queue(queue_name)
    channel.default_exchange.publish(message, routing_key: queue.name)
  end

  def close
    @connection.close
  end
end

On the development platform, your .env file might include:

RABBITMQ_HOST='127.0.0.1'
RABBITMQ_USERNAME='developmentUser'
RABBITMQ_PASSWORD='developmentPassword'

For production and staging, define your environment variables in the Stackhero dashboard under the Ruby service configuration as shown below:

RABBITMQ_HOST='<XXXXXX>.stackhero-network.com'
RABBITMQ_USERNAME='production'
RABBITMQ_PASSWORD='secretProductionPassword'

Ruby applications often use the HTTP protocol on ports 80 (HTTP) and 443 (HTTPS). If your application needs additional ports or different protocols (TCP or UDP), configure the "Ports Redirections" settings in your Ruby service via the Stackhero dashboard.

You will need to specify the entry port (open publicly), the destination port (open within your Ruby service), and the protocol (TCP or UDP).

For storing files such as user photos or documents, using an object storage solution is highly recommended. Object storage allows you to share files across multiple services and instances and decouples the storage layer from your code. This is considered a best practice.

We recommend MinIO as an easy, fast, and powerful solution that is compatible with the Amazon S3 protocol.

If you choose local file storage, you can use the persistent storage provided with your Ruby instance. This local storage is available under the directory /persistent/storage/.

However, local file storage is generally not recommended as it may not be the best practice for long-term scalability and reliability.

WARNING: Never store data outside the /persistent/storage/ folder!

Storing data in any location other than the persistent storage folder can result in data loss when your instance is rebooted, updated, or when you push new code.

If you are using macOS, you might find it inconvenient to type your SSH private key password every time you push your code. Although security is essential, you can improve convenience by securely storing your password in Apple's Keychain.

It can be tempting to remove the password from your SSH private key, but this is not advisable.

Instead, store your key password in Keychain using the following command for a key named id_ed25519:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

After running this command, you should not be prompted for your key password again. If you use an RSA key, substitute id_ed25519 with id_rsa as shown below:

ssh-add --apple-use-keychain ~/.ssh/id_rsa