What Pixelfed requires
The prerequisites page at docs.pixelfed.org lists PHP-FPM, a database, an HTTPS front end, Composer, Git, Redis for caching and the job queue, GD, JPEGOptim, OptiPNG, PNGQuant and ffmpeg for video thumbnails. The documented sources do not agree on versions, so check the one you deploy:
| Component | docs.pixelfed.org | Repository (dev branch) |
|---|---|---|
| PHP | 8.3 or newer | composer.json requires ^8.4 or ^8.5; README recommends 8.5 |
| MySQL | 5.7+ | 8+ officially supported |
| MariaDB | 10.3+ | 11+ officially supported |
| PostgreSQL | 10+ | 14+, best effort, not recommended |
| Redis | required | redis:8-alpine in docker-compose.yml |
| Image tools | GD, JPEGOptim, OptiPNG, PNGQuant, ffmpeg | Dockerfile adds those tools plus gifsicle, and compiles ffmpeg and libvips |
Neither source gives RAM or core figures. What grows is disk: every upload and its optimised copies land under storage/ unless you move media to object storage. The defaults in the configuration reference set the ceiling per account, and you can change them in .env:
MAX_PHOTO_SIZE15000 KB per photo or videoMAX_ACCOUNT_SIZE1000000 KB per accountMAX_ALBUM_LENGTH4 media per postPF_MAX_USERS1000 registrationsIMAGE_QUALITY80, withPF_OPTIMIZE_IMAGESon
Our arithmetic, not a documented figure: 1,000 accounts each allowed about 1 GB is a worst case near 1 TB of media, so size the disks for the community you actually expect and watch du -sh storage/ as it grows.
The machine
Pixelfed is an application with its database on the same box, the job the single-socket class on our dedicated server range is described for. A small instance for a club fits a Moonshot cartridge with NVMe; an instance with many active accounts and remote media flowing in belongs on a dual-socket line, which the range lists for busy databases. Install Ubuntu from the free images on our Linux dedicated servers, or pick the disk layout yourself on a custom build.
- Whole machine, root. MySQL, Redis and the image workers share nothing with another tenant.
- Unmetered port. No transfer allowance and no overage fee as photos federate out to followers on other servers.
- Console. Every racked single-, dual- and quad-socket server has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC); among cartridges only the m510 and m710x have one, the rest have power control. A console makes a bad firewall rule recoverable.
- IPv6. Added at checkout at no charge, as individual addresses.
- Lead time. A ready server: about 30 minutes, counted from payment approval. A build to order: 4 to 24 hours.
Leave out the X-Gene ARM line. It boots only Ubuntu 16.04 and 18.04, which current Docker Engine releases no longer support, and this route depends on Docker; our advice is an x86-64 line.
Host preparation and the compose files
Install Docker Engine plus the Compose plugin first; the repository's setup guide assumes both. Check the Engine version the second command prints: only from Docker Engine 28 is a port published on 127.0.0.1 unreachable from other hosts on the same network segment (moby#45610). The first line sets IPV6=yes in ufw's defaults; ufw only writes IPv6 copies of rules added after that.
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable
sudo docker version --format '{{.Server.Version}}'
sudo git clone https://github.com/pixelfed/pixelfed /opt/pixelfed
cd /opt/pixelfed
sudo chown -R www-data:www-data storage/ bootstrap/cache/
sudo cp .env.docker.example .env
sudo chmod 600 .env
sudo sed -i 's/"8080:8080"/"127.0.0.1:8080:8080"/' docker-compose.yml
grep -n '8080:8080' docker-compose.yml
echo "base64:$(openssl rand -base64 32)"
Our change, not part of the repository's setup guide: the compose file publishes the app as "8080:8080", and we rebind it. A mapping without an address listens on every host address, 0.0.0.0 and [::] alike, and Docker-published traffic bypasses ufw's rules on IPv4, and on IPv6 as well once Docker's own IPv6 networking is enabled. The sed line binds it to loopback, and the grep should show 127.0.0.1:8080:8080.
The last line prints an application key. The setup guide sends you to a web generator for APP_KEY; we generate it on the server instead, because config/app.php uses the AES-256-CBC cipher, which takes 32 random bytes. Then sudo nano .env and set, each value in double quotes: APP_KEY to that string, APP_URL to "https://photos.example.org", APP_DOMAIN and ADMIN_DOMAIN to the bare host name, long random values for DB_PASSWORD and DB_ROOT_PASSWORD, and your SMTP details. Leave OPEN_REGISTRATION="false" as the example ships it; the config default is true.
First start and the admin account
These are the setup guide's commands. The image build compiles ffmpeg and libvips, so the first build takes a while. The guide says to give the database and Redis 30 seconds for their first boot. We add --no-interaction to the personal client command so it takes its default name instead of waiting for input, and user:create comes last because it asks questions.
cd /opt/pixelfed
sudo docker compose build
sudo docker compose up -d db redis
sleep 30
sudo docker compose up -d
sudo docker compose exec pixelfed php artisan instance:actor
sudo docker compose exec pixelfed php artisan import:cities
sudo docker compose exec pixelfed php artisan passport:keys
sudo docker compose exec pixelfed php artisan passport:client --personal --no-interaction
sudo docker compose exec pixelfed php artisan user:create
user:create prompts for name, username, email, then the password twice without echoing it (6 characters minimum). Three yes/no questions follow: make the user an admin, mark the email verified, and finally "Are you sure you want to create this user?". That last one defaults to no, and on no nothing is created, so answer yes to all three. Skip the --password flag: as an argument, the password would be recorded by bash and visible to ps while the command runs. Right now the app answers only on 127.0.0.1 and nothing public proxies to it, so the admin exists before anyone outside can reach the site.
The compose stack replaces the systemd and cron pieces of a manual install: the horizon container runs php artisan horizon, and scheduler runs php artisan schedule:work. Installing without Docker, the docs give a pixelfed.service unit whose ExecStart runs artisan horizon as the app user with Restart=always, plus a crontab line that calls artisan schedule:run every minute.
nginx, TLS and federation
sudo apt install nginx certbot python3-certbot-nginx.- Add a server block for your host name that listens on 80 and [::]:80 and proxies
/tohttp://127.0.0.1:8080. The guide writeslocalhost:8080; 127.0.0.1 is our choice, because the port is now published on IPv4 loopback only andlocalhostmay resolve to ::1 first. Keep the setup guide's headers (Host,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto),client_max_body_size 500Mandproxy_read_timeout 300s. Test withsudo nginx -t, then reload. sudo ufw allow 80,443/tcp. nginx runs on the host, so ufw blocks it on IPv4 and IPv6 alike until this rule exists.- Point the host name's A record, and an AAAA record if you added IPv6, at the server, and wait until
dig +short photos.example.orgreturns its address. Then runsudo certbot --nginx -d photos.example.org.
The Docker example ships ACTIVITY_PUB="true" and AP_REMOTE_FOLLOW="true", the two settings the installation guide names for federation, and instance:actor above generated the instance actor. The docs' ActivityPub page says Pixelfed uses the protocol to exchange activities with other Pixelfed servers and other fediverse software such as Mastodon. Sign in as the admin and confirm registration is still closed: with ENABLE_CONFIG_CACHE on, the configuration reference says settings can be managed from the admin dashboard, so check it there as well as in .env.
Media: local disk or S3
By default media stays on local disk under storage/, which the compose file mounts into all three app containers. Setting PF_ENABLE_CLOUD="true" turns on S3 or other object storage; the example file then reads FILESYSTEM_CLOUD, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, AWS_URL, AWS_ENDPOINT and AWS_USE_PATH_STYLE_ENDPOINT, and, per the configuration reference, PF_LOCAL_AVATAR_TO_CLOUD stores local avatars on S3 as well. Our inference: once media lives in a bucket, the backup below no longer covers it, and the bucket needs its own copy.
Backups and restore checks
Our backup set: a consistent dump of the database, plus storage/ (uploads, OAuth keys, logs) and .env, which holds APP_KEY and every password. We skip mysql-9-data/ and redis-data/: the dump replaces the first, and the second holds cache and queue state. Save this as /usr/local/sbin/pixelfed-backup.sh, mode 700.
set -euo pipefail
umask 077
cd /opt/pixelfed
OUT=/var/backups/pixelfed
mkdir -p -m 700 "$OUT"
DAY=$(date +%F)
docker compose exec -T db sh -c 'exec mysqldump --single-transaction --no-tablespaces -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' | gzip > "$OUT/db-$DAY.sql.gz"
tar -czf "$OUT/storage-$DAY.tar.gz" storage .env
gunzip -t "$OUT/db-$DAY.sql.gz"
tar -tzf "$OUT/storage-$DAY.tar.gz" > /dev/null
zcat "$OUT/db-$DAY.sql.gz" | tail -n 1
The password is read from the db container's own environment, so it never enters your shell history; mysqldump still warns that it was given on its command line, and while the dump runs it can show in the process list, on the host as well as inside the container. Create the log directory once with sudo mkdir -p -m 700 /var/backups/pixelfed, then add to root's crontab: 30 3 * * * bash /usr/local/sbin/pixelfed-backup.sh >> /var/backups/pixelfed/backup.log 2>&1. Copy both files off the machine; a backup on the same disks fails with them.
Be clear about what the last three lines prove. gunzip -t shows the compressed dump is not corrupt; tar -tzf shows the file archive can be read from end to end; a final line reading -- Dump completed shows mysqldump finished rather than stopping halfway. None of them proves a restore. To prove one, on a spare machine: copy both files over and confirm them with sudo ls -l and sudo gunzip -t; clone the repository and repeat the loopback edit; unpack .env and storage/ with sudo tar -xzf; start only the db service and give it about 30 seconds; load the dump with sudo zcat db-DATE.sql.gz | sudo docker compose exec --no-TTY db sh -c 'exec mysql --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" "$MYSQL_DATABASE"'; then build and start everything else and sign in.
Updates, licence and limits
To update: back up, fetch the new code (sudo git pull), rebuild the images (sudo docker compose build), then recreate the containers with sudo docker compose up -d. The compose file sets AUTORUN_LARAVEL_MIGRATION on the app container, so pending migrations are applied at its start; that is our reading of the file, not a documented step. If upstream has changed docker-compose.yml, git refuses to pull over your edit: sudo git stash, pull, sudo git stash pop, and recheck the port line. The manual-install docs pull the dev branch, then run composer install, config:cache, route:cache and migrate --force.
Pixelfed is licensed AGPL-3.0, per its LICENSE file. A general reading on our part, not legal advice. Section 13: whoever runs a modified version for users who reach it over a network must offer those users its source. Separately, anyone who distributes copies, modified or not, must pass the source on with them. Both duties sit with whoever runs or distributes the software, not with the host of the hardware.
Limits worth knowing: the install guide still calls Pixelfed beta and deploys from dev, PostgreSQL is best effort, and a federated instance carries moderation work no server takes off you. For another fediverse service on the same pattern, see Lemmy on bare metal.
Questions
Must Pixelfed run in Docker?
No. The docs describe a manual install with PHP-FPM, nginx or Apache, a systemd unit for Horizon and a scheduler cron line. The README says that since September 2026 the project recommends Docker Compose, and that its Dockerfile is the part kept current with releases.
MySQL, MariaDB or PostgreSQL?
The compose file runs MySQL 9. The README lists MySQL 8+ and MariaDB 11+ as officially supported and PostgreSQL 14+ as best effort, not recommended. The docs' troubleshooting page notes that MariaDB on Debian defaults to a collation Pixelfed does not expect, and gives the setting to change.
How do I keep strangers from signing up?
Keep OPEN_REGISTRATION="false" in .env; the Docker example ships it that way, while the config default is true. Create your own account with php artisan user:create before nginx makes the site public, then check the admin dashboard's settings too. PF_MAX_USERS caps registrations if you open them later.
Does a Pixelfed instance federate with Mastodon?
Yes, when ActivityPub is on. The docs say Pixelfed exchanges activities with other Pixelfed servers and other fediverse software such as Mastodon. Turn on ACTIVITY_PUB and AP_REMOTE_FOLLOW, run instance:actor, and serve the site over HTTPS on the domain set in APP_DOMAIN.
Can photos go to object storage instead of local disk?
Yes. PF_ENABLE_CLOUD switches media to S3 or a compatible store, using the AWS_* settings in the example .env, including an endpoint and path-style option for non-Amazon stores. Back the bucket up separately; a dump of the database does not include it.