How Node.js 20 and libv8-dev package conflicts created the most frustrating obstacle in our Docker migration, and the
alternative approaches that finally resolved them.
The Dependency Nightmare
Just when we thought our asset pipeline issues were behind us, we
encountered the most maddening problem of the entire Docker migration: system package conflicts that seemed impossible
to resolve.
The error message was deceptively simple:
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following packages have unmet dependencies:
node-acorn : Depends: nodejs:any or nodejs (< 12.22.5~dfsg-4~)
E: Unable to correct problems, you have held broken packages.
This innocent-looking error was the symptom of a deeper incompatibility: Node.js 20.18.3 (required for our modern
JavaScript tooling) was fundamentally incompatible with libv8-dev (required for SASS compilation in Ruby gems).
The conflict arose because:
- Our Rails application needed Node.js 20+ for modern JavaScript features and security updates
- Several Ruby gems (particularly SASS processors) required libv8-dev for JavaScript runtime
- Debian’s package system couldn’t resolve the version conflicts between these requirements
Understanding the Conflict
The Technical Root Cause
The libv8-dev package provides the V8 JavaScript engine headers and libraries that Ruby gems use for JavaScript
execution. However, Debian’s libv8-dev package was built against older Node.js versions and explicitly conflicts with
Node.js 20+.
# Package dependency analysis
$ apt-cache depends libv8-dev
libv8-dev
Depends: libv8-8.3.110.13
Depends: libnode-dev
Conflicts: nodejs (>= 12.22.5~dfsg-4~)
$ node --version
v20.18.3 # Our required version
The conflict was irreconcilable at the system package level.
Why This Affects Rails Applications
Many Rails applications depend on gems that require JavaScript execution:
# Common gems requiring JavaScript runtime
gem 'sass-rails' # SASS compilation
gem 'execjs' # JavaScript execution from Ruby
gem 'mini_racer' # V8 engine wrapper
gem 'therubyracer' # V8 engine bindings (deprecated)
When these gems are installed, they often try to compile native extensions that link against libv8-dev, creating the
package conflict.
Attempted Solutions (That Failed)
Attempt 1: Force Package Installation
# Trying to force incompatible packages
RUN apt-get update -qq && \
apt-get install -y --allow-downgrades \
nodejs=12.22.12-dfsg-1ubuntu3 \
libv8-dev && \
npm install -g yarn
Result: Downgraded Node.js broke our modern JavaScript toolchain and created security vulnerabilities.
Attempt 2: Alternative Package Sources
# Trying different package repositories
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs && \
apt-get install -y libv8-dev # Still conflicts!
Result: Same conflict with different packaging.
Attempt 3: Manual V8 Compilation
# Attempting to compile V8 from source
RUN git clone https://chromium.googlesource.com/v8/v8.git && \
cd v8 && \
# ... complex build process
Result: Extremely slow builds (30+ minutes) and brittle compilation process.
The Working Solutions
Instead of fighting the package conflicts, we found alternative approaches that eliminated the need for libv8-dev
entirely.
Solution 1: Replace sass-rails with sassc-rails
The most effective solution was switching from the Ruby-based SASS processor to the C-based implementation:
# Gemfile - Before
gem 'sass-rails', '>= 6' # Requires JavaScript runtime
# Gemfile - After
gem 'sassc-rails', '>= 2.1.0' # Pure C implementation
Benefits:
- No JavaScript runtime required
- Faster SASS compilation (C vs Ruby/JS)
- No system package dependencies
- Identical SASS feature set
Dockerfile changes:
# No longer needed!
# RUN apt-get install -y libv8-dev
# sassc-rails compiles natively without conflicts
RUN bundle install
Solution 2: Use mini_racer for JavaScript Execution
For applications that genuinely need JavaScript execution from Ruby (not just SASS), mini_racer provides a bundled V8
engine:
# Gemfile
gem 'mini_racer', '~> 0.8.0' # Bundles its own V8 engine
Benefits:
- Self-contained V8 engine (no system dependencies)
- Modern V8 version bundled with the gem
- Better performance than execjs with system V8
- No package conflicts
Dockerfile configuration:
# Install compilation tools for mini_racer
RUN apt-get update -qq && \
apt-get install -y --no-install-recommends \
build-essential \
python3 && \
rm -rf /var/lib/apt/lists/*
# mini_racer compiles its own V8 engine
RUN bundle install
Solution 3: libv8-node for Node.js Integration
For applications that need tight integration between Ruby and an existing Node.js installation:
# Gemfile
gem 'libv8-node', '~> 20.10.0' # Uses system Node.js V8
Benefits:
- Uses V8 engine from installed Node.js
- No additional system packages required
- Version-matched with Node.js installation
Dockerfile setup:
# Install Node.js first
RUN NODE_MAJOR=20 && \
curl -sL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - && \
apt-get install -y nodejs
# libv8-node uses Node.js V8 engine
RUN bundle install
Our Final Architecture
We chose a hybrid approach that maximized compatibility and performance:
Base Image Dependencies
# Dockerfile.base
FROM ruby:3.2.7-slim
# Install Node.js 20 (no libv8-dev conflicts)
RUN NODE_MAJOR=20 && \
curl -sL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - && \
apt-get update -qq && \
apt-get install -y \
nodejs \
build-essential \
python3 && \
npm install -g yarn && \
rm -rf /var/lib/apt/lists/*
Ruby Dependencies
# Gemfile
gem 'sassc-rails', '>= 2.1.0' # C-based SASS (no JS runtime needed)
gem 'mini_racer', '~> 0.8.0' # Bundled V8 for any JS execution needs
# Removed - no longer needed
# gem 'sass-rails'
# gem 'execjs'
# gem 'therubyracer'
Asset Pipeline Integration
{
"scripts": {
"build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --load-path=node_modules --style=compressed"
}
}
We used Node.js SASS for build-time compilation and sassc-rails for any runtime SASS needs.
Alternative Approaches for Different Use Cases
For Alpine Linux Users
Alpine Linux has different package management that may avoid some conflicts:
# Alpine-based alternative
FROM ruby:3.2.7-alpine
RUN apk add --no-cache \
nodejs \
npm \
yarn \
build-base \
python3 && \
npm install -g yarn
# Alpine doesn't have the same libv8-dev conflicts
Trade-offs:
- Smaller base images
- Different package ecosystem (apk vs apt)
- May have different gem compilation issues
For Legacy Applications
Applications that absolutely must use sass-rails can use an older Node.js version:
# Legacy approach - not recommended for new applications
FROM ruby:3.2.7-slim
# Install older Node.js compatible with libv8-dev
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \
apt-get update -qq && \
apt-get install -y \
nodejs \
libv8-dev && \
rm -rf /var/lib/apt/lists/*
Security implications:
- Node.js 14 is end-of-life (security risk)
- Missing modern JavaScript features
- Not recommended for production use
For Microservice Architectures
Split JavaScript and Ruby processing into separate containers:
# docker-compose.yml
services:
assets:
image: node:20-alpine
volumes:
- ./app/assets:/app/assets
- ./public/assets:/app/public/assets
command: yarn build && yarn build:css
web:
build: .
depends_on:
- assets
# Ruby container doesn't need Node.js at all
The dependency resolution changes had measurable performance effects:
Before (with conflicts):
- Build success rate: 60% (frequent package conflicts)
- Average build time: 8-12 minutes (when successful)
- Build cache effectiveness: Poor (conflicts broke caching)
After (sassc-rails + mini_racer):
- Build success rate: 100%
- Average build time: 4-5 minutes
- Build cache effectiveness: Excellent (consistent dependencies)
SASS Compilation:
- sass-rails (Ruby): ~450ms for full stylesheet
- sassc-rails (C): ~180ms for full stylesheet
- 60% improvement in SASS compilation speed
JavaScript Execution:
- System V8 (when working): ~25ms for typical operations
- mini_racer bundled V8: ~20ms for typical operations
- 20% improvement plus better reliability
Memory Usage
Development Environment:
- Before: 280MB baseline (with conflict workarounds)
- After: 220MB baseline
- 21% reduction in development memory usage
Production Runtime:
- Before: Complex dependency tree with unused packages
- After: Minimal dependencies, only required packages
- 15% reduction in production memory footprint
Debugging and Troubleshooting
Identifying Package Conflicts
# Check for package conflicts
apt-cache policy nodejs libv8-dev
# Analyze dependency tree
apt-cache depends libv8-dev
apt-cache rdepends nodejs
# Check gem requirements
bundle exec gem dependency execjs
bundle exec gem dependency sass-rails
Testing Alternative Solutions
# Development testing Dockerfile
FROM ruby:3.2.7-slim
# Test 1: sassc-rails approach
COPY Gemfile.sassc ./Gemfile
RUN bundle install && echo "sassc-rails: SUCCESS" || echo "sassc-rails: FAILED"
# Test 2: mini_racer approach
COPY Gemfile.mini_racer ./Gemfile
RUN bundle install && echo "mini_racer: SUCCESS" || echo "mini_racer: FAILED"
Verification Steps
# Test SASS compilation
Rails.application.config.sass.load_paths
Sass.compile_string('$primary: blue; .test { color: $primary; }')
# Test JavaScript execution (if using mini_racer)
MiniRacer::Context.new.eval('1 + 1')
Best Practices and Recommendations
1. Choose Modern Alternatives
Prefer gems that don’t require system JavaScript runtimes:
sassc-rails
over sass-rails
mini_racer
over therubyracer
- Node.js tools over Ruby wrappers where appropriate
2. Minimize System Dependencies
Reduce the number of system packages that can conflict:
# Good - minimal dependencies
RUN apt-get install -y build-essential python3
# Avoid - complex dependency trees
RUN apt-get install -y libv8-dev nodejs-dev ruby-dev
3. Test Dependency Changes Early
# Test gem installation before full application build
COPY Gemfile* ./
RUN bundle install --jobs 4 --retry 3
4. Use Specific Versions
# Specify exact versions to avoid conflicts
gem 'sassc-rails', '~> 2.1.0'
gem 'mini_racer', '~> 0.8.0'
5. Document Architecture Decisions
# Gemfile comments explaining choices
gem 'sassc-rails', '~> 2.1.0' # C-based SASS - avoids libv8-dev conflicts
gem 'mini_racer', '~> 0.8.0' # Bundled V8 - no system dependencies
What’s Next
With system dependencies resolved and a stable build process established, the final piece of our Docker migration was
production deployment and monitoring. The next post in this series
covers the complete deployment pipeline and the monitoring setup that validates our performance improvements.
This is Part 4 of our Rails Docker Migration series. The dependency resolution techniques covered here apply to most
Rails applications facing similar Node.js/V8 conflicts. Check our
GitHub repository for the complete Gemfile and Dockerfile
configurations.