← Back to Guide Index

Wafrn: Fediverse Social Network Setup

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.

Table of Contents

What is Wafrn?

Wafrn is:

Architecture Overview

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

];

}

}}}

Step 2: Create Systemd Service

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

}}}

Configuration

Step 4: Configure .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 S3 Storage Setup

Garage provides S3-compatible object storage for media uploads.

Step 6: Enable Garage

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

}}}

Step 8: Mount S3 Bucket

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

}}}

Bluesky Integration

After initial setup is stable:

Step 9: Prepare Bluesky

# 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

}}}

Step 11: Create Admin User

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

}}}

Step 13: Create Invite Codes

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/

}}}

NixOS Backup Service

# 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

}}}

Restore from Backup

# 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

}}}

Database Connection Errors

# 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

}}}

SSL/Certificate Issues

# 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 {} +

}}}

S3/Garage Mount Issues

# 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:

Database Optimization

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

}}}

References