Container Machine User Setup Script Mechanism: How Apple Container Creates Users Without useradd

The container machine user setup script mechanism is a container-agnostic shell script that directly manipulates /etc/passwd, /etc/group, and /etc/shadow to create users without relying on image-specific tools like useradd or adduser.

This mechanism ensures consistent user provisioning across diverse container images in the apple/container repository. The script, create-user.sh, resides in the MachineAPIServer plugin and executes during first-time container initialization to establish a predictable, non-root user environment for development workloads.

How the User Setup Script Works

The script located at Sources/Plugins/MachineAPIServer/Resources/create-user.sh performs four distinct operations to establish a fully functional user account. Because it directly edits Unix user database files rather than calling high-level utilities, it functions identically across Alpine, Ubuntu, Fedora, or any other Linux distribution.

Direct File Manipulation for Group Creation

The script first checks if a group with the specified GID exists using getent. If the group is missing, it appends a new entry directly to /etc/group:

if ! getent group "${CONTAINER_GID}"
echo "${CONTAINER_USER}:x:${CONTAINER_GID}:" >> /etc/group

This bypasses the need for distribution-specific group creation commands.

User Entry Creation in /etc/passwd and /etc/shadow

Next, the script verifies whether a user with the target UID exists. If not, it creates two critical entries:

  1. Passwd entry: Defines the username, UID, GID, home directory, and shell
  2. Shadow entry: Establishes a disabled password with standard aging parameters
if ! getent passwd "${CONTAINER_UID}"
echo "${CONTAINER_USER}:x:${CONTAINER_UID}:${CONTAINER_GID}::${CONTAINER_HOME}:${CONTAINER_SHELL}" >> /etc/passwd
echo "${CONTAINER_USER}:!:19000:0:99999:7:::" >> /etc/shadow

The shadow entry uses ! to disable password login while maintaining valid account metadata.

Home Directory Initialization

The script creates the user's home directory, populates it with standard skeleton files from /etc/skel, and sets ownership:

mkdir -p "${CONTAINER_HOME}"
cp -a /etc/skel/. "${CONTAINER_HOME}"
chown -R "${CONTAINER_UID}:${CONTAINER_GID}" "${CONTAINER_HOME}"

This ensures the user has standard dotfiles and configuration files immediately upon first login.

Password-less Sudo Configuration

Finally, the script grants unrestricted sudo access without password prompts by creating a dedicated sudoers file:

mkdir -p /etc/sudoers.d
echo "${CONTAINER_USER} ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/${CONTAINER_USER}"
chmod 440 "/etc/sudoers.d/${CONTAINER_USER}"

The file permissions (440) are explicitly set to satisfy sudoers security requirements.

Required Environment Variables

The create-user.sh script expects five environment variables to be set before execution:

  • CONTAINER_USER – The username to create
  • CONTAINER_UID – The numeric user ID (UID)
  • CONTAINER_GID – The numeric group ID (GID), often matching the UID
  • CONTAINER_HOME – Absolute path to the home directory
  • CONTAINER_SHELL – The login shell executable (e.g., /bin/bash)

These variables enable the script to operate without hardcoded values, making it reusable across different container configurations.

Script Execution Context

According to the apple/container source code, the MachineAPIServer plugin invokes this script during the container's startup sequence. The plugin bundles the script as a resource and executes it within the container context, ensuring the user exists before development tools or VS Code extensions attempt to access the environment.

Practical Usage Examples

Standalone Execution

During a custom container bootstrap, you can execute the script directly after setting the required variables:

export CONTAINER_USER=devuser
export CONTAINER_UID=1000
export CONTAINER_GID=1000
export CONTAINER_HOME=/home/devuser
export CONTAINER_SHELL=/bin/bash

/path/to/create-user.sh

After execution, devuser exists with a fully populated home directory and can execute sudo commands without authentication.

MachineAPIServer Plugin Integration

The MachineAPIServer triggers the script using Swift's Process API:

let userSetupScript = Bundle.module.path(forResource: "create-user", ofType: "sh")!
var env = ProcessInfo.processInfo.environment
env["CONTAINER_USER"] = "dev"
env["CONTAINER_UID"] = "1000"
env["CONTAINER_GID"] = "1000"
env["CONTAINER_HOME"] = "/home/dev"
env["CONTAINER_SHELL"] = "/bin/bash"

let task = Process()
task.executableURL = URL(fileURLWithPath: "/bin/sh")
task.arguments = [userSetupScript]
task.environment = env
try task.run()
task.waitUntilExit()

This integration ensures the container machine user setup script mechanism runs automatically during initialization, providing consistent user environments across all managed containers.

Summary

  • The create-user.sh script in Sources/Plugins/MachineAPIServer/Resources/ creates users by directly appending to /etc/passwd, /etc/group, and /etc/shadow rather than using distribution-specific tools.
  • The mechanism requires five environment variables: CONTAINER_USER, CONTAINER_UID, CONTAINER_GID, CONTAINER_HOME, and CONTAINER_SHELL.
  • It copies skeleton files from /etc/skel to initialize the home directory and sets proper ownership using chown.
  • The script grants password-less sudo privileges by writing a dedicated file to /etc/sudoers.d/.
  • MachineAPIServer automatically invokes this script during container initialization to ensure a predictable non-root user for development workloads.

Frequently Asked Questions

Where is the create-user.sh script located in the repository?

The script resides at Sources/Plugins/MachineAPIServer/Resources/create-user.sh in the apple/container repository. This location places it within the MachineAPIServer plugin's resources, allowing the Swift code to bundle and execute it during container initialization.

Why does the script modify /etc files directly instead of using useradd?

The script manipulates /etc/passwd, /etc/group, and /etc/shadow directly to remain container-agnostic. Not all base images include useradd or adduser utilities (particularly minimal images like Alpine or distroless containers). By writing to the standard Unix user database files directly, the mechanism works consistently regardless of the base image, as long as the /etc directory is writable.

What permissions does the created user have?

The created user receives full password-less sudo privileges through a dedicated sudoers file at /etc/sudoers.d/${CONTAINER_USER}. The entry ALL=(ALL) NOPASSWD:ALL allows the user to execute any command as any user without authentication prompts, while the file permissions (440) restrict read access to root and the sudo group for security compliance.

How is the script triggered during container initialization?

The MachineAPIServer plugin invokes the script during the first-time initialization flow. It locates the bundled script using Bundle.module.path(forResource:), sets the required environment variables, and executes it via Process (Swift's wrapper for spawned processes). This occurs early in the container lifecycle to ensure the user exists before development tools attempt to access the environment.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →