Pixelfed-Instanz mit Docker Compose installieren

Pixelfed-Instanz mit Docker Compose installieren
Photo by Elena Rossini / Unsplash

Überblick & Architektur

Die empfohlene Methode basiert auf dem jippi/docker-pixelfed-Projekt, dem derzeit aktivsten und am besten dokumentierten Docker-Setup für Pixelfed. Es konsolidiert die ursprüngliche Arbeit, geplante Verbesserungen und Dokumentation an einem Ort. Das Setup besteht aus mehreren Containern: web (PHP/Apache oder PHP-FPM+Nginx), worker (Laravel Horizon), db (MariaDB), redis und optional einem integrierten proxy (Nginx + Let's Encrypt).


1. Voraussetzungen

Hardware (Minimum)

Für eine kleine Instanz mit bis zu 25 Nutzern werden empfohlen: 2 CPU-Kerne, 2–4 GB RAM, 20–50 GB Speicher (SSD/NVMe bevorzugt, besonders für die Datenbank), sowie 100 Mbit/s Netzwerk.

Software auf dem Host

# Ubuntu/Debian
apt update && apt install -y docker.io docker-compose-plugin git curl

# Docker-Dienst aktivieren
systemctl enable --now docker

# Docker-Version prüfen (Compose v2 muss verfügbar sein)
docker compose version

Netzwerk

Port 80 (HTTP) und Port 443 (HTTPS) müssen zum Server weitergeleitet sein. Eine Domain oder Subdomain mit entsprechendem A-Record, der auf die Server-IP zeigt, ist zwingend erforderlich.


2. Verzeichnis anlegen & Repository klonen

mkdir -p /data
git clone https://github.com/jippi/docker-pixelfed.git /data/pixelfed
cd /data/pixelfed

Systemvoraussetzungen prüfen

Das scripts/-Verzeichnis enthält nützliche Hilfsskripte. Das erste davon prüft, ob Server und Software die Anforderungen erfüllen:

scripts/check-requirements

3. Konfiguration (.env)

Schnellstart (empfohlen)

Statt die über 1.000 Zeilen lange .env-Datei manuell zu durchforsten, gibt es ein interaktives Setup-Skript, das durch die wichtigsten Einstellungen führt:

scripts/setup

Manuelle Konfiguration

cp .env.docker .env
nano .env

Die wichtigsten Pflichtfelder:

# =============================================
# App-Grundeinstellungen
# =============================================
APP_NAME="Meine Pixelfed Instanz"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://pixelfed.meinedomain.de
APP_DOMAIN="pixelfed.meinedomain.de"   # KEIN https://, KEIN abschließender Slash!
ADMIN_DOMAIN="pixelfed.meinedomain.de"

# =============================================
# PHP-Version & Runtime (wichtig für PHP-Betrieb)
# =============================================
DOCKER_APP_PHP_VERSION=8.4
DOCKER_APP_RUNTIME=apache   # oder: nginx (FPM)
DOCKER_APP_RELEASE=latest   # oder: staging / edge / ein SemVer-Tag

# =============================================
# Datenbank
# =============================================
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=pixelfed
DB_USERNAME=pixelfed
DB_PASSWORD=SICHERES_ZUFALLSPASSWORT_HIER   # z. B. via: pwgen -s 32 1

# =============================================
# Redis (Cache, Queue, Session)
# =============================================
REDIS_HOST=redis
CACHE_DRIVER=redis
BROADCAST_DRIVER=redis
QUEUE_DRIVER=redis
SESSION_DRIVER=redis

# =============================================
# ActivityPub / Fediverse
# =============================================
ACTIVITY_PUB=true
AP_REMOTE_FOLLOW=true
AP_INBOX=true
AP_OUTBOX=true
AP_SHAREDINBOX=true

# =============================================
# Registrierung & E-Mail
# =============================================
OPEN_REGISTRATION=false          # true = öffentliche Registrierung
ENFORCE_EMAIL_VERIFICATION=true  # false wenn kein SMTP konfiguriert

# E-Mail (Beispiel: SMTP)
MAIL_DRIVER=smtp
MAIL_HOST=smtp.meinprovider.de
MAIL_PORT=587
MAIL_USERNAME=pixelfed@meinedomain.de
MAIL_PASSWORD=MAIL_PASSWORT
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="pixelfed@meinedomain.de"
MAIL_FROM_NAME="Pixelfed"

# =============================================
# Uploads & Limits
# =============================================
MAX_PHOTO_SIZE=15000           # in KB (hier 15 MB)
MAX_ALBUM_LENGTH=10
IMAGE_QUALITY=85
PF_COSTAR_ENABLED=false

# =============================================
# Proxy-Trust (wichtig hinter Reverse Proxy!)
# =============================================
TRUST_PROXIES="*"              # oder konkrete IP des Proxies

# =============================================
# Integrierter Nginx-Proxy + Let's Encrypt
# (nur wenn kein externer Reverse Proxy genutzt wird)
# =============================================
DOCKER_PROXY_ACME_EMAIL=admin@meinedomain.de

4. Das docker-compose.yml

Das neue docker-compose.yml enthält einen optionalen (standardmäßig aktivierten) Nginx-Proxy für SSL/TLS-Terminierung sowie einen ACME/Let's Encrypt-Dienst, der SSL-Zertifikate automatisch erstellt und erneuert.

Das Repository bringt die Datei fertig mit. Für den Standardfall mit integriertem Proxy reicht das mitgelieferte docker-compose.yml. Wer einen externen Reverse Proxy (Host-Nginx, Traefik usw.) nutzt, muss den internen Proxy deaktivieren und den Port exponieren. Dazu folgendes in die .env eintragen:

# Internen Proxy deaktivieren (für externen Reverse Proxy)
DOCKER_PROXY_PROFILE=disabled

Und im docker-compose.yml (oder einer docker-compose.override.yml) den Port des web-Containers freigeben:

# docker-compose.override.yml
services:
  web:
    ports:
      - "127.0.0.1:8080:80"

Der vollständige Compose-Stack sieht konzeptionell so aus (aus dem offiziellen Repo):

# docker-compose.yml (vereinfachte Darstellung der Dienste)
services:

  web:
    image: ${DOCKER_APP_IMAGE:-jippi/pixelfed}:${DOCKER_APP_RELEASE:-latest}-${DOCKER_APP_RUNTIME:-apache}-${DOCKER_APP_PHP_VERSION:-8.4}-${DOCKER_APP_DEBIAN_RELEASE:-bookworm}
    restart: unless-stopped
    env_file:
      - .env
    volumes:
      - "${DOCKER_APP_HOST_STORAGE_PATH:-./.storage/web}:/var/www/storage"
      - "${DOCKER_APP_HOST_CACHE_PATH:-./.cache/web}:/var/www/bootstrap/cache"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - internal
      - proxy

  worker:
    image: ${DOCKER_APP_IMAGE:-jippi/pixelfed}:${DOCKER_APP_RELEASE:-latest}-${DOCKER_APP_RUNTIME:-apache}-${DOCKER_APP_PHP_VERSION:-8.4}-${DOCKER_APP_DEBIAN_RELEASE:-bookworm}
    restart: unless-stopped
    env_file:
      - .env
    volumes:
      - "${DOCKER_APP_HOST_STORAGE_PATH:-./.storage/worker}:/var/www/storage"
      - "${DOCKER_APP_HOST_CACHE_PATH:-./.cache/worker}:/var/www/bootstrap/cache"
    command: gosu www-data php artisan horizon
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - internal

  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-rootpasswort}"
      MYSQL_DATABASE: "${DB_DATABASE:-pixelfed}"
      MYSQL_USER: "${DB_USERNAME:-pixelfed}"
      MYSQL_PASSWORD: "${DB_PASSWORD}"
    volumes:
      - "./.storage/db:/var/lib/mysql"
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - internal

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - "./.storage/redis:/data"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - internal

  proxy:
    image: nginxproxy/nginx-proxy:alpine
    restart: unless-stopped
    profiles:
      - "${DOCKER_PROXY_PROFILE:-proxy}"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/tmp/docker.sock:ro"
      - "./.storage/proxy/certs:/etc/nginx/certs"
      - "./.storage/proxy/html:/usr/share/nginx/html"
    networks:
      - proxy

  acme:
    image: nginxproxy/acme-companion
    restart: unless-stopped
    profiles:
      - "${DOCKER_PROXY_PROFILE:-proxy}"
    depends_on:
      - proxy
    environment:
      DEFAULT_EMAIL: "${DOCKER_PROXY_ACME_EMAIL}"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./.storage/proxy/certs:/etc/nginx/certs"
      - "./.storage/proxy/html:/usr/share/nginx/html"
      - "./.storage/proxy/acme:/etc/acme.sh"
    networks:
      - proxy

