#cloud-config # Set hostname hostname: photoprism-pi fqdn: photoprism-pi.local # Update and upgrade packages package_update: true package_upgrade: true # Install required packages packages: - apt-transport-https - ca-certificates - curl - gnupg - lsb-release - git - openssl - avahi-daemon # Install Docker Engine (latest version) runcmd: # Print welcome message - printf "\e[0s;35m[SETUP] Starting PhotoPrism installation on Raspberry Pi...\e[0m\n" # Uninstall old versions if they exist - printf "\e[0;35m[SETUP] Removing old Docker versions if present...\e[0m\n" - for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do apt-get remove -y $pkg || true; done - printf "\e[0;35m[SETUP] ✓ Old Docker versions removed or not present\e[0m\n" # Add Docker's official GPG key - printf "\e[0;35m[SETUP] Adding Docker repository GPG key...\e[0m\n" - apt-get update - apt-get install -y ca-certificates curl - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - chmod a+r /etc/apt/keyrings/docker.asc - printf "\e[0;35m[SETUP] ✓ Docker GPG key added\e[0m\n" # Add Docker repository - printf "\e[0;35m[SETUP] Adding Docker repository...\e[0m\n" - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null - apt-get update - printf "\e[0;35m[SETUP] ✓ Docker repository added\e[0m\n" # Install Docker Engine - printf "\e[0;35m[SETUP] Installing Docker Engine...\e[0m\n" - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - printf "\e[0;35m[SETUP] ✓ Docker Engine installed\e[0m\n" # Create directories for PhotoPrism and Traefik - printf "\e[0;35m[SETUP] Creating directory structure for PhotoPrism...\e[0m\n" - mkdir -p /opt/photoprism/photos - mkdir -p /opt/photoprism/storage - mkdir -p /opt/photoprism/database - mkdir -p /opt/photoprism/import - mkdir -p /opt/photoprism/originals - mkdir -p /opt/photoprism/traefik/certs - mkdir -p /opt/photoprism/traefik/conf.d - mkdir -p /etc/traefik - chown -R 1000:1000 /opt/photoprism - chmod -R 755 /opt/photoprism - chmod -R ug+rwX,o-rwx /opt/photoprism/storage - chmod -R ug+rwX,o-rwx /opt/photoprism/database - chmod -R ug+rwX,o-rwx /opt/photoprism/import - chmod -R ug+rwX,o-rwx /opt/photoprism/originals - chmod -R ug+rwX,o-rwx /opt/photoprism/photos - printf "\e[0;35m[SETUP] ✓ PhotoPrism directory structure created\e[0m\n" # Create mount points for external drives - printf "\e[0;35m[SETUP] Creating mount points for external drives...\e[0m\n" - mkdir -p /mnt/a - mkdir -p /mnt/b - mkdir -p /mnt/c - mkdir -p /mnt/d - chown -R 1000:1000 /mnt - printf "\e[0;35m[SETUP] ✓ External drive mount points created\e[0m\n" # Configure external drives in fstab - printf "\e[0;35m[SETUP] Configuring external drives in fstab...\e[0m\n" - | cat >> /etc/fstab << 'EOF' /dev/sda1 /mnt/a auto nofail,noatime,noauto,x-systemd.automount,x-systemd.device-timeout=10s,uid=1000,gid=1000 0 0 /dev/sdb1 /mnt/b auto nofail,noatime,noauto,x-systemd.automount,x-systemd.device-timeout=10s,uid=1000,gid=1000 0 0 /dev/sdc1 /mnt/c auto nofail,noatime,noauto,x-systemd.automount,x-systemd.device-timeout=10s,uid=1000,gid=1000 0 0 /dev/sdd1 /mnt/d auto nofail,noatime,noauto,x-systemd.automount,x-systemd.device-timeout=10s,uid=1000,gid=1000 0 0 EOF - printf "\e[0;35m[SETUP] ✓ External drives configured in fstab\e[0m\n" # Set up swap - printf "\e[0;35m[SETUP] Setting up swap space...\e[0m\n" - | cat > /usr/local/bin/swapon.sh << 'EOF' #!/usr/bin/env bash # add 8 GB of swap if no swap was configured yet if [[ -z $(swapon --show) ]]; then fallocate -l 8G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile swapon --show free -h echo '/swapfile none swap sw 0 0' | tee -a /etc/fstab fi EOF - chmod +x /usr/local/bin/swapon.sh - /usr/local/bin/swapon.sh - printf "\e[0;35m[SETUP] ✓ Swap space configured\e[0m\n" # Verify the installation is successful - printf "\e[0;35m[SETUP] Verifying Docker installation...\e[0m\n" - docker run --rm hello-world - printf "\e[0;35m[SETUP] ✓ Docker installation verified\e[0m\n" # Ensure docker group exists and pi user is added to it - printf "\e[0;35m[SETUP] Ensuring pi user is in docker group...\e[0m\n" - getent group docker || groupadd docker - usermod -aG docker pi - printf "\e[0;35m[SETUP] ✓ Pi user added to docker group\e[0m\n" # Enable services - printf "\e[0;35m[SETUP] Enabling required services...\e[0m\n" - systemctl enable docker - systemctl enable avahi-daemon - printf "\e[0;35m[SETUP] ✓ Services enabled\e[0m\n" # Add a service to start PhotoPrism on boot - printf "\e[0;35m[SETUP] Creating PhotoPrism service...\e[0m\n" - | cat > /etc/systemd/system/photoprism.service << 'EOF' [Unit] Description=PhotoPrism Service After=docker.service network-online.target Requires=docker.service network-online.target [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/opt/photoprism ExecStart=/usr/bin/docker compose up -d ExecStop=/usr/bin/docker compose down [Install] WantedBy=multi-user.target EOF - printf "\e[0;35m[SETUP] ✓ PhotoPrism service created\e[0m\n" # Enable PhotoPrism service - printf "\e[0;35m[SETUP] Enabling and starting PhotoPrism service...\e[0m\n" - systemctl enable photoprism.service - systemctl start photoprism.service - printf "\e[0;35m[SETUP] ✓ PhotoPrism service started\e[0m\n" # Configure Avahi for .local domain - printf "\e[0;35m[SETUP] Restarting and configuring Avahi for .local domain...\e[0m\n" - systemctl restart avahi-daemon - printf "\e[0;35m[SETUP] ✓ Avahi configured for .local domain\e[0m\n" # Update MOTD and issue file with actual IP address - printf "\e[0;35m[SETUP] Updating MOTD and console login message with actual IP address...\e[0m\n" - chmod +x /usr/local/bin/update-motd-ip.sh - /usr/local/bin/update-motd-ip.sh - PRIMARY_IFACE=$(ip route | grep default | awk '{print $5}') - IP=$(ip addr show $PRIMARY_IFACE 2>/dev/null | grep -oP 'inet \K[\d.]+' || hostname -I | awk '{print $1}') - | cat > /etc/issue << EOF Welcome to PhotoPrism Pi! You should soon be able to access the web interface by navigating to one of the following URLs: http://$IP:2342/ https://photoprism-pi.local/ For further information and help with troubleshooting: https://docs.photoprism.app/photoprism-pi/ https://docs.photoprism.app/getting-started/troubleshooting/ EOF - printf "\e[0;35m[SETUP] ✓ MOTD and console login message updated with actual IP\e[0m\n" # Write configuration files write_files: - path: /usr/local/bin/update-motd-ip.sh permissions: '0755' content: | #!/bin/bash IP=$(hostname -I | awk '{print $1}') sed -i "s|http://:2342/|http://$IP:2342/|g" /etc/motd - path: /etc/motd permissions: '0644' content: | Welcome to PhotoPrism Pi! You should soon be able to access the web interface by navigating to one of the following URLs: http://:2342/ https://photoprism-pi.local/ Your initial password for the "admin" account is "photoprismpi". Please change it after logging in for the first time, especially if your device is connected to the Internet or a shared network. For further information and help with troubleshooting: https://docs.photoprism.app/photoprism-pi/ https://docs.photoprism.app/getting-started/troubleshooting/ - path: /opt/photoprism/compose.yaml permissions: '0644' content: | name: photoprism services: photoprism: image: photoprism/photoprism:latest depends_on: - mariadb restart: always security_opt: - seccomp=unconfined - apparmor=unconfined user: "1000:1000" ports: - "2342:2342" labels: - "traefik.enable=true" - "traefik.http.routers.photoprism.rule=Host(`photoprism-pi.local`)" - "traefik.http.routers.photoprism.entrypoints=websecure" - "traefik.http.routers.photoprism.tls=true" - "traefik.http.services.photoprism.loadbalancer.server.port=2342" - "traefik.docker.network=photoprism" environment: PHOTOPRISM_ADMIN_PASSWORD: "photoprismpi" PHOTOPRISM_AUTH_MODE: "passwd" PHOTOPRISM_SITE_URL: "https://photoprism-pi.local/" PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App" PHOTOPRISM_HTTP_COMPRESSION: "gzip" PHOTOPRISM_WORKERS: 3 PHOTOPRISM_LOG_LEVEL: "info" PHOTOPRISM_TRACE: "false" PHOTOPRISM_READONLY: "false" PHOTOPRISM_EXPERIMENTAL: "false" PHOTOPRISM_DISABLE_CHOWN: "false" PHOTOPRISM_DISABLE_WEBDAV: "false" PHOTOPRISM_DISABLE_SETTINGS: "false" PHOTOPRISM_DISABLE_TENSORFLOW: "false" PHOTOPRISM_DISABLE_FACES: "false" PHOTOPRISM_DISABLE_CLASSIFICATION: "false" PHOTOPRISM_DISABLE_RAW: "false" PHOTOPRISM_RAW_PRESETS: "false" PHOTOPRISM_JPEG_QUALITY: 82 PHOTOPRISM_FFMPEG_SIZE: 1920 PHOTOPRISM_AUTO_INDEX: 300 # delay before automatically indexing files in SECONDS when uploading via WebDAV (-1 to disable) PHOTOPRISM_AUTO_IMPORT: -1 # delay before automatically importing files in SECONDS when uploading via WebDAV (-1 to disable) PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow) PHOTOPRISM_UPLOAD_NSFW: "true" # allow uploads that MAY be offensive PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all) PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import) PHOTOPRISM_UPLOAD_LIMIT: 5000 # maximum size of uploaded files and uncompressed archive contents in MB PHOTOPRISM_ORIGINALS_LIMIT: 5000 # maximum size of original media files in MB (larger files are skipped) PHOTOPRISM_RESOLUTION_LIMIT: 300 # maximum resolution of original media files in Pixels (larger files are skipped) PHOTOPRISM_SIDECAR_YAML: "false" # creates YAML sidecar files to back up picture metadata PHOTOPRISM_INDEX_SCHEDULE: "" # indexing SCHEDULE in cron format (e.g. "@every 3h" for every 3 hours; "" to disable) PHOTOPRISM_BACKUP_ALBUMS: "true" # creates YAML files to back up album metadata PHOTOPRISM_BACKUP_DATABASE: "true" # creates regular backups based on the configured schedule PHOTOPRISM_BACKUP_SCHEDULE: "daily" # backup SCHEDULE in cron format (e.g. "0 12 * * *" for daily at noon) or at a random time (daily, weekly) PHOTOPRISM_DATABASE_DRIVER: "mysql" PHOTOPRISM_DATABASE_SERVER: "mariadb:3306" PHOTOPRISM_DATABASE_NAME: "photoprism" PHOTOPRISM_DATABASE_USER: "photoprism" PHOTOPRISM_DATABASE_PASSWORD: "insecure" PHOTOPRISM_INIT: "yt-dlp" working_dir: "/photoprism" volumes: - "/opt/photoprism/storage:/photoprism/storage" - "/opt/photoprism/originals:/photoprism/originals" - "/mnt:/photoprism/originals/mnt:slave" - "/opt/photoprism/import:/photoprism/import" mariadb: restart: always image: mariadb:11 security_opt: - seccomp=unconfined - apparmor=unconfined command: --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120 volumes: - "/opt/photoprism/database:/var/lib/mysql" environment: MARIADB_AUTO_UPGRADE: "1" MARIADB_INITDB_SKIP_TZINFO: "1" MARIADB_DATABASE: "photoprism" MARIADB_USER: "photoprism" MARIADB_PASSWORD: "insecure" MARIADB_ROOT_PASSWORD: "insecure" traefik: restart: always image: traefik:v3.5 ports: - "80:80" - "443:443" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "/opt/photoprism/traefik:/etc/traefik:rw" labels: - "traefik.enable=true" command: - "--api.insecure=false" - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - "--entrypoints.web.http.redirections.entrypoint.scheme=https" - "--providers.docker.network=photoprism" - "--providers.docker.defaultRule=Host(`{{ normalize .Name }}.local`)" - "--log.level=DEBUG" watchtower: restart: always image: containrrr/watchtower environment: WATCHTOWER_CLEANUP: "true" WATCHTOWER_POLL_INTERVAL: 7200 volumes: - "/var/run/docker.sock:/var/run/docker.sock" networks: default: name: photoprism driver: bridge - path: /etc/avahi/avahi-daemon.conf permissions: '0644' content: | [server] #host-name=foo #domain-name=local #browse-domains=0pointer.de, zeroconf.org use-ipv4=yes use-ipv6=no allow-interfaces=eth0 #deny-interfaces=eth1 #check-response-ttl=no #use-iff-running=no #enable-dbus=yes #disallow-other-stacks=no #allow-point-to-point=no #cache-entries-max=4096 #clients-max=4096 #objects-per-client-max=1024 #entries-per-entry-group-max=32 ratelimit-interval-usec=1000000 ratelimit-burst=1000 [wide-area] enable-wide-area=yes [publish] #disable-publishing=no #disable-user-service-publishing=no #add-service-cookie=no #publish-addresses=yes publish-hinfo=no publish-workstation=no #publish-domain=yes #publish-dns-servers=192.168.50.1, 192.168.50.2 #publish-resolv-conf-dns-servers=yes #publish-aaaa-on-ipv4=yes #publish-a-on-ipv6=no [reflector] #enable-reflector=no #reflect-ipv=no #reflect-filters=_airplay._tcp.local,_raop._tcp.local [rlimits] #rlimit-as= #rlimit-core=0 #rlimit-data=8388608 #rlimit-fsize=0 #rlimit-nofile=768 #rlimit-stack=8388608 #rlimit-nproc=3 # Configure users users: - name: pi gecos: PhotoPrism User sudo: ALL=(ALL) NOPASSWD:ALL groups: adm,dialout,cdrom,floppy,sudo,audio,dip,video,plugdev,netdev,docker shell: /bin/bash lock_passwd: false # Set passwords chpasswd: expire: false list: | pi:raspberry # Automatically reboot after cloud-init completes power_state: delay: "+1" mode: reboot message: "Rebooting system after cloud-init completes setup" timeout: 30 condition: True