Introduction to Baremetal
Once you've grown beyond the confines and limitations of the cloud deployment providers, it's time to get serious: hosting your own code on big iron. Prepare for performance like you've only dreamed of! Also be prepared for IT and infrastructure responsibilities like you've only had nightmares of.
With Redwood's Baremetal deployment option, the source (like your dev machine) will SSH into one or more remote machines and execute commands in order to update your codebase, run any database migrations and restart services.
Deploying from a client (like your own development machine) consists of running a single command:
First time deploy:
yarn rw deploy baremetal production --first-run
Subsequent deploys:
yarn rw deploy baremetal production
If you haven't done any kind of remote server work before, you may be in a little over your head to start with. But don't worry: until relatively recently (cloud computing, serverless, lambda functions) this is how all websites were deployed, so we've got a good 30 years of experience getting this working!
If you're new to connecting to remote servers, check out the Intro to Servers guide we wrote just for you.
Deployment Lifecycle
The Baremetal deploy runs several commands in sequence. These can be customized, to an extent, and some of them skipped completely:
git clone --depth=1
to retrieve the latest code- Symlink the latest deploy
.env
to the shared.env
in the app dir yarn install
- installs dependencies- Runs prisma DB migrations
- Generate Prisma client libs
- Runs data migrations
- Builds the web and/or api sides
- Symlink the latest deploy dir to
current
in the app dir - Restart the serving process(es)
- Remove older deploy directories
First Run Lifecycle
If the --first-run
flag is specified then step 6 above will execute the following commands instead:
pm2 start [service]
- starts the serving process(es)pm2 save
- saves the running services to the deploy users config file for future startup. See Starting on Reboot for further information
Directory Structure
Once you're deployed and running, you'll find a directory structure that looks like this:
└── var
└── www
└── myapp
├── .env <────────────────┐
├── current ───symlink──┐ │
└── releases │ │
└── 20220420120000 <┘ │
├── .env ─symlink─┘
├── api
├── web
├── ...
There's a symlink current
pointing to directory named for a timestamp (the timestamp of the last deploy) and within that is your codebase, the latest revision having been clone
d. The .env
file in that directory is then symlinked back out to the one in the root of your app path, so that it can be shared across deployments.
So a reference to /var/www/myapp/current
will always be the latest deployed version of your codebase. If you wanted to setup nginx to serve your web side, you would point it to /var/www/myapp/current/web/dist
as the root
and it will always be serving the latest code: a new deploy will change the current
symlink and nginx will start serving the new files instantaneously.
App Setup
Run the following to add the required config files to your codebase:
yarn rw setup deploy baremetal
This will add dependencies to your package.json
and create two files:
deploy.toml
contains server config for knowing which machines to connect to and which commands to runecosystem.config.js
for PM2 to know what service(s) to monitor
If you see an error from gyp
you may need to add some additional dependencies before yarn install
will be able to complete. See the README for node-type
for more info: https://github.com/nodejs/node-gyp#installation
Configuration
Before your first deploy you'll need to add some configuration.
ecosystem.config.js
By default, baremetal assumes you want to run the yarn rw serve
command, which provides both the web and api sides. The web side will be available on port 8910 unless you update your redwood.toml
file to make it available on another port. The default generated ecosystem.config.js
will contain this config only, within a service called "serve":
module.exports = {
apps: [
{
name: 'serve',
cwd: 'current',
script: 'node_modules/.bin/rw',
args: 'serve',
instances: 'max',
exec_mode: 'cluster',
wait_ready: true,
listen_timeout: 10000,
},
],
}
If you follow our recommended config below, you could update this to only serve the api side, because the web side will be handled by nginx. That could look like:
module.exports = {
apps: [
{
name: 'api',
cwd: 'current',
script: 'node_modules/.bin/rw',
args: 'serve api',
instances: 'max',
exec_mode: 'cluster',
wait_ready: true,
listen_timeout: 10000,
},
],
}
deploy.toml
This file contains your server configuration: which servers to connect to and which commands to run on them.
[[production.servers]]
host = "server.com"
username = "user"
agentForward = true
sides = ["api","web"]
packageManagerCommand = "yarn"
monitorCommand = "pm2"
path = "/var/www/app"
processNames = ["serve"]
repo = "git@github.com:myorg/myapp.git"
branch = "main"
keepReleases = 5
This lists a single server, in the production
environment, providing the hostname and connection details (username
and agentForward
), which sides
are hosted on this server (by default it's both web and api sides), the path
to the app code and then which PM2 service names should be (re)started on this server.
Config Options
host
- hostname to the serverport
- [optional] ssh port for server connection, defaults to 22username
- the user to login aspassword
- [optional] if you are using password authentication, include that hereprivateKey
- [optional] if you connect with a private key, include the content of the key here, as a buffer:privateKey: Buffer.from('...')
. Use this orprivateKeyPath
, not both.privateKeyPath
- [optional] if you connect with a private key, include the path to the key here:privateKeyPath: path.join('path','to','key.pem')
Use this orprivateKey
, not both.passphrase
- [optional] if your private key contains a passphrase, enter it hereagentForward
- [optional] if you have agent forwarding enabled, set this totrue
and your own credentials will be used for further SSH connections from the server (like when connecting to GitHub)sides
- An array of sides that will be built on this serverpackageManagerCommand
- The package manager bin to call, defaults toyarn
but could be updated to be prefixed with another command first, for example:doppler run -- yarn
monitorCommand
- The monitor bin to call, defaults topm2
but could be updated to be prefixed with another command first, for example:doppler run -- pm2
path
- The absolute path to the root of the application on the servermigrate
- [optional] Whether or not to run migration processes on this server, defaults totrue
processNames
- An array of service names fromecosystem.config.js
which will be (re)started on a successful deployrepo
- The path to the git repo to clonebranch
- [optional] The branch to deploy (defaults tomain
)keepReleases
- [optional] The number of previous releases to keep on the server, including the one currently being served (defaults to 5)
The easiest connection method is generally to include your own public key in the server's ~/.ssh/authorized_keys
mannually or by running ssh-copy-id user@server.com
from your local machine, enable agent forwarding, and then set agentForward = true
in deploy.toml
. This will allow you to use your own credentials when pulling code from GitHub (required for private repos). Otherwise you can create a deploy key and keep it on the server.
Using Environment Variables in deploy.toml
Similarly to redwood.toml
, deploy.toml
supports interpolation of environment variables. For more details on how to use the environment variable interpolation see Using Environment Variables in redwood.toml
Multiple Servers
If you start horizontally scaling your application you may find it necessary to have the web and api sides served from different servers. The configuration files can accommodate this:
[[production.servers]]
host = "api.server.com"
username = "user"
agentForward = true
sides = ["api"]
path = "/var/www/app"
processNames = ["api"]
[[production.servers]]
host = "web.server.com"
username = "user"
agentForward = true
sides = ["web"]
path = "/var/www/app"
migrate = false
processNames = ["web"]
module.exports = {
apps: [
{
name: 'api',
cwd: 'current',
script: 'node_modules/.bin/rw',
args: 'serve api',
instances: 'max',
exec_mode: 'cluster',
wait_ready: true,
listen_timeout: 10000,
},
{
name: 'web',
cwd: 'current',
script: 'node_modules/.bin/rw',
args: 'serve web',
instances: 'max',
exec_mode: 'cluster',
wait_ready: true,
listen_timeout: 10000,
},
],
}
Note the inclusion of migrate = false
so that migrations are not run again on the web server (they only need to run once and it makes sense to keep them with the api side).
You can add as many [[servers]]
blocks as you need.