A lightweight, secure Alpine Linux base image with S6 overlay, cron, and envsubst pre-installed.
This image is available on multiple registries:
tundrasoft/alpineghcr.io/tundrasoft/alpine# Pull from Docker Hub (recommended)
docker pull tundrasoft/alpine:latest
# Pull from GitHub Container Registry
docker pull ghcr.io/tundrasoft/alpine:latest
# Run with basic setup
docker run -d --name my-app tundrasoft/alpine:latest
# Run with custom timezone and user
docker run -d \
-e TZ=Asia/Kolkata \
-e PUID=1001 \
-e PGID=1001 \
--name my-app \
tundrasoft/alpine:latest
| Version | Tags |
|---|---|
| latest | Latest stable release |
| edge | Edge/development version |
| 3.24 | 3.24.1 |
| 3.23 | 3.23.5, 3.23.4, 3.23.3, 3.23.2 |
| 3.22 | 3.22.5, 3.22.4, 3.22.3, 3.22.2, 3.22.1, 3.22.0 |
| 3.21 | 3.21.7, 3.21.6, 3.21.5, 3.21.4, 3.21.3 |
| 3.20 | 3.20.7, 3.20.6 |
| 3.19 | 3.19.1 |
tundra user (UID/GID: 1000)Use as a base image in your Dockerfile:
# From Docker Hub
FROM tundrasoft/alpine:latest
# Your application setup here
# From GitHub Container Registry
FROM ghcr.io/tundrasoft/alpine:latest
# Your application setup here
For specific versions:
FROM tundrasoft/alpine:3.22.0
# or
FROM ghcr.io/tundrasoft/alpine:3.22.0
| Variable | Description | Default |
|---|---|---|
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 |
| Path | Description |
|---|---|
/crons | Directory for cron job files (automatically loaded) |
This image uses S6 Overlay for process supervision and service management. S6 is a lightweight init system that provides service supervision, dependency management, and graceful shutdown handling.
The S6 service hierarchy follows this structure:
s6-rc.d/
├── base/ # Foundational services (always run)
├── user/ # User-defined service bundle
│ └── contents.d/ # Services included in user bundle
├── timezone/ # Timezone configuration service
├── init-user/ # User/group initialization
├── os-ready/ # Oneshot: Triggered when OS is ready
├── config-start/ # Oneshot: Configuration phase starts
├── config-cron/ # Oneshot: Load cron jobs
├── config-ready/ # Oneshot: Configuration complete
├── crond/ # Longrun: Cron daemon process
├── service-start/ # Oneshot: Service phase starts
└── service-ready/ # Oneshot: Services initialized
The container initialization follows this dependency chain:
Container Start
↓
[base] (supervisor)
↓
[timezone] → Sets TZ from env var
[init-user] → Configures tundra user/group
↓
[os-ready] ← When base services complete
↓
[config-start] → Configuration phase begins
↓
[config-cron] → Loads cron jobs from /crons
[config-ready] ← Configuration complete
↓
[service-start] → Application services begin
↓
[crond] → Cron daemon starts
[service-ready] ← All services initialized
↓
Container Ready (running indefinitely)
| Service | Type | Purpose | Dependencies |
|---|---|---|---|
timezone | oneshot | Sets timezone from TZ environment variable | None |
init-user | oneshot | Configures tundra user/group IDs using PUID/PGID | None |
os-ready | oneshot | Signals OS initialization complete | base, init-user, timezone |
config-start | oneshot | Signals configuration phase start | os-ready |
config-cron | oneshot | Loads cron jobs from /crons directory | config-start |
config-ready | oneshot | Signals configuration complete | config-cron, config-start |
service-start | oneshot | Signals service initialization phase | config-ready |
crond | longrun | Runs cron daemon process | config-cron, service-start |
service-ready | oneshot | Signals all services initialized | crond, service-start |
1. Create Service Directory Structure
FROM tundrasoft/alpine:latest
# Create service with proper structure
RUN mkdir -p /etc/s6-overlay/s6-rc.d/my-app/dependencies.d
# Set service type (longrun = daemon, oneshot = runs once)
RUN echo "longrun" > /etc/s6-overlay/s6-rc.d/my-app/type
# Create the run script (entry point for the service)
COPY my-app-run.sh /etc/s6-overlay/s6-rc.d/my-app/run
RUN chmod +x /etc/s6-overlay/s6-rc.d/my-app/run
# Declare dependencies
RUN touch /etc/s6-overlay/s6-rc.d/my-app/dependencies.d/service-start
# Add to user bundle so it starts automatically
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/my-app
2. Create the Run Script (my-app-run.sh)
#!/command/with-contenv bash
# This script runs the service
# 'with-contenv' provides environment variables from s6
# 'exec' replaces the shell with the application (required for signals)
exec 2>&1 # Redirect stderr to stdout for s6 logging
# Run your application
exec my-application \
--config /etc/my-app.conf \
--user tundra \
--log-level info
3. Optional: Add Finish Script (runs when service stops)
# /etc/s6-overlay/s6-rc.d/my-app/finish
#!/command/execlineb -S0
# Cleanup code here (runs when service is stopping)
echo "my-app is stopping..."
4. Optional: Add Timeout Handler
# /etc/s6-overlay/s6-rc.d/my-app/timeout-finish
# Timeout in milliseconds before force-killing the service
echo "5000" > /etc/s6-overlay/s6-rc.d/my-app/timeout-finish
FROM tundrasoft/alpine:latest
# Install application
RUN apk add --no-cache nginx
# Create service
RUN mkdir -p /etc/s6-overlay/s6-rc.d/webserver/dependencies.d
RUN echo "longrun" > /etc/s6-overlay/s6-rc.d/webserver/type
RUN cat > /etc/s6-overlay/s6-rc.d/webserver/run << 'EOF'
#!/command/with-contenv bash
exec 2>&1
exec nginx -g "daemon off;"
EOF
RUN chmod +x /etc/s6-overlay/s6-rc.d/webserver/run
RUN touch /etc/s6-overlay/s6-rc.d/webserver/dependencies.d/service-start
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/webserver
FROM tundrasoft/alpine:latest
# Create oneshot service that generates config
RUN mkdir -p /etc/s6-overlay/s6-rc.d/generate-config/dependencies.d
RUN echo "oneshot" > /etc/s6-overlay/s6-rc.d/generate-config/type
RUN cat > /etc/s6-overlay/s6-rc.d/generate-config/up << 'EOF'
#!/command/with-contenv bash
# This script runs once during boot
# 'up' is used for oneshot services instead of 'run'
envsubst < /etc/my-app.template > /etc/my-app.conf
echo "Generated /etc/my-app.conf with:"
cat /etc/my-app.conf
EOF
RUN chmod +x /etc/s6-overlay/s6-rc.d/generate-config/up
RUN touch /etc/s6-overlay/s6-rc.d/generate-config/dependencies.d/config-start
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/generate-config
FROM tundrasoft/alpine:latest
# Create service with periodic health check
RUN mkdir -p /etc/s6-overlay/s6-rc.d/app-service/dependencies.d
RUN echo "longrun" > /etc/s6-overlay/s6-rc.d/app-service/type
RUN cat > /etc/s6-overlay/s6-rc.d/app-service/run << 'EOF'
#!/command/with-contenv bash
exec 2>&1
exec my-service --config /etc/my-service.conf
EOF
RUN chmod +x /etc/s6-overlay/s6-rc.d/app-service/run
# Add finish script for graceful shutdown
RUN cat > /etc/s6-overlay/s6-rc.d/app-service/finish << 'EOF'
#!/command/execlineb -S0
# S6 sends TERM, wait a bit, then sends KILL
echo "Shutting down my-service..."
EOF
RUN chmod +x /etc/s6-overlay/s6-rc.d/app-service/finish
RUN touch /etc/s6-overlay/s6-rc.d/app-service/dependencies.d/service-start
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/app-service
This image provides dynamic cron job loading with environment variable substitution support:
/crons directory$VARIABLE_NAME syntaxThe config-cron service processes all files in /crons, expands environment variables using envsubst, and installs them in the tundra user's crontab.
File: /crons/daily-backup
# Run backup at 2 AM daily
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Run container:
docker run -d \
-v /host/crons:/crons:ro \
tundrasoft/alpine:latest
Verify:
docker exec <container> crontab -l
# Output: 0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
File: /crons/dynamic-jobs
# Schedule from environment variables
$SCHEDULE_BACKUP /home/tundra/backup.sh
$SCHEDULE_CLEANUP /home/tundra/cleanup.sh
$SCHEDULE_HEALTH_CHECK curl http://localhost:8080/health
# Multiple jobs, one with custom schedule
0 */6 * * * /usr/local/bin/sync.sh
Run container:
docker run -d \
-e SCHEDULE_BACKUP='0 2 * * *' \
-e SCHEDULE_CLEANUP='0 4 * * 0' \
-e SCHEDULE_HEALTH_CHECK='*/5 * * * *' \
-v /host/crons:/crons:ro \
tundrasoft/alpine:latest
Result:
docker exec <container> crontab -l
# 0 2 * * * /home/tundra/backup.sh
# 0 4 * * 0 /home/tundra/cleanup.sh
# */5 * * * * curl http://localhost:8080/health
# 0 */6 * * * /usr/local/bin/sync.sh
File: /crons/production-jobs
# Backup with compression and rotation
$BACKUP_TIME /usr/local/bin/backup.sh --compress --rotate 7 >> /var/log/cron-backup.log 2>&1
# Database maintenance
$DB_MAINTAIN_TIME /usr/local/bin/db-vacuum.sh --analyze >> /var/log/cron-db.log 2>&1
# Log rotation (using logrotate)
$LOG_ROTATE_TIME /usr/sbin/logrotate /etc/logrotate.conf
# Cleanup old files
$CLEANUP_TIME find /var/tmp -type f -mtime +$CLEANUP_DAYS -delete
# Health check with alerting
$HEALTH_CHECK_TIME /usr/local/bin/health-check.sh || mail -s "Alert: Health check failed" [email protected]
Run container with production settings:
docker run -d \
--name prod-app \
-e BACKUP_TIME='0 1 * * *' \
-e DB_MAINTAIN_TIME='0 3 * * 0' \
-e LOG_ROTATE_TIME='0 0 * * *' \
-e CLEANUP_TIME='0 4 * * *' \
-e CLEANUP_DAYS='30' \
-e HEALTH_CHECK_TIME='*/10 * * * *' \
-v /data/app/crons:/crons:ro \
-v /data/app/scripts:/usr/local/bin:ro \
tundrasoft/alpine:latest
Warning: Cron files are executed with the
tundrauser privileges. Ensure they come from trusted sources.
# Mount from read-only, trusted source
docker run -d \
-v /secure/trusted/crons:/crons:ro \
tundrasoft/alpine:latest
# Verify file permissions before mounting
ls -la /secure/trusted/crons/
# drwxr-xr-x - owned by trusted user
# -rw-r--r-- - files not world-writable
# DON'T: Mount /tmp (world-writable)
docker run -d -v /tmp:/crons tundrasoft/alpine:latest
# DON'T: World-writable cron directory
chmod 777 /data/crons
docker run -d -v /data/crons:/crons tundrasoft/alpine:latest
# DON'T: Untrusted scripts in crons
# Any user could modify cron jobs!
# 1. Use read-only mount
-v /trusted/crons:/crons:ro
# 2. Validate permissions
find /path/to/crons -type f ! -perm 0644 -exec ls -la {} \;
find /path/to/crons -type d ! -perm 0755 -exec ls -la {} \;
# 3. Use restrictive umask in cron files
umask 0077 # Prevent world-readable secrets
# 4. Scan cron files for suspicious content
grep -r 'chmod\|curl.*http\|ssh' /path/to/crons
# 5. Monitor crontab changes
docker exec <container> crontab -l > /var/log/crontab-snapshot.txt
docker build \
--build-arg ALPINE_BRANCH=v3.22 \
--build-arg S6_VERSION=3.1.6.2 \
-t my-alpine-image .
| Argument | Description | Example |
|---|---|---|
ALPINE_BRANCH | Alpine Linux branch (latest patch version auto-detected) | v3.22, v3.21, edge |
S6_VERSION | S6 Overlay version | 3.1.6.2 |
Practical examples are available in the examples/ directory:
See examples/README.md for detailed build/run instructions, debugging tips, and common patterns.
This repository runs several security scanners:
For security issues, please use GitHub's private vulnerability reporting.
Alpine Linux is a security-focused, lightweight Linux distribution (~5MB) based on musl libc and BusyBox.
Key features:
S6 Overlay provides an init system and process supervisor for containers.
Key features:
When to use:
Full crond daemon from BusyBox with dynamic job loading and environment variable support.
Features:
GNU gettext envsubst utility for environment variable substitution. Used for template expansion in configuration files and cron jobs.
Usage:
envsubst < template.conf > final.conf
envsubst '$VARIABLE1:$VARIABLE2' < config.tmpl
Complete timezone database (from tzdata) with TZ environment variable support for easy configuration.
Usage:
docker run -e TZ=America/New_York tundrasoft/alpine:latest
docker run -e TZ=Asia/Tokyo tundrasoft/alpine:latest
Common timezones:
UTC - Coordinated Universal TimeAmerica/New_York - Eastern TimeEurope/London - Greenwich Mean TimeAsia/Tokyo - Japan Standard TimeAustralia/Sydney - Australian Eastern Time1. Image starts (docker run)
↓
2. Entrypoint: /init (S6 init)
↓
3. Base services start (supervisor)
↓
4. Timezone service runs → Sets TZ
↓
5. Init-user service runs → Configures tundra user
↓
6. os-ready signal sent
↓
7. config-start oneshot runs
↓
8. config-cron service loads /crons
↓
9. config-ready signal sent
↓
10. service-start oneshot runs
↓
11. User services start (from /etc/s6-overlay/s6-rc.d/user/contents.d/*)
↓
12. crond daemon starts
↓
13. service-ready signal sent
↓
14. Container ready (PID 1 = s6-svscan)
↓
15. SIGTERM received → Graceful shutdown
↓
16. Services stopped in reverse order
↓
17. Container exits
/
├── /init # S6 init system (PID 1)
├── /run/service/ # Active service directory
├── /etc/s6-overlay/
│ ├── s6-rc.d/ # Service definitions
│ │ ├── user/
│ │ │ └── contents.d/ # Services to auto-start
│ │ ├── base/
│ │ ├── timezone/
│ │ ├── init-user/
│ │ ├── os-ready/
│ │ ├── config-start/
│ │ ├── config-cron/
│ │ ├── config-ready/
│ │ ├── service-start/
│ │ ├── crond/
│ │ └── service-ready/
│ └── s6-rc
├── /crons/ # Mount point for cron files
├── /home/tundra/ # Home directory
└── /var/spool/cron/crontabs/ # Installed crontabs
Built-in Variables:
| Variable | Type | Description | Example |
|---|---|---|---|
PUID | int | User ID for tundra user | 1000 |
PGID | int | Group ID for tundra group | 1000 |
TZ | string | Timezone identifier | America/New_York |
PATH | string | Command search path | /scripts:$PATH |
LANG | string | Locale setting | C.UTF-8 |
S6 Variables (read-only):
| Variable | Description |
|---|---|
S6_CMD_WAIT_FOR_SERVICES_MAXTIME | Max wait for service startup (0 = no limit) |
S6_GLOBAL_PATH | Search path for commands |
Available for Custom Use:
Any variables prefixed with custom names (e.g., BACKUP_SCHEDULE, APP_CONFIG_URL) can be used in cron files and templates with $VARIABLE_NAME syntax and expanded via envsubst.
Each service is a directory with configuration files:
| File | Type | Purpose |
|---|---|---|
type | file | longrun (daemon) or oneshot (run-once) |
run | script | Main script for longrun services |
up | script | Initialization script for oneshot services |
finish | script | Cleanup script when service stops |
timeout-finish | file | Milliseconds to wait before force-kill |
dependencies.d/ | dir | Contains dependency file names |
notification-fd | file | File descriptor for readiness notification |
Pattern 1: Oneshot → Longrun Dependency
# Service A: generates config (oneshot)
type: oneshot
up: /generate-config.sh
dependencies.d/: [os-ready]
# Service B: uses config (longrun)
type: longrun
run: /start-app.sh
dependencies.d/: [service-a] # Waits for service-a to complete
Pattern 2: Multiple Services, Single Trigger
# Service A (longrun)
dependencies.d/: [service-start]
# Service B (longrun)
dependencies.d/: [service-start]
# Both start after service-start trigger
Pattern 3: Graceful Shutdown
# Service definition
type: longrun
run: /usr/bin/myapp
finish: /scripts/shutdown-hook.sh
timeout-finish: 10000 # 10 seconds before SIGKILL
version: '3.8'
services:
app:
image: tundrasoft/alpine:latest
container_name: my-app
environment:
- TZ=America/New_York
- PUID=1001
- PGID=1001
- BACKUP_TIME='0 2 * * *'
- CLEANUP_TIME='0 4 * * 0'
volumes:
- ./crons:/crons:ro
- ./scripts:/usr/local/bin:ro
- app-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Another service depending on the base image
worker:
build:
context: ./worker
dockerfile: Dockerfile
depends_on:
- app
environment:
- TZ=America/New_York
- PUID=1001
volumes:
app-data:
Issue: Service not starting
# Check S6 supervisor status
docker exec <container> s6-svstat /run/service/*
# View service logs
docker logs <container>
# Inspect service definition
docker exec <container> cat /etc/s6-overlay/s6-rc.d/my-service/type
docker exec <container> ls -la /etc/s6-overlay/s6-rc.d/my-service/dependencies.d/
Issue: Cron jobs not running
# Verify crontab was loaded
docker exec <container> crontab -l
# Check cron logs
docker exec <container> tail -f /var/log/messages
# Verify file permissions
docker exec <container> ls -la /crons/
Issue: Container exits immediately
#
Content type
Image
Digest
sha256:a8cdca62b…
Size
7.7 MB
Last updated
3 days ago
docker pull tundrasoft/alpine