Skip to content

Rootless Docker Operation

fuomag9 edited this page Sep 26, 2026 · 6 revisions

Rootless Docker Operation

Run Caddy Proxy Manager containers as non-root users.

Table of Contents

  1. What This Does
  2. Why Bother
  3. PUID and PGID Explained
  4. Default Configuration
  5. Custom UID/GID Setup
  6. File Ownership and Permissions
  7. XDG Directories (Caddy Service)
  8. Docker Volumes vs Bind Mounts
  9. Development vs Production
  10. Troubleshooting

What This Does

Runs the process inside the container as a non-root user. PUID is the user ID, PGID is the group ID. These are build-time arguments, not runtime variables.

File permissions matter because volume-mounted files are owned by whatever user created them.


Why Bother

If someone compromises the container, they get a non-root user instead of root. That limits the damage.

Docker recommends this for production. Compliance-heavy environments (PCI-DSS, HIPAA) often require it.

For development and testing, it's less critical but still good practice.


PUID and PGID Explained

What Are They?

  • PUID (Process User ID): Unix user ID for the container process
  • PGID (Process Group ID): Unix group ID for the container process

Default Values

Caddy Proxy Manager uses different IDs for each service:

Service PUID PGID Reason
Web 10001 10001 Non-system user, isolated from caddy
Caddy 10000 10000 Non-system user, isolated from web

Why different IDs?

  • Security isolation between services
  • Separate file ownership
  • Clear audit trails

Why 10000/10001?

  • Avoids conflict with system users (typically < 1000)
  • Avoids conflict with normal users (typically 1000-60000)
  • Easy to identify in process listings

Default Configuration

Current Setup

Without customization, containers run as:

# docker-compose.yml (excerpt)
services:
  web:
    build:
      args:
        PUID: ${PUID:-10001}  # Default 10001
        PGID: ${PGID:-10001}  # Default 10001

  caddy:
    build:
      args:
        PUID: ${PUID:-10000}  # Default 10000
        PGID: ${PGID:-10000}  # Default 10000

How It Works

  1. Dockerfile creates non-root user with specified UID/GID
  2. Application runs as this user
  3. Files created in volumes owned by this user

Default Permissions

Inside container:

  • Process runs as user 10001 (web) or 10000 (caddy)
  • Files created with these ownership

On host:

  • Volume files owned by UID 10001/10000
  • May not match your host user

Custom UID/GID Setup

Finding Your UID/GID

Check your current user:

# Your user ID
id -u

# Your group ID
id -g

# Full user info
id

Example output:

uid=1000(john) gid=1000(john) groups=1000(john),4(adm),24(cdrom),27(sudo)...

In this example:

  • PUID = 1000
  • PGID = 1000

Setting Custom UID/GID

Option A: Environment Variables (Recommended)

Add to .env file:

# .env
PUID=1000
PGID=1000

Option B: Export Variables

export PUID=1000
export PGID=1000

Option C: Inline with Docker Compose

PUID=1000 PGID=1000 docker compose up --build -d

Applying Changes

IMPORTANT: PUID/PGID are build arguments, not runtime variables.

You MUST rebuild containers:

# Stop containers
docker compose down

# Rebuild with new UID/GID
docker compose up --build -d

Verify:

# Check process user inside web container
docker compose exec web id

# Should show uid=1000 gid=1000 (or your values)

File Ownership and Permissions

Volume Ownership

When you set PUID/PGID to match your host user:

Benefits:

  • Easy file access from host
  • No permission denied errors
  • Simple backup/restore
  • Development workflow friendly

Example with PUID=1000:

ls -la data/
# Output:
drwxr-xr-x  5 john john 4096 Dec 28 12:00 .
-rw-r-----  1 john john  524288 Dec 28 12:00 caddy-proxy-manager.db

Database File Permissions

