nuxt-ship
v0.1.4
Published
Zero-config Docker deployment for Nuxt 4 — build locally, ship over SSH.
Maintainers
Readme
nuxt-ship 🚢
Zero-config Docker deployment for Nuxt 4 — the Nuxt port of goravel-ship.
Build your Nuxt app locally, bake .output into a Docker image, and ship it to any server via SSH — no Docker Hub, no CI/CD setup required.
Installation
npm install -D nuxt-shipOr use without installing:
npx nuxt-ship installScaffold the required files into your project:
npx nuxt-ship installThis creates:
Dockerfile— copies pre-built.output/into anode:20-alpineimage.dockerignoredocker-compose-prod.yml— production compose config.env.prod.example— production env templatedeploy.sh— remote deployment script
Edit .env.prod.example with your production values, then you're ready to ship.
Usage
npx nuxt-ship --user=<user> --ip=<server-ip> [flags]Flags
| Flag | Alias | Default | Required |
| ---------------- | ----- | ----------------------------- | -------- |
| --user | -u | — | ✅ |
| --ip | — | — | ✅ |
| --port | -P | 22 | ❌ |
| --path | -p | /opt/app | ❌ |
| --tag | -t | latest | ❌ |
| --image | -i | (auto from package.json) | ❌ |
| --container | -c | (same as image name) | ❌ |
| --root | — | false | ❌ |
| --skip-build | — | false | ❌ |
| --strip-sourcemaps | — | false | ❌ |
--skip-buildreuses an existing.output/directory instead of runningnpm run build.
--strip-sourcemapsrecursively deletes every*.mapfile under.output/beforedocker build— defense in depth in casesourcemap: falsewas forgotten innuxt.config.
Examples
Minimal:
npx nuxt-ship -u root --ip=192.168.1.100Custom port and path:
npx nuxt-ship -u deploy --ip=1.2.3.4 -P 2222 -p /home/deploy/myappWith version tag:
npx nuxt-ship -u root --ip=1.2.3.4 -t v1.2.0Skip build (reuse existing .output):
npx nuxt-ship -u root --ip=1.2.3.4 --skip-buildWith sudo on the remote:
npx nuxt-ship -u deploy --ip=1.2.3.4 --rootGit Bash (MINGW64) note: Prepend
MSYS_NO_PATHCONV=1to prevent path conversion:MSYS_NO_PATHCONV=1 npx nuxt-ship -u root --ip=1.2.3.4 -p /home/deploy/app
Source-code protection
No source files (.vue, .ts, pages/, components/, server/api/, nuxt.config.ts, package.json, node_modules, .git) ever leave your machine. The Docker image is built from the pre-built .output/ directory only — that's just the bundled Nitro server and static client assets.
For maximum source protection, set the following in nuxt.config.ts:
export default defineNuxtConfig({
sourcemap: {
server: false, // no .mjs.map files → no source reconstruction
client: false, // no source visible in browser DevTools
},
nitro: {
minify: true, // minify the server bundle (default false!)
},
});If you might forget, pass --strip-sourcemaps and nuxt-ship will recursively delete every *.map under .output/ before docker build. Note: this is insurance, not a substitute — the right thing is to disable sourcemaps at build time.
What ends up on the server
After a successful deploy, /opt/app/ (or your --path) contains only:
.env
docker-compose.ymlEverything else is cleaned up automatically:
| File | Status after success |
| -------------------- | ------------------------- |
| <hash>.tar.gz | deleted by deploy.sh |
| deploy.sh | self-deletes |
| .env | kept — used by compose |
| docker-compose.yml | kept — for up/down/logs |
The .output/ build artifacts live inside the Docker image only, never on the host filesystem. The image itself is in the local Docker daemon and can be inspected only by users in the docker group on the server.
How it works
- Rebuilds
.envfrom.env.prod.exampleon every deploy (always up to date) - Runs
npm run buildlocally → produces.output/ - Runs
docker build— Dockerfile justCOPY .output ./.output, nonpm installinside the image - Saves the image as
<sha256-hash>.tar.gz(unguessable filename) - Opens a single SSH tunnel — password asked only once
- Uploads via SCP:
tar.gz,.env,docker-compose-prod.yml(→docker-compose.ymlon server),deploy.sh - Runs
deploy.shon the server:- Loads the image via
docker load— no Docker Hub needed - Passes
IMAGE_NAMEenv so compose uses the correct local image - Recreates the container via
docker compose up -d - Prunes dangling images
- Cleans up
tar.gzand self-deletesdeploy.sh - On error: also removes
docker-compose.ymlfor a clean state
- Loads the image via
- Closes the SSH tunnel and removes the local
tar.gz
Configuration tips
Container name
By default --container equals --image (auto-detected from package.json name field). If your docker-compose.yml uses a different container_name, pass it explicitly:
npx nuxt-ship -u root --ip=1.2.3.4 --container my_app_containerPort
The default docker-compose-prod.yml maps host 3000 → container 3000. Change the port mapping there if needed (and the PORT in .env.prod.example).
External network
The compose file expects an external network called app_network. Create it once on the server:
docker network create app_networkThis makes it easy to share the network with other services (db, redis, reverse proxy, etc.).
Server requirements
- Docker + Docker Compose v2
- User must be in the
dockergroup (run once):sudo usermod -aG docker <username> newgrp docker - An external network named
app_network(see above), or edit the compose file.
Security
All user-controlled values (--user, --ip, --path, --tag, --image, --container) are validated against strict whitelist regexes at the CLI layer and wrapped with POSIX single-quote escaping at every shell interpolation site. Values that don't match the whitelist are rejected before any subprocess is started.
Whitelists:
| Flag | Regex |
| ------------- | -------------------------------------------------------------------------------------- |
| --user | ^[a-z_][a-z0-9_-]{0,31}$ |
| --ip | IPv4, IPv6, or RFC 1123 hostname |
| --path | ^/[A-Za-z0-9._\-/]{1,255}$ (no .., no //) |
| --tag | ^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$ |
| --image | ^[a-z0-9][a-z0-9._-]{0,253}(/...)*$ (Docker name conventions, max 6 path segments) |
| --container | ^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$ |
The auto-detected image name from package.json name is sanitized and revalidated before use, so a crafted upstream package can't smuggle metacharacters into Docker commands.
To report a vulnerability privately: use GitHub Security Advisories on the repository ("Report a vulnerability" tab) rather than opening a public issue.
License
MIT
