• Dec. 2, 2025, 9:28 a.m.

    1. Context & Philosophy

    BookStack is an open-source platform for organising and storing information using a structured "Book > Chapter > Page" hierarchy.

    This guide documents the installation of BookStack on a YunoHost VPS (base.kalvin.my) using Docker Compose. Instead of a standard root-based installation, this setup prioritises a Humane-Tech approach: strictly controlling data ownership by running containers as a specific non-root administrative user (kalvin0x58c) to prevent permission locks and keep the system clean.


    2. Prerequisites

    System Requirements

    • OS: Debian 12 (Bookworm) with YunoHost installed.
    • Docker: Installed and running.
    • Domain: A subdomain configured in YunoHost (e.g., base.kalvin.my).
    • User Info: A non-root user with sudo privileges.
      • User: kalvin0x58c.
      • PUID (User ID): 23691.
      • PGID (Group ID): 23691.

    3. Installation & Directory Setup

    I store Docker configurations in my home directory to ensure full ownership without needing root access for backups.

    mkdir -p ~/docker/bookstack
    cd ~/docker/bookstack
    nano compose.yaml
    

    The compose.yaml Configuration

    We use the LinuxServer.io images because they handle PUID/PGID mapping excellently.

    To generate the required unique 32-byte APP_KEY for security, use:

    echo "base64:$(openssl rand -base64 32)"
    

    4. Troubleshooting Logs (The "Gotchas")

    While the setup seemed straightforward, I ran into specific issues regarding permissions, database connections, and encryption keys.

    Issue A: "Permission Denied" on Docker Socket

    Symptom: Error accessing /var/run/docker.sock when running docker compose up.
    Cause: My non-root user was not in the docker group.
    Fix:

    sudo usermod -aG docker kalvin0x58c
    newgrp docker
    

    Issue B: Laravel APP_KEY Cipher Error

    Symptom: A white screen or 500 Error. Logs showed RuntimeException: Unsupported cipher or incorrect key length.
    Cause:

    1. Format: The key must be prefixed with base64:.
    2. Conflict: Docker Compose environment variables override local configuration files (.env). A broken key in compose.yaml was overwriting the manual fixes.
      The Fix:
      I removed APP_KEY from compose.yaml to force the container to respect the file-based .env config, then cleared the application cache, which is crucial:

    <!-- end list -->

    docker exec bookstack php artisan config:clear
    docker exec bookstack php artisan cache:clear
    

    Issue C: "Access Denied" (Stale Configuration)

    Symptom: SQLSTATE[HY000] [1045] Access denied for user.... This showed the app was trying to use the wrong or default database credentials.
    Cause: The container generates an internal configuration file in the /config volume during the first run, and it ignores later changes to the compose.yaml database variables.
    The "Nuclear" Fix:
    To force a configuration regeneration, I had to wipe the generated persistent volumes:

    # STOPS containers, DELETES config/data, and RECREATES fresh
    docker compose down -v
    sudo rm -rf ./config ./db_data
    docker compose up -d --force-recreate
    

    5. Final Working Configuration

    This configuration runs securely under my own user ID, isolated in Docker.

    compose.yaml:

    services:
      bookstack:
        image: lscr.io/linuxserver/bookstack:latest
        container_name: bookstack
        environment:
          - PUID=23691
          - PGID=23691
          - TZ=Asia/Kuala_Lumpur
          - APP_URL=https://base.kalvin.my
          - DB_HOST=bookstack_db
          - DB_USER=bookstack
          - DB_PASS=SecretPassword
          - DB_DATABASE=bookstackapp
        volumes:
          - ./config:/config
        ports:
          - 6875:80
        restart: unless-stopped
        depends_on:
          - bookstack_db
    
      bookstack_db:
        image: lscr.io/linuxserver/mariadb:latest
        container_name: bookstack_db
        environment:
          - PUID=23691
          - PGID=23691
          - TZ=Asia/Kuala_Lumpur
          - MYSQL_ROOT_PASSWORD=RootPassword
          - MYSQL_DATABASE=bookstackapp
          - MYSQL_USER=bookstack
          - MYSQL_PASSWORD=SecretPassword
        volumes:
          - ./db_data:/config
        restart: unless-stopped
    

    ./config/www/.env (Key snippets):

    APP_KEY=base64:YourActualGeneratedKey==
    # Mail Settings (Always wrap passwords with special characters in double quotes)
    MAIL_PASSWORD="!PasswordWithSymbols@"
    

    6. Exposing to the Web (YunoHost Reverse Proxy)

    Since the Docker app runs internally on localhost:6875, YunoHost is used as the bridge to provide HTTPS access.

    1. Installed the Redirect app in the YunoHost Web Admin.
    2. Configured:
      • Domain: base.kalvin.my
      • Destination: http://127.0.0.1:6875

    7. Lessons Learnt

    1. Variable Precedence: Docker Compose environment variables always overwrite file-based configurations.
    2. Caching: When in doubt about configuration changes, php artisan config:clear is essential.
    3. Mail Config: Always wrap passwords with special characters in double quotes "" to prevent parsing errors.
    4. Security: Immediately change the default credentials (admin@admin.com / password) in the user profile settings after the first successful login.