The SQLite database holds password hashes, session tokens and encrypted secrets. Since v1.13.1, the web container removes the world permission bits from caddy-proxy-manager.db and its -journal, -wal and -shm files whenever it opens the database (so -rw-r--r-- becomes -rw-r-----). Owner and group bits are left unchanged:

  • Access through the files' owner or group keeps working, e.g. a host backup job running as the owner or in the group (see Can't Edit Files on Host, Option C).
  • A job running as an unrelated user can no longer read the database. Adding world read access back does not last: it is removed again on the next start.
  • If the mode cannot be changed (e.g. the files belong to another user), the web container logs Could not restrict permissions on <file>: … and keeps running.

Permission Issues

Symptom: Permission denied errors

Error: EACCES: permission denied, open '/app/data/caddy-proxy-manager.db'

Cause:

  • Volume files owned by different UID than container user
  • Mismatch between PUID and actual file owner

Solution:

# Option 1: Change file ownership to match PUID
sudo chown -R 10001:10001 data/

# Option 2: Rebuild container with matching PUID
PUID=$(stat -c '%u' data) PGID=$(stat -c '%g' data) docker compose up --build -d

# Option 3: Set PUID/PGID to your user and rebuild
PUID=$(id -u) PGID=$(id -g) docker compose up --build -d

XDG Directories (Caddy Service)

What Are XDG Directories?

XDG Base Directory specification for application data storage.

Caddy service environment:

environment:
  XDG_CONFIG_HOME: /config
  XDG_DATA_HOME: /data

Purpose:

  • XDG_CONFIG_HOME - Configuration files (/config)
  • XDG_DATA_HOME - Application data (/data)

Caddy uses these for:

  • Certificate storage (/data/caddy/certificates/)
  • Config persistence (/config/caddy/)
  • ACME account keys

Volume Mapping

volumes:
  - ./caddy-config:/config  # XDG_CONFIG_HOME
  - ./caddy-data:/data      # XDG_DATA_HOME

Ownership: Owned by PUID/PGID set for Caddy service (default 10000:10000)


Docker Volumes vs Bind Mounts

Default Configuration (v1.0+)

Starting with version 1.0, Caddy Proxy Manager uses Docker named volumes instead of bind mounts by default.

Current docker-compose.yml:

volumes:
  - caddy-manager-data:/app/data
  - caddy-data:/data
  - caddy-config:/config
  - caddy-logs:/logs

volumes:
  caddy-manager-data:
  caddy-data:
  caddy-config:
  caddy-logs:

Benefits of Docker volumes:

  • Automatic permission handling
  • No host filesystem permission conflicts
  • Better data isolation
  • Cross-platform compatibility
  • Managed by Docker daemon

Drawbacks:

  • Less direct access from host
  • Need Docker commands to access data
  • Harder to inspect files directly

Switching to Bind Mounts (Local Folders)

If you prefer direct access to files on your host filesystem, you can switch to bind mounts.

Step 1: Modify docker-compose.yml

Change the volumes section:

services:
  web:
    volumes:
      - ./data:/app/data  # Bind mount instead of named volume

  caddy:
    volumes:
      - ./caddy-data:/data
      - ./caddy-config:/config
      - ./caddy-logs:/logs

# Remove or comment out the volumes section at the bottom
# volumes:
#   caddy-manager-data:
#   caddy-data:
#   caddy-config:
#   caddy-logs:

Step 2: Create directories

mkdir -p data caddy-data caddy-config caddy-logs

Step 3: Set proper permissions

This is critical to avoid permission issues:

# Option A: Match container PUID/PGID (default 10001 for web, 10000 for caddy)
sudo chown -R 10001:10001 data/
sudo chown -R 10000:10000 caddy-data/ caddy-config/ caddy-logs/

# Option B: Set PUID/PGID to match your user and rebuild
PUID=$(id -u) PGID=$(id -g) docker compose up --build -d

# Then set ownership to your user
sudo chown -R $(id -u):$(id -g) data/ caddy-data/ caddy-config/ caddy-logs/

