Loops is a TikTok-like video sharing platform (with ActivityPub federation) built with Laravel. This guide covers installation, configuration, and deployment.
- PHP: 8.3+
- MySQL: 8.0+
- Redis: 6.0+
- FFmpeg: 4.5+ (8.0+ recommended)
- Node.js: 20+
- Composer: 2.0+
- BCMath
- Ctype
- Fileinfo
- JSON
- Mbstring
- OpenSSL
- PDO
- Tokenizer
- XML
- GD or Imagick
- Redis
git clone https://github.com/joinloops/loops-server.git
cd loops-servercomposer install --no-dev --optimize-autoloadernpm install
npm run buildCopy the environment file and configure:
cp .env.example .envGenerate application key:
php artisan key:generateLink storage directory:
php artisan storage:linkEdit .env file with your settings:
# Application
APP_NAME="Loops"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://your-domain.com
# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=loops
DB_USERNAME=loops_user
DB_PASSWORD=your_secure_password
# Redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0
# Cache & Sessions
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
# Mail Configuration (choose one)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
# Storage
# Loops uses S3-compatible storage by default. To use S3, uncomment and fill
# in the values below. To run on local disk instead, leave them commented and
# see "Using Local Storage Instead of S3" below.
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_DEFAULT_REGION=us-east-1
# AWS_BUCKET=
# AWS_USE_PATH_STYLE_ENDPOINT=false
# AWS_URL=
# Video Processing
FFMPEG_BINARIES=/usr/bin/ffmpeg
FFPROBE_BINARIES=/usr/bin/ffprobeNote
By default Loops stores avatars and videos on S3-compatible storage. If you'd rather keep everything on your server's local disk, the steps below switch storage over with a single config change — no code modifications required.
Loops resolves its media disk by the name s3. Avatars, videos, and thumbnails are all read and written through Storage::disk('s3') and the FFmpeg processing pipeline. Because Laravel disks are just named configuration entries, you can repoint that name at the local driver and every storage call will use your server's filesystem instead of an S3 bucket.
Open config/filesystems.php and replace the s3 disk definition with a local one:
's3' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],The disk keeps the name s3, but now stores files under storage/app/public and serves them from APP_URL/storage. Any AWS_* values in your .env are simply ignored.
You already ran this in step 4 of installation, but if not:
php artisan storage:linkThis symlinks public/storage to storage/app/public so uploaded media is reachable over HTTP.
If you've cached your configuration (as recommended for production), rebuild it so the change takes effect:
php artisan config:clear
php artisan config:cache- Disk space: All avatars and videos now live on your server's disk. Make sure the volume backing
storage/has room for your expected media, and keep an eye on it as your instance grows. - The disk is still named
s3: This is cosmetic. The name is only an identifier, so everything works — it just looks unusual in your config. - Serving: Files are served directly by your web server from
APP_URL/storagerather than streamed through PHP. The Nginx and Apache examples in this guide already expose thestoragesymlink. - Permissions: Ensure
storage/is writable by your web and queue user (see File Permissions). - Custom URLs: This relies on media URLs being generated through
Storage::disk('s3')->url(). If you've customized anything to build URLs directly fromAWS_URL, those spots won't pick up the local path.
This approach works reliably today. A dedicated media-disk environment variable is planned to make the choice between local and S3 storage more explicit down the line.
CREATE DATABASE loops CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'loops_user'@'localhost' IDENTIFIED BY 'your_secure_password';
GRANT ALL PRIVILEGES ON loops.* TO 'loops_user'@'localhost';
FLUSH PRIVILEGES;php artisan migrateSince registration is disabled by default, create your first admin account:
php artisan create-admin-accountFollow the prompts to set up your admin credentials.
php artisan passport:keysLoops uses Redis-backed queues with Horizon for queue management.
php artisan vendor:publish --provider="Laravel\Horizon\HorizonServiceProvider"Edit config/horizon.php as needed for your environment.
php artisan horizonFor production, use a process manager like Supervisor (make sure you replace the paths and user accordingly):
[program:loops-horizon]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/loops/artisan horizon
autostart=true
autorestart=true
user=www
redirect_stderr=true
stdout_logfile=/var/www/loops/storage/logs/horizon.log
stopwaitsecs=3600Loops supports multiple mail providers. Configure one of the following:
MAIL_MAILER=smtp
MAIL_HOST=your-smtp-host
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tlsMAIL_MAILER=mailgun
MAILGUN_DOMAIN=your-domain.com
MAILGUN_SECRET=your-secret-keyMAIL_MAILER=ses
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_DEFAULT_REGION=us-east-1MAIL_MAILER=postmark
POSTMARK_TOKEN=your-server-tokenMAIL_MAILER=resend
RESEND_KEY=your-api-keyLoops supports Cloudflare Turnstile and hCaptcha for spam protection.
LOOPS_CAPTCHA=true
LOOPS_CAPTCHA_DRIVER=turnstile
TURNSTILE_SITE_KEY=your-site-key
TURNSTILE_SECRET_KEY=your-secret-keyLOOPS_CAPTCHA=true
LOOPS_CAPTCHA_DRIVER=hcaptcha
HCAPTCHA_SITE_KEY=your-site-key
HCAPTCHA_SECRET_KEY=your-secret-key2FA is supported. No additional configuration required - users can enable it in their profile settings.
server {
listen 80;
listen [::]:80;
server_name your-domain.com;
root /var/www/loops/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
# Handle large video uploads
client_max_body_size 100M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
}<VirtualHost *:80>
ServerName your-domain.com
DocumentRoot /var/www/loops/public
<Directory /var/www/loops/public>
AllowOverride All
Require all granted
</Directory>
# Handle large video uploads
LimitRequestBody 104857600
ErrorLog ${APACHE_LOG_DIR}/loops_error.log
CustomLog ${APACHE_LOG_DIR}/loops_access.log combined
</VirtualHost>Set appropriate permissions:
sudo chown -R www-data:www-data /var/www/loops
sudo chmod -R 755 /var/www/loops
sudo chmod -R 775 /var/www/loops/storage
sudo chmod -R 775 /var/www/loops/bootstrap/cacheAdd to your crontab:
# Laravel Scheduler
* * * * * cd /var/www/loops && php artisan schedule:run >> /dev/null 2>&1php artisan config:cache
php artisan route:cache
php artisan view:cacheAdd to your PHP configuration:
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=2
opcache.fast_shutdown=1Optimize php-fpm settings for video processing:
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
# Increase limits for video uploads
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 300
memory_limit = 512M- Disable debug mode in production (
APP_DEBUG=false) - Use HTTPS for all connections
- Regularly update dependencies
- Monitor logs for suspicious activity
- Use strong passwords and enable 2FA
- Keep FFmpeg updated for security patches
To update Loops:
# Pull latest changes
git pull origin main
# Update dependencies
composer install --no-dev --optimize-autoloader
# Install frontend deps + rebuild frontend
npm ci && npm run build
# Run migrations
php artisan migrate
# Clear caches
php artisan cache:clear
php artisan config:clear
php artisan view:clear
# Rebuild caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Restart services
php artisan horizon:terminate