How to Run Ghost CMS in Rootless Docker (Complete Setup Guide)

Ghost CMS running in rootless Docker with security shield

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.

Ghost CMS running in rootless Docker with security shield

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.

FeatureRoot DockerRootless Docker
Daemon runs asrootyour user
Container escape riskFull root on hostUnprivileged user
Bind port < 1024Works out of the boxNeeds sysctl or net_bind_service cap
Volume ownershipUsually just worksRequires UID mapping awareness
systemd integrationSystem serviceUser 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:

SymptomLikely causeFix
Ghost container exits immediately, no logsContent directory has wrong ownershipFix volume UID (see below)
Ghost starts but can't connect to DBMySQL not ready when Ghost startedAdd healthcheck + depends_on condition
Port binding fails on 80/443Rootless Docker can't bind privileged portsUse port 2368 + Nginx reverse proxy
Container starts but Ghost 502sURL mismatch — Ghost url env doesn't match actual domainSet 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.

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.

Subscribe to Ghost SEO

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe