portus
v0.2.2
Published
A Docker scaffolding for Node.js projects — scans your project and generates a production-ready Dockerfile, .dockerignore, and docker-compose.yaml.
Maintainers
Readme
Portus
A Docker scaffolding for Node.js projects. Portus scans your project, detects your package manager, runtime, and framework, and generates a production-ready Dockerfile, .dockerignore, and docker-compose.yaml tailored to what it finds — plus a GitHub Actions workflow to build and publish the images it generates.
Requirements
Node.js 18 or later to run Portus itself. Docker is required separately to build and run the generated Dockerfile and docker-compose.yaml.
Installation
npm install -g portuspnpm add -g portusyarn global add portusUsage
Generate Docker configuration for the current project:
portus initThis scans the repository, prints what was detected, and writes a Dockerfile, .dockerignore, and a Docker Compose file. If any of these files already exist, you will be prompted before they are overwritten. Portus recognizes any of the four filenames Docker Compose itself auto-discovers (compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml), and writes back to whichever one already exists rather than creating a duplicate.
In a project with more than one package that has a detected framework — a workspace monorepo, or independent sibling apps with no workspace tooling at all — init prompts interactively for which package to target, with an option to select all of them. This only happens in an interactive terminal; non-interactive runs (CI, piped output, or --yes) default to the first detected package instead.
Generate Docker configuration for every detected package, skipping the interactive picker:
portus init --allSkip overwrite prompts and the interactive package picker (useful in CI or when re-running repeatedly):
portus init --yesPreview what Portus detects without writing any files:
portus scanGenerate a portus.config.json scaffold pre-filled with the currently detected package manager, Node version, and per-package ports:
portus configEdit the generated file to override any values Portus got wrong, then run portus init to generate Docker configuration using those overrides.
Generate a GitHub Actions workflow that builds and pushes a Docker image for every detected package:
portus ciThis assumes Dockerfiles already exist — run portus init first if they don't. Images are pushed to the GitHub Container Registry (ghcr.io) using the workflow's built-in GITHUB_TOKEN, so there's no registry account or secret to set up. Pull requests build the image to validate it without pushing anything; only a push to the tracked branch actually publishes. If your default branch isn't main:
portus ci --branch developAnalyze an existing Docker setup for common issues:
portus doctordoctor exits with a non-zero status code when errors are found, so it can be used as a CI check. When more than one package is involved, it checks every one of their Dockerfiles and .dockerignore files, not just one, along with the compose file. When it finds issues it can safely fix on its own, it prompts interactively for which ones to apply.
Commands
| Command | Description |
| --- | --- |
| portus init | Scan the project and generate Dockerfile, .dockerignore, and a Docker Compose file |
| portus init -a, --all | Generate Docker configuration for every detected package |
| portus init -y, --yes | Same as init, without overwrite confirmation prompts or the interactive package picker |
| portus scan | Report detected package manager, runtime, project structure, framework, and port without writing files |
| portus config | Generate a portus.config.json scaffold from the detected setup |
| portus config -y, --yes | Same as config, without an overwrite confirmation prompt |
| portus ci | Generate a GitHub Actions workflow that builds and pushes a Docker image for every detected package |
| portus ci -b, --branch <branch> | Set the branch the workflow triggers on (defaults to main) |
| portus ci -y, --yes | Same as ci, without an overwrite confirmation prompt |
| portus doctor | Analyze existing Docker configuration and report issues, across every detected package |
| portus --version | Check the installed version |
What gets detected
- Package manager — resolved from lockfiles (pnpm, yarn, bun, npm), defaulting to npm if none are found.
- Runtime and version — Node or Bun, with the Node version resolved from
engines.node,.nvmrc, or.node-version, in that order. - Project structure — workspace packages resolved from
pnpm-workspace.yaml,package.json#workspaces, orlerna.json, with Turborepo and Nx recognized as build orchestrators. If none of these are present but two or more immediate subdirectories each have their own independentpackage.json(for example, separateclient/andserver/folders with no shared root lockfile), Portus treats that as a multi-app project too — see Multi-app and monorepo support below. - Framework — classified per package from an ordered rule set, so meta-frameworks are correctly distinguished from the libraries underneath them.
Any of these can be overridden with a portus.config.json file — see Configuration below.
Configuration
Portus works without any configuration, but detection can be overridden with a portus.config.json file at the repository root:
{
"packageManager": "pnpm",
"node": "20",
"healthcheck": true,
"packages": {
"api": { "port": 3000 },
"web": { "framework": "nextjs" },
"worker": { "healthcheck": false }
}
}packageManagerandnodeoverride detection globally.healthcheckat the root sets the default for every package; set it tofalseto stop Portus from generating aHEALTHCHECKinstruction and composehealthcheckblock anywhere, useful for local-only images or setups relying on an external health-checking system.packages.<name>overrides apply to a specific package by name, whether that's the root package in a single-app project or a package in a monorepo or multi-app project:portoverrides the port used in the generated Dockerfile and Docker Compose file.frameworkoverrides which framework Portus treats the package as, for cases where detection is missing or wrong.healthcheckoverrides the root-level default for that one package specifically (for example, disabling it only for a background worker with no HTTP endpoint to probe, while leaving it on for everything else).
Run portus config to generate a starting file pre-filled with the currently detected values, or write one by hand. Invalid entries are ignored with a warning rather than causing a failure; init, scan, ci, and doctor all apply a valid config consistently — including doctor, which won't warn about a HEALTHCHECK you've deliberately disabled.
Framework support
Portus generates a tailored, production-optimized Dockerfile depending on the framework it detects:
Next.js
With output: "standalone" set in next.config.*, Portus builds a minimal runtime image from .next/standalone, with no node_modules in the final image at all. Without standalone output configured, it falls back to a production-dependency-only build.
Nuxt
Built from Nuxt's .output directory, which is self-contained by default.
SvelteKit
Branched by adapter, detected from svelte.config.*. adapter-node produces a Node-runnable image with production-only dependencies. adapter-static produces a static image served by nginx. Other adapters (Vercel, Netlify, Cloudflare, auto) fall back to a generic build.
Remix
Adapter detected from installed packages. @remix-run/serve (the Node default) gets a production build with a dedicated production-dependency stage. Vercel, Cloudflare, and Deno adapters target non-Node runtimes; Portus still generates a Node-based build for these but prints a warning, since it will likely not run as-is on those platforms.
Express, Fastify, Koa, Hono, NestJS If a build script is present, a dedicated build stage compiles the project and only production dependencies are installed for the runtime image. Without a build step, only a production install runs, with no unnecessary dev-dependency stage.
React, Vue, Svelte, Create React App, Angular, Astro
Built and served as static assets from nginx on port 80. For Angular, the output directory is read directly from angular.json rather than assumed, since Angular's build output path varies by project name and CLI version.
Every generated Node-based runtime image runs as a non-root user by default and receives an explicit PORT environment variable matching the framework's detected (or configured) port, rather than relying on the application defaulting to the right value on its own. Static, nginx-served images are left running as the image's default user, since nginx's master process needs root to bind port 80 by default.
Multi-app and monorepo support
Portus generates Docker configuration for more than one package in two different situations, and treats them differently on purpose:
Workspace monorepos — a pnpm-workspace.yaml, package.json#workspaces, or lerna.json is present, meaning all packages share a single lockfile at the repository root. Dockerfiles install dependencies from the repository root and build or run each package using the detected package manager's workspace filtering (for example, pnpm --filter <package> run build), since there's no per-package lockfile to install from independently. A single .dockerignore is written at the repository root, and each service's Docker Compose build context is the repository root with the Dockerfile path pointed at the specific package. Because dependencies are installed from a shared root context, monorepo images currently ship the full node_modules tree rather than a production-only pruned one — a known trade-off compared to the single-package build.
Multi-app projects with no shared workspace — two or more sibling directories each have their own independent package.json and lockfile, with no workspace manifest tying them together (for example, separate client/ and server/ folders maintained independently). Portus detects this automatically and generates a Dockerfile for each app exactly as it would for a standalone single-app project — using the same framework-specific templates, with each app's own directory as its own build context — rather than assuming a shared root lockfile that doesn't exist. Each app also gets its own .dockerignore. A single Docker Compose file is still generated at the repository root, with each service's build context pointed at its own app directory.
In both cases, only packages with a detected framework are containerized — shared libraries, internal utilities, or other packages Portus doesn't recognize as a web framework (for example, an Electron desktop wrapper) are correctly left out. With portus init --all (or by selecting "All of them" in the interactive picker), every detected package gets a Dockerfile; if two services would default to the same port, later ones are automatically remapped to the next free host port. portus ci follows the same distinction, so the generated workflow's build context for each service in the matrix always matches what its docker-compose.yaml service actually uses.
portus doctor checks every framework-detected package automatically, not just one, since analysis is read-only and there's no reason to hide part of the picture by default.
Interactive mode
Portus prompts interactively in two situations, both only when running in an interactive terminal:
portus init, when more than one package has a detected framework, to choose which package (or all of them) to generate configuration for.portus doctor, when it finds issues it can safely fix on its own (currently: missing recommended.dockerignoreentries), to choose which fixes to apply.
Both are skipped automatically in non-interactive contexts such as CI, falling back to sensible defaults instead of prompting.
Docker configuration output
Dockerfile
Multi-stage where applicable, with a parallel production-only dependency install stage swapped into the final image so development dependencies never ship to production. Base images are pinned to a specific version tag. Node-based runner stages include a HEALTHCHECK that probes the app over HTTP using Node's built-in fetch, no extra packages required; nginx-based runner stages use wget, which the base image already ships with. HEALTHCHECK can be disabled through portus.config.json.
.dockerignore
A solid baseline (node_modules, .git, environment files, editor directories, logs) plus the detected framework's build output directory, so stale local build artifacts and generated files are never sent to the Docker build context.
docker-compose.yaml (or whichever compose filename already exists in the project)
A service definition with the correct build context, the actual port the container listens on (port 80 for nginx-served static frontends, the framework's port otherwise), a restart policy, a matching healthcheck block, and — for Node-based services only — PORT/NODE_ENV environment variables and an env_file reference if a .env file is present. Static, nginx-served services don't receive Node-specific environment variables, since they don't run Node.
.github/workflows/docker.yml
A matrix build with one entry per detected package, using the same build context and Dockerfile path each package's docker-compose.yaml service uses. Builds on pull requests to validate the Dockerfile without publishing anything; pushes to the tracked branch build and publish each image to ghcr.io, tagged with both latest and the commit SHA, using layer caching scoped per service so multiple packages in a matrix don't collide. Authentication uses the workflow's built-in GITHUB_TOKEN, so no registry secret needs to be configured. Publishing to a different registry (Docker Hub, ECR, and so on) requires manually editing the login and tag steps — Portus doesn't attempt to guess registry credentials it has no way of knowing.
Doctor checks
portus doctor analyzes the Dockerfile and .dockerignore for every framework-detected package, plus the Docker Compose file, for:
- Missing files
- Single-stage builds
- Unpinned or
:latestbase image tags - Missing non-root
USERinstruction - Dependency installs running after
COPY . ., which defeats layer caching - Possible hardcoded secrets in
ENVorARGinstructions - Missing
EXPOSEinstruction - Missing
HEALTHCHECKinstruction (skipped for packages wherehealthcheckis disabled viaportus.config.json) - Missing recommended
.dockerignoreentries - Invalid or incomplete Docker Compose structure, including missing restart policies or port mappings
Missing .dockerignore entries can be applied automatically through the interactive prompt. Dockerfile and Docker Compose issues are reported only, since fixing them safely would require guessing at values Portus doesn't actually know (which version to pin an image to, where to insert a user in a possibly hand-edited file).
License
MIT
