How to (properly) deploy Node.js applications
Edit on GitHubRecently I’ve been involved in a new Typescript project all of my own where I would end up deploying it on production on a raw AWS machine, so no help from dev friendly PaaSs environments, as I usually prefer to work.
I always disagreed with the idea of uploading the code directly to the server or
Docker image both by hand or with git clone
and transpile it on the server the
same way I do in my development machine, both due to security issues (NEVER
have a compiler on a publicly accesible server, you are just only making it
easier to run an exploit and install a backdoor to your machine… or worse),
and because none of them are standard procedures. On the other hand, I’ve always
found the npm pack command
like it’s being missused, people relying just only on
npm publish (that uses
npm pack
internally), while it’s being in fact designed to prepare and pack
ready to use Node.js packages, and people usually ends up doing any other more
convoluted and insecure aproaches just because they are not willing to publish
their server code in the npm registry or
Github Packages Registry thinking it
will make them be available to anybody (maybe they don’t know it’s possible to
have private packages in both
npm registry and
Github Packages Registry?),
or they don’t want to deal with the hassle of access to private packages, or
they just doesn’t know how to do it.
So, since I was fully responsible of this project and had total freedom about
how to develop it, I decided to give npm pack
a try and see how well it could
fit for deployment of private servers in raw machines… and must to say, it has
gone REALLY well 😎
Pack the package
First of all, we need to automate transpilation of the Typescript code. Usually
build command is included in a build
script, so we need to run it when npm
run the
prepare
script. prepare
script is not only being run internally by npm pack
to
prepare the package before it’s being published, but also is being run when the
package itself is being used as a dependency directly from a git repository, so
by adding the prepare
script, it will help us on the development stage too:
{
"scripts": {
"build": "tsc",
"prepare": "npm run build"
}
}
Once package can be automatically build, we need to deploy it. For this, I’ve
added a custom deploy
script that both pack the package itself, and later
upload it to the AWS server. For the packaging I’m just using the npm pack
command to generate a tarfile with the content of the package. This tarfile can
be later uploaded to a static HTTP server and use its URL as dependency, or
install it directly from the filesystem, we can use it however we want. In my
case, it’s a server app instead of a library, so for the upload, I’m just only
using scp
to copy the generated tarfile to the server, no more. Before that, I
needed to set up my private key on the server, and whitelist my public IP to be
able to access to the server. Due to that, as a nice extra, I’ve also add a
ssh
script that opens me a SSH session on the server, just as a convenience
:-) :
{
"scripts": {
"deploy": "npm pack && npm run upload",
"ssh": "ssh ubuntu@ip-10-0-4-9.ap-south-1.compute.internal",
"upload": "scp *.tgz ubuntu@ip-10-0-4-9.ap-south-1.compute.internal:"
}
}
Executables
This config would be enough for libraries, but we are publishing an executable,
so we need to define it in the package.json
file so it gets automated for us.
This is done by setting the
bin field to
the executable file (Node.js standard is server.js
, and in fact it’s
package.json
default value for the start
script; for
Express apps convention is app.js
):
{
"bin": "server.js"
}
In my case, I was running a Fastify app by using the
fastify-cli tool, so I need to use a
customized start
script. So, to run it, we need to point the bin
field to a
shell script that will run it in our name:
#!/usr/bin/env bash
set -Eeuo pipefail
PREFIX=$(dirname `dirname -- "$( readlink -f -- "$0"; )";`)
npm start --prefix $PREFIX -- "$@"
This is a bit convoluted because when running npm start
, it will search by
default for a package.json
file both in the current folder or upper ones, and
if npm
doesn’t find it, it will give us a
npm ERR! enoent ENOENT: no such file or directory, open '/home/ubuntu/package.json'
error. So, to prevent it, we need to tell npm
explicitly where the correct
package.json
that needs to be run is located by using the
–prefix flag, that (since
I have that script inside a bin
folder inside my project) it’s in the parent
folder of where’s the script is located, so we get the resolved path of the
script and later the parent dir of its container dir.
Finally, to install and run the executable in the package, we can just execute npx giving the tarfile as the package name to be installed locally and executed…
npx ll-hls-streamer-0.0.0.tgz
…or we can install the package globally and have the executable available in our path as any other regular system wide installed application:
sudo npm install -g ll-hls-streamer-0.0.0.tgz
ll-hls-streamer
Comment on Twitter
You can leave a comment by replying this tweet.