Back to Tutorials

Manage Volumes

Create named volumes, register bind mounts, attach storage to containers, inspect usage, and clean up unused volumes.

Prerequisites

  • Docker is installed and running (green status indicator in Zenithal sidebar)

Scenario 1: Create a Named Volume for PostgreSQL

Named volumes are Docker-managed storage that persist data across container restarts and rebuilds.

  1. Click Volumes in the sidebar
  2. Click the + button (top-right) to open the Create Volume sheet
  3. The sheet shows two volume types as segmented buttons — select Named Volume (the default)
  4. Enter a volume name: postgres-data
    • Names must use lowercase letters, numbers, hyphens, or underscores
    • Example valid names: postgres-data, redis_cache, app01-uploads
  5. Click Create
  6. The volume appears in the Volumes list showing:
    • Volume name (postgres-data)
    • Driver badge (local)
    • Mount point path on the host filesystem

There is no driver selection in the UI — all volumes are created with the default local driver.

Scenario 2: Register a Bind Mount

Bind mounts map a folder on your Mac directly into a container. They don't create Docker-managed volumes — they reference an existing directory.

  1. Click Volumes in the sidebar
  2. Click the + button to open the Create Volume sheet
  3. Select Bind Mount (the second segmented button)
  4. Enter a friendly name: my-project-files
  5. Set the Host Folder — either:
    • Type a path directly: /Users/you/Projects/my-app
    • Click the Browse button to open a folder picker
  6. Click Create

Path Validation

Zenithal validates that the host folder is a valid macOS path. Accepted prefixes:

  • /Users/, /Volumes/, /tmp/, /private/, ~/

If you enter a Linux-style path (e.g., /home/user/data), Zenithal shows a warning — these paths don't exist on macOS and will cause mount failures.

Scenario 3: Attach a Named Volume to a PostgreSQL Container

This is the most common volume workflow — creating persistent storage for a database.

  1. First, create a named volume called postgres-data (see Scenario 1)
  2. Click Containers in the sidebar
  3. Click the + button to open the Run Container sheet
  4. In the Image field, enter: postgres:16-alpine
  5. Zenithal auto-configures the container:
    • Container name set automatically
    • Port 5432 mapped automatically
    • POSTGRES_PASSWORD and POSTGRES_DB added with a generated secure password
    • Mount path suggested as /var/lib/postgresql (v18+) or /var/lib/postgresql/data (v16 and older)
  6. Expand the Storage section
  7. Switch the volume type picker from Host Folder to Named Volume
  8. Select postgres-data from the dropdown
  9. Set Container Path to: /var/lib/postgresql/data (for postgres 16)
  10. Read Only: leave OFF (databases need write access)
  11. Click Run

Verifying Persistence

To confirm the volume is working:

  1. Connect to the running postgres container via the Terminal tab
  2. Create a test database:
sql
psql -U postgres -c "CREATE DATABASE testdb;"
  1. Stop and delete the container
  2. Create a new postgres container with the same postgres-data volume mounted
  3. Connect again and verify the database still exists:
sql
psql -U postgres -c "\l"

Scenario 4: Attach a Bind Mount to an Nginx Container

Use a bind mount to serve local files directly from your Mac.

  1. Open the Run Container sheet (Containers → +)
  2. Image: nginx:alpine
  3. Container Name: my-nginx
  4. Expand the Storage section — Host Folder is selected by default
  5. Set the host folder:
    • Click the Browse button and select ~/Projects/my-site
    • Or type the path directly
  6. Set Container Path to: /usr/share/nginx/html
    • Zenithal suggests this path automatically for nginx images (lightbulb icon)
  7. Toggle Read Only to ON (nginx only needs to read the files)
  8. Add a port mapping: Host 8080 → Container 80 (tcp)
  9. Click Run
  10. Open http://localhost:8080 in your browser to see your site

Example File

Create ~/Projects/my-site/index.html before running:

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello from Zenithal</title>
</head>
<body style="font-family: system-ui; text-align: center; padding: 4rem;">
    <h1>Hello from Nginx</h1>
    <p>This file is served from a bind mount on your Mac</p>
</body>
</html>

Scenario 5: Inspect Volume Usage

  1. Click Volumes in the sidebar
  2. The list shows all named volumes (anonymous 64-character hash volumes are hidden)
  3. Each volume row displays:
    • Icon: lock icon (orange) if in use by a container, drive icon if unused
    • Name: the volume identifier
    • Driver: badge showing the storage driver (typically local)
    • Status: either "Used by: container-name" (orange text) or the mount point path
  4. Volumes currently attached to a container show the lock icon and cannot be deleted

Scenario 6: Clean Up Unused Volumes

Over time, unused volumes accumulate and consume disk space. Zenithal makes it easy to prune them.

  1. Click Volumes in the sidebar
  2. Click the Cleanup button (trash icon) in the toolbar
  3. A confirmation dialog appears: "This will permanently remove all volumes not used by any container. Data in these volumes cannot be recovered."
  4. Click Cleanup to confirm
  5. Zenithal shows the result:
    • Success: "Removed X unused volume(s). Reclaimed: Y MB"
    • Nothing to clean: "No unused volumes found."
    • Volumes attached to any container (running or stopped) are never removed

Scenario 7: Delete a Specific Volume

  1. Click Volumes in the sidebar
  2. Find the volume you want to delete
  3. Click the trash icon on the volume row
    • If the volume is in use (lock icon), the delete button is grayed out
  4. Confirm in the dialog: "Are you sure you want to permanently delete 'volume-name'? This data cannot be recovered."
  5. Click Delete

If the volume is still attached to a stopped container, you'll see: "This volume is currently in use by a container. Please stop the container first."

What You'll See

  • Volumes with a lock icon are in use — they cannot be deleted
  • Unused volumes show a drive icon and a delete button
  • The Cleanup button removes all unused volumes at once
  • Anonymous volumes (64-character hashes) are hidden from the list

Tips

  • Named volumes vs bind mounts: Use named volumes for data you want Docker to manage (databases, caches). Use bind mounts for files you edit on your Mac (source code, config files)
  • Database storage: Always use a named volume for database data directories (/var/lib/postgresql for postgres 18+, /var/lib/postgresql/data for older, /var/lib/mysql, /data/db). Without a volume, data is lost when the container is removed
  • PostgreSQL 18+ mount path: Starting with PostgreSQL 18, mount at /var/lib/postgresql (the parent directory) instead of /var/lib/postgresql/data. This allows pg_upgrade --link to work without mount boundary issues
  • Read-only mounts: Toggle Read Only ON for bind mounts where the container only reads files (e.g., nginx serving static HTML). This prevents accidental writes
  • Mount path suggestions: When creating a container, Zenithal shows suggested mount paths for known images (e.g., /usr/share/nginx/html for nginx)
  • Pruning is safe: Cleanup only removes volumes with zero container references — running and stopped containers' volumes are protected
  • Bind mounts don't appear here: Bind mounts are not Docker-managed volumes and don't show in the Volumes list. Check the container's Inspect view instead
  • Volume lifecycle: Deleting a container does NOT delete its named volumes — you must prune or delete them separately

Related Tutorials