Step 4: Restart containers

docker compose down
docker compose up -d

Permission Troubleshooting with Bind Mounts

Common Issue: Permission denied errors

Error: EACCES: permission denied, open '/app/data/caddy-proxy-manager.db'

Cause: UID/GID mismatch between container user and host files

Solutions:

Solution 1: Fix ownership (preserves container defaults)

# Stop containers
docker compose down

# Fix ownership
sudo chown -R 10001:10001 data/
sudo chown -R 10000:10000 caddy-data/ caddy-config/ caddy-logs/

# Make directories accessible
chmod -R 755 data/ caddy-data/ caddy-config/ caddy-logs/

# Start containers
docker compose up -d

On start, the web container removes the world permission bits this adds to the database files again (see Database File Permissions).

Solution 2: Rebuild with your user ID

# Stop and remove containers
docker compose down

# Add to .env file
echo "PUID=$(id -u)" >> .env
echo "PGID=$(id -g)" >> .env

# Rebuild containers
docker compose up --build -d

# Set ownership to your user
sudo chown -R $(id -u):$(id -g) data/ caddy-data/ caddy-config/ caddy-logs/

Solution 3: Use ACLs (Advanced)

# Give both your user and container user access
sudo setfacl -R -m u:$(id -u):rwx data/
sudo setfacl -R -m u:10001:rwx data/
sudo setfacl -R -d -m u:$(id -u):rwx data/
sudo setfacl -R -d -m u:10001:rwx data/

# Same for caddy directories
sudo setfacl -R -m u:$(id -u):rwx caddy-data/ caddy-config/ caddy-logs/
sudo setfacl -R -m u:10000:rwx caddy-data/ caddy-config/ caddy-logs/
sudo setfacl -R -d -m u:$(id -u):rwx caddy-data/ caddy-config/ caddy-logs/
sudo setfacl -R -d -m u:10000:rwx caddy-data/ caddy-config/ caddy-logs/

Migrating from Bind Mounts to Docker Volumes

If you're upgrading from an older version or want to switch back to volumes:

Step 1: Backup your data

tar czf caddy-proxy-manager-backup.tar.gz data/ caddy-data/ caddy-config/ caddy-logs/

Step 2: Restore docker-compose.yml to use volumes

Revert to the default configuration shown at the top of this section.

Step 3: Copy data to Docker volumes

# Stop containers
docker compose down

# Create volumes by starting containers briefly
docker compose up -d
docker compose down

# Copy data to volumes
docker run --rm -v caddy-proxy-manager_caddy-manager-data:/target -v $(pwd)/data:/source alpine cp -a /source/. /target/
docker run --rm -v caddy-proxy-manager_caddy-data:/target -v $(pwd)/caddy-data:/source alpine cp -a /source/. /target/
docker run --rm -v caddy-proxy-manager_caddy-config:/target -v $(pwd)/caddy-config:/source alpine cp -a /source/. /target/
docker run --rm -v caddy-proxy-manager_caddy-logs:/target -v $(pwd)/caddy-logs:/source alpine cp -a /source/. /target/

# Start containers with volumes
docker compose up -d

Step 4: Verify and cleanup

# Test that everything works
# Access your dashboard and verify settings

# Once confirmed working, optionally remove old directories
rm -rf data/ caddy-data/ caddy-config/ caddy-logs/

Accessing Data in Docker Volumes

Inspect volume location:

docker volume inspect caddy-proxy-manager_caddy-manager-data

View files in a volume:

docker run --rm -v caddy-proxy-manager_caddy-manager-data:/data alpine ls -la /data

Copy file from volume to host:

docker run --rm -v caddy-proxy-manager_caddy-manager-data:/data -v $(pwd):/backup alpine cp /data/caddy-proxy-manager.db /backup/

Copy file from host to volume:

docker run --rm -v caddy-proxy-manager_caddy-manager-data:/data -v $(pwd):/backup alpine cp /backup/caddy-proxy-manager.db /data/

