Deploying Node.js Apps with Ansible
While working on a side project, I discovered that serving the application as static assets from a sub-directory on my web server wasn’t going to work. Thanks to CORS, and the fact my application is built with SvelteKit, the only other option for me was to run a Node.js instance. I wanted to get it set up quickly so that I could prioritize application development, but I also wanted to take this opportunity to clean up my homelab automation repository. What if I need to run another Node.js instance later on? It seemed obvious the right thing to do was to spend a week and half automating something I could have accomplished in an afternoon.
Important note: I only spent a week and half on this because I was missing a semi-colon in a configuration file completely unrelated to all of this. 🙃
Background
I’ve previously written about how I have automated much of my homelab management with Ansible. If you’re unfamiliar with Ansible, and spend a significant amount of time managing multiple computers/servers, do yourself a favor and quit reading this blog post and go start learning Ansible. A great series of video tutorials can be found over at Learn Linux TV.
Unfortunately for me, I learned about Ansible after having already set up much of my homelab. Fortunately for you, I’ve been maintaining a repository since I discovered it. Now, as I need new functionality, I build it in to the repo instead of managing my homelab manually. Should my homelab (or home) ever go up in smoke, it will be significantly easier to set it all up again.
Automation
Enough of the snooze-fest, let’s automate!
Firstly, go open my homelab automation repository at https://github.com/Dilden/Ansible-Proxmox-Automation. Peruse that if you like then go ahead and give it a star while you’re at it.
Next, notice how I’ve incorporated roles. Roles make it simple to partition and reuse functionality. For instance, in the roles
directory, I’ve created another directory; nodejs. This lets Ansible know to treat the directory name as a role which can now be used within playbooks. When I want to install Node.js on any new server, I can specify the target within my playbook, and simply call the nodejs role. This makes playbooks easier to read and write. It also simplifies the organization of logic.
Node.js
I’m not going to dig into how to create servers. If you’re interested in that, check out my playbook books/create-containers.yml
and my proxmox
role. For now, let’s focus on installing Node.js on an existing server. In this example, the server is referred to as nodejs
in the inventory and is running Ubuntu 20.04 (Focal Fossa).
Playbook
---
- hosts: nodejs
roles:
- nodejs
This is it for the playbook. Seriously. This playbook will target nodejs
(from the inventory file) and run the tasks from the nodejs
role.
Role
---
- name: Install GPG
tags: nodejs, install, setup
apt:
name: gnupg
update_cache: yes
state: present
- name: Install the gpg key for nodejs LTS
apt_key:
url: "https://deb.nodesource.com/gpgkey/nodesource.gpg.key"
state: present
- name: Install the nodejs LTS repos
apt_repository:
repo: "deb https://deb.nodesource.com/node_{{ NODEJS_VERSION }}.x {{ ansible_distribution_release }} main"
state: present
update_cache: yes
- name: Install NodeJS
tags: nodesjs, install
apt:
name: nodejs
state: latest
One of the wonderful things about Ansible is that it remains easy to read. Just in case you struggled with it, here’s what the 4 tasks do:
- Install “gpg” package
- Add the repository key for NodeSource
- Install the specified version of Node for the specified distribution. These variables are located at
defaults/main.yml
within the role. - Install the latest version of Node.js
defaults/main.yml
---
NODEJS_VERSION: "18"
ansible_distribution_release: "focal"
Now, installing Node.js on the server is as simple as running ansible-playbook INSERT_YOUR_PLAYBOOK_NAME_HERE.yml
Deploy the App 🚀
Getting Node.js running on your server is one thing but how do you get your code to that server? It’s a little more complicated.
Playbook
---
- hosts: nodejs
roles:
- app
Not this part. This part is simple.
Role
- name: Build app locally
tags: app, build, deploy
shell: npm run build
args:
chdir: ~/Dev/projects/app/
delegate_to: 127.0.0.1
- name: Copy build to server
tags: app, build, deploy
copy:
src: ~/Dev/projects/app/build/
dest: /var/www/html/
owner: www-data
group: www-data
mode: 0644
- name: Copy package-lock.json to server
tags: app, build, deploy
copy:
src: ~/Dev/projects/app/package-lock.json
dest: /var/www/html/package-lock.json
owner: www-data
group: www-data
mode: 0644
- name: Copy package.json to server
tags: app, build, deploy
copy:
src: ~/Dev/projects/app/package.json
dest: /var/www/html/package.json
owner: www-data
group: www-data
mode: 0644
- name: Create service file
tags: app, build, deploy
template:
src: files/service
dest: /etc/systemd/system/nodejs.service
register: service_conf
- name: Reload systemd daemon
tags: app, build, deploy, systemd
systemd:
daemon_reload: yes
when: service_conf.changed
- name: Install dependencies from lockfile
tags: app, build, deploy
shell: npm ci
args:
chdir: /var/www/html/
- name: Start NodeJS service
tags: app, build, deploy
service:
name: nodejs
state: started
enabled: yes
Ok so it’s not actually that complicated. But still, let’s break down the 8 tasks:
- Firstly, I need a build of the application. This is an npm script specific to SvelteKit. The shell command
npm run build
is being done in the directory~/Dev/projects/app/
on my local machine via “delegate_to.” - The entire build directory gets copied to the server.
- package-lock.json and package.json are copied to the server.
- A systemd service worker file is created using a template specified in
files/service
within this role. I also registerservice_conf
so I can observe if the file changes.
See my post on starting systemd services within vagrant machines for more information. - If
service_conf
changed, reload the systemd deamon. - Use
npm ci
to install dependencies from the lockfile. - Ensure the nodejs service is running on the server.
files/service
[Unit]
Description=nodejs server
[Service]
ExecStart=/usr/bin/node /var/www/html/index.js
Restart=on-failure
# Output to syslog
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=nodejs-example
Environment=NODE_ENV=production PORT=80
[Install]
WantedBy=multi-user.target
This is the service worker template. It calls node on our application’s index.js
. Notice how I’ve specified in the environment to run Node.js on port 80.
End
Now, to deploy the app is as simple as running our Node.js install followed by the app deployment. If you combine them together into one playbook…
---
- hosts: nodejs
roles:
- nodejs
- app
ansible-playbook books/deploy-app.yml
(or whatever you named your playbook) and you’re all set!
Archived Comments
These comments have been imported from a previous commenting system, for the sake of posterity. If you left a comment using the old system and would like to have it removed, please get in touch with me using this form.