P03: OpenRC Service Migration
Project Goal: Convert systemd service files to OpenRC init scripts, gaining deep understanding of both init systems and mastering service management on Alpine Linux.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Intermediate |
| Time Estimate | 1 week |
| Main Language | Shell (POSIX sh) |
| Knowledge Area | Init Systems / Service Management |
| Software Required | Alpine Linux (VM or Docker), OpenRC |
| Prerequisites | Basic Linux administration, shell scripting fundamentals |
| Produces | A migration toolkit with converted service scripts and documentation |
Learning Objectives
After completing this project, you will be able to:
- Explain the fundamental philosophical differences between systemd and OpenRC init systems
- Parse and understand systemd unit files including all common directives
- Write production-quality OpenRC init scripts from scratch
- Handle service dependencies correctly in both systems
- Implement service supervision and automatic restart in OpenRC
- Debug service startup issues on Alpine Linux
- Migrate complex multi-service applications from systemd-based distributions to Alpine
Theoretical Foundation
Core Concepts
What is an Init System?
An init system is the first process started by the Linux kernel (PID 1). It has three primary responsibilities:
Kernel Boot Complete
│
▼
┌───────────────────────────────────────────────────────┐
│ INIT SYSTEM (PID 1) │
├───────────────────────────────────────────────────────┤
│ │
│ 1. SYSTEM INITIALIZATION │
│ ┌─────────────────────────────────────────────┐ │
│ │ • Mount filesystems │ │
│ │ • Set hostname │ │
│ │ • Configure networking │ │
│ │ • Start system services │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 2. SERVICE MANAGEMENT │
│ ┌─────────────────────────────────────────────┐ │
│ │ • Start/stop/restart services │ │
│ │ • Track running services │ │
│ │ • Handle dependencies │ │
│ │ • Supervise processes │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 3. PROCESS REAPING │
│ ┌─────────────────────────────────────────────┐ │
│ │ • Adopt orphaned processes │ │
│ │ • Clean up zombie processes │ │
│ │ • Handle SIGCHLD signals │ │
│ └─────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────┘
systemd vs OpenRC: Fundamental Philosophy
┌─────────────────────────────────────────────────────────────────────────┐
│ INIT SYSTEM PHILOSOPHY COMPARISON │
├────────────────────────────────┬────────────────────────────────────────┤
│ systemd │ OpenRC │
├────────────────────────────────┼────────────────────────────────────────┤
│ │ │
│ MONOLITHIC │ MODULAR │
│ ┌──────────────────────┐ │ ┌──────────────────────┐ │
│ │ One binary does: │ │ │ Separate tools: │ │
│ │ • Init │ │ │ • openrc (init) │ │
│ │ • Service mgmt │ │ │ • rc-service │ │
│ │ • Logging (journald) │ │ │ • rc-update │ │
│ │ • Device mgmt │ │ │ • syslog (logging) │ │
│ │ • Cron replacement │ │ │ • standard tools │ │
│ │ • Network config │ │ │ │ │
│ └──────────────────────┘ │ └──────────────────────┘ │
│ │ │
│ DECLARATIVE CONFIG │ IMPERATIVE SCRIPTS │
│ ┌──────────────────────┐ │ ┌──────────────────────┐ │
│ │ [Unit] │ │ │ #!/sbin/openrc-run │ │
│ │ Description=... │ │ │ │ │
│ │ [Service] │ │ │ start() { │ │
│ │ ExecStart=... │ │ │ # shell code │ │
│ │ [Install] │ │ │ } │ │
│ │ WantedBy=... │ │ │ │ │
│ └──────────────────────┘ │ └──────────────────────┘ │
│ │ │
│ SOCKET ACTIVATION │ CLASSIC DAEMON MODEL │
│ Service starts on demand │ Service starts at boot │
│ │ │
│ BINARY LOGS (journald) │ TEXT LOGS (syslog) │
│ journalctl -u nginx │ /var/log/messages │
│ │ │
│ CGROUPS INTEGRATION │ TRADITIONAL PROCESS TRACKING │
│ Built-in resource control │ PID files │
│ │ │
└────────────────────────────────┴────────────────────────────────────────┘
OpenRC Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ OpenRC ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ /etc/ │
│ ├── init.d/ SERVICE SCRIPTS │
│ │ ├── nginx ────► #!/sbin/openrc-run │
│ │ ├── postgresql start() { ... } │
│ │ ├── sshd stop() { ... } │
│ │ └── ... │
│ │ │
│ ├── conf.d/ SERVICE CONFIGURATION │
│ │ ├── nginx ────► NGINX_OPTS="--some-option" │
│ │ ├── postgresql Variables sourced by init scripts │
│ │ └── ... │
│ │ │
│ ├── runlevels/ RUNLEVEL DIRECTORIES │
│ │ ├── sysinit/ ────► System initialization │
│ │ │ └── devfs -> ... (mount /dev, etc.) │
│ │ │ │
│ │ ├── boot/ ────► Boot-time services │
│ │ │ ├── hostname -> ... (networking, hostname) │
│ │ │ └── networking -> ... │
│ │ │ │
│ │ ├── default/ ────► Normal operation │
│ │ │ ├── nginx -> /etc/init.d/nginx (most services) │
│ │ │ ├── sshd -> /etc/init.d/sshd │
│ │ │ └── crond -> /etc/init.d/crond │
│ │ │ │
│ │ └── shutdown/ ────► Shutdown sequence │
│ │ └── savecache -> ... (cleanup, unmount) │
│ │ │
│ └── rc.conf GLOBAL OPENRC CONFIGURATION │
│ rc_parallel="YES" │
│ rc_logger="YES" │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Dependency System Comparison
┌─────────────────────────────────────────────────────────────────────────┐
│ DEPENDENCY HANDLING COMPARISON │
├──────────────────────────────────┬──────────────────────────────────────┤
│ systemd │ OpenRC │
├──────────────────────────────────┼──────────────────────────────────────┤
│ │ │
│ [Unit] │ depend() { │
│ After=network.target │ after net │
│ Requires=postgresql.service │ need postgresql │
│ Wants=redis.service │ use redis │
│ Before=nginx.service │ before nginx │
│ Conflicts=apache2.service │ # (conflicts via keywords) │
│ │ } │
│ │ │
│ KEYWORD MEANINGS: │ KEYWORD MEANINGS: │
│ ───────────────── │ ───────────────── │
│ After = ordering only │ after = ordering only │
│ Before = reverse ordering │ before = reverse ordering │
│ Requires = hard dependency │ need = hard dependency │
│ Wants = soft dependency │ use = soft dependency │
│ Conflicts= mutual exclusion │ provide = virtual service │
│ │ keyword = tags for grouping │
│ │ │
└──────────────────────────────────┴──────────────────────────────────────┘
Why This Matters
Understanding init systems is critical for several reasons:
- Operational Reality: If you deploy to Alpine Linux (common in Docker), you MUST understand OpenRC
- Debugging: Service startup failures are common; understanding the init system is essential for debugging
- Security: Init systems are PID 1 - a compromise here compromises everything
- Portability: Writing services that work across distributions requires understanding both systems
- System Design: Init systems reveal how Unix systems are structured and how services interact
Historical Context
Timeline of Init Systems
────────────────────────────────────────────────────────────────────────────
1983 1999 2004 2010 2014
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────┐ ┌───────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐
│SysV │ │daemontools│ │ OpenRC │ │ systemd │ │ Alpine │
│init │ │(DJB) │ │(Gentoo) │ │(Lennart │ │uses │
│ │ │ │ │ │ │Poettering)│ │OpenRC │
└─────┘ └───────────┘ └──────────┘ └─────────┘ │widely │
│ │ │ └─────────┘
│ Sequential │ Dependency │
│ Shell scripts │ based │ Parallel
│ /etc/rc.d/ │ Shell scripts│ Socket activation
│ │ /etc/init.d/ │ Binary config
│ │ │ /lib/systemd/system/
└──────────────────────────┴──────────────┴───────────────────────►
Why OpenRC survived:
• Simplicity - shell scripts are readable and debuggable
• Portability - works on BSD, Linux, any POSIX system
• Minimalism - no extra daemons, no binary logs
• Philosophy - "do one thing well" Unix approach
• Alpine Linux - needed something simpler than systemd
Common Misconceptions
| Misconception | Reality |
|---|---|
| “OpenRC is outdated” | OpenRC is actively maintained and supports modern features like cgroups v2, supervise-daemon |
| “systemd is required for modern Linux” | Many production systems run OpenRC (Alpine containers, Gentoo servers) |
| “OpenRC can’t do process supervision” | supervise-daemon (Alpine 3.9+) provides robust supervision |
| “Converting services is just syntax translation” | Dependency semantics differ; direct translation can cause boot failures |
| “OpenRC scripts must define start/stop functions” | The openrc-run helper provides defaults; you only override what you need |
Project Specification
What You Will Build
A comprehensive systemd-to-OpenRC migration toolkit consisting of:
- Migration Scripts: Automated converters for common systemd patterns
- Example Conversions: 5+ real-world service conversions with detailed explanations
- Validation Tools: Scripts to verify converted services work correctly
- Documentation: A reference guide for future migrations
Functional Requirements
- Parse systemd unit files and extract:
- Service description
- Executable and arguments
- User/group settings
- Dependencies (After, Before, Requires, Wants)
- Restart behavior
- Environment variables
- Working directory
- Generate OpenRC init scripts that:
- Use proper OpenRC idioms (openrc-run, not raw shell)
- Handle dependencies correctly
- Support start/stop/restart/status
- Implement reload if the original supports it
- Use supervise-daemon for automatic restart
- Include configuration in /etc/conf.d/
- Convert at least 5 real services:
- A simple single-process daemon
- A service with database dependency
- A service requiring network
- A service with environment files
- A service with multiple exec lines
- Validate conversions by:
- Starting/stopping services
- Testing dependency ordering
- Verifying process ownership
- Checking log output
Non-Functional Requirements
- Scripts must be POSIX-compliant (work with ash, not require bash)
- All converted services must pass
rc-service <name> checkconfig(if available) - Documentation must explain each systemd directive mapping
- Converted services should be production-quality
Example Usage/Output
Converting a systemd service:
$ cat /lib/systemd/system/myapp.service
[Unit]
Description=My Application Server
After=network.target
Requires=postgresql.service
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /opt/myapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
$ ./convert-service.sh /lib/systemd/system/myapp.service
Converting: myapp.service
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Analysis:
Description: My Application Server
ExecStart: /usr/bin/node /opt/myapp/server.js
User/Group: myapp/myapp
Dependencies: network.target (→ net), postgresql.service (→ postgresql)
Restart: always with 5s delay (→ supervise-daemon)
Type: simple (→ command_background=true)
Generated files:
┌─ /etc/init.d/myapp ─────────────────────────────────────────────────────
│ #!/sbin/openrc-run
│
│ description="My Application Server"
│
│ depend() {
│ need net
│ need postgresql
│ }
│
│ command="/usr/bin/node"
│ command_args="/opt/myapp/server.js"
│ command_user="myapp:myapp"
│ directory="/opt/myapp"
│ pidfile="/run/${RC_SVCNAME}.pid"
│
│ # Use supervise-daemon for automatic restart
│ supervisor=supervise-daemon
│ respawn_delay=5
│ respawn_max=0
│
│ reload() {
│ ebegin "Reloading ${RC_SVCNAME}"
│ supervise-daemon ${RC_SVCNAME} --signal HUP
│ eend $?
│ }
└─────────────────────────────────────────────────────────────────────────
┌─ /etc/conf.d/myapp ─────────────────────────────────────────────────────
│ # Configuration for myapp service
│ # Sourced by /etc/init.d/myapp
│
│ # Environment variables
│ export NODE_ENV="production"
│
│ # Uncomment to override defaults:
│ # command_args="/opt/myapp/server.js --port 8080"
│ # respawn_delay=10
└─────────────────────────────────────────────────────────────────────────
Next steps:
1. Review generated files
2. sudo cp myapp /etc/init.d/
3. sudo cp myapp.conf /etc/conf.d/myapp
4. sudo chmod +x /etc/init.d/myapp
5. sudo rc-update add myapp default
6. sudo rc-service myapp start
Real World Outcome
Upon completion, you will have:
- Working toolkit: Scripts that automate 80% of service conversion
- Reference implementations: 5+ production-quality converted services
- Deep understanding: Know both init systems well enough to write services from scratch
- Debugging skills: Ability to troubleshoot service startup failures
Solution Architecture
High-Level Design
┌─────────────────────────────────────────────────────────────────────────┐
│ SERVICE MIGRATION TOOLKIT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ INPUT PROCESSING OUTPUT │
│ ───── ────────── ────── │
│ │
│ ┌─────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ systemd │ │ │ │ /etc/init.d/ │ │
│ │ .service │────────►│ Parser │ │ script │ │
│ │ file │ │ │ └──────────────┘ │
│ └─────────────┘ │ Extracts: │ ▲ │
│ │ • [Unit] │ │ │
│ │ • [Service] │ ┌──────────────┐ │
│ │ • [Install] │────────►│ Generator │────┤
│ │ │ │ │ │
│ └───────────────┘ │ Produces: │ │
│ │ • init.d │ │
│ │ • conf.d │ │
│ │ • docs │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ /etc/conf.d/ │ │
│ │ config │ │
│ └──────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ VALIDATION │ │
│ │ ┌──────────┐ ┌───────────────┐ ┌────────────┐ ┌──────────┐ │ │
│ │ │ Syntax │ │ Dependency │ │ Runtime │ │ Log │ │ │
│ │ │ Check │ │ Resolution │ │ Test │ │ Check │ │ │
│ │ └──────────┘ └───────────────┘ └────────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key Components
| Component | Purpose | Implementation |
|---|---|---|
| Parser | Extract data from systemd unit files | Shell script using sed/awk |
| Generator | Create OpenRC init scripts | Shell script with templates |
| Validator | Verify converted services | Shell scripts + rc-service |
| Examples | Real-world conversion examples | Directory of pairs (systemd/OpenRC) |
Data Structures
The parser extracts a structured representation:
Parsed Service Structure:
├── unit/
│ ├── description "My Application Server"
│ ├── after ["network.target", "postgresql.service"]
│ ├── before []
│ ├── requires ["postgresql.service"]
│ └── wants []
├── service/
│ ├── type "simple"
│ ├── user "myapp"
│ ├── group "myapp"
│ ├── exec_start "/usr/bin/node /opt/myapp/server.js"
│ ├── exec_reload "/bin/kill -HUP $MAINPID"
│ ├── restart "always"
│ ├── restart_sec "5"
│ ├── working_dir "/opt/myapp"
│ └── environment {"NODE_ENV": "production"}
└── install/
└── wanted_by ["multi-user.target"]
Algorithm Overview
Convert systemd to OpenRC:
1. PARSE systemd unit file
FOR each section [Unit], [Service], [Install]:
Extract key=value pairs
Handle multi-line values
Resolve variables
2. MAP dependencies
network.target → "net"
*.service → service name (without .service)
multi-user.target → "default" runlevel
Requires → "need"
Wants → "use"
After → "after"
Before → "before"
3. DETERMINE service type
Type=simple → command_background=true, pidfile
Type=forking → no command_background, pidfile from PIDFile=
Type=oneshot → no supervision, run once
Type=notify → command_background=true (supervise-daemon handles)
4. HANDLE restart behavior
Restart=always/on-failure → supervisor=supervise-daemon
RestartSec=N → respawn_delay=N
5. GENERATE init script
Write /etc/init.d/<name> with:
- Shebang: #!/sbin/openrc-run
- description
- depend() function
- command, command_args, command_user
- supervisor settings if restart enabled
6. GENERATE conf.d file
Write /etc/conf.d/<name> with:
- Environment variables
- Overridable settings
7. VALIDATE
- Check script syntax: sh -n /etc/init.d/<name>
- Dry-run: rc-service <name> status
- Test start/stop cycle
Implementation Guide
Development Environment Setup
Option 1: Docker (Recommended for Development)
# Create a testing environment
docker run -it --name alpine-openrc --privileged alpine:latest sh
# Inside container:
apk add openrc
# Note: OpenRC won't fully work in Docker without --privileged
# and some additional setup, but you can test script syntax
# For full testing, use a VM
Option 2: Virtual Machine (Recommended for Full Testing)
# Download Alpine ISO
wget https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-standard-3.19.0-x86_64.iso
# Create VM in VirtualBox/VMware/QEMU
# Install Alpine with: setup-alpine
# After installation:
apk update
apk add vim curl
Option 3: Use an existing Alpine server or Raspberry Pi
Project Structure
openrc-migration/
├── bin/
│ ├── convert-service.sh # Main conversion script
│ ├── parse-systemd.sh # systemd unit file parser
│ ├── generate-openrc.sh # OpenRC script generator
│ └── validate-service.sh # Service validation
│
├── lib/
│ ├── common.sh # Shared functions
│ ├── mappings.sh # systemd → OpenRC mappings
│ └── templates/
│ ├── init-script.tpl # OpenRC init script template
│ └── conf-file.tpl # conf.d file template
│
├── examples/
│ ├── 01-simple-daemon/
│ │ ├── original.service # systemd unit
│ │ ├── converted/
│ │ │ ├── init.d-script # OpenRC init script
│ │ │ └── conf.d-file # Configuration
│ │ └── README.md # Explanation
│ │
│ ├── 02-database-dependent/
│ ├── 03-environment-heavy/
│ ├── 04-forking-daemon/
│ └── 05-multi-exec/
│
├── tests/
│ ├── test-parser.sh
│ ├── test-generator.sh
│ └── test-integration.sh
│
└── docs/
├── MAPPING-REFERENCE.md # Complete systemd → OpenRC reference
└── TROUBLESHOOTING.md # Common issues and solutions
The Core Question You’re Answering
“How do you translate declarative service specifications (systemd) into imperative service scripts (OpenRC) while preserving intended behavior?”
This requires understanding:
- What does each systemd directive actually DO?
- What is the equivalent OpenRC mechanism?
- When is there NO equivalent and what’s the workaround?
Concepts You Must Understand First
Before implementing, verify you can answer:
- Process lifecycle: How does a daemon process work? What is a PID file?
- Shell scripting: Can you write a shell function that handles errors correctly?
- Service dependencies: What’s the difference between “runs after” and “requires”?
- Process supervision: What does it mean to supervise a process?
- Unix permissions: What happens when a service runs as a non-root user?
Questions to Guide Your Design
Parsing:
- How will you handle multi-line values in systemd units?
- How will you handle systemd variable interpolation (e.g.,
%nfor service name)? - What do you do with directives that have no OpenRC equivalent?
Generation:
- Should the generator create start/stop functions, or rely on defaults?
- How do you handle services that need pre-start setup?
- How do you translate systemd’s Environment= and EnvironmentFile=?
Validation:
- How do you test a service without actually running it?
- How do you verify dependency ordering is correct?
Thinking Exercise
Before writing any code, trace through this conversion manually:
# Input: redis.service
[Unit]
Description=Redis In-Memory Data Store
After=network.target
[Service]
Type=forking
User=redis
Group=redis
ExecStart=/usr/bin/redis-server /etc/redis.conf
ExecStop=/usr/bin/redis-cli shutdown
PIDFile=/var/run/redis/redis.pid
TimeoutStopSec=5
[Install]
WantedBy=multi-user.target
Work through:
- What OpenRC dependencies does
After=network.targettranslate to? Type=forkingmeans Redis forks and the parent exits - how does OpenRC handle this?- How do you implement
ExecStopin OpenRC? - What’s the equivalent of
TimeoutStopSec?
Write out your expected output before checking the hints.
Hints in Layers
Hint 1 - Starting Point (Conceptual Direction)
Start with the simplest possible conversion: a Type=simple service with no dependencies. This teaches you the basic structure before handling edge cases.
The core OpenRC script is simpler than you might expect:
#!/sbin/openrc-run
description="Service description"
command="/path/to/binary"
command_args="arguments"
OpenRC’s openrc-run wrapper provides start/stop/status for you.
Hint 2 - Next Level (More Specific Guidance)
For dependency mapping, here’s the translation table:
# systemd target → OpenRC service
network.target → net
network-online.target → net (with 'use dns' if DNS needed)
remote-fs.target → netmount
local-fs.target → localmount
time-sync.target → ntpd or chronyd
multi-user.target → default runlevel
# systemd dependency type → OpenRC keyword
Requires=X.service → need X
Wants=X.service → use X
After=X.service → after X
Before=X.service → before X
For Type=forking, DON’T set command_background=true (the daemon handles backgrounding itself).
Hint 3 - Technical Details (Approach/Pseudocode)
Here’s a parser skeleton:
#!/bin/sh
# parse-systemd.sh - Parse a systemd unit file
parse_unit_file() {
file="$1"
section=""
while IFS= read -r line; do
# Skip comments and empty lines
case "$line" in
"#"*|";"*|"") continue ;;
esac
# Detect section headers
case "$line" in
"["*"]")
section="${line#[}"
section="${section%]}"
continue
;;
esac
# Parse key=value
key="${line%%=*}"
value="${line#*=}"
# Output: SECTION_KEY=value
echo "${section}_${key}=${value}"
done < "$file"
}
For supervise-daemon (Alpine’s process supervisor):
# Enable automatic restart
supervisor=supervise-daemon
respawn_delay=5 # Wait 5 seconds between restarts
respawn_max=0 # Infinite restarts (0 = no limit)
respawn_period=60 # Reset counter after 60 seconds
Hint 4 - Tools/Debugging (Verification Methods)
Validation commands:
# Check script syntax
sh -n /etc/init.d/myservice
# Check if script is valid openrc-run script
head -1 /etc/init.d/myservice | grep -q "openrc-run" && echo "Valid shebang"
# Show what would be executed
rc-service myservice describe
# Check dependencies
rc-service myservice depend
# Test start (add --dry-run if available)
rc-service myservice start
# Check status
rc-service myservice status
# View supervise-daemon status
supervise-daemon myservice --status
# Debug startup issues
rc-service -v myservice start # Verbose mode
# Check if service is in a runlevel
rc-update show | grep myservice
Interview Questions This Project Prepares You For
- “Explain the difference between systemd and SysV/OpenRC init systems.”
- Focus on: declarative vs imperative, socket activation, dependency handling
- “How would you debug a service that fails to start on boot?”
- OpenRC:
rc-service -v <name> start, check /var/log/messages, verify dependencies - systemd:
journalctl -u <name>,systemctl status <name>
- OpenRC:
- “What is process supervision and why is it important?”
- Automatic restart on crash, health monitoring, resource tracking
- “How do service dependencies work? What’s the difference between ‘requires’ and ‘after’?”
- Requires/need: must be running
- After: ordering only (doesn’t start the dependency)
- “You need to migrate an application from Ubuntu to Alpine. What do you need to consider?”
- Init system (systemd → OpenRC)
- C library (glibc → musl)
- Package manager (apt → apk)
- Shell (bash → ash)
Books That Will Help
| Topic | Book | Specific Chapters |
|---|---|---|
| Linux System Administration | “How Linux Works” by Brian Ward | Ch 6: How User Space Starts |
| Shell Scripting | “Classic Shell Scripting” by Arnold Robbins | Ch 7: Input/Output |
| System Design | “The Linux Command Line” by William Shotts | Ch 17: System Administration |
| Init Systems | “Linux Service Management Made Easy with systemd” by Donald A. Tevault | Conceptual comparison chapters |
Implementation Phases
Phase 1: Manual Conversions (Days 1-2)
Convert services BY HAND without scripts:
- Pick 3 simple systemd services
- Read the systemd unit file carefully
- Write the OpenRC script from scratch
- Test on Alpine
This builds intuition before automation.
Phase 2: Parser Development (Days 2-3)
Build the systemd parser:
- Handle sections: [Unit], [Service], [Install]
- Handle multi-value directives (After=a After=b)
- Handle EnvironmentFile= (read and parse env files)
- Output structured data (could be shell variables or simple format)
Phase 3: Generator Development (Days 3-4)
Build the OpenRC generator:
- Template-based generation
- Handle all Type= variants
- Generate conf.d files
- Handle supervisor settings
Phase 4: Validation Tools (Day 5)
Build validation:
- Syntax checking
- Dependency resolution testing
- Integration tests
Phase 5: Documentation and Examples (Days 6-7)
- Document all mappings
- Create comprehensive examples
- Write troubleshooting guide
Key Implementation Decisions
- How to handle Type=notify?
- systemd’s notify type sends readiness signals
- OpenRC: use
command_background=truewith supervise-daemon - The service won’t notify OpenRC of readiness, but supervision still works
- How to handle EnvironmentFile=?
- Parse the file and include variables in conf.d
- Or source the file in start_pre()
- How to handle ExecStartPre/ExecStartPost?
- Use
start_pre()andstart_post()functions
- Use
- How to handle Restart=on-failure vs Restart=always?
- supervise-daemon always restarts on exit
- For on-failure only, you’d need custom logic (check exit code)
- Often, Restart=always is acceptable
systemd to OpenRC Complete Mapping Reference
Command Mapping
| Action | systemd | OpenRC |
|---|---|---|
| Start service | systemctl start nginx |
rc-service nginx start |
| Stop service | systemctl stop nginx |
rc-service nginx stop |
| Restart service | systemctl restart nginx |
rc-service nginx restart |
| Reload service | systemctl reload nginx |
rc-service nginx reload |
| Service status | systemctl status nginx |
rc-service nginx status |
| Enable at boot | systemctl enable nginx |
rc-update add nginx default |
| Disable at boot | systemctl disable nginx |
rc-update del nginx default |
| Check if enabled | systemctl is-enabled nginx |
rc-update show \| grep nginx |
| List all services | systemctl list-units --type=service |
rc-status -a |
| List running | systemctl list-units --state=running |
rc-status |
| Reload daemon config | systemctl daemon-reload |
Not needed (scripts, not cached) |
| View logs | journalctl -u nginx |
cat /var/log/messages \| grep nginx |
| Follow logs | journalctl -fu nginx |
tail -f /var/log/messages \| grep nginx |
Unit File Directive Mapping
[Unit] Section
| systemd Directive | OpenRC Equivalent | Notes |
|---|---|---|
Description= |
description= |
Direct mapping |
After= |
after in depend() |
Ordering only |
Before= |
before in depend() |
Ordering only |
Requires= |
need in depend() |
Hard dependency |
Wants= |
use in depend() |
Soft dependency |
Conflicts= |
No direct equivalent | Use custom logic |
Documentation= |
Comment in script | No functional equivalent |
ConditionPathExists= |
start_pre() check |
Check in pre-start |
[Service] Section
| systemd Directive | OpenRC Equivalent | Notes |
|---|---|---|
Type=simple |
command_background=true |
OpenRC backgrounds it |
Type=forking |
Default (no background) | Daemon forks itself |
Type=oneshot |
command_background=false |
Run once, no PID tracking |
Type=notify |
command_background=true |
No notify support |
Type=dbus |
Not supported | Use simple equivalent |
ExecStart= |
command= + command_args= |
Split into parts |
ExecStop= |
Custom stop() function |
Or let OpenRC handle |
ExecReload= |
reload() function |
Must define manually |
ExecStartPre= |
start_pre() function |
|
ExecStartPost= |
start_post() function |
|
ExecStopPre= |
stop_pre() function |
|
ExecStopPost= |
stop_post() function |
|
User= |
command_user= |
|
Group= |
command_user=user:group |
Combined with user |
WorkingDirectory= |
directory= |
|
Environment= |
Export in conf.d | Or start_pre() |
EnvironmentFile= |
Source in start_pre() |
Or parse into conf.d |
PIDFile= |
pidfile= |
|
Restart=always |
supervisor=supervise-daemon |
Alpine 3.9+ |
RestartSec= |
respawn_delay= |
With supervise-daemon |
TimeoutStartSec= |
start_stop_daemon_args="--wait" |
Approximate |
TimeoutStopSec= |
retry= |
e.g., retry="TERM/30/KILL/5" |
KillMode= |
Custom stop logic | |
KillSignal= |
Signal in stop() | |
StandardOutput= |
Logging in script | Or redirect in command |
StandardError= |
Logging in script | Or redirect in command |
SyslogIdentifier= |
Logger in script | logger -t identifier |
LimitNOFILE= |
rc_ulimit= |
In conf.d |
MemoryLimit= |
cgroups v2 | Alpine 3.19+ |
[Install] Section
| systemd Directive | OpenRC Equivalent | Notes |
|---|---|---|
WantedBy=multi-user.target |
rc-update add X default |
Default runlevel |
WantedBy=graphical.target |
rc-update add X default |
Same as multi-user |
RequiredBy= |
Manual dependency | Not automatic |
Also= |
Manual | Not automatic |
Target to Runlevel Mapping
| systemd Target | OpenRC Runlevel/Service | Notes |
|---|---|---|
sysinit.target |
sysinit runlevel |
System initialization |
local-fs.target |
localmount service |
Filesystems |
remote-fs.target |
netmount service |
Network filesystems |
network.target |
net (virtual) |
Network configured |
network-online.target |
net + use dns |
Network + DNS ready |
multi-user.target |
default runlevel |
Normal operation |
graphical.target |
default runlevel |
Same as multi-user |
rescue.target |
single runlevel |
Single-user mode |
shutdown.target |
shutdown runlevel |
System shutdown |
OpenRC Directory Structure Deep Dive
/etc/
├── init.d/ # SERVICE SCRIPTS
│ │
│ │ # Core OpenRC scripts (don't modify)
│ ├── functions.sh # Shared functions (sourced by scripts)
│ │
│ │ # System services
│ ├── hostname # Sets hostname
│ ├── networking # Network configuration
│ ├── localmount # Mount local filesystems
│ ├── netmount # Mount network filesystems
│ ├── bootmisc # Miscellaneous boot tasks
│ │
│ │ # Your services
│ ├── nginx # Example: web server
│ ├── postgresql # Example: database
│ └── myapp # Your application
│
├── conf.d/ # SERVICE CONFIGURATION
│ │
│ │ # Variables sourced by init scripts
│ ├── nginx # NGINX_OPTS="-g 'daemon off;'"
│ ├── postgresql # PGDATA="/var/lib/postgresql/data"
│ └── myapp # MYAPP_CONFIG="/etc/myapp.conf"
│
├── runlevels/ # RUNLEVEL DEFINITIONS
│ │
│ │ # Each directory contains symlinks to /etc/init.d/*
│ │
│ ├── sysinit/ # First: System initialization
│ │ ├── devfs -> ../init.d/devfs
│ │ ├── dmesg -> ../init.d/dmesg
│ │ └── mdev -> ../init.d/mdev
│ │
│ ├── boot/ # Second: Boot services
│ │ ├── bootmisc -> ../init.d/bootmisc
│ │ ├── hostname -> ../init.d/hostname
│ │ ├── hwclock -> ../init.d/hwclock
│ │ ├── modules -> ../init.d/modules
│ │ ├── networking -> ../init.d/networking
│ │ └── urandom -> ../init.d/urandom
│ │
│ ├── default/ # Third: Normal operation (most services)
│ │ ├── crond -> ../init.d/crond
│ │ ├── nginx -> ../init.d/nginx
│ │ ├── sshd -> ../init.d/sshd
│ │ └── myapp -> ../init.d/myapp
│ │
│ └── shutdown/ # Last: Shutdown tasks
│ ├── killprocs -> ../init.d/killprocs
│ ├── mount-ro -> ../init.d/mount-ro
│ └── savecache -> ../init.d/savecache
│
├── rc.conf # GLOBAL OPENRC CONFIGURATION
│ │
│ │ # System-wide settings
│ │ rc_parallel="YES" # Start services in parallel
│ │ rc_logger="YES" # Log boot to /var/log/rc.log
│ │ rc_log_path="/var/log/rc.log"
│ │
└── inittab # INIT CONFIGURATION
│ # Defines what happens at each runlevel
│ ::sysinit:/sbin/openrc sysinit
│ ::boot:/sbin/openrc boot
│ ::default:/sbin/openrc default
Runlevel Boot Sequence
Power On
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ KERNEL BOOT │
│ Load kernel → Initialize hardware → Mount initramfs → Start init (PID 1)│
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ SYSINIT RUNLEVEL │
│ /etc/runlevels/sysinit/* │
│ • devfs - Mount /dev │
│ • dmesg - Log kernel messages │
│ • mdev - Device manager │
│ • sysfs - Mount /sys │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ BOOT RUNLEVEL │
│ /etc/runlevels/boot/* │
│ • hostname - Set system hostname │
│ • hwclock - Set system clock │
│ • modules - Load kernel modules │
│ • networking - Configure network interfaces │
│ • localmount - Mount local filesystems │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ DEFAULT RUNLEVEL │
│ /etc/runlevels/default/* │
│ • sshd - SSH server │
│ • crond - Cron daemon │
│ • nginx - Web server │
│ • postgresql- Database │
│ • myapp - Your application │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
System Ready (Login prompt or running services)
Example Conversions
Example 1: Simple Web Application
systemd (Original):
# /lib/systemd/system/webapp.service
[Unit]
Description=Simple Web Application
After=network.target
[Service]
Type=simple
User=webapp
Group=webapp
WorkingDirectory=/opt/webapp
ExecStart=/usr/bin/python3 /opt/webapp/app.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
OpenRC (Converted):
#!/sbin/openrc-run
# /etc/init.d/webapp
description="Simple Web Application"
depend() {
need net
}
command="/usr/bin/python3"
command_args="/opt/webapp/app.py"
command_user="webapp:webapp"
directory="/opt/webapp"
pidfile="/run/${RC_SVCNAME}.pid"
# Process supervision (replaces Restart=always)
supervisor=supervise-daemon
respawn_delay=3
respawn_max=0
# /etc/conf.d/webapp
# Configuration for webapp service
# Uncomment to add Python arguments
# command_args="/opt/webapp/app.py --port 8080"
# Uncomment to override respawn settings
# respawn_delay=5
Example 2: Database-Dependent Service
systemd (Original):
# /lib/systemd/system/api-server.service
[Unit]
Description=API Server
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=api
Group=api
Environment=DATABASE_URL=postgresql://localhost/api
Environment=API_PORT=3000
ExecStart=/usr/bin/node /opt/api/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
OpenRC (Converted):
#!/sbin/openrc-run
# /etc/init.d/api-server
description="API Server"
depend() {
need net
need postgresql
}
command="/usr/bin/node"
command_args="/opt/api/server.js"
command_user="api:api"
pidfile="/run/${RC_SVCNAME}.pid"
# Process supervision
supervisor=supervise-daemon
respawn_delay=10
respawn_max=0
# Load environment before starting
start_pre() {
# Source environment from conf.d
[ -f /etc/conf.d/${RC_SVCNAME} ] && . /etc/conf.d/${RC_SVCNAME}
# Export required variables
export DATABASE_URL
export API_PORT
}
reload() {
ebegin "Reloading ${RC_SVCNAME}"
supervise-daemon ${RC_SVCNAME} --signal HUP
eend $?
}
# /etc/conf.d/api-server
# Configuration for api-server
# Database connection
DATABASE_URL="postgresql://localhost/api"
# API settings
API_PORT="3000"
# Uncomment to use external config file
# EXTRA_OPTS="--config /etc/api-server/config.json"
Example 3: Forking Daemon (Redis)
systemd (Original):
# /lib/systemd/system/redis.service
[Unit]
Description=Redis In-Memory Data Store
After=network.target
[Service]
Type=forking
User=redis
Group=redis
ExecStart=/usr/bin/redis-server /etc/redis.conf
ExecStop=/usr/bin/redis-cli shutdown
PIDFile=/var/run/redis/redis.pid
TimeoutStopSec=5
Restart=always
[Install]
WantedBy=multi-user.target
OpenRC (Converted):
#!/sbin/openrc-run
# /etc/init.d/redis
description="Redis In-Memory Data Store"
depend() {
need net
use logger
}
# Note: No command_background for forking daemons
command="/usr/bin/redis-server"
command_args="/etc/redis.conf"
command_user="redis:redis"
pidfile="/var/run/redis/redis.pid"
# Retry sequence: TERM signal, wait 5s, KILL signal, wait 3s
retry="TERM/5/KILL/3"
start_pre() {
# Ensure PID directory exists
checkpath --directory --owner redis:redis --mode 0755 /var/run/redis
}
stop() {
ebegin "Stopping ${RC_SVCNAME}"
# Use redis-cli for graceful shutdown
if [ -f "${pidfile}" ]; then
/usr/bin/redis-cli shutdown
# Wait for process to exit
local timeout=5
while [ $timeout -gt 0 ] && [ -f "${pidfile}" ]; do
sleep 1
timeout=$((timeout - 1))
done
fi
eend 0
}
Example 4: Service with Environment File
systemd (Original):
# /lib/systemd/system/myapp.service
[Unit]
Description=My Application with Environment File
After=network.target
[Service]
Type=simple
User=myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/bin/myapp
Restart=always
[Install]
WantedBy=multi-user.target
OpenRC (Converted):
#!/sbin/openrc-run
# /etc/init.d/myapp
description="My Application with Environment File"
depend() {
need net
}
command="/usr/bin/myapp"
command_user="myapp"
pidfile="/run/${RC_SVCNAME}.pid"
supervisor=supervise-daemon
respawn_delay=5
start_pre() {
# Check environment file exists
if [ ! -f /etc/myapp/env ]; then
eerror "Environment file /etc/myapp/env not found"
return 1
fi
# Source environment file
# Note: This makes vars available to the command
. /etc/myapp/env
# Export all variables from env file
export $(grep -v '^#' /etc/myapp/env | xargs)
}
Example 5: Multi-Exec Service with Pre/Post Hooks
systemd (Original):
# /lib/systemd/system/complex-app.service
[Unit]
Description=Complex Application with Hooks
After=network.target
[Service]
Type=simple
User=app
ExecStartPre=/usr/bin/complex-app --check-config
ExecStartPre=/usr/bin/complex-app --migrate-db
ExecStart=/usr/bin/complex-app --run
ExecStartPost=/usr/bin/complex-app --notify-started
ExecStop=/usr/bin/complex-app --graceful-stop
ExecStopPost=/usr/bin/complex-app --cleanup
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
OpenRC (Converted):
#!/sbin/openrc-run
# /etc/init.d/complex-app
description="Complex Application with Hooks"
depend() {
need net
}
command="/usr/bin/complex-app"
command_args="--run"
command_user="app"
pidfile="/run/${RC_SVCNAME}.pid"
supervisor=supervise-daemon
respawn_delay=10
start_pre() {
ebegin "Checking configuration"
/usr/bin/complex-app --check-config
local ret=$?
eend $ret "Configuration check failed" || return $ret
ebegin "Running database migrations"
/usr/bin/complex-app --migrate-db
ret=$?
eend $ret "Database migration failed" || return $ret
}
start_post() {
ebegin "Sending startup notification"
/usr/bin/complex-app --notify-started
eend $? "Notification failed (non-fatal)"
# Return 0 even if notification fails - it's not critical
return 0
}
stop() {
ebegin "Stopping ${RC_SVCNAME}"
/usr/bin/complex-app --graceful-stop
eend $?
}
stop_post() {
ebegin "Running cleanup"
/usr/bin/complex-app --cleanup
eend $? "Cleanup failed (non-fatal)"
return 0
}
Testing Strategy
Unit Tests
#!/bin/sh
# tests/test-parser.sh
test_parse_simple_service() {
result=$(./bin/parse-systemd.sh tests/fixtures/simple.service)
# Check description extracted
echo "$result" | grep -q "Unit_Description=Simple Service" || {
echo "FAIL: Description not parsed"
return 1
}
# Check ExecStart extracted
echo "$result" | grep -q "Service_ExecStart=/usr/bin/simple" || {
echo "FAIL: ExecStart not parsed"
return 1
}
echo "PASS: test_parse_simple_service"
}
test_parse_multi_after() {
result=$(./bin/parse-systemd.sh tests/fixtures/multi-deps.service)
# Multiple After= lines should be combined
echo "$result" | grep -q "network.target" || {
echo "FAIL: network.target not in After"
return 1
}
echo "$result" | grep -q "postgresql.service" || {
echo "FAIL: postgresql.service not in After"
return 1
}
echo "PASS: test_parse_multi_after"
}
# Run tests
test_parse_simple_service
test_parse_multi_after
Integration Tests
#!/bin/sh
# tests/test-integration.sh
# Run on an actual Alpine system
setup() {
# Create test service
cat > /tmp/test-app << 'EOF'
#!/bin/sh
while true; do
echo "Test app running: $(date)"
sleep 5
done
EOF
chmod +x /tmp/test-app
}
test_converted_service_starts() {
# Generate service
./bin/convert-service.sh tests/fixtures/simple.service > /tmp/test-service
# Install
cp /tmp/test-service /etc/init.d/test-service
chmod +x /etc/init.d/test-service
# Start
rc-service test-service start
sleep 2
# Verify running
rc-service test-service status | grep -q "started" || {
echo "FAIL: Service not started"
rc-service test-service stop
return 1
}
# Cleanup
rc-service test-service stop
rm /etc/init.d/test-service
echo "PASS: test_converted_service_starts"
}
test_dependencies_resolved() {
# Install converted service with dependencies
cp tests/fixtures/converted/api-server /etc/init.d/
chmod +x /etc/init.d/api-server
# Check dependencies are recognized
rc-service api-server depend | grep -q "need.*postgresql" || {
echo "FAIL: postgresql dependency not recognized"
return 1
}
echo "PASS: test_dependencies_resolved"
}
# Run
setup
test_converted_service_starts
test_dependencies_resolved
Manual Testing Checklist
[ ] Service script passes syntax check: sh -n /etc/init.d/myservice
[ ] Service starts successfully: rc-service myservice start
[ ] Service shows as started: rc-service myservice status
[ ] Service stops cleanly: rc-service myservice stop
[ ] Service restarts correctly: rc-service myservice restart
[ ] Reload works (if applicable): rc-service myservice reload
[ ] Service survives reboot (if added to runlevel)
[ ] Logs appear in expected location
[ ] Process runs as correct user: ps aux | grep myservice
[ ] PID file created correctly: cat /run/myservice.pid
[ ] Dependencies start first: rc-service --debug myservice start
Common Pitfalls & Debugging
Pitfall 1: Service Starts But Immediately Dies
Symptom:
$ rc-service myapp start
* Starting myapp ... [ ok ]
$ rc-service myapp status
* status: crashed
Cause: The process needs to stay in foreground but is exiting.
Fix: For simple daemons, ensure command_background=true:
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"
For forking daemons, DON’T set command_background.
Debug:
# Run manually to see output
/usr/bin/myapp --your-args
# Check if it stays running or exits
Pitfall 2: Dependencies Not Starting
Symptom:
$ rc-service myapp start
* Starting myapp ...
* ERROR: cannot start myapp: postgresql is not started
Cause: need requires the dependency to be running, but it’s not started.
Fix: Start the dependency first, or use use instead of need if it’s optional:
depend() {
need net # Must have network
use postgresql # Nice to have, but works without
}
Pitfall 3: Environment Variables Not Available
Symptom: Service starts but behaves as if environment variables aren’t set.
Cause: Variables must be exported in the execution context.
Fix: Export in start_pre():
start_pre() {
export DATABASE_URL="postgresql://localhost/db"
export NODE_ENV="production"
}
Or source from conf.d and export:
# In /etc/conf.d/myapp
DATABASE_URL="postgresql://localhost/db"
# In /etc/init.d/myapp
start_pre() {
export DATABASE_URL
}
Pitfall 4: PID File Permission Issues
Symptom:
* Starting myapp ...
* ERROR: failed to start myapp
Cause: Service user can’t write to /run directory.
Fix: Create a subdirectory with correct permissions:
start_pre() {
checkpath --directory --owner myapp:myapp --mode 0755 /run/myapp
}
pidfile="/run/myapp/${RC_SVCNAME}.pid"
Pitfall 5: supervise-daemon Not Working
Symptom: Service doesn’t restart automatically after crash.
Cause: supervise-daemon requires Alpine 3.9+ and proper configuration.
Fix: Verify setup:
# Check Alpine version
cat /etc/alpine-release
# Verify supervise-daemon is installed
which supervise-daemon
# Correct configuration
supervisor=supervise-daemon
respawn_delay=5
respawn_max=0
Debugging Commands Reference
# Verbose service start
rc-service -v myservice start
# Show service dependencies
rc-service myservice depend
# Check OpenRC status
rc-status
# Show all services (including stopped)
rc-status -a
# Check script syntax
sh -n /etc/init.d/myservice
# View rc.log (if enabled)
cat /var/log/rc.log
# View system log
tail -f /var/log/messages
# Check if process is running
pgrep -af myservice
# Check process tree
pstree -p $(cat /run/myservice.pid)
# supervise-daemon status
supervise-daemon myservice --status
Extensions & Challenges
Extension 1: Socket-Activated Service Equivalent
systemd supports socket activation where services start on-demand. Implement an equivalent using OpenRC + inetd or xinetd.
Challenge: Convert this socket-activated service:
# myapp.socket
[Unit]
Description=My App Socket
[Socket]
ListenStream=8080
Accept=no
[Install]
WantedBy=sockets.target
Extension 2: Automatic Migration Tool
Build a fully automated tool that:
- Scans a systemd-based system for all enabled services
- Generates OpenRC equivalents for each
- Creates a migration script that can be run on Alpine
Extension 3: Unit Test Framework
Create a test framework that:
- Spins up Alpine Docker containers
- Installs converted services
- Verifies they start/stop correctly
- Tests failure scenarios
Extension 4: Template Expansion
systemd supports templates like myapp@.service that create multiple instances. Implement equivalent OpenRC functionality using symlinks and variable extraction.
Extension 5: Resource Limits
systemd uses cgroups for resource limits. Implement equivalent functionality using:
rc_ulimitfor process limits- cgroups v2 (Alpine 3.19+) for memory/CPU limits
Real-World Connections
Where You’ll Use This
-
Docker/Kubernetes: Alpine is the most popular container base image. Understanding OpenRC helps debug init-system-like behavior in containers.
-
Edge Computing: Alpine is common in embedded/edge devices. Many IoT deployments use Alpine.
-
Cloud-Native: Tools like Podman, containerd, and K3s often run on Alpine.
-
Security-Focused Deployments: Alpine’s minimalism is preferred in security-conscious environments.
Industry Context
- Docker Hub: Over 50% of container images are based on Alpine
- Kubernetes: Many operators and sidecars use Alpine as their base
- GitHub Actions: Many actions use Alpine for smaller image sizes
Related Technologies
| Technology | Relationship |
|---|---|
| runit | Alternative lightweight init (used by Void Linux) |
| s6 | Another minimalist supervision suite |
| dinit | Modern dependency-based init |
| finit | Fast init with plugins |
| containerd | Uses similar process supervision concepts |
Resources
Official Documentation
Tutorials and Guides
Reference Implementations
/etc/init.d/on any Alpine system - study existing scripts- Alpine aports - see how packages create init scripts
Tools
- ShellCheck: Lint shell scripts for common errors
- checkbashisms: Find bash-specific code in sh scripts
Self-Assessment Checklist
Before considering this project complete, verify:
Knowledge
- I can explain the difference between
needandafterin OpenRC - I understand why Type=simple uses
command_background=true - I know what happens when a service has unmet dependencies
- I can describe the OpenRC boot sequence (sysinit → boot → default)
- I understand how supervise-daemon provides process supervision
Skills
- I can write an OpenRC init script from scratch
- I can debug service startup failures using
rc-service -v - I can configure automatic restart using supervise-daemon
- I can implement pre-start and post-stop hooks
- I can set up proper logging for OpenRC services
Deliverables
- Parser extracts all common systemd directives
- Generator produces valid OpenRC scripts
- At least 5 example conversions are complete and tested
- Documentation covers the mapping reference
- All converted services start and stop correctly on Alpine
Submission/Completion Criteria
Your project is complete when:
- Code Quality
- All shell scripts pass
shellcheckwithout errors - Scripts are POSIX-compliant (work with ash)
- Code is well-commented
- All shell scripts pass
- Functionality
- Parser handles multi-line values and variable interpolation
- Generator produces working OpenRC scripts for all Type= variants
- Validation tools correctly identify issues
- Examples
- 5+ real-world services converted
- Each example includes README with explanation
- Examples cover: simple, forking, environment files, dependencies, hooks
- Testing
- All converted services verified on real Alpine system
- Unit tests for parser
- Integration tests documented
- Documentation
- Complete mapping reference (systemd → OpenRC)
- Troubleshooting guide with common issues
- Usage instructions for toolkit
What’s Next?
After completing this project, consider:
- Project 4: Alpine Docker Optimization - Apply your OpenRC knowledge to create minimal container images
- Project 5: Alpine Diskless Kiosk - Use OpenRC in a diskless environment
- Deep dive into supervise-daemon - Explore advanced supervision features
- Contribute to Alpine - Submit init scripts to Alpine’s aports repository
This project teaches you not just how to convert services, but how to think about service management across different init systems - a skill that transfers to any Linux administration role.