Backup a volume:

docker run --rm -v caddy-proxy-manager_caddy-manager-data:/data -v $(pwd):/backup alpine tar czf /backup/caddy-manager-data.tar.gz -C /data .

Restore a volume:

docker run --rm -v caddy-proxy-manager_caddy-manager-data:/data -v $(pwd):/backup alpine tar xzf /backup/caddy-manager-data.tar.gz -C /data

Development vs Production

Development Setup

Recommended: Match host user

# .env
PUID=1000  # Your user
PGID=1000  # Your group

Benefits:

  • Easy file editing on host
  • Simple debugging
  • No permission issues
  • Quick iteration

Production Setup

Recommended: Use dedicated UIDs

# .env (or use defaults)
PUID=10001  # Web service
PGID=10001

# Caddy uses separate PUID=10000 by default

Benefits:

  • Clear security boundaries
  • Easy to identify processes
  • Audit trail clarity
  • Follows best practices

Troubleshooting

Permission Denied on Container Start

Error:

Error: EACCES: permission denied, mkdir '/app/data'

Solution:

# Change ownership of volume directories
sudo chown -R 10001:10001 data/
sudo chown -R 10000:10000 caddy-data/ caddy-config/

# Or rebuild with your user
PUID=$(id -u) PGID=$(id -g) docker compose up --build -d

Can't Edit Files on Host

Symptom: Permission denied when editing data/ files on host

Cause: Files owned by container UID (10001), not your user

Solution:

Option A: Change PUID to your user (Recommended for dev)

PUID=$(id -u) PGID=$(id -g) docker compose up --build -d

Option B: Use sudo to edit files

sudo nano data/caddy-proxy-manager.db

Option C: Add yourself to group 10001

sudo groupadd -g 10001 caddypm
sudo usermod -aG caddypm $(whoami)
# Log out and back in

sudo chgrp -R caddypm data/
sudo chmod -R g+rw data/

Process Running as Root

Symptom: docker compose exec web id shows uid=0(root)

Cause: Rebuild didn't take effect or environment variable not set

Solution:

# Ensure variables are set
echo $PUID
echo $PGID

# Force rebuild
docker compose down
docker compose build --no-cache
docker compose up -d

# Verify
docker compose exec web id

Different PUID for Web vs Caddy

Q: Can I use different PUID/PGID for each service?

A: Yes! This is the default configuration.

Configuration:

Currently, the same PUID/PGID environment variables are used for both services with different defaults.

To customize per-service:

Edit docker-compose.yml:

services:
  web:
    build:
      args:
        PUID: ${WEB_PUID:-10001}
        PGID: ${WEB_PGID:-10001}

  caddy:
    build:
      args:
        PUID: ${CADDY_PUID:-10000}
        PGID: ${CADDY_PGID:-10000}

Then in .env:

WEB_PUID=1000
WEB_PGID=1000
CADDY_PUID=10000
CADDY_PGID=10000

File Ownership After Rebuild

Q: After changing PUID, existing files have wrong ownership

A: You must change file ownership manually

# Stop containers
docker compose down

# Change ownership to new PUID/PGID
sudo chown -R 1000:1000 data/  # Replace 1000 with your PUID/PGID

# Start with new IDs
PUID=1000 PGID=1000 docker compose up --build -d

Security Recommendations

Production Best Practices

  1. Use dedicated UIDs (not 0/root)

  2. Keep defaults unless specific need to change

  3. Restrict volume permissions:

    chmod 700 data caddy-data caddy-config
  4. Monitor process ownership:

    docker compose exec web ps aux
    docker compose exec caddy ps aux
  5. Regular audits of file permissions

Development Best Practices

  1. Match host user for easy development
  2. Document PUID/PGID in project README
  3. Consistent across team (use .env file)
  4. Don't commit .env (security risk)

Related Documentation


Need help? Open an issue with your PUID/PGID configuration and error details.

Clone this wiki locally