How to Run Ghost CMS in Rootless Docker (Complete Setup Guide)
Quick answer: Running Ghost in rootless Docker means the Docker daemon (and your containers) run as a regular user, not root. You configure user namespaces, bind ports above 1024, and adjust volume ownership — then use a standard Docker Compose file. Ghost runs fine; the main gotchas are port binding and file permission on the content directory.
On the Ghost forum, a recurring self-hosters' headache reads something like this: "Ghost freezes silently after deploying with Docker Compose on rootless Docker — logs show nothing." The container starts, MySQL spins up, and then Ghost just sits there doing nothing.
This is almost always a permissions issue, not a Ghost bug. When you switch Docker to rootless mode, user namespace remapping changes how UIDs inside containers map to your host filesystem. Ghost's content directory needs to be owned by the right UID or it can't write anything — and it fails silently.
This guide walks through the full setup: enabling rootless Docker, writing the correct Compose file, fixing the content volume permissions, and verifying everything works. It covers the exact spots where things go wrong.

What is rootless Docker and why should you use it?
Standard Docker runs a daemon as root. That means any container that breaks out of its sandbox has root on your host. For most hobby setups that's acceptable, but if you're on a shared VPS or you care about security hygiene, it's a real risk.
Rootless Docker moves the daemon into a regular user's session. The daemon process, and every container it runs, operates with that user's privileges. A container escape doesn't get you root — it gets you a normal user account. That's a meaningful security improvement for public-facing servers.
| Feature | Root Docker | Rootless Docker |
|---|---|---|
| Daemon runs as | root | your user |
| Container escape risk | Full root on host | Unprivileged user |
| Bind port < 1024 | Works out of the box | Needs sysctl or net_bind_service cap |
| Volume ownership | Usually just works | Requires UID mapping awareness |
| systemd integration | System service | User service (loginctl linger) |
How do I enable rootless Docker on Ubuntu?
The Docker project ships a helper script that does most of the work. You need uidmap installed first, then run the install script as your regular user.
# Install dependencies
sudo apt-get install -y uidmap dbus-user-session
# Install rootless Docker (run as your target user, NOT root)
dockerd-rootless-setuptool.sh install
# Add to your shell profile
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
# Enable as a user systemd service
systemctl --user enable docker
systemctl --user start docker
# Make the user session linger (so Docker starts on boot)
sudo loginctl enable-linger $USER
Verify it's running rootless:
docker info | grep rootless
# Should output: rootlesskit
If you see docker.sock path errors, check that $XDG_RUNTIME_DIR is set. On Ubuntu 22.04+ it's typically /run/user/1000.
What does the Docker Compose file look like for Ghost with rootless Docker?
The Compose structure is almost identical to a root Docker setup, with one key difference: you should bind Ghost's port to 127.0.0.1 above 1024 and use a reverse proxy (Nginx or Caddy) in front. Ports below 1024 require extra capabilities even in rootless mode.
services:
ghost:
image: ghost:5
container_name: ghost_app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
url: https://yourdomain.com
database__client: mysql
database__connection__host: db
database__connection__user: ghost
database__connection__password: STRONG_PASSWORD
database__connection__database: ghost_db
NODE_ENV: production
volumes:
- ./ghost_content:/var/lib/ghost/content
ports:
- "127.0.0.1:2368:2368"
db:
image: mysql:8.0
container_name: ghost_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ROOT_PASSWORD
MYSQL_DATABASE: ghost_db
MYSQL_USER: ghost
MYSQL_PASSWORD: STRONG_PASSWORD
command: --default-authentication-plugin=mysql_native_password
volumes:
- ./db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
The healthcheck on MySQL is important — without it, Ghost starts before MySQL is ready and throws connection errors that look like Ghost crashes. This is a common source of confusion in rootless setups.
Why does Ghost freeze or not start in rootless Docker?
Silent freezes in rootless Docker almost always come from one of three causes:
| Symptom | Likely cause | Fix |
|---|---|---|
| Ghost container exits immediately, no logs | Content directory has wrong ownership | Fix volume UID (see below) |
| Ghost starts but can't connect to DB | MySQL not ready when Ghost started | Add healthcheck + depends_on condition |
| Port binding fails on 80/443 | Rootless Docker can't bind privileged ports | Use port 2368 + Nginx reverse proxy |
| Container starts but Ghost 502s | URL mismatch — Ghost url env doesn't match actual domain | Set url= correctly in environment |
How do I fix Ghost content directory permissions in rootless Docker?
This is the trickiest part. In rootless Docker, UIDs inside containers are remapped. The Ghost Docker image runs as UID 1000 inside the container, but in rootless mode that maps to a sub-UID on your host — not your actual UID 1000.
The practical fix is to check what UID owns the files after the first run and chown accordingly:
# Start Ghost once to let it create the content directory
docker compose up -d ghost
# Check who owns the content files on the host
ls -la ghost_content/
# You'll likely see something like:
# drwxr-xr-x 1 100999 100999 ...
# Fix ownership so Ghost can write
# First find your subuid mapping:
cat /etc/subuid | grep $USER
# Example output: youruser:100000:65536
# Ghost container runs as UID 1000 internally
# In rootless mode that maps to host UID: subuid_start + 1000 - 1 = 100999
# So:
sudo chown -R 100999:100999 ghost_content/
Alternatively, use a named Docker volume instead of a bind mount — Docker manages ownership automatically:
volumes:
- ghost_content:/var/lib/ghost/content
volumes:
ghost_content:
Named volumes are simpler for rootless setups. Bind mounts give you easier access for backups — pick based on your needs. If you're also working through memory issues on low-RAM VPS setups, named volumes also reduce the chance of permission-related OOM loops.
How do I set up Nginx as a reverse proxy for rootless Ghost?
Since rootless Docker can't bind port 80 directly, Nginx (running on the host or in its own container) handles SSL termination and proxies to Ghost on 127.0.0.1:2368.
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:2368;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Get an SSL cert with Certbot before configuring this:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com
Once Nginx is proxying, set url: https://yourdomain.com in your Ghost Compose environment. Ghost uses this URL to generate correct links — if it doesn't match, you'll get broken admin redirects. You can read more about URL-related issues in Ghost admin login breaking after URL changes.
Can I use Podman instead of rootless Docker for Ghost?
Yes. Podman is rootless by default and supports Docker Compose files via podman-compose or the Docker Compose plugin. The same Compose file from above works with minor adjustments.
# Install podman and podman-compose
sudo apt-get install -y podman
pip3 install podman-compose
# Run the same compose file
podman-compose up -d
Podman uses the same user namespace remapping approach, so the same UID ownership caveats apply. The main advantage: no daemon required — Podman spawns containers as direct child processes of your shell, which is even simpler from a security standpoint.
For production use, Podman Quadlet (systemd unit files generated from container definitions) is worth exploring — it gives you systemd-native lifecycle management without a daemon. The Ghost restart troubleshooting guide covers systemd service patterns that translate directly to Podman Quadlet.
How do I make rootless Ghost start automatically on boot?
Since rootless Docker runs as a user service, you need two things: the Docker user service enabled, and your login session to linger after you log out.
# Enable Docker user service (already done during install)
systemctl --user enable docker
# Enable session linger so user services start at boot
sudo loginctl enable-linger $USER
# Create a systemd user service for Docker Compose
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/ghost-compose.service << 'EOF'
[Unit]
Description=Ghost CMS Docker Compose
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/youruser/ghost
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=default.target
EOF
systemctl --user enable ghost-compose
systemctl --user start ghost-compose
This is the cleanest way to handle auto-start — no cron hacks, no root service wrappers. Your Ghost stack boots with your user session and stays up as long as the host is running.
How do I back up Ghost data in a rootless Docker setup?
Backups in rootless Docker work the same as in root Docker — you just need to be mindful of the UID remapping when copying files externally. The safest backup approach uses Docker's own mechanisms:
#!/bin/bash
# backup-ghost.sh
BACKUP_DIR="/home/$USER/backups/ghost"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# Backup Ghost content via docker cp (handles UID mapping automatically)
docker cp ghost_app:/var/lib/ghost/content "$BACKUP_DIR/content_$DATE"
# Backup MySQL with mysqldump
docker exec ghost_db mysqldump -u ghost -pSTRONG_PASSWORD ghost_db > "$BACKUP_DIR/db_$DATE.sql"
echo "Backup completed: $BACKUP_DIR"
Run this on a cron schedule. For full disaster recovery, also export through Ghost Admin — Settings → Labs → Export Content. That JSON export restores posts, tags, and member data independently of your Docker volumes. Pair this with Cloudflare caching so your site stays up even during backup windows.
Related Ghost Guides
- Ghost Installation on Ubuntu: Step-by-Step Guide — full bare-metal setup if you prefer Ghost CLI over Docker
- Ghost CMS Restart Not Working: Every Fix — covers systemd, CLI, and Docker restart scenarios
- Ghost 500 Internal Server Error: Causes and Fixes — covers database connection errors that surface in Docker setups
- Fix Out-of-Memory Crashes on a 1 GB RAM Server for Ghost — Docker adds memory overhead; this guide helps tune it
- Ghost Admin Login Not Working After URL Change — common issue when reconfiguring Ghost's URL in Docker
- How to Update Ghost — updating Ghost in Docker (pull new image, restart compose)
- 502 Bad Gateway Ghost CMS Error Fixed — Nginx proxy misconfiguration often surfaces here
Frequently Asked Questions
Does Ghost officially support rootless Docker?
Ghost's Docker image works in rootless mode — the image itself doesn't care. The official Ghost Docker docs cover standard Compose setups; rootless is a host-level Docker configuration choice. You're responsible for handling the UID remapping and port binding differences.
Why does Ghost log nothing when it fails in rootless Docker?
Ghost can exit before it writes any logs if the content directory isn't writable. The process starts, tries to access /var/lib/ghost/content, gets a permissions error, and exits. Check docker logs ghost_app immediately after starting — if it's empty, the volume ownership is wrong.
What ports should I expose in rootless Docker for Ghost?
Use Ghost's default port 2368 bound to 127.0.0.1. Don't try to bind 80 or 443 from the container directly — rootless Docker can't do that without extra kernel capabilities. Let Nginx or Caddy on the host handle 80/443 and proxy to 2368.
Can I run Ghost rootless Docker on a 1 GB RAM VPS?
Yes, but tightly. Ghost + MySQL + Docker overhead can push memory usage to 700–800 MB at idle. Add a 1–2 GB swap file and tune MySQL's innodb_buffer_pool_size down to 64–128 MB. Use the memory optimization guide for specifics.
What's the difference between rootless Docker and Podman for Ghost?
Both achieve the same security goal. Rootless Docker still has a daemon (running as your user). Podman has no daemon — containers are direct child processes. For Ghost specifically, both work fine. Podman is marginally more secure; rootless Docker is more familiar if you already know Docker tooling.
How do I check if my Docker is running in rootless mode?
Run docker info | grep -i rootless. If rootless mode is active, you'll see rootlesskit in the output. You can also check ps aux | grep dockerd — in rootless mode the dockerd process runs under your username, not root.
Does rootless Docker affect Ghost's email sending?
No. Email goes out through SMTP over standard outbound ports (587, 465) which work fine in rootless mode. Inbound connections are the ones that require port binding — and those go through your reverse proxy, not Ghost directly.
Can I use Docker Desktop's rootless mode for Ghost development?
Docker Desktop handles rootless mode differently depending on the OS. On Linux, it uses a VM so rootless behavior is somewhat abstracted. For local Ghost development, the standard Docker Desktop setup is fine — rootless mode matters most on production Linux servers where security boundaries are real.
The cleanest path: start with a named Docker volume (not a bind mount), add the MySQL healthcheck, set your Ghost URL correctly, and put Nginx in front. Those four things eliminate 90% of rootless Docker issues Ghost users hit. Once it's stable, the security benefits of not running Docker as root are real and worth the small setup overhead.