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.
- π Quick Start
- π·οΈ Available Tags
- β¨ Features
- π Usage
- ποΈ Build-Time Optimization
- π§ Development Mode
- βοΈ Service Management
- β° Cron Jobs
- π§ Building
- π Security
- π Components
- π Reference
- π Changelog
- π€ Contributing
This image is available on multiple registries:
- Docker Hub:
tundrasoft/node - GitHub Container Registry:
ghcr.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:latestWith 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) |
- π’ Node.js LTS Runtime - Official Node.js binaries with
npmandnpx - π§ Alpine Linux Base - Minimal, secure base OS
- π§ S6 Overlay - Advanced process supervision and service management
- π€ Pre-configured User - Non-root
tundrauser (UID/GID: 1000) - π¦ glibc compatibility shim - Node's official glibc binaries run on Alpine via a distroless glibc layer
- π Timezone Support - Easy timezone configuration
- β° Cron Support - Dynamic cron job loading with environment variables
- π Health Monitoring - Built-in health checks for application monitoring
- π envsubst - Environment variable substitution in config files
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.jspackage.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=startPin 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 branchThe image decides what to run based on two environment variables:
SCRIPTβ run a script defined inpackage.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:latestRun a package.json script:
docker run -v $(pwd):/app \
-e SCRIPT=start \
tundrasoft/node:latestRun 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.jsNODE_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- β‘ Faster cold starts - No dependency downloads at runtime
- π¦ Reproducible builds -
npm cipins exact versions from the lockfile - π Offline compatible - Works in isolated environments
- π― Layer caching - Separate install layer for better Docker caching
- π Production ready - No surprise downloads in production
Development 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:latestWhat this enables:
- π
DEBUG=1: Verbose startup output with argument inspection - ποΈ
WATCH=1: File watching with auto-restart on changes (node --watch) - π
NODE_ENV=development: Overrides the production default for frameworks that key off it
With 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:latestThis 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:
- Automatic restart on failure
- Graceful shutdown handling
- Proper signal handling
- Logging integration
- Health monitoring
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:latestS6 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/redisThis image provides dynamic cron job loading with environment variable substitution support:
- Create cron files in the
/cronsdirectory - Use environment variables with
$VARIABLE_NAMEsyntax - Pass environment variables when running the container
- S6 automatically loads and installs jobs at startup
File: /crons/daily-cleanup
# Run cleanup at 3 AM daily
0 3 * * * find /tmp -type f -mtime +7 -deleteRun container:
docker run -d \
-v /host/crons:/crons:ro \
tundrasoft/node:latestFile: /crons/health-check
# Check application health every 5 minutes
*/5 * * * * wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1Run container:
docker run -d \
-p 8080:8080 \
-e FILE=/app/server.js \
-v /host/crons:/crons:ro \
-v $(pwd):/app \
tundrasoft/node:latestFile: /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:latestdocker 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.0This repository implements comprehensive security scanning:
- π‘οΈ Multi-layered scanning with Trivy, CodeQL, Semgrep, and Grype
- π Secret detection with GitLeaks (runs early in build process)
- π Automated reporting to GitHub Security tab
- π Daily security scans and vulnerability monitoring
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:latestFile 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:latestProduction 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.0For security issues, please use GitHub's private vulnerability reporting.
- Alpine Linux - Minimal, secure, and reliable
- S6 Overlay v3 - Process supervision with lifecycle management
- OpenSSL 3.x - Cryptographic and TLS support
- Node.js LTS - Official
nodejs.orgLinux binaries - npm / npx - Bundled package manager (
/usr/local/lib/node_modules/npm) - glibc shim - Loader and runtime libraries from
gcr.io/distroless/cc-debian12
- wget - HTTP client (from the base image)
- Bash/sh - Shell scripting
- Healthcheck script - S6-integrated service monitoring
| 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.jsSymptoms: 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.jsSymptoms: 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=devSymptoms: 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:latestSymptoms: 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.- π΄ Fork the repository
- π Create a feature branch:
git checkout -b feature/amazing-feature - πΎ Commit changes:
git commit -m 'Add amazing feature' - π€ Push to branch:
git push origin feature/amazing-feature - π Open a Pull Request
See CHANGELOG.md for release notes and CHANGELOG-GUIDE.md for contribution guidelines.
Built with β€οΈ by TundraSoft
View on GitHub β’ Docker Hub β’ Report Issue