Skip to content

Build types

Every application has one of three build types. You pick it when you create the app, and you can change it later under Source and build → Edit settings on the app page. The change applies from the next deployment.

After you choose a repository and branch, Shiplon reads the app’s folder (the repository root, or the root directory you set) and pre-selects a type with the reason it chose it. The first rule that matches wins:

Shiplon finds Suggestion Also filled in
A Dockerfile Dockerfile
package.json with a start script Node.js server Build command, if there’s a build script
package.json with only a build script Static site (built first) Build command, and output folder build for Create React App
Anything else, for example index.html Static site (served as it is)

The suggestion never overrides a choice you’ve made. If you change the root directory, Shiplon checks that folder again.

The package manager follows your lockfile, so the suggested build command is pnpm run build, yarn run build or npm run build to match.

For plain HTML and for tools whose build produces static files.

  • Without a build command, the files in the output directory (default: the app’s folder itself) are served as they are. Nothing is installed.
  • With a build command, Shiplon installs dependencies, runs your build in Node.js 22, then serves the output directory. The usual folders are dist for Vite and Astro, build for Create React App, and public for Hugo.

Files are served by nginx. A request for a path that doesn’t exist gets a 404.

For apps that start their own server: Next.js with next start, Express, Fastify, NestJS and similar.

  1. Install: your lockfile decides the tool. pnpm-lock.yaml → pnpm install --frozen-lockfile, yarn.lock → yarn install --frozen-lockfile, package-lock.json → npm ci, only package.json → npm install. You can set your own Install command.
  2. Build: runs your Build command, if you set one, for example npm run build.
  3. Start: runs the Start command, default npm start.

Your server must listen on 0.0.0.0 (not localhost) and on the app’s Port (default 3000). Shiplon also sets the PORT environment variable to that port, so the most portable code is:

const port = process.env.PORT || 3000;
app.listen(port, "0.0.0.0");

Apps build and run on Node.js 22.

Shiplon builds the Dockerfile in your app’s folder as it is and runs the image. Use it for any language or setup the other two types don’t cover. Your image must:

  • listen on 0.0.0.0 on the app’s Port; the PORT environment variable is set to it;
  • run as a non-root user, given by number, for example USER 10001. Images that run as root, or set their user by name (USER node), are refused, because Shiplon can’t verify a name is non-root. The official Node.js image’s node user is USER 1000.

A minimal example for a Node.js app:

FROM node:22-alpine
WORKDIR /app
RUN chown 1000:1000 /app
USER 1000
COPY --chown=1000:1000 . .
RUN npm ci && npm run build
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]

Using environment variables while building

Section titled “Using environment variables while building”

Environment variables are not passed to docker build as build arguments, so they can’t leak into image layers. If a build step needs them, for example to download a private package, mount them as a build secret in that step:

RUN --mount=type=secret,id=appenv,required=false \
set -a; [ ! -f /run/secrets/appenv ] || . /run/secrets/appenv; set +a; \
npm ci && npm run build

At runtime, every variable is set in the container’s environment as usual.

  • A site with no server code (marketing site, docs, a React or Vue app talking to an external API): Static site. It’s the fastest to build and uses the least memory.
  • A JavaScript app with a server (API, Next.js with server rendering): Node.js server.
  • Anything else, or you need system packages, another runtime version or full control: Dockerfile.