Deploy by framework

Custom Dockerfile

Use your own Dockerfile when a managed preset does not fit your HTTP server.

Nitroship detects and builds Node servers, JavaScript frameworks, and supported backend frameworks without a Dockerfile. A custom Dockerfile is the escape hatch when you need a different toolchain, unsupported adapter, native dependency, or startup command.

Without build.framework, the hosted builder picks a Dockerfile before Next.js, managed presets, and the generic static fallback. Setting build.framework explicitly chooses that managed preset instead, even if a Dockerfile exists. Do not set build.framework and build.dockerfile together: that is a configuration error.

Requirements

  • A Dockerfile that builds, installs dependencies, and starts your server.
  • A server inside the container that accepts HTTP requests. Which port it uses is explained below.
  • A compute region. There is no default, so set one in the console or in nitroship.json.

Which Dockerfile is used

The builder looks in the source root (the folder you deploy, or the root directory you chose for a Git connection) and uses the first file found in this order:

  1. The file named in build.dockerfile in nitroship.json
  2. Dockerfile.nitroship
  3. Dockerfile

To keep a separate Dockerfile for deployment, create Dockerfile.nitroship or point build.dockerfile at it.

nitroship.json
{"build": {"dockerfile": "docker/Dockerfile.production"}}
  • build.dockerfile must be a relative path from the source root. Absolute paths, backslashes, paths that leave the root (../), and . are not allowed.
  • If the named file is missing, or a symlink points outside the source folder, the build fails. It never falls back to another Dockerfile or another deployment route.

To return to automatic managed detection, remove the Dockerfile from the deployment root and remove build.dockerfile. Or leave the file in place and choose a preset explicitly:

nitroship.json
{"build": {"framework": "laravel"}, "compute": {"regions": ["kr-icn"]}}

For example, Rails' generated Kamal Dockerfile listens on port 80. Use build.framework: "rails" for the managed build, or adapt that Dockerfile to listen on 0.0.0.0:$PORT on an unprivileged port.

COPY paths are relative to the source root

Even when the Dockerfile lives in a subfolder, the build runs from the source root (docker build -f <path> .). Write COPY paths relative to the source root, not the Dockerfile's folder.

Deploy

Install the CLI

curl -fsSL https://nitroship.co/install.sh | bash
ntro login

Already installed? Skip ahead. Full options, PATH setup, and updates: Install the CLI.

Dockerfile (Node example)
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
nitroship.json
{"compute": {"regions": ["kr-icn"], "port": 3000}}
ntro apps create my-server
ntro deploy . --prod

When it finishes, check the server's response at the URL the CLI printed. If something is off, ntro builds logs shows the build log and the port that was chosen.

Configuration

Server settings such as region and port go in App Settings > Compute in the console or in compute in nitroship.json. When both are set, the file wins field by field. All fields are listed under CLI configuration > Compute settings.

Dockerfile builds do not use build.installCommand or build.command from nitroship.json (the build log shows a warning). Put every install and build step in the Dockerfile.

How the port is chosen

  1. The port from the console or nitroship.json, if set
  2. Otherwise, the lowest-numbered TCP port in the Dockerfile's EXPOSE
  3. Otherwise, 3000

If you expose several ports, set port explicitly to avoid surprises.

The runtime passes PORT to your process. Bind to 0.0.0.0, not localhost, and make the server read PORT rather than hard-coding a different port. EXPOSE is metadata; it does not make the process listen.

Publish static files from the image

By default the container handles every request. To serve static files such as JS, CSS, and images straight from static storage instead, tell the builder which folder in the final image holds them with build.outputDirectory.

nitroship.json
{
  "build": {
    "dockerfile": "docker/Dockerfile.production",
    "outputDirectory": "/app/app/static/assets"
  },
  "compute": {"regions": ["kr-icn"], "port": 3000}
}
  • The path is an absolute path inside the final image (but not / itself). Static sites use a path relative to the source root instead, so do not mix them up.
  • The builder reads only the final stage. Intermediate stages of a multi-stage build are not inspected, and symlinks are skipped.
  • The folder's contents, not the folder itself, become the site root. In the example, /app/app/static/assets/x.js in the image is served at /x.js, not /assets/x.js.
  • SSR and APIs still run in the container. Leave out outputDirectory for a container-only deployment.

If you set it, open a file address such as /x.js after deploying to confirm it is served from the site root.

Good to know

  • Unlike managed presets, your Dockerfile owns dependency installation, build steps, startup, and static extraction. Managed presets select their known client/public assets automatically; a custom image needs build.outputDirectory to publish them to the CDN.
  • Dockerfile deployments require the hosted builder. Drop --local.
  • Containers run with reduced privileges. Images that rely on ports below 1024, mount, Docker-in-Docker, setuid helpers, or specific device access may behave differently. Listen on a port of 1024 or above, such as 3000 or 8080.
  • Design for a read-only root filesystem and writable, ephemeral /tmp. Keep durable uploads in S3/object storage and data in an external database, not on the instance filesystem.
  • Deploying does not run database migrations or provision queue workers and schedulers. Run migrations from CI or your own machine, and use Nitroship Cron for scheduled HTTP jobs. Instances can scale to zero.

Managed Next.js · Node servers · Static generators · Environment variables · Managing deployments

On this page