A lightweight Bun runtime image built on Alpine Linux with S6 overlay, native musl binaries, and developer-friendly utilities.
This image is published to both Docker Hub and the GitHub Container Registry:
tundrasoft/bunghcr.io/tundrasoft/bun# Pull from Docker Hub
docker pull tundrasoft/bun:latest
# Pull from GitHub Container Registry
docker pull ghcr.io/tundrasoft/bun:latest
# Run a local Bun application (mount your code into /app)
docker run -d \
-p 8080:8080 \
-e FILE=/app/server.ts \
-v $(pwd):/app \
--name bun-app \
tundrasoft/bun:latest
# Run with a custom timezone and a package.json script
docker run -d \
-e TZ=Asia/Kolkata \
-e SCRIPT=start \
-v $(pwd):/app \
--name my-bun-app \
tundrasoft/bun: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 |
| 1.4 | 1.4.2, 1.4.1, 1.4.0 |
| 1.3 | 1.3.14, 1.3.13, 1.3.12, 1.3.11, 1.3.10 |
tundra user (UID/GID 1000)--hot and --watch modesUse as a base image in your Dockerfile:
# From Docker Hub
FROM tundrasoft/bun:latest
COPY . /app
# From GitHub Container Registry
FROM ghcr.io/tundrasoft/bun:latest
COPY . /app
For specific versions:
FROM tundrasoft/bun:1.3.14
# or with a specific Alpine version
FROM tundrasoft/bun:alpine-3.22-1.3.14
The image decides what to run based on two environment variables:
FILE — run a single file directly (bun run <FILE>)SCRIPT — run a script defined in package.json (bun run <SCRIPT>)SCRIPT takes precedence over FILE. If neither is set, a minimal demo server is started.
Run a file:
docker run -p 8080:8080 \
-e FILE=/app/server.ts \
-v $(pwd):/app \
tundrasoft/bun:latest
Run a package.json script:
docker run -v $(pwd):/app \
-e SCRIPT=start \
tundrasoft/bun:latest
Run with environment variables:
docker run -d \
-e FILE=/app/server.ts \
-e PUID=1001 \
-e PGID=1001 \
-e TZ=America/New_York \
-v $(pwd):/app \
tundrasoft/bun:latest
| Variable | Description | Default |
|---|---|---|
SCRIPT | Run a script from package.json (takes precedence over FILE) | N/A |
FILE | The file to run directly with bun run | N/A |
BUN_INSTALL | Directory for Bun's global installs and module cache | /bun |
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 |
NODE_ENV | Standard Node/Bun environment hint (e.g., production) | N/A |
DEBUG | Enable debug mode with verbose output (1 to enable) | N/A |
WATCH | Restart on file changes via --watch (1 to enable) | N/A |
HOT | Hot-reload in place via --hot (1 to enable; takes precedence over WATCH) | 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 |
| Path | Description |
|---|---|
/app | Application root directory (recommended to mount as a volume) |
/crons | Directory for cron job files (automatically loaded) |
/bun | Bun cache and global install directory (BUN_INSTALL, for persisting dependencies) |
Install dependencies during the image build to eliminate cold-start downloads.
Install from a lockfile (best layer caching):
FROM tundrasoft/bun:latest
# Copy manifests first so this layer is cached until they change
COPY package.json bun.lock /app/
RUN bun install --frozen-lockfile
COPY . /app
ENV FILE=/app/index.ts
Install production-only dependencies:
FROM tundrasoft/bun:latest
COPY package.json bun.lock /app/
RUN bun install --frozen-lockfile --production
COPY . /app
ENV SCRIPT=start
Compile to a single-file executable:
FROM tundrasoft/bun:latest AS build
COPY . /app
RUN bun install --frozen-lockfile
RUN bun build /app/index.ts --compile --outfile /app/server
FROM tundrasoft/bun:latest
COPY --from=build /app/server /app/server
ENV FILE=/app/server
Benefits:
--frozen-lockfile pins exact versionsCombine DEBUG, WATCH/HOT, and a volume mount for a fast feedback loop.
Watch mode (restart on change):
docker run -it \
-e DEBUG=1 \
-e WATCH=1 \
-e FILE=/app/main.ts \
-v $(pwd):/app \
-p 8080:8080 \
tundrasoft/bun:latest
Hot reload (update in place, preserve state):
docker run -it \
-e HOT=1 \
-e FILE=/app/server.ts \
-v $(pwd):/app \
-p 8080:8080 \
tundrasoft/bun:latest
DEBUG=1: verbose startup output with argument inspectionWATCH=1: file watching with a full restart on changes (bun --watch)HOT=1: hot module reloading in place (bun --hot), taking precedence over WATCHWith a package.json script:
docker run -it \
-e DEBUG=1 \
-e HOT=1 \
-e SCRIPT=dev \
-v $(pwd):/app \
-p 8080:8080 \
tundrasoft/bun:latest
This image uses S6 Overlay for process supervision and service management. The Bun service runs your application under S6, which provides:
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.ts \
tundrasoft/bun:latest
# Extended graceful shutdown (10 seconds)
docker run -d \
-e S6_KILL_FINISH_MAXTIME=10000 \
-e FILE=/app/server.ts \
tundrasoft/bun:latest
# Infinite startup wait (for slow-starting apps)
docker run -d \
-e S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
-e FILE=/app/server.ts \
tundrasoft/bun: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 image with additional services:
FROM tundrasoft/bun:latest
RUN apk add --no-cache redis
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
The image supports dynamic cron job loading with environment-variable substitution:
/crons directory$VARIABLE_NAME syntax/crons/daily-cleanup:
# Run cleanup at 3 AM daily
0 3 * * * find /tmp -type f -mtime +7 -delete
docker run -d \
-v /host/crons:/crons:ro \
tundrasoft/bun:latest
/crons/health-check:
# Check application health every 5 minutes
*/5 * * * * wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
docker run -d \
-p 8080:8080 \
-e FILE=/app/server.ts \
-v /host/crons:/crons:ro \
-v $(pwd):/app \
tundrasoft/bun:latest
/crons/maintenance-jobs:
$BACKUP_TIME /usr/local/bin/backup.sh >> /var/log/cron-backup.log 2>&1
$LOG_ROTATE_TIME logrotate /etc/logrotate.conf
$CLEANUP_TIME rm -rf /tmp/bun-cache-*
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/bun:latest
docker build \
--build-arg ALPINE_VERSION=latest \
--build-arg BUN_VERSION=1.3.14 \
-t my-bun-image .
| Argument | Description | Example |
|---|---|---|
ALPINE_VERSION | Alpine Linux version (base image) | latest, 3.22, 3.21 |
BUN_VERSION | Bun runtime version | 1.3.14, 1.2.19 |
The x86_64 image uses Bun's x64-musl-baseline build for maximum CPU compatibility (no AVX2 requirement); arm64 uses aarch64-musl. 32-bit ARM (armv7) is not supported, as Bun does not ship binaries for it.
This repository runs layered security scanning:
Bun has no built-in permission sandbox, so isolation is enforced at the container level rather than by the runtime.
Container runtime hardening:
# Read-only root filesystem
docker run --read-only --tmpfs /tmp --tmpfs /run tundrasoft/bun:latest
# Specific user and dropped capabilities
docker run --user 1000:1000 --cap-drop=ALL tundrasoft/bun:latest
# Resource limits
docker run --memory=512m --cpus=1 --pids-limit=100 tundrasoft/bun:latest
File system:
# Mount application files as read-only
docker run -v $(pwd):/app:ro tundrasoft/bun:latest
# Mount secrets read-only
docker run -v /host/secrets:/secrets:ro,Z tundrasoft/bun:latest
Production:
# Use specific version tags, not 'latest'
docker run tundrasoft/bun:1.3.14
# Use custom networks
docker network create --driver bridge secure-app-net
docker run --network secure-app-net tundrasoft/bun:1.3.14
# Enable logging
docker run --log-driver=json-file --log-opt max-size=10m tundrasoft/bun:1.3.14
For security issues, use GitHub's private vulnerability reporting.
Base system:
Runtime:
Utilities:
| Stage | Description | Services |
|---|---|---|
| Boot | Initialize system and user | os-ready → service-ready |
| Config | Load configuration | config-start → config-ready |
| Main | Run application/cron | bun or crond |
| Shutdown | Clean termination | S6 async handlers |
/bun/ - Bun cache & global installs (mounted volume)
/app/ - Application code
/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/bun:latest
environment:
- FILE=/app/src/main.ts
- TZ=UTC
volumes:
- ./src:/app
- bun-cache:/bun
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "/usr/bin/healthcheck.sh"]
interval: 30s
timeout: 10s
retries: 3
volumes:
bun-cache:
Application not starting:
# View logs for startup errors
docker logs <container-id>
# Run with DEBUG for verbose output
docker run -it -e DEBUG=1 -e FILE=/app/main.ts tundrasoft/bun:latest
# Check healthcheck status
docker exec <container-id> /usr/bin/healthcheck.sh
# Verify the file exists and is readable
docker exec <container-id> ls -la /app/main.ts
Slow cold start (first-run dependency download):
# Persist the cache with a volume
docker run -v bun-cache:/bun -e FILE=/app/main.ts tundrasoft/bun:latest
# Or install dependencies during the build
FROM tundrasoft/bun:latest
COPY package.json bun.lock /app/
RUN bun install --frozen-lockfile
COPY . /app
ENV FILE=/app/main.ts
Module resolution problems ("Cannot find module"):
# Ensure dependencies are installed into /app/node_modules
docker run -v $(pwd):/app -w /app tundrasoft/bun:latest bun install
Watch/hot mode not reloading:
# Verify the source is mounted and reload mode is active
docker run -it -e WATCH=1 -e DEBUG=1 \
-e FILE=/app/main.ts \
-v $(pwd):/app \
tundrasoft/bun:latest
git checkout -b feature/amazing-featuregit commit -m 'Add amazing feature'git push origin feature/amazing-featureSee CONTRIBUTING.md for guidelines, and CHANGELOG.md for release notes.
Content type
Image
Digest
sha256:c7d050de6…
Size
42 MB
Last updated
about 6 hours ago
docker pull tundrasoft/bun