Pixelfed-Instanz mit Docker Compose installieren
Ü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_sizein Nginx erhöhen - Federation funktioniert nicht:
ACTIVITY_PUB=true,TRUST_PROXIESgesetzt,instance:actorausgeführt? - Queue läuft nicht:
docker compose logs workerprüfen, Horizon-Status:php artisan horizon:status - PHP-Fehler 500:
APP_DEBUG=truetemporär setzen, Logs lesen, danach wieder auffalse