A lightweight Node.js runtime image built on Alpine Linux with S6 overlay, npm, and developer-friendly utilities.
Built on tundrasoft/alpineโ : Alpine Linux, s6-overlayโ process supervision, and an unprivileged tundra user (UID/GID 1000) that your application runs as. Node's official Linux binaries are linked against glibc, which Alpine does not ship, so the image layers in the glibc loader and runtime libraries from gcr.io/distroless/cc-debian12 (the same approach as tundrasoft/denoโ ) rather than relying on a separately compiled musl build.
This image is available on multiple registries:
tundrasoft/nodeghcr.io/tundrasoft/node# Pull from Docker Hub (recommended)
docker pull tundrasoft/node:latest
# Pull from GitHub Container Registry
docker pull ghcr.io/tundrasoft/node:latest
# Run a local Node.js application (mount your code into /app)
docker run -d \
-p 8080:8080 \
-e FILE=/app/server.js \
-v $(pwd):/app \
--name node-app \
tundrasoft/node:latest
# Run a package.json script with a custom timezone
docker run -d \
-e TZ=Asia/Kolkata \
-e SCRIPT=start \
-v $(pwd):/app \
--name my-node-app \
tundrasoft/node:latest
With no FILE or SCRIPT set, the container runs a small built-in demo server on port 8080 so you can confirm the image works.
| Version | Tags |
|---|---|
| latestโ | Latest stable release |
| edgeโ | Edge/development version |
| 24.19โ | 24.19.0โ |
| 24.18โ | 24.18.1โ , 24.18.0โ |
| 24.17โ | 24.17.0โ |
| 24.16โ | 24.16.0โ |
Images are built weekly for the five newest Node.js LTS releases (from nodejs.org/dist/index.json) on each supported Alpine branch (the three newest stable branches plus edge):
| Tag | Meaning |
|---|---|
latest | Newest LTS release on the latest stable Alpine branch |
<major>.<minor>.<patch> (e.g. 24.19.0) | Each built LTS release, on the latest stable Alpine branch |
<major>.<minor>, <major> (e.g. 24.19, 24) | Newest LTS release only, on the latest stable Alpine branch |
alpine-<branch>-<major>.<minor>.<patch> (e.g. alpine-3.22-24.19.0) | Each built LTS release on a specific Alpine branch (including edge) |
npm and npxtundra user (UID/GID: 1000)Use as a base image in your Dockerfile. The application runs as the unprivileged tundra user (UID/GID 1000), so copy files with that ownership; otherwise the container recursively chowns /app at every boot, which is slow for a large node_modules.
Single-file app:
FROM tundrasoft/node:24
COPY --chown=tundra:tundra server.js /app/
ENV FILE=/app/server.js
package.json app:
FROM tundrasoft/node:24
COPY --chown=tundra:tundra package.json package-lock.json /app/
RUN npm ci && chown -R tundra:tundra /app
COPY --chown=tundra:tundra . /app
ENV SCRIPT=start
Pin the base image as tightly as you need:
FROM ghcr.io/tundrasoft/node:24 # GitHub Container Registry mirror
FROM tundrasoft/node:24.19.0 # exact Node.js release
FROM tundrasoft/node:alpine-3.22-24.19.0 # exact Node.js release on a specific Alpine branch
The image decides what to run based on two environment variables:
SCRIPT โ run a script defined in package.json (npm run <SCRIPT>)FILE โ run a single file directly (node <FILE>)SCRIPT takes precedence over FILE. If neither is set, a minimal demo server is started on port 8080.
Run a file:
docker run -p 8080:8080 \
-e FILE=/app/server.js \
-v $(pwd):/app \
tundrasoft/node:latest
Run a package.json script:
docker run -v $(pwd):/app \
-e SCRIPT=start \
tundrasoft/node:latest
Run with environment variables:
docker run -d \
-e FILE=/app/server.js \
-e NODE_OPTIONS=--max-old-space-size=512 \
-e PUID=1001 \
-e PGID=1001 \
-e TZ=America/New_York \
-v $(pwd):/app \
tundrasoft/node:latest
| Variable | Description | Default |
|---|---|---|
SCRIPT | Run a script from package.json via npm run (takes precedence over FILE) | N/A |
FILE | The file to run directly with node | N/A |
NODE_ENV | Node.js environment hint; also makes npm install/npm ci skip devDependencies | production |
NODE_OPTIONS | Extra Node.js CLI flags applied to every node process (e.g. --max-old-space-size=512) | empty |
NPM_CONFIG_CACHE | npm cache directory | /npm-cache |
NPM_CONFIG_UPDATE_NOTIFIER | npm "new version available" notice | false |
PUID | User ID for the tundra user | 1000 |
PGID | Group ID for the tundra group | 1000 |
TZ | Timezone (e.g., Asia/Kolkata, America/New_York) | UTC |
DEBUG | Enable debug mode with verbose output (1 to enable) | N/A |
WATCH | Restart on file changes via node --watch (1 to enable; FILE and demo modes only) | N/A |
S6_CMD_WAIT_FOR_SERVICES_MAXTIME | Max time (ms) to wait for services to start (0 = infinite) | 0 |
S6_KILL_FINISH_MAXTIME | Grace period (ms) for graceful shutdown | 5000 |
Any other NPM_CONFIG_* variable is honoured by npm as usual (for example NPM_CONFIG_LOGLEVEL=warn).
๐ Reference: Node.js CLI optionsโ ยท npm configโ
| Path | Description |
|---|---|
/app | Application root directory (recommended to mount as volume) |
/crons | Directory for cron job files (automatically loaded) |
/npm-cache | npm cache directory (NPM_CONFIG_CACHE, for persisting downloads) |
Install dependencies while building the image to eliminate cold-start downloads:
Install from a lockfile (best layer caching):
FROM tundrasoft/node:24
# Copy manifests first so this layer is cached until they change
COPY --chown=tundra:tundra package.json package-lock.json /app/
RUN npm ci && chown -R tundra:tundra /app
COPY --chown=tundra:tundra . /app
ENV FILE=/app/index.js
NODE_ENV=production is set in the image, so npm ci installs only production dependencies. Pass --include=dev when a build step needs devDependencies.
Build with devDependencies, ship without them:
FROM tundrasoft/node:24 AS build
COPY package.json package-lock.json /app/
RUN npm ci --include=dev
COPY . /app
RUN npm run build
FROM tundrasoft/node:24
COPY --chown=tundra:tundra package.json package-lock.json /app/
RUN npm ci && chown -R tundra:tundra /app
COPY --from=build --chown=tundra:tundra /app/dist /app/dist
ENV FILE=/app/dist/index.js
npm ci pins exact versions from the lockfileDevelopment mode combines DEBUG, WATCH, and a volume mount for a fast feedback loop:
Development setup:
docker run -it \
-e DEBUG=1 \
-e WATCH=1 \
-e NODE_ENV=development \
-e FILE=/app/main.js \
-v $(pwd):/app \
-p 8080:8080 \
tundrasoft/node:latest
What this enables:
DEBUG=1: Verbose startup output with argument inspectionWATCH=1: File watching with auto-restart on changes (node --watch)NODE_ENV=development: Overrides the production default for frameworks that key off itWith a package.json script:
npm run does not forward --watch to node, so WATCH=1 is ignored in SCRIPT mode. Put the flag in the script instead:
{
"scripts": {
"dev": "node --watch src/main.js"
}
}
docker run -it \
-e DEBUG=1 \
-e SCRIPT=dev \
-v $(pwd):/app \
-p 8080:8080 \
tundrasoft/node:latest
This image uses S6 Overlayโ for advanced process supervision and service management. S6 is a lightweight init system that provides reliable service supervision, dependency management, and graceful shutdown handling.
The Node service runs your application via the S6 system, ensuring:
Control S6 service supervision timeouts:
# Custom startup timeout (30 seconds max wait)
docker run -d \
-e S6_CMD_WAIT_FOR_SERVICES_MAXTIME=30000 \
-e FILE=/app/server.js \
tundrasoft/node:latest
# Extended graceful shutdown (10 seconds)
docker run -d \
-e S6_KILL_FINISH_MAXTIME=10000 \
-e FILE=/app/server.js \
tundrasoft/node:latest
# Infinite startup wait (for slow-starting apps)
docker run -d \
-e S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
-e FILE=/app/server.js \
tundrasoft/node:latest
S6 provides dependency management through trigger points:
| Trigger | Description |
|---|---|
os-ready | Container booted, basic setup complete |
config-start | Start configuration changes |
config-ready | Configuration complete |
service-start | Application services begin |
service-ready | All services initialized |
You can extend the Node image with additional services:
FROM tundrasoft/node:latest
# Install additional tools
RUN apk add --no-cache redis
# Create Redis service
RUN mkdir -p /etc/s6-overlay/s6-rc.d/redis/dependencies.d
RUN echo "longrun" > /etc/s6-overlay/s6-rc.d/redis/type
RUN cat > /etc/s6-overlay/s6-rc.d/redis/run << 'EOF'
#!/command/with-contenv sh
exec 2>&1
exec redis-server --bind 127.0.0.1
EOF
RUN chmod +x /etc/s6-overlay/s6-rc.d/redis/run
RUN touch /etc/s6-overlay/s6-rc.d/redis/dependencies.d/service-start
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/redis
This image provides dynamic cron job loading with environment variable substitution support:
/crons directory$VARIABLE_NAME syntaxFile: /crons/daily-cleanup
# Run cleanup at 3 AM daily
0 3 * * * find /tmp -type f -mtime +7 -delete
Run container:
docker run -d \
-v /host/crons:/crons:ro \
tundrasoft/node:latest
File: /crons/health-check
# Check application health every 5 minutes
*/5 * * * * wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
Run container:
docker run -d \
-p 8080:8080 \
-e FILE=/app/server.js \
-v /host/crons:/crons:ro \
-v $(pwd):/app \
tundrasoft/node:latest
File: /crons/maintenance-jobs
# Database backup
$BACKUP_TIME /usr/local/bin/backup.sh >> /var/log/cron-backup.log 2>&1
# Log rotation
$LOG_ROTATE_TIME logrotate /etc/logrotate.conf
# Cleanup caches
$CLEANUP_TIME rm -rf /npm-cache/_logs/*
Run container with environment substitution:
docker run -d \
-e BACKUP_TIME='0 2 * * *' \
-e LOG_ROTATE_TIME='0 0 * * *' \
-e CLEANUP_TIME='0 4 * * 0' \
-v /host/crons:/crons:ro \
tundrasoft/node:latest
docker build \
--build-arg ALPINE_VERSION=latest \
--build-arg NODE_VERSION=24.19.0 \
-t my-node-image .
| Argument | Description | Example |
|---|---|---|
ALPINE_VERSION | Alpine Linux version (base image) | latest, 3.22, 3.21 |
NODE_VERSION | Node.js runtime version (official linux tarball) | 24.19.0, 22.18.0 |
Node's official Linux binaries are linked against glibc, so the image layers in the dynamic linker and runtime libraries from gcr.io/distroless/cc-debian12 (/usr/local/lib, with /lib and /lib64 loader symlinks and LD_LIBRARY_PATH set). 32-bit ARM (armv7) is not supported.
docker build --build-arg NODE_VERSION=24.19.0 -t tundrasoft/node:test .
tests/smoke.sh tundrasoft/node:test 24.19.0
This repository implements comprehensive security scanning:
Node.js has no built-in permission sandbox enabled by default, so isolation is enforced at the container level rather than by the runtime. Node's experimental permission modelโ can be opted into via NODE_OPTIONS when your application supports it.
For security issues, please use GitHub's private vulnerability reportingโ .
Container Runtime Security:
# Run with read-only root filesystem
docker run --read-only --tmpfs /tmp --tmpfs /run tundrasoft/node:latest
# Use specific user and drop capabilities
docker run --user 1000:1000 --cap-drop=ALL tundrasoft/node:latest
# Limit resources
docker run --memory=512m --cpus=1 --pids-limit=100 tundrasoft/node:latest
File System Security:
# Mount application files as read-only
docker run -v $(pwd):/app:ro tundrasoft/node:latest
# Mount secrets securely
docker run -v /host/secrets:/secrets:ro,Z tundrasoft/node:latest
Production Deployment:
# Always use specific version tags
docker run tundrasoft/node:24.19.0 # Not 'latest'
# Use custom networks
docker network create --driver bridge secure-app-net
docker run --network secure-app-net tundrasoft/node:24.19.0
# Enable logging
docker run --log-driver=json-file --log-opt max-size=10m tundrasoft/node:24.19.0
For security issues, please use GitHub's private vulnerability reportingโ .
nodejs.org Linux binaries/usr/local/lib/node_modules/npm)gcr.io/distroless/cc-debian12| Stage | Description | Services |
|---|---|---|
| Boot | Initialize system and user | os-ready โ service-ready |
| Config | Load configuration | config-start โ config-ready |
| Main | Run application/cron | node or crond |
| Shutdown | Clean termination | S6 async handlers |
/npm-cache/ - npm cache directory (mounted volume)
/app/ - Application code
/usr/local/bin/ - node, npm, npx
/usr/local/lib/ - glibc shim + node_modules/npm
/etc/s6-overlay/ - S6 service definitions
/etc/crontabs/ - Cron jobs (if using cron)
/etc/timezone - TZ configuration
/run/s6/ - S6 runtime (temporary)
services:
app:
image: tundrasoft/node:latest
environment:
- FILE=/app/src/main.js
- TZ=UTC
volumes:
- ./src:/app
- npm-cache:/npm-cache
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "/usr/bin/healthcheck.sh"]
interval: 30s
timeout: 10s
retries: 3
volumes:
npm-cache:
Symptoms: Container exits immediately or hangs
Debug steps:
# View logs to see startup errors
docker logs <container-id>
# Run with DEBUG mode for verbose output
docker run -it -e DEBUG=1 -e FILE=/app/main.js tundrasoft/node:latest
# Check healthcheck status
docker exec <container-id> /usr/bin/healthcheck.sh
# Verify file exists and is readable
docker exec <container-id> ls -la /app/main.js
Symptoms: Error: Cannot find module 'express' (or any dependency)
Solutions:
# Ensure dependencies are installed into /app/node_modules
docker run -v $(pwd):/app -w /app --entrypoint="" tundrasoft/node:latest npm ci
# Or install dependencies during the build
FROM tundrasoft/node:24
COPY --chown=tundra:tundra package.json package-lock.json /app/
RUN npm ci && chown -R tundra:tundra /app
COPY --chown=tundra:tundra . /app
ENV FILE=/app/main.js
Symptoms: A build tool (e.g. tsc, vite) is not found during npm run build
Cause: NODE_ENV=production is set in the image, so npm ci/npm install skip devDependencies.
Solution:
RUN npm ci --include=dev
Symptoms: First run takes a long time to download dependencies
Solution:
# Persist the npm cache using a volume
docker run -v npm-cache:/npm-cache \
-e SCRIPT=start tundrasoft/node:latest
Symptoms: File changes don't trigger app restart with WATCH=1
Check:
# Verify watch mode is working (FILE mode only)
docker run -it -e WATCH=1 -e DEBUG=1 \
-e FILE=/app/main.js \
-v $(pwd):/app \
tundrasoft/node:latest
# Look for "Restarting" messages in logs. In SCRIPT mode, add --watch to the
# script in package.json instead; WATCH=1 is ignored there.
git checkout -b feature/amazing-featuregit commit -m 'Add amazing feature'git push origin feature/amazing-featureSee CHANGELOG.mdโ for release notes and CHANGELOG-GUIDE.mdโ for contribution guidelines.
Built with โค๏ธ by TundraSoftโ
Content type
Image
Digest
sha256:9722c281bโฆ
Size
58 MB
Last updated
3 days ago
docker pull tundrasoft/node