Repository navigation
Rootless Docker Operation
Run Caddy Proxy Manager containers as non-root users.
- What This Does
- Why Bother
- PUID and PGID Explained
- Default Configuration
- Custom UID/GID Setup
- File Ownership and Permissions
- XDG Directories (Caddy Service)
- Docker Volumes vs Bind Mounts
- Development vs Production
- Troubleshooting
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.
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 (Process User ID): Unix user ID for the container process
- PGID (Process Group ID): Unix group ID for the container process
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
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- Dockerfile creates non-root user with specified UID/GID
- Application runs as this user
- Files created in volumes owned by this user
Inside container:
- Process runs as user
10001(web) or10000(caddy) - Files created with these ownership
On host:
- Volume files owned by UID
10001/10000 - May not match your host user
Check your current user:
# Your user ID
id -u
# Your group ID
id -g
# Full user info
idExample output:
uid=1000(john) gid=1000(john) groups=1000(john),4(adm),24(cdrom),27(sudo)...
In this example:
- PUID = 1000
- PGID = 1000
Option A: Environment Variables (Recommended)
Add to .env file:
# .env
PUID=1000
PGID=1000Option B: Export Variables
export PUID=1000
export PGID=1000Option C: Inline with Docker Compose
PUID=1000 PGID=1000 docker compose up --build -dIMPORTANT: 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 -dVerify:
# Check process user inside web container
docker compose exec web id
# Should show uid=1000 gid=1000 (or your values)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.dbThe 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.
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 -dXDG Base Directory specification for application data storage.
Caddy service environment:
environment:
XDG_CONFIG_HOME: /config
XDG_DATA_HOME: /dataPurpose:
-
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
volumes:
- ./caddy-config:/config # XDG_CONFIG_HOME
- ./caddy-data:/data # XDG_DATA_HOMEOwnership: Owned by PUID/PGID set for Caddy service (default 10000:10000)
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
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-logsStep 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 -dCommon 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 -dOn 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/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 -dStep 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/Inspect volume location:
docker volume inspect caddy-proxy-manager_caddy-manager-dataView files in a volume:
docker run --rm -v caddy-proxy-manager_caddy-manager-data:/data alpine ls -la /dataCopy 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 /dataRecommended: Match host user
# .env
PUID=1000 # Your user
PGID=1000 # Your groupBenefits:
- Easy file editing on host
- Simple debugging
- No permission issues
- Quick iteration
Recommended: Use dedicated UIDs
# .env (or use defaults)
PUID=10001 # Web service
PGID=10001
# Caddy uses separate PUID=10000 by defaultBenefits:
- Clear security boundaries
- Easy to identify processes
- Audit trail clarity
- Follows best practices
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 -dSymptom: 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 -dOption B: Use sudo to edit files
sudo nano data/caddy-proxy-manager.dbOption 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/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 idQ: 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=10000Q: 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-
Use dedicated UIDs (not 0/root)
-
Keep defaults unless specific need to change
-
Restrict volume permissions:
chmod 700 data caddy-data caddy-config
-
Monitor process ownership:
docker compose exec web ps aux docker compose exec caddy ps aux
-
Regular audits of file permissions
- Match host user for easy development
- Document PUID/PGID in project README
- Consistent across team (use .env file)
- Don't commit .env (security risk)
- Environment Variables Reference - PUID/PGID variables
- Installation Guide - Initial setup
- Security Configuration - Container security
- Troubleshooting - Permission issues
Need help? Open an issue with your PUID/PGID configuration and error details.