networks:
  internal:
    internal: true
  proxy:

5. Nginx-Konfiguration (externer Host-Nginx)

Falls Nginx direkt auf dem Host als Reverse Proxy läuft (empfohlen bei mehreren Diensten auf dem Server):

# /etc/nginx/sites-available/pixelfed.meinedomain.de

# HTTP → HTTPS Redirect
server {
    listen 80;
    listen [::]:80;
    server_name pixelfed.meinedomain.de;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
        allow all;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name pixelfed.meinedomain.de;

    # SSL (Let's Encrypt via Certbot)
    ssl_certificate     /etc/letsencrypt/live/pixelfed.meinedomain.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pixelfed.meinedomain.de/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/pixelfed.meinedomain.de/chain.pem;

    # Moderne SSL-Konfiguration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;

    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "strict-origin-when-cross-origin";

    # Upload-Größe (muss >= MAX_PHOTO_SIZE in .env sein!)
    client_max_body_size 50M;

    # Gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
    gzip_vary on;

    # Logs
    access_log /var/log/nginx/pixelfed.access.log;
    error_log  /var/log/nginx/pixelfed.error.log;

    # ActivityPub & WebFinger – KRITISCH: darf nicht gecacht/blockiert werden
    location ~ ^/(\.well-known|api|oauth|users|@)/ {
        proxy_pass         http://127.0.0.1:8080;
        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;
        proxy_read_timeout 120s;
    }

    # Hauptproxy
    location / {
        proxy_pass         http://127.0.0.1:8080;
        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;
        proxy_read_timeout 120s;
        proxy_connect_timeout 30s;
        proxy_send_timeout 120s;

        # WebSocket-Support (für Horizon-Dashboard)
        proxy_http_version 1.1;
        proxy_set_header   Upgrade    $http_upgrade;
        proxy_set_header   Connection "upgrade";
    }
}

Symlink aktivieren und testen:

ln -s /etc/nginx/sites-available/pixelfed.meinedomain.de \
      /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

6. SSL-Zertifikat (Certbot)

apt install -y certbot python3-certbot-nginx

# Zertifikat ausstellen (Nginx muss laufen, Port 80 offen)
certbot --nginx -d pixelfed.meinedomain.de \
  --email admin@meinedomain.de \
  --agree-tos --non-interactive

# Auto-Renewal testen
certbot renew --dry-run

7. Dienste starten

cd /data/pixelfed
docker compose up -d

Logs beobachten:

docker compose logs --tail=100 --follow
# Oder nur bestimmte Container:
docker compose logs web worker proxy

Das Setup lädt alle Docker-Images, startet die Container und beginnt mit dem automatischen Setup.


8. Einmalige Initialisierungsschritte

Sobald alle Container laufen (Healthchecks grün):

# 1. APP_KEY generieren (wird automatisch in .env eingetragen)
docker compose exec -u www-data web php artisan key:generate

# 2. Datenbankmigrationen ausführen
docker compose exec -u www-data web php artisan migrate --force

# 3. ActivityPub-Actor erstellen (WICHTIG für Federation!)
docker compose exec -u www-data web php artisan instance:actor

# 4. Caches aufbauen
docker compose exec -u www-data web php artisan config:cache
docker compose exec -u www-data web php artisan route:cache
docker compose exec -u www-data web php artisan view:cache

