Running a small Node.js app for Shopify on EC2 with PM2: what I learned
Lessons from deploying an internal bundles tool on a single EC2 instance: process management with PM2, renewing 24-hour Shopify tokens with the client credentials grant, and the security and inventory mistakes I made along the way.
I built a small internal tool to manage product bundles for a Shopify store. Nothing fancy: a Node.js backend, a vanilla HTML/CSS/JS frontend with no framework, and a single EC2 instance. It taught me more about running things in production than most tutorials did, so here are the parts I wish I had known earlier.
Why a single EC2 instance
The app has a handful of internal users and talks to the Shopify Admin API. I considered Lambda, but the app keeps a token in memory and serves a static frontend from the same process, so one small instance ([t3.micro]) was the simplest thing that worked. If traffic were spiky or public, I would decide differently.
PM2 instead of running node directly
My first deploy was
node server.js inside an SSH session. It died as soon as I closed the terminal. PM2 fixed three problems at once:- The process restarts if it crashes.
- It survives reboots with
pm2 startupandpm2 save. - Logs are in one place with
pm2 logs.
A minimal
ecosystem.config.js:1
2
3
4
5
6
7
8
module.exports = {
apps: [{
name: 'bundles-manager',
script: 'server.js',
env: { NODE_ENV: 'production', PORT: 3000 },
max_memory_restart: '300M'
}]
};max_memory_restart matters on a small instance. A slow memory leak will not take the whole box down, PM2 just restarts the process.Shopify auth: client credentials and a token that expires
Shopify moved apps toward the client credentials grant, where you exchange a client ID and secret for an access token that lasts 24 hours. My old static token stopped being an option, so the app had to renew its own token.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let token = null;
let expiresAt = 0;
async function getToken() {
if (token && Date.now() < expiresAt - 60_000) return token;
const res = await fetch(`https://${SHOP}.myshopify.com/admin/oauth/access_token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.SHOPIFY_CLIENT_ID,
client_secret: process.env.SHOPIFY_CLIENT_SECRET
})
});
if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
const data = await res.json();
token = data.access_token;
expiresAt = Date.now() + data.expires_in * 1000;
return token;
}Every API call goes through
getToken(). I renew lazily, one minute before expiry, instead of using a timer. A timer breaks silently if the process restarts at the wrong moment. A lazy check cannot get out of sync.Things that bit me
Secrets in the repo. I started with a
.env file on the instance. It works, but the better option is Systems Manager Parameter Store with an IAM role attached to the instance, so there is nothing to leak from disk.Security group too open. I had port 3000 open to the world during testing and forgot about it. Now only 443 is public and SSH is restricted to my IP. Session Manager would remove the need for SSH entirely.
Inventory bugs from assuming API order. Bundles depend on the inventory of their components. I assumed the API returned variants in a stable order and it did not. Keying everything by variant ID fixed a whole class of bugs.
Local development. For webhooks and testing against the real store I used ngrok with a static domain, so I did not have to update URLs in Shopify every time I restarted the tunnel.
What I would change
- Put the app behind an Application Load Balancer or at least Nginx with a proper TLS certificate.
- Move secrets to Parameter Store from day one.
- Add a CloudWatch alarm on instance status checks. Right now I find out it is down when someone tells me.
How do you handle short-lived API tokens in long-running Node processes? I went with lazy renewal, but I am curious whether people prefer a background refresh.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article