This guide covers installing and configuring Wafrn, a Tumblr-inspired social network that connects to the Fediverse and optionally Bluesky. Wafrn is deployed using Docker Compose and can use Garage for S3-compatible object storage.
Wafrn is:
Wafrn consists of several containerized services:
┌─────────────────────────────────────────────────────────────┐
│ WAFRN STACK │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Frontend │ Caddy + Angular (ports 80, 443) │
│ │ (Caddy) │ Serves web UI, handles TLS │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Backend │ Node.js API (internal) │
│ │ (Node.js) │ Handles posts, users, federation │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PostgreSQL │ │ Redis │ │Optional: PDS │ │
│ │ (DB) │ │ (Cache) │ │ (Bluesky) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Optional: S3 Storage (Garage) for media uploads │
│ │
└─────────────────────────────────────────────────────────────┘
}}}
== Prerequisites ==
=== Hardware Requirements ===
- *CPU*: 2+ cores (ARM or x86_64)
- *RAM*: 2GB minimum, 4GB+ recommended (with Bluesky)
- *Storage*: 20GB minimum, 100GB+ recommended for media
- *Network*: Public IPv4, ports 80 and 443 available
=== Software Requirements ===
- NixOS with Docker enabled
- Domain name with DNS control
- SMTP server for emails (Gmail, Brevo, etc.)
=== DNS Requirements ===
You need *3 domains* minimum:
1. `wafrn.yourdomain.com` - Main instance
2. `cache.wafrn.yourdomain.com` - Image cache
3. `media.wafrn.yourdomain.com` - Media storage
For Bluesky integration, add:
4. `bsky.yourdomain.com` - Bluesky PDS
5. `*.bsky.yourdomain.com` - User subdomains
== Installation ==
=== Step 1: Create Wafrn User and Directory ===
# Add to your NixOS configuration
{ config, pkgs, ... }:
let
wafrnDir = "/var/lib/wafrn";
in
{
users.users.wafrn = {
isSystemUser = true;
group = "wafrn";
home = wafrnDir;
createHome = true;
extraGroups = [ "docker" ];
};
users.groups.wafrn = {};
# Enable Docker
virtualisation.docker = {
enable = true;
autoPrune = {
enable = true;
dates = "weekly";
};
};
# Install required tools
environment.systemPackages = with pkgs; [
docker-compose
git
];
}
}}}
systemd.services.wafrn = {
description = "Wafrn Social Network";
after = [ "docker.service" "network.target" ];
requires = [ "docker.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "wafrn";
Group = "wafrn";
WorkingDirectory = wafrnDir;
ExecStart = pkgs.writeScript "wafrn-start" ''
#!${pkgs.bash}/bin/bash
cd ${wafrnDir}
if [ ! -f .env ]; then
echo "ERROR: .env file not found. Please create it first."
exit 1
fi
${pkgs.docker-compose}/bin/docker-compose -f docker-compose.simple.yml up -d
'';
ExecStop = pkgs.writeScript "wafrn-stop" ''
#!${pkgs.bash}/bin/bash
cd ${wafrnDir}
${pkgs.docker-compose}/bin/docker-compose -f docker-compose.simple.yml down
'';
ExecReload = pkgs.writeScript "wafrn-reload" ''
#!${pkgs.bash}/bin/bash
cd ${wafrnDir}
${pkgs.docker-compose}/bin/docker-compose -f docker-compose.simple.yml restart
'';
};
};
}}}
=== Step 3: Clone and Initialize ===
# Switch to wafrn user
sudo -u wafrn -i
# Clone wafrn
cd /var/lib/wafrn
git clone https://codeberg.org/wafrn/wafrn.git .
# Run environment setup script
bash install/envsecretsetup.sh
# This creates a template .env file
}}}
Edit /var/lib/wafrn/.env:
# Admin credentials
ADMIN_USER=admin
ADMIN_EMAIL=admin@yourdomain.com
ADMIN_PASSWORD=$(openssl rand -base64 32) # Generate strong password
# JWT Secret for authentication
JWT_SECRET=$(openssl rand -hex 64) # 128 character hex string
# Domains
DOMAIN_NAME=wafrn.yourdomain.com
CACHE_DOMAIN=cache.wafrn.yourdomain.com
MEDIA_DOMAIN=media.wafrn.yourdomain.com
FRONTEND_MEDIA_URL="https://media.wafrn.yourdomain.com"
FRONTEND_CACHE_URL="https://cache.wafrn.yourdomain.com/api/cache?media="
# Instance metadata
FRONTEND_SHORT_TITLE="Wafrn"
FRONTEND_LONG_TITLE="My Wafrn Instance"
FRONTEND_DESCRIPTION="A federated social media instance running on Wafrn"
# Registration settings
REGISTRATION_LEVEL=PUBLIC # PUBLIC, PRIVATE, or INVITE
REVIEW_REGISTRATIONS=true # Manual approval required
REGISTRATION_MINIMUM_GE=18
# SMTP Configuration (required for email verification)
SMTP_HOST=smtp.gmail.com
SMTP_USER=your-email@gmail.com
SMTP_PORT=587
SMTP_PASSWORD=your-app-specific-password
SMTP_FROM=wafrn@yourdomain.com
# Database credentials
POSTGRES_USER=wafrn
POSTGRES_PASSWORD=$(openssl rand -base64 32) # Generate strong password
POSTGRES_DBNAME=wafrn
# Let's Encrypt email
ACME_EMAIL=your-email@yourdomain.com
# Web Push (optional)
WEBPUSH_EMAIL=mailto:wafrn@yourdomain.com
WEBPUSH_PRIVATE=
WEBPUSH_PUBLIC=
# Bluesky (disable initially)
ENABLE_BSKY=false
COMPOSE_PROFILES=default
PDS_DOMAIN_NAME=bsky.wafrn.yourdomain.com
PDS_JWT_SECRET=$(openssl rand -hex 32)
PDS_ADMIN_PASSWORD=$(openssl rand -base64 24)
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(openssl ecparam -name secp256k1 -genkey -noout | openssl ec -outform DER | tail -c 32 | xxd -p -c 64)
}}}
=== Step 5: Configure Secrets with sops-nix ===
For production, use sops-nix:
# Add to your NixOS secrets
sops.secrets = {
wafrn-jwt-secret = { owner = "wafrn"; };
wafrn-admin-password = { owner = "wafrn"; };
wafrn-db-password = { owner = "wafrn"; };
wafrn-smtp-password = { owner = "wafrn"; };
wafrn-env = { owner = "wafrn"; };
};
# Create template for .env
sops.templates."wafrn-env".content = ''
ADMIN_USER=admin
ADMIN_EMAIL=admin@yourdomain.com
ADMIN_PASSWORD=''${config.sops.placeholder.wafrn-admin-password}
JWT_SECRET=''${config.sops.placeholder.wafrn-jwt-secret}
DOMAIN_NAME=wafrn.yourdomain.com
CACHE_DOMAIN=cache.wafrn.yourdomain.com
MEDIA_DOMAIN=media.wafrn.yourdomain.com
FRONTENDMEDIAURL="https://media.wafrn.yourdomain.com"
FRONTENDCACHEURL="https://cache.wafrn.yourdomain.com/api/cache?media="
FRONTENDSHORTTITLE="Wafrn"
FRONTENDLONGTITLE="My Wafrn Instance"
FRONTEND_DESCRIPTION="A federated social media instance"
REGISTRATION_LEVEL=PUBLIC
REVIEW_REGISTRATIONS=true
REGISTRATIONMINIMUMAGE=18
SMTP_HOST=smtp.gmail.com
SMTP_USER=your-email@gmail.com
SMTP_PORT=587
SMTP_PASSWORD=''${config.sops.placeholder.wafrn-smtp-password}
SMTP_FROM=wafrn@yourdomain.com
POSTGRES_USER=wafrn
POSTGRES_PASSWORD=''${config.sops.placeholder.wafrn-db-password}
POSTGRES_DBNAME=wafrn
ACME_EMAIL=your-email@yourdomain.com
ENABLE_BSKY=false
COMPOSE_PROFILES=default
PDSDOMAINNAME=bsky.wafrn.yourdomain.com
PDSJWTSECRET=placeholder
PDSADMINPASSWORD=placeholder
PDSPLCROTATIONKEYK256PRIVATEKEY_HEX=placeholder
'';
# Update wafrn service to use template
systemd.services.wafrn.serviceConfig.ExecStartPre = pkgs.writeScript "wafrn-setup" ''
#!${pkgs.bash}/bin/bash
install -o wafrn -g wafrn -m 600 ${config.sops.templates."wafrn-env".path} /var/lib/wafrn/.env
'';
}}}
Garage provides S3-compatible object storage for media uploads.
services.garage = {
enable = true;
package = pkgs.garage;
settings = {
metadata_dir = "/var/lib/garage/meta";
data_dir = "/var/lib/garage/data";
replication_factor = 1; # Single node
rpc_bind_addr = "127.0.0.1:3901";
s3_api = {
api_bind_addr = "127.0.0.1:3900";
root_domain = "garage.yourdomain.com";
s3_region = "garage";
};
admin = {
api_bind_addr = "127.0.0.1:3903";
};
};
};
# Create garage user
users.users.garage = {
isSystemUser = true;
group = "garage";
home = "/var/lib/garage";
createHome = true;
};
users.groups.garage = {};
}}}
=== Step 7: Create Bucket and Keys ===
# Wait for garage to start
sleep 5
# Create a bucket for wafrn media
garage bucket create wafrn-media
# Create an access key
garage key create wafrn-key
# Get key ID (save this!)
garage key info wafrn-key
# Grant access to bucket
garage bucket allow wafrn-media --key wafrn-key --read --write
# Enable website access (optional)
garage bucket website wafrn-media --enable
}}}
Use s3fs to mount the bucket as a filesystem:
{ config, pkgs, ... }:
let
s3Credentials = pkgs.writeText "s3-credentials" ''
AKIAIOSFODNN7EXAMPLE:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
'';
in
{
# Install s3fs
environment.systemPackages = with pkgs; [ s3fs ];
# Load credentials from sops in production
sops.secrets.wafrn-s3-credentials = {
owner = "wafrn";
path = "/var/lib/wafrn/.s3-credentials";
};
# Mount bucket
fileSystems."/var/lib/wafrn/uploads" = {
device = "s3fs#wafrn-media";
fsType = "fuse";
options = [
"_netdev"
"allow_other"
"nonempty"
"use_path_request_style"
"url=http://127.0.0.1:3900"
"passwd_file=/var/lib/wafrn/.s3-credentials"
];
};
}
}}}
== DNS Configuration ==
=== Required DNS Records ===
# Main instance
wafrn.yourdomain.com A YOURSERVERIP
# Cache domain (for image optimization)
cache.wafrn.yourdomain.com A YOURSERVERIP
# Media domain (for uploads)
media.wafrn.yourdomain.com A YOURSERVERIP
# Optional: Garage S3 endpoint
garage.yourdomain.com A YOURSERVERIP
*.garage.yourdomain.com A YOURSERVERIP
# For Bluesky (enable later):
bsky.wafrn.yourdomain.com A YOURSERVERIP
*.bsky.wafrn.yourdomain.com A YOURSERVERIP
}}}
After initial setup is stable:
# 1. Enable Bluesky profile in docker-compose
# Edit /var/lib/wafrn/.env:
COMPOSE_PROFILES=bluesky
ENABLE_BSKY=false # Keep false for now
}}}
=== Step 10: Start PDS Container ===
cd /var/lib/wafrn
sudo -u wafrn docker-compose -f docker-compose.simple.yml up -d pds
# Wait for PDS to initialize
sleep 10
}}}
cd /var/lib/wafrn
sudo -u wafrn ./install/bsky/create-admin.sh
# Or specify custom username:
sudo -u wafrn ./install/bsky/create-admin.sh myusername
}}}
=== Step 12: Enable Bluesky ===
# Edit .env and set:
ENABLE_BSKY=true
# Restart wafrn
sudo systemctl restart wafrn
}}}
cd /var/lib/wafrn
sudo -u wafrn ./install/bsky/add-insert-code.sh
# Copy the generated invite code
}}}
=== Step 14: Enable Bluesky for User ===
1. Login to wafrn web interface as admin
2. Go to user profile settings
3. Click "Enable Bluesky"
4. Enter invite code
== Backup and Maintenance ==
=== Automated Backups ===
# Wafrn has a built-in backup script
sudo -u wafrn /var/lib/wafrn/install/manage.sh backup
# This creates a timestamped backup in /var/lib/wafrn/backups/
}}}
# Add automated daily backups
systemd.services.wafrn-backup = {
description = "Wafrn Backup";
serviceConfig = {
Type = "oneshot";
User = "wafrn";
Group = "wafrn";
WorkingDirectory = "/var/lib/wafrn";
ExecStart = "/var/lib/wafrn/install/manage.sh backup";
};
};
systemd.timers.wafrn-backup = {
description = "Daily Wafrn Backup";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
};
};
# Cleanup old backups (keep 14 days)
systemd.services.wafrn-backup-cleanup = {
description = "Cleanup old Wafrn backups";
serviceConfig = {
Type = "oneshot";
User = "wafrn";
Group = "wafrn";
ExecStart = pkgs.writeScript "cleanup-backups" ''
#!${pkgs.bash}/bin/bash
cd /var/lib/wafrn/backups
find . -type d -mtime +14 -exec rm -rf {} +
'';
};
};
systemd.timers.wafrn-backup-cleanup = {
description = "Weekly Wafrn Backup Cleanup";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "weekly";
};
};
}}}
=== Updates ===
# Check changelog first
cat /var/lib/wafrn/CHANGELOG.md
# Create backup before updating
sudo -u wafrn /var/lib/wafrn/install/manage.sh backup
# Run update
sudo -u wafrn /var/lib/wafrn/install/manage.sh update
}}}
# List available backups
ls -la /var/lib/wafrn/backups/
# Restore specific backup
sudo -u wafrn /var/lib/wafrn/install/manage.sh restore /var/lib/wafrn/backups/BACKUP_NAME
# Restart service
sudo systemctl restart wafrn
}}}
== Troubleshooting ==
=== Service Won't Start ===
# Check systemd status
systemctl status wafrn
# Check docker containers
sudo -u wafrn docker-compose -f /var/lib/wafrn/docker-compose.simple.yml ps
# View logs
sudo -u wafrn docker-compose -f /var/lib/wafrn/docker-compose.simple.yml logs -f
# Check specific service
sudo -u wafrn docker-compose -f /var/lib/wafrn/docker-compose.simple.yml logs backend -f
}}}
# Check PostgreSQL container
sudo -u wafrn docker-compose -f /var/lib/wafrn/docker-compose.simple.yml logs db
# Verify credentials match in .env
# Check .env file permissions (should be readable by wafrn user)
ls -la /var/lib/wafrn/.env
}}}
=== Email Not Sending ===
# Test SMTP connection
telnet smtp.gmail.com 587
# Check backend logs for email errors
sudo -u wafrn docker-compose logs backend | grep -i email
# Verify SMTP settings in .env
}}}
# Check Caddy logs (inside frontend container)
sudo -u wafrn docker-compose logs frontend
# Verify DNS is correct
dig wafrn.yourdomain.com
# Check if ports 80/443 are available
ss -tlnp | grep -E ':80|:443'
}}}
=== Storage Full ===
# Check disk usage
df -h
# Check Docker volumes
docker system df
# Prune old images and containers
sudo -u wafrn docker system prune -a
# Clean old backups
find /var/lib/wafrn/backups -type d -mtime +7 -exec rm -rf {} +
}}}
# Check garage status
systemctl status garage
garage status
# Verify credentials
cat /var/lib/wafrn/.s3-credentials
# Test mount manually
s3fs wafrn-media /tmp/test-mount \
-o passwd_file=/var/lib/wafrn/.s3-credentials \
-o url=http://127.0.0.1:3900 \
-o use_path_request_style \
-o dbglevel=info -f
}}}
== Performance Tuning ==
=== For Higher Traffic ===
Use the `docker-compose.advanced.yml` instead of `simple`:
ExecStart = pkgs.writeScript "wafrn-start" ''
#!${pkgs.bash}/bin/bash
cd ${wafrnDir}
${pkgs.docker-compose}/bin/docker-compose -f docker-compose.advanced.yml up -d
'';
}}}
This provides:
Add to docker-compose environment:
db:
command: >
-c 'max_connections=200'
-c 'shared_buffers=256MB'
-c 'effective_cache_size=768MB'
}}}
=== Redis Tuning ===
redis:
command: >
redis-server
--maxmemory 256mb
--maxmemory-policy allkeys-lru
}}}