# 5. Storage-Link setzen (für öffentliche Uploads)
docker compose exec -u www-data web php artisan storage:link

# 6. Ersten Admin-User anlegen
docker compose exec -u www-data web php artisan user:create
# Danach zum Admin machen:
docker compose exec -u www-data web php artisan user:admin DEIN_USERNAME

# 7. E-Mail manuell verifizieren (falls kein SMTP)
docker compose exec -u www-data web php artisan user:verifyemail DEIN_USERNAME

9. PHP richtig zum Laufen bringen

Der häufigste Fallstrick. Folgendes sicherstellen:

PHP-Version explizit setzen in .env:

DOCKER_APP_PHP_VERSION=8.4   # Immer explizit, nie weglassen

Datei-Permissions — wenn Uploads oder Cache-Fehler auftreten:

# Temporär für Rechte-Fix (danach wieder entfernen!)
DOCKER_APP_RUNTIME_CHOWN="/var/www/storage /var/www/bootstrap/cache"
# Oder manuell:
docker compose exec web chown -R www-data:www-data /var/www/storage
docker compose exec web chown -R www-data:www-data /var/www/bootstrap/cache

PHP-Konfiguration prüfen:

docker compose exec web php -m          # Geladene Module
docker compose exec web php artisan about  # Laravel/Pixelfed-Status

Nach jeder .env-Änderung:

docker compose exec -u www-data web php artisan config:clear
docker compose exec -u www-data web php artisan config:cache
docker compose restart web worker

10. Stabilitäts-Checkliste

Bereich Maßnahme
Backups Tägliches Backup von .storage/db/ (Datenbank) und .storage/web/ (Uploads)
Updates docker compose pull && docker compose up -d + danach php artisan migrate --force
Queue-Worker Sicherstellen dass worker-Container immer läuft (restart: unless-stopped)
Redis Persistenz appendonly yes in Redis-Config oder Volume auf SSD
MariaDB-Version MariaDB 10.11 LTS oder 11.x verwenden, kein MySQL 8 (Kompatibilitätsprobleme)
Firewall Nur Port 80/443 öffnen; Docker-Port 8080 nur auf 127.0.0.1 binden
Log-Rotation /etc/logrotate.d/ für Docker-Logs konfigurieren
Monitoring docker compose ps in Cron oder Healthcheck-Script
TRUSTED_PROXIES In .env auf * oder IP des Proxies setzen, sonst falsche IP-Logs

Cronjob für Laravel-Scheduler

# Als root hinzufügen: crontab -e
* * * * * docker compose -f /data/pixelfed/docker-compose.yml exec -T -u www-data web php artisan schedule:run >> /dev/null 2>&1

11. Updates einspielen

cd /data/pixelfed
docker compose pull
docker compose up -d
docker compose exec -u www-data web php artisan migrate --force
docker compose exec -u www-data web php artisan config:cache
docker compose exec -u www-data web php artisan route:cache
docker compose exec -u www-data web php artisan view:cache

Typische Fehlerquellen auf einen Blick

  • 502 Bad Gateway: Container noch nicht bereit, Healthcheck abwarten — docker compose logs web
  • Uploads schlagen fehl: Permissions auf .storage/web/ prüfen, client_max_body_size in Nginx erhöhen
  • Federation funktioniert nicht: ACTIVITY_PUB=true, TRUST_PROXIES gesetzt, instance:actor ausgeführt?
  • Queue läuft nicht: docker compose logs worker prüfen, Horizon-Status: php artisan horizon:status
  • PHP-Fehler 500: APP_DEBUG=true temporär setzen, Logs lesen, danach wieder auf false