Skip to content

Instantly share code, notes, and snippets.

@andr-ec
Last active July 14, 2025 16:45
Show Gist options
  • Save andr-ec/26b167d5f8422b5cf78a98bb4ac8b7e6 to your computer and use it in GitHub Desktop.
Save andr-ec/26b167d5f8422b5cf78a98bb4ac8b7e6 to your computer and use it in GitHub Desktop.
A shell script that generates a dockerfile from a devbox.json and devbox.lock using nixery.dev
#!/usr/bin/env bash
# generate-dockerfile.sh
# Creates a base Dockerfile from devbox.json and devbox.lock files
# Usage: ./generate-dockerfile.sh [devbox.json] [devbox.lock] [output-dockerfile]
set -euo pipefail
# Help function
show_help() {
cat << EOF
generate-dockerfile.sh - Generate Docker images from devbox configurations
USAGE:
./generate-dockerfile.sh [devbox.json] [devbox.lock] [output-dockerfile]
ARGUMENTS:
devbox.json Path to devbox.json file (default: devbox.json)
devbox.lock Path to devbox.lock file (default: devbox.lock)
output-dockerfile Output Dockerfile path (default: Dockerfile.generated)
EXAMPLES:
# Generate Dockerfile with default files
./generate-dockerfile.sh
# Specify custom files
./generate-dockerfile.sh my-devbox.json my-devbox.lock MyDockerfile
# Generate with different output name
./generate-dockerfile.sh devbox.json devbox.lock Dockerfile.custom
REQUIREMENTS:
- jq (for JSON parsing)
- Valid devbox.json and devbox.lock files
NOTES:
- Generated Dockerfile uses nixery.dev with specific commit hashes
- Review and customize TODO sections in generated Dockerfile
- All packages from devbox.json will be included
EOF
}
# Check for help flag
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
show_help
exit 0
fi
# Default file paths
DEVBOX_JSON="${1:-devbox.json}"
DEVBOX_LOCK="${2:-devbox.lock}"
OUTPUT_DOCKERFILE="${3:-Dockerfile.generated}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if required files exist
if [[ ! -f "$DEVBOX_JSON" ]]; then
log_error "devbox.json not found at: $DEVBOX_JSON"
exit 1
fi
if [[ ! -f "$DEVBOX_LOCK" ]]; then
log_error "devbox.lock not found at: $DEVBOX_LOCK"
exit 1
fi
# Check if jq is available
if ! command -v jq &> /dev/null; then
log_error "jq is required but not installed. Please install jq first."
exit 1
fi
log_info "Reading packages from $DEVBOX_JSON..."
# Extract packages from devbox.json
PACKAGES=$(jq -r '.packages[]' "$DEVBOX_JSON" | sed 's/@latest$//')
if [[ -z "$PACKAGES" ]]; then
log_error "No packages found in $DEVBOX_JSON"
exit 1
fi
log_info "Found packages: $(echo $PACKAGES | tr '\n' ' ')"
# Function to get commit hash for a package from devbox.lock
get_commit_hash() {
local package="$1"
local package_key="${package}@latest"
# Extract the commit hash from the resolved field
local resolved=$(jq -r --arg pkg "$package_key" '.packages[$pkg].resolved // empty' "$DEVBOX_LOCK")
if [[ -n "$resolved" && "$resolved" != "null" ]]; then
# Extract commit hash from GitHub URL (format: github:NixOS/nixpkgs/COMMITHASH#package)
echo "$resolved" | sed -n 's/.*github:NixOS\/nixpkgs\/\([^#]*\)#.*/\1/p'
else
log_warn "No commit found for package: $package"
echo ""
fi
}
log_info "Generating Dockerfile..."
# Start generating the Dockerfile
cat > "$OUTPUT_DOCKERFILE" << 'EOF'
# Auto-generated Dockerfile from devbox.json and devbox.lock
# Generated on: $(date)
#
# This is a base Dockerfile - modify as needed for your specific application
EOF
# Add package comments
echo "# Packages and their specific nixpkgs commits:" >> "$OUTPUT_DOCKERFILE"
stage_counter=1
declare -a stages=()
declare -a stage_names=()
for package in $PACKAGES; do
commit=$(get_commit_hash "$package")
if [[ -n "$commit" ]]; then
echo "# $package: commit $commit" >> "$OUTPUT_DOCKERFILE"
# Create stage name (sanitize package name for Docker)
stage_name=$(echo "$package" | tr '-' '_' | tr '.' '_')
stage_names+=("$stage_name")
stages+=("$package:$commit")
else
log_warn "Skipping $package - no commit hash found"
fi
done
echo "" >> "$OUTPUT_DOCKERFILE"
# Group packages by commit hash
declare -A commit_groups
for i in "${!stages[@]}"; do
package_info="${stages[$i]}"
package=$(echo "$package_info" | cut -d':' -f1)
commit=$(echo "$package_info" | cut -d':' -f2)
if [[ -n "${commit_groups[$commit]+isset}" ]]; then
commit_groups[$commit]="${commit_groups[$commit]}/$package"
else
commit_groups[$commit]="$package"
fi
done
# Find the most common commit hash and packages that can be grouped
max_count=0
main_commit=""
for commit in "${!commit_groups[@]}"; do
count=$(echo "${commit_groups[$commit]}" | tr '/' '\n' | wc -l)
if [[ $count -gt $max_count ]]; then
max_count=$count
main_commit=$commit
fi
done
# Generate main tools stage with grouped packages
echo "# Stage 1: Main tools" >> "$OUTPUT_DOCKERFILE"
if [[ -n "$main_commit" ]]; then
main_packages="shell/${commit_groups[$main_commit]}"
echo "FROM nixery.dev/$main_packages:$main_commit AS base" >> "$OUTPUT_DOCKERFILE"
echo "# Grouped packages with commit $main_commit: ${commit_groups[$main_commit]}" >> "$OUTPUT_DOCKERFILE"
else
echo "FROM nixery.dev/shell AS base" >> "$OUTPUT_DOCKERFILE"
fi
echo "" >> "$OUTPUT_DOCKERFILE"
# Generate individual package stages for packages not in main group
stage_counter=2
for i in "${!stages[@]}"; do
package_info="${stages[$i]}"
package=$(echo "$package_info" | cut -d':' -f1)
commit=$(echo "$package_info" | cut -d':' -f2)
stage_name="${stage_names[$i]}"
# Skip packages already included in main tools stage
if [[ "$commit" == "$main_commit" ]]; then
continue
fi
echo "# Stage $stage_counter: $package with specific commit" >> "$OUTPUT_DOCKERFILE"
echo "FROM nixery.dev/$package:$commit AS ${stage_name}_layer" >> "$OUTPUT_DOCKERFILE"
echo "" >> "$OUTPUT_DOCKERFILE"
((stage_counter++))
done
# Generate final stage
cat >> "$OUTPUT_DOCKERFILE" << 'EOF'
# Final stage: Combine all packages
FROM base
EOF
# Add COPY commands only for packages not in main group
echo "# Copy nix store from each package stage" >> "$OUTPUT_DOCKERFILE"
for i in "${!stages[@]}"; do
package_info="${stages[$i]}"
commit=$(echo "$package_info" | cut -d':' -f2)
stage_name="${stage_names[$i]}"
# Skip packages already included in main tools stage
if [[ "$commit" == "$main_commit" ]]; then
continue
fi
echo "COPY --from=${stage_name}_layer /nix /nix" >> "$OUTPUT_DOCKERFILE"
done
echo "" >> "$OUTPUT_DOCKERFILE"
# Add profile creation and environment setup
cat >> "$OUTPUT_DOCKERFILE" << 'EOF'
# Make binaries available in PATH using selective linking
RUN mkdir -p /bin && \
for pattern in nodejs ffmpeg elixir erlang beam python ruby go rust; do \
ln -sf /nix/store/*$pattern*/bin/* /bin/ 2>/dev/null || true; \
done && \
# Also link any other binaries that might be present
for store_path in /nix/store/*/bin; do \
if [ -d "$store_path" ]; then \
ln -sf "$store_path"/* /bin/ 2>/dev/null || true; \
fi; \
done
# Set up environment
ENV PATH=/bin:/usr/bin:$PATH
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
# Create working directory
WORKDIR /app
# TODO: Add your application-specific environment variables here
# ENV MY_APP_ENV=production
# ENV MY_APP_PORT=8080
# TODO: Copy your application files
# COPY . .
# TODO: Install dependencies or build your application
# RUN npm install && npm run build
# RUN pip install -r requirements.txt
# RUN go mod download && go build
# TODO: Set up any additional configuration
# COPY config/ ./config/
# TODO: Expose ports as needed
# EXPOSE 8080
# Default command (modify as needed)
CMD ["/bin/bash"]
EOF
# Replace the date placeholder
sed -i "s/\$(date)/$(date)/" "$OUTPUT_DOCKERFILE"
log_info "Dockerfile generated successfully: $OUTPUT_DOCKERFILE"
log_info ""
log_info "Next steps:"
log_info "1. Review the generated Dockerfile"
log_info "2. Uncomment and modify the TODO sections for your application"
log_info "3. Add your application-specific build steps"
log_info "4. Test with: docker build -t my-app ."
log_info ""
log_warn "Remember to:"
log_warn "- Add your application files (COPY commands)"
log_warn "- Set proper environment variables"
log_warn "- Install application dependencies (uncomment appropriate package manager)"
log_warn "- Configure the correct CMD or ENTRYPOINT"
@andr-ec
Copy link
Author

andr-ec commented Jul 10, 2025

I generated this script with AI, double check it works before using it

Generate Dockerfile from Devbox

A shell script that automatically generates a base Dockerfile from devbox.json and devbox.lock files, using specific nixpkgs commits for reproducible builds.

Requirements

  • jq for JSON parsing
  • Valid devbox.json and devbox.lock files
  • Docker for building the generated Dockerfile

Usage

# Basic usage (uses devbox.json and devbox.lock in current directory)
./generate-dockerfile.sh

# Custom file paths
./generate-dockerfile.sh my-devbox.json my-devbox.lock

# Custom output file
./generate-dockerfile.sh devbox.json devbox.lock MyDockerfile

# Show help
./generate-dockerfile.sh --help

Example Output

The script generates a Dockerfile like this:

# Auto-generated Dockerfile from devbox.json and devbox.lock
# Generated on: Thu Jul 10 12:30:28 PM MDT 2025

# Packages and their specific nixpkgs commits:
# elixir: commit 7cc0bff31a3a705d3ac4fdceb030a17239412210
# nodejs: commit 076e8c6678d8c54204abcb4b1b14c366835a58bb

# Stage 1: Shell base
FROM nixery.dev/shell:7cc0bff31a3a705d3ac4fdceb030a17239412210 AS base

# Stage 2: elixir
FROM nixery.dev/elixir:7cc0bff31a3a705d3ac4fdceb030a17239412210 AS elixir_layer

# Stage 3: nodejs
FROM nixery.dev/nodejs:076e8c6678d8c54204abcb4b1b14c366835a58bb AS nodejs_layer

# ... more stages ...

# Final stage: Combine all packages
FROM base

# Copy nix store from each package stage
COPY --from=elixir_layer /nix /nix
COPY --from=nodejs_layer /nix /nix

# Create a Nix profile with symlinks to all package binaries
RUN mkdir -p /nix/var/nix/profiles/default/bin && \
    for store_path in /nix/store/*; do \
        if [ -d "$store_path/bin" ]; then \
            for binary in "$store_path/bin"/*; do \
                [ -f "$binary" ] && ln -sf "$binary" "/nix/var/nix/profiles/default/bin/$(basename "$binary")" || true; \
            done; \
        fi; \
    done

# Set up environment
ENV PATH=/nix/var/nix/profiles/default/bin:/bin:/usr/bin
ENV LANG=C.UTF-8

WORKDIR /app

# TODO sections for customization:
# - Add application files
# - Install dependencies  
# - Set environment variables
# - Configure ports
# - Set up CMD/ENTRYPOINT

CMD ["/bin/bash"]

Customization

After generating the Dockerfile, customize the TODO sections:

  1. Add your application files:

    COPY . .
  2. Install dependencies (uncomment as needed):

    RUN npm install && npm run build
    RUN pip install -r requirements.txt
    RUN go mod download && go build
  3. Set environment variables:

    ENV MY_APP_ENV=production
    ENV MY_APP_PORT=8080
  4. Expose ports:

    EXPOSE 8080
  5. Set the correct command:

    CMD ["node", "server.js"]

How It Works

  1. Reads packages from devbox.json (excluding PostgreSQL)
  2. Extracts commit hashes from devbox.lock for each package
  3. Generates multi-stage Dockerfile with separate stages per package
  4. Creates Nix profile that symlinks all binaries to a standard location
  5. Sets up environment with proper PATH and locale settings
  6. Adds TODO sections for application-specific customization

Building the Generated Dockerfile

# Generate the Dockerfile
./generate-dockerfile.sh

# Build the Docker image
docker build -t my-app -f Dockerfile.generated .

# Test the image
docker run --rm my-app node --version

Notes

  • Uses nixery.dev for package sources with specific commit hashes
  • Generated Dockerfile follows Nix best practices with profile management
  • All packages maintain their exact versions from devbox.lock
  • The script is language-agnostic (works for any devbox setup)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment