Introduction
Containerization has become essential for reproducible bioinformatics workflows. While Docker is widely used in development and cloud environments, Singularity (now Apptainer) is the preferred container runtime in HPC clusters due to security and performance requirements. Many workflows need to support both platforms, making it crucial to create Docker images that can be seamlessly converted and used with Singularity.
This guide provides best practices for creating Docker images that are fully compatible with Singularity, ensuring your containers work reliably across both Docker and HPC environments. We’ll cover compatibility considerations, Dockerfile design patterns, common issues, and testing strategies.
Why Singularity Compatibility Matters
Singularity/Apptainer is designed for HPC environments where:
- No root access: Users cannot run Docker daemon
- Security: Containers run as the user, not root
- Performance: Native performance with minimal overhead
- File system integration: Seamless access to host file systems
Key Challenge: While Singularity can pull Docker images directly using the docker:// prefix, not all Docker images work correctly when converted. Understanding the differences and following best practices ensures your images work in both environments.
Docker vs. Singularity: Key Differences
| Aspect | Docker | Singularity/Apptainer |
|---|---|---|
| User context | Runs as root (by default) | Runs as the host user |
| File permissions | Root-owned files | User-owned files |
| Volume mounts | Explicit -v flag |
Automatic bind mounts |
| Environment variables | Inherited from host | Merged with container |
| Entrypoint/CMD | Can be overridden | Can be overridden |
| Network | Isolated network | Uses host network |
| Device access | Limited | Full access to host devices |
Core Principles for Singularity-Compatible Docker Images
1. Avoid Root-Only Operations
Problem: Docker images often create files as root, which causes permission issues in Singularity (where containers run as the user).
Solution: Use non-root users and proper file permissions.
# ❌ Bad: Creates root-owned files
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y tool
RUN echo "data" > /data/output.txt
# ✅ Good: Use non-root user
FROM ubuntu:22.04
# Create a non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Install tools
RUN apt-get update && apt-get install -y tool && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create directories with proper permissions
RUN mkdir -p /data && chown -R appuser:appuser /data
# Switch to non-root user
USER appuser
WORKDIR /data
Best Practice: Always create and use a non-root user for production images.
2. Use Absolute Paths for Executables
Problem: Relative paths and PATH-dependent execution can fail in Singularity.
Solution: Use absolute paths or ensure PATH is properly set.
# ❌ Bad: Relative paths
RUN ./configure && make && make install
# ✅ Good: Absolute paths or explicit PATH
ENV PATH="/usr/local/bin:${PATH}"
RUN /usr/local/bin/configure && \
make && \
make install
# Or ensure tools are in standard locations
RUN ./configure --prefix=/usr/local && \
make && \
make install
3. Handle Environment Variables Correctly
Problem: Environment variables set in Dockerfiles may not persist or merge correctly in Singularity.
Solution: Use ENV for persistent variables and provide defaults.
# ✅ Good: Explicit environment variables
ENV TOOL_VERSION=1.2.3
ENV PATH="/opt/tool/bin:${PATH}"
ENV LD_LIBRARY_PATH="/opt/tool/lib:${LD_LIBRARY_PATH}"
# Make environment variables available at runtime
ENV TOOL_CONFIG="/opt/tool/config"
Note: Singularity will merge these with host environment variables, so avoid overriding critical system variables.
4. Avoid Hardcoded User IDs
Problem: Hardcoded UIDs/GIDs can conflict with host user IDs in Singularity.
Solution: Use symbolic user names or make UIDs configurable.
# ❌ Bad: Hardcoded UID
RUN useradd -u 1000 appuser
# ✅ Good: Let system assign UID or use high-range UID
RUN useradd -r -u 9001 -g appuser appuser
# Or better: Use symbolic name (Singularity will map to host user)
RUN groupadd -r appuser && useradd -r -g appuser appuser
5. Design for Read-Only Containers
Problem: Some Docker images write to system directories, which may be read-only in Singularity.
Solution: Write to user-writable directories or use bind mounts.
# ✅ Good: Write to user-writable locations
ENV HOME=/home/appuser
WORKDIR /home/appuser
# Create writable directories
RUN mkdir -p /home/appuser/{data,output,scratch} && \
chown -R appuser:appuser /home/appuser
# Use /tmp for temporary files (usually writable in Singularity)
ENV TMPDIR=/tmp
Dockerfile Best Practices
Minimal Base Images
Start with minimal base images to reduce size and attack surface:
# ✅ Good: Minimal base image
FROM python:3.9-slim
# ❌ Avoid: Full OS image
FROM ubuntu:22.04
Recommended base images:
python:3.9-slim(Python applications)debian:bullseye-slim(General purpose)alpine:latest(Ultra-minimal, but may have compatibility issues)biocontainers/base(Bioinformatics-specific)
Multi-Stage Builds
Use multi-stage builds to keep final images small:
# Build stage
FROM python:3.9-slim as builder
WORKDIR /build
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
g++ \
make \
&& \
rm -rf /var/lib/apt/lists/*
# Install Python packages
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Runtime stage
FROM python:3.9-slim
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy installed packages from builder
COPY --from=builder /root/.local /home/appuser/.local
# Set up environment
ENV PATH="/home/appuser/.local/bin:${PATH}"
ENV PYTHONPATH="/home/appuser/.local/lib/python3.9/site-packages:${PYTHONPATH}"
# Switch to non-root user
USER appuser
WORKDIR /home/appuser
# Verify installation
RUN python -c "import tool; print(tool.__version__)"
Layer Optimization
Combine related operations in single RUN commands to reduce layers:
# ❌ Bad: Multiple layers
RUN apt-get update
RUN apt-get install -y tool1
RUN apt-get install -y tool2
RUN apt-get clean
# ✅ Good: Single layer with cleanup
RUN apt-get update && \
apt-get install -y --no-install-recommends \
tool1 \
tool2 \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Proper Entrypoint Design
Design entrypoints that work in both Docker and Singularity:
# ✅ Good: Flexible entrypoint script
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["tool"]
# entrypoint.sh content:
#!/bin/bash
set -euo pipefail
# Handle both direct tool execution and command passthrough
if [ "$#" -eq 0 ] || [ "$1" = "tool" ]; then
exec /usr/local/bin/tool "${@:2}"
else
exec "$@"
fi
Version Pinning
Pin all versions for reproducibility:
# ✅ Good: Pinned versions
FROM python:3.9.16-slim
ENV TOOL_VERSION=1.2.3
# ❌ Bad: Latest tags
FROM python:latest
ENV TOOL_VERSION=latest
Common Issues and Solutions
Issue 1: Permission Denied Errors
Symptom: Files created in Docker are owned by root, causing permission errors in Singularity.
Solution:
# Create directories with proper ownership
RUN mkdir -p /data && \
chown -R appuser:appuser /data
# Or use world-writable directories for shared data
RUN mkdir -p /shared && \
chmod 777 /shared
Runtime fix (if you can’t modify the image):
# In Singularity, bind mount to writable location
singularity exec --bind /host/data:/data image.sif tool --input /data/input
Issue 2: Missing Libraries or Dependencies
Symptom: Tools fail with “library not found” errors in Singularity.
Solution: Ensure all dependencies are included and library paths are set:
# Install all runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libc6 \
libstdc++6 \
libgcc-s1 \
ca-certificates \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Set library paths
ENV LD_LIBRARY_PATH="/usr/local/lib:${LD_LIBRARY_PATH}"
Issue 3: Environment Variable Conflicts
Symptom: Environment variables from host override container settings.
Solution: Use SINGULARITYENV_ prefix or design for variable merging:
# Set defaults that can be overridden
ENV TOOL_CONFIG="/opt/tool/default.conf"
ENV TOOL_DATA="/opt/tool/data"
Runtime (if needed):
# Override in Singularity
export SINGULARITYENV_TOOL_CONFIG=/host/path/config
singularity exec image.sif tool
Issue 4: Network Access Issues
Symptom: Tools can’t access network resources in Singularity.
Solution: Design for host network (Singularity’s default):
# Don't assume isolated network
# Use host-accessible URLs or provide configuration
ENV API_URL="http://localhost:8080"
Note: Singularity uses host network by default, which is usually fine but may differ from Docker’s isolated network.
Issue 5: Device Access
Symptom: Tools can’t access GPUs or other devices.
Solution: Use --nv flag for NVIDIA GPUs or --containall for isolation:
# GPU access in Singularity
singularity exec --nv image.sif tool --gpu
# Device access
singularity exec --device /dev/device image.sif tool
Complete Example: Bioinformatics Tool Container
Here’s a complete example of a Singularity-compatible Docker image for a bioinformatics tool:
# Multi-stage build for bioinformatics tool
FROM python:3.9.16-slim as builder
WORKDIR /build
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
g++ \
make \
zlib1g-dev \
libbz2-dev \
liblzma-dev \
curl \
&& \
rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Install tool from source
RUN curl -L https://github.com/org/tool/releases/download/v1.2.3/tool-1.2.3.tar.gz | \
tar -xz && \
cd tool-1.2.3 && \
./configure --prefix=/usr/local && \
make && \
make install
# Runtime stage
FROM python:3.9.16-slim
# Metadata
LABEL maintainer="Your Name <email@example.com>"
LABEL version="1.2.3"
LABEL description="Bioinformatics tool container for variant analysis"
# Create non-root user
RUN groupadd -r appuser && \
useradd -r -g appuser -u 9001 appuser && \
mkdir -p /home/appuser/{data,output,scratch} && \
chown -R appuser:appuser /home/appuser
# Install runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libc6 \
libstdc++6 \
zlib1g \
libbz2-1.0 \
liblzma5 \
ca-certificates \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Copy installed packages and tool from builder
COPY --from=builder /root/.local /home/appuser/.local
COPY --from=builder /usr/local/bin/tool /usr/local/bin/tool
COPY --from=builder /usr/local/lib/libtool* /usr/local/lib/
# Set up environment
ENV PATH="/home/appuser/.local/bin:/usr/local/bin:${PATH}"
ENV LD_LIBRARY_PATH="/usr/local/lib:${LD_LIBRARY_PATH}"
ENV PYTHONPATH="/home/appuser/.local/lib/python3.9/site-packages:${PYTHONPATH}"
ENV HOME=/home/appuser
ENV TMPDIR=/tmp
# Create entrypoint script
RUN cat > /usr/local/bin/entrypoint.sh << 'EOF' && \
chmod +x /usr/local/bin/entrypoint.sh
#!/bin/bash
set -euo pipefail
# Change to user's working directory if mounted
if [ -d "/home/appuser/data" ]; then
cd /home/appuser/data
fi
# Execute command
if [ "$#" -eq 0 ]; then
exec /usr/local/bin/tool --help
elif [ "$1" = "tool" ]; then
exec /usr/local/bin/tool "${@:2}"
else
exec "$@"
fi
EOF
# Switch to non-root user
USER appuser
WORKDIR /home/appuser
# Verify installation
RUN tool --version && \
python -c "import tool; print('Tool Python module loaded')"
# Set entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["tool"]
Build and test:
# Build Docker image
docker build -t tool:1.2.3 -f Dockerfile .
# Test in Docker
docker run --rm tool:1.2.3 tool --version
# Convert to Singularity
singularity build tool.sif docker://tool:1.2.3
# Test in Singularity
singularity exec tool.sif tool --version
Testing Strategies
1. Test in Both Environments
Always test your Docker image in both Docker and Singularity:
#!/bin/bash
# test_container.sh
IMAGE="tool:1.2.3"
SIF="tool.sif"
echo "Testing Docker image..."
docker run --rm ${IMAGE} tool --version
docker run --rm ${IMAGE} tool --help
echo "Converting to Singularity..."
singularity build ${SIF} docker://${IMAGE}
echo "Testing Singularity image..."
singularity exec ${SIF} tool --version
singularity exec ${SIF} tool --help
echo "Testing with user context..."
singularity exec ${SIF} whoami
singularity exec ${SIF} id
2. Test File Permissions
Verify files can be created and written:
# Test file creation in Singularity
singularity exec tool.sif bash -c "touch /tmp/test.txt && ls -l /tmp/test.txt"
# Test in user directory
singularity exec --bind $(pwd):/data tool.sif bash -c "cd /data && touch test.txt && ls -l test.txt"
3. Test Environment Variables
Verify environment variables work correctly:
# Test environment variable inheritance
export TEST_VAR="host_value"
singularity exec --env TEST_VAR tool.sif bash -c "echo \$TEST_VAR"
# Test container environment variables
singularity exec tool.sif env | grep TOOL
4. Test Network Access
Verify network functionality:
# Test network access
singularity exec tool.sif curl -I https://www.example.com
# Test API access (if applicable)
singularity exec tool.sif tool --api-url https://api.example.com status
5. Test GPU Access (if applicable)
# Test NVIDIA GPU access
singularity exec --nv tool.sif nvidia-smi
# Test CUDA functionality
singularity exec --nv tool.sif tool --gpu --test
Integration with Nextflow
Module Configuration
In Nextflow modules, handle both Docker and Singularity:
process TOOL {
container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ?
'docker://quay.io/biocontainers/tool:1.2.3--build' :
'quay.io/biocontainers/tool:1.2.3--build' }"
input:
path input_file
output:
path output_file
script:
"""
tool --input ${input_file} --output ${output_file}
"""
}
Custom Image Configuration
For custom images:
process TOOL {
container "${ workflow.containerEngine == 'singularity' ?
'docker://docker.io/username/tool:1.2.3' :
'docker.io/username/tool:1.2.3' }"
// Ensure proper working directory
cwd "${workDir}"
input:
path input_file
output:
path output_file
script:
"""
# Use absolute paths or ensure working directory is set
tool --input ${input_file} --output ${output_file}
"""
}
Checklist for Singularity-Compatible Docker Images
Use this checklist when creating Docker images for Singularity compatibility:
Dockerfile Design
- Non-root user: Create and use a non-root user
- Proper permissions: Set correct file/directory permissions
- Absolute paths: Use absolute paths for executables and libraries
- Environment variables: Set ENV variables with defaults
- Minimal base: Use minimal base images (slim, alpine)
- Layer optimization: Combine RUN commands to reduce layers
- Version pinning: Pin all versions (base image, tools, dependencies)
- Cleanup: Remove build dependencies and cache in same layer
Compatibility
- No hardcoded UIDs: Avoid hardcoded user/group IDs
- Writable directories: Create user-writable directories
- Library paths: Set LD_LIBRARY_PATH for custom libraries
- Entrypoint flexibility: Design entrypoint to handle both direct execution and command passthrough
- Network assumptions: Don’t assume isolated network
- Device access: Document GPU/device requirements if applicable
Testing
- Docker test: Test image in Docker environment
- Singularity conversion: Successfully convert to Singularity
- Singularity execution: Test tool execution in Singularity
- File permissions: Verify file creation/writing works
- Environment variables: Test variable inheritance and merging
- Network access: Verify network functionality
- Nextflow integration: Test with Nextflow workflows
Documentation
- README: Document image contents and usage
- Version tags: Use semantic versioning for tags
- Build instructions: Provide build commands
- Usage examples: Include Docker and Singularity examples
- Known issues: Document any limitations or workarounds
Advanced Topics
Handling Conda Environments
If your tool uses Conda, ensure compatibility:
FROM continuumio/miniconda3:4.12.0
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Install conda packages
RUN conda install -y -c bioconda \
tool=1.2.3 \
dependency1=2.0.0 \
&& \
conda clean -afy
# Set up environment
ENV PATH="/opt/conda/bin:${PATH}"
# Fix permissions
RUN chown -R appuser:appuser /opt/conda
USER appuser
Handling R Packages
For R-based tools:
FROM r-base:4.2.2
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Install R packages as user
USER appuser
RUN R -e "install.packages(c('package1', 'package2'), repos='https://cloud.r-project.org')"
# Set R library path
ENV R_LIBS_USER="/home/appuser/R/x86_64-pc-linux-gnu-library/4.2"
Multi-Architecture Support
Build for multiple architectures:
# Build for AMD64 and ARM64
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t tool:1.2.3 \
--push \
-f Dockerfile .
Note: Test Singularity conversion for each architecture separately.
Troubleshooting Guide
Problem: “Permission denied” when writing files
Solution:
# Ensure directories are writable
RUN mkdir -p /data && chmod 777 /data
# Or use user's home directory
ENV HOME=/home/appuser
WORKDIR /home/appuser
Problem: “Command not found” in Singularity
Solution:
# Use absolute paths
ENV PATH="/usr/local/bin:${PATH}"
# Verify in Dockerfile
RUN which tool && tool --version
Problem: Library loading errors
Solution:
# Set library paths
ENV LD_LIBRARY_PATH="/usr/local/lib:${LD_LIBRARY_PATH}"
# Include all required libraries
RUN ldd /usr/local/bin/tool
Problem: Environment variables not working
Solution:
# Use ENV for persistent variables
ENV TOOL_CONFIG="/opt/tool/config"
# Test in container
RUN echo $TOOL_CONFIG
Summary
Creating Docker images that work seamlessly with Singularity requires attention to:
- User context: Design for non-root execution
- File permissions: Ensure writable directories
- Path handling: Use absolute paths and proper PATH setup
- Environment variables: Set defaults that can be overridden
- Testing: Test in both Docker and Singularity environments
Following these best practices ensures your containers work reliably across both Docker and HPC environments, enabling reproducible workflows that can run anywhere.
Comments