How can I run a Node.js script in production?

Hey. I want to run a Node.js script in my running production app shell.

Something like this:

<node binary location> my_script.js

I tried connecting to my app with fly ssh console. There’s no node runtime accessible as far as I can see. I put Volta in my package.json file, I saw someone mention that on here, it didn’t help.

Do I need to make some changes to my fly.toml file or anything like that? Help appreciated, thanks.

I would need to see you Dockerfile to provide specific advice, but, no you don’t need Volta in your package.json.

Volta will install to the directory specified by VOLTA_HOME so setting VOLTA_HOME=/usr/local generally makes things “just work”. If you don’t have that environment variable set, volta will install binaries into /root/.volta/bin

I’m getting the same doubt! I have no idea how to run a .js script.

When i try to use fly ssh console -C './app/~path_to_script' I get permission error.

Try fly ssh console -C 'node ./app/~path_to_script'. If that doesn’t work, make sure you have nodejs installed in your dockerfile.

Thanks for your reply.

I don’t have a dockerfile in my Node.js app. Can I add one?

@ben-io I used flyctl template as a Dockerfile (using Volta) and i’m not being able to find node on my vm.
/bin/sh: 2: node: not found

Here’s the Dockerfile:

FROM debian:bullseye as builder

ARG NODE_VERSION=16.15.1
ARG YARN_VERSION=1.22.19

RUN apt-get update; apt install -y curl python-is-python3 pkg-config build-essential
RUN curl https://get.volta.sh | bash
ENV VOLTA_HOME /root/.volta
ENV PATH /root/.volta/bin:$PATH
RUN volta install node@${NODE_VERSION} yarn@${YARN_VERSION}

#######################################################################

RUN mkdir /app
WORKDIR /app

# Yarn will not install any package listed in "devDependencies" when NODE_ENV is set to "production"
# to install all modules: "yarn install --production=false"
# Ref: https://classic.yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-production-true-false

ENV NODE_ENV production

COPY . .

RUN yarn install && yarn run build
FROM debian:bullseye

LABEL fly_launch_runtime="nodejs"

COPY --from=builder /root/.volta /root/.volta
COPY --from=builder /app /app

WORKDIR /app
ENV NODE_ENV production
ENV PATH /root/.volta/bin:$PATH


CMD [ "yarn", "run", "start" ]

Do you think i should change my Dockerfile to this example?

Can’t figure out why node is not installed on VM.

The Dockerfile you have will ensure that you are running the specified version of Node. The example you linked will run the latest Node 18 version.

I’m not sure why volta is mis-behaving, but here’s a Dockerfile that matches yours where I replaced vola with node-build (another node version manager):

FROM debian:bullseye as builder

ARG NODE_VERSION=16.15.1
ARG YARN_VERSION=1.22.19

ENV PATH=/usr/local/node/bin:$PATH
RUN apt-get update; apt install -y curl python-is-python3 pkg-config build-essential && \
    curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
    /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
    npm install -g yarn@$YARN_VERSION && \
    rm -rf /tmp/node-build-master

#######################################################################

RUN mkdir /app
WORKDIR /app

# Yarn will not install any package listed in "devDependencies" when NODE_ENV is set to "production"
# to install all modules: "yarn install --production=false"
# Ref: https://classic.yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-production-true-false

ENV NODE_ENV production

COPY . .

RUN yarn install && yarn run build
FROM debian:bullseye

LABEL fly_launch_runtime="nodejs"

COPY --from=builder /usr/local/node /usr/local/node
COPY --from=builder /app /app

WORKDIR /app
ENV NODE_ENV production
ENV PATH /usr/local/node/bin:$PATH

CMD [ "yarn", "run", "start" ]

If that works for you, I’m inclined to update flyctl so that Dockerfiles generated for node users going forward use node-build.

Absolutely. Remove the [build] section from your fly.toml and if you are using yarn, try the Dockerfile I just posted. All you should have to change is the NODE_VERSION line.

If you are using npm or pnpm, other adjustments will need to be made. Post here if you need help.

Hey @rubys ! Thanks for the help with the Dockerfile.

I’m still not being able to find Node in my VM… I’m getting exec: "node": executable file not found in $PATH and when i try to type echo $PATH, I get this: /cnb/process:/cnb/lifecycle:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Looks like your PATH is being changed somehow. You should have /usr/local/node/bin in your PATH.

Now I’m curious. Where exactly are you typing this in? As Ben suggests, your PATH should have been set up by the Dockerfile.

When I run fly ssh console with the -C option, I find node:

% fly ssh console -C "node -v"
Connecting to fdaa:0:d445:a7b:d4:635e:f924:2... complete
v16.15.1

I deployed using that Dockerfile, the following package.json:

{
  "name": "welcome",
  "version": "1.0.0",
  "main": "index.js",
  "author": "Sam Ruby <rubys@intertwingly.net>",
  "license": "MIT",
  "scripts": {
    "build": "yarn install",
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

And the following app.js (straight from the express web site), modified only to support port 8080:

const express = require('express')
const app = express()
const port = 8080

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

Perfect, thanks!

That’s weird!

Here are my full specs!

I’m also using express (^4.18.1), prisma (^3.15.2) and bull (^4.10.1) and my app is working properly, except for the script on console part.

relevant scripts:

 "scripts": {
    "start": "node dist/server.js",
    "worker": "node dist/worker.js",
    ...
  },

server.ts:


  const app = express();

  ...routes and middlewares here...

((port = process.env.PORT || 8080) => {
  const server = app.listen(port, () =>
    console.log(
      `> Listening on port ${port} from environment: ${process.env.NODE_ENV}`,
    ),
  );

  process.on('SIGTERM', () => {
    console.debug('SIGTERM signal received: closing HTTP server');
    server.close(() => {
      console.debug('HTTP server closed');
      process.exit(0);
    });
  });
})();

my fly.toml file:

# fly.toml file generated for hippro-backend on 2023-02-08T17:53:54-03:00

app = "hippro-backend"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[build]
  builder = "heroku/buildpacks:20"

[env]
  PORT = "8080"
  NODE_ENV = "production"

[deploy]
  release_command = "npx prisma migrate deploy"

[experimental]
  auto_rollback = true

[processes]
  web = "npm run start"
  worker = "npm run worker"


[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["web"]
  protocol = "tcp"
  script_checks = []
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"   

My Dockerfile now (same as you passed):

FROM debian:bullseye as builder

ARG NODE_VERSION=16.15.1
ARG YARN_VERSION=1.22.19

ENV PATH=/usr/local/node/bin:$PATH
RUN apt-get update; apt install -y curl python-is-python3 pkg-config build-essential && \
    curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
    /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
    npm install -g yarn@$YARN_VERSION && \
    rm -rf /tmp/node-build-master

#######################################################################

RUN mkdir /app
WORKDIR /app

# Yarn will not install any package listed in "devDependencies" when NODE_ENV is set to "production"
# to install all modules: "yarn install --production=false"
# Ref: https://classic.yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-production-true-false

ENV NODE_ENV production

COPY . .

RUN yarn install && yarn run build
FROM debian:bullseye

LABEL fly_launch_runtime="nodejs"

COPY --from=builder /usr/local/node /usr/local/node
COPY --from=builder /app /app

WORKDIR /app
ENV NODE_ENV production
ENV PATH /usr/local/node/bin:$PATH

CMD [ "yarn", "run", "start" ]

and my dockerignore:

fly.toml
Dockerfile
.dockerignore
node_modules
.github
.git
.env

I’m at the project directory. When I use fly status, this is what I get:

App
  Name     = hippro-backend          
  Owner    = personal                
  Version  = 129                     
  Status   = running                 
  Hostname = hippro-backend.fly.dev  
  Platform = nomad                   

Deployment Status
  ID          = b2156634-a51c-7b2b-c545-46eb497fab43         
  Version     = v129                                         
  Status      = successful                                   
  Description = Deployment completed successfully            
  Instances   = 2 desired, 2 placed, 2 healthy, 0 unhealthy  

Instances
ID      	PROCESS	VERSION	REGION	DESIRED	STATUS 	HEALTH CHECKS     	RESTARTS	CREATED    
bf636055	worker 	129    	gru   	run    	running	                  	0       	21m15s ago	
d0f0b7ce	web    	129    	gru   	run    	running	1 total, 1 passing	0       	21m15s ago	

And when I try to use fly ssh console -C 'node -v', I’m getting:

fly ssh console -C 'node -v'
Connecting to fdaa:1:3e61:a7b:fa:fac0:315f:2... complete
exec: "node": executable file not found in $PATH

When I try fly ssh console then echo $PATH:

fly ssh console
Connecting to fdaa:1:3e61:a7b:101:fcc6:6ef6:2... complete
# echo $PATH
/cnb/process:/cnb/lifecycle:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin```

Try commenting out the [build] section in your fly.toml file like @rubys said, to use the Dockerfile build config

Now build failed. I believe I need to configure something differently for Typescript, right?

#9 [builder 6/6] RUN yarn install && yarn run build
#9 0.516 yarn install v1.22.19
#9 0.577 [1/4] Resolving packages...
#9 0.740 [2/4] Fetching packages...
#9 6.721 [3/4] Linking dependencies...
#9 7.569 [4/4] Building fresh packages...
#9 11.24 $ tsc
#9 11.25 /bin/sh: 1: tsc: not found
#9 11.26 error Command failed with exit code 127.
#9 11.26 info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
#9 ERROR: executor failed running [/bin/sh -c yarn install && yarn run build]: exit code: 127```

Yes, make sure you have declared a dependency on typescript in your package.json. If it’s not there, run npm install --save-dev typescript

You definitely don’t need a Docker file to run Node on Fly. If you just deploy a Node project Fly can audodetect it and use the appropriate buildpack.

Check the docs:

This is the Dockerfile I use though which generates fairly small images:

FROM node:18-alpine3.15

USER root

WORKDIR /usr/src/app

COPY package.json .
COPY package-lock.json .
RUN npm i --production

COPY . .

ENV NODE_ENV production
ENV PORT 8080
ENV HOST 0.0.0.0

CMD ["npm", "run", "start"]

Just FYI: what fly launch will do is look at your package.json. If it contains a script named start, it will generate a Dockerfile. Otherwise it will make use of buildpacks.

If there are other common usage patterns that we can look for and support via dockerfiles, let us know and we will update the scanner.

1 Like

Hey guys, I deleted the [build] section from fly.toml and used rubys Docfile with a small change to make typescript work (from RUN yarn install && yarn run build to RUN yarn install --production=false && yarn run build) and it worked!

I’m still crappy at dockers, so thank you all for your help! @rubys @frode @ben-io

2 Likes

And thank you for the feedback. I’ve already started updating flyctl launch so that in the near future typescript programs such as yours will “just work”. Keep the input coming!

1 Like