Skip to content

Peertube

Installing dependencies

First ensure that we have the basics (by this point, they should already be installed):

sudo -s
apt update && apt install curl sudo unzip vim

Now we install Node.js v12:

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
apt install -y nodejs

And the latest stable version of Yarn:

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
apt update && apt install yarn

Make sure it's installed and check the version with:

yarn --version

Install remaining dependencies:

apt install ffmpeg postgresql postgresql-contrib openssl g++ make redis-server git python3-dev

If you haven't yet installed and enabled the services, do so now:

systemctl enable redis --now
systemctl enable postgresql --now

Setting up Redis

The following assumes that the base Redis configuration has already been done as per the Rspamd guide.

Generate a password:

pwgen -Bvsc 64 1

This results in something like the following:

rtVh7zMTFHWm7HWVFXfcgNLYYtjC9scTt4mHVwjnrvqwsJ9qv9CjdwcCWtpqNVbL

Create a configuration file:

nano /etc/redis/redis-funkwhale.conf

Enter the following, using the password you generated above:

include /etc/redis/common.conf
# Listen on localhost
bind 127.0.0.1 ::1
port 6384
unixsocket /var/run/redis-peertube/redis-server.sock
unixsocketperm 700
daemonize yes
supervised systemd
pidfile /var/run/redis-peertube/redis-server.pid
loglevel notice
logfile /var/log/redis/redis-peertube.log
dbfilename dump-peertube.rdb
requirepass <enter password here>
# maxmemory <bytes>
# maxmemory-policy noeviction
# maxmemory-samples 5

Change permissions on the configuration files:

chown -Rc redis:redis /etc/redis

Enable and start the services

systemctl enable redis-server@peertube
systemctl start redis-server@peertube.service

Check that the service are running with:

ps aux | grep redis

or

service redis-server@peertube status

Install Peertube

Create the peertube user:

useradd -m -d /var/www/video.example.com -s /bin/bash -p peertube peertube

and create a password:

passwd peertube

Create the database:

sudo -u postgres createuser -P peertube
sudo -u postgres createdb -O peertube -E UTF8 -T template0 peertube_prod

Then enable extensions PeerTube needs:

sudo -u postgres psql -c "CREATE EXTENSION pg_trgm;" peertube_prod
sudo -u postgres psql -c "CREATE EXTENSION unaccent;" peertube_prod

Fetch the latest tagged version of Peertube

VERSION=$(curl -s https://api.github.com/repos/chocobozzz/peertube/releases/latest | grep tag_name | cut -d '"' -f 4) && echo "Latest Peertube version is $VERSION"

Open the peertube directory, create a few required directories

$ cd /var/www/video.example.com && sudo -u peertube mkdir config storage versions && cd versions

Download the latest version of the Peertube client, unzip it and remove the zip

$ sudo -u peertube wget -q "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip"
$ sudo -u peertube unzip peertube-${VERSION}.zip && sudo -u peertube rm peertube-${VERSION}.zip

Install Peertube:

$ cd ../ && sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest
$ cd ./peertube-latest && sudo -H -u peertube yarn install --production --pure-lockfile

Peertube Configuration

Copy example configuration:

cd /var/www/video.example.com && sudo -u peertube cp peertube-latest/config/production.yaml.example config/production.yaml

Then edit the config/production.yaml file according to your webserver configuration. An example follows (I've omitted the commented lines):

listen:
  hostname: 'localhost'
  port: 9000

# Correspond to your reverse proxy server_name/listen configuration

webserver:
  https: true
  hostname: 'video.example.com'
  port: 443

rates_limit:
  api:
    window: 10 seconds
    max: 50
  login:
    window: 5 minutes
    max: 15
  signup:
    window: 5 minutes
    max: 2
  ask_send_email:
    window: 5 minutes
    max: 3

trust_proxy:
  - 'loopback'

database:
  hostname: 'localhost'
  port: 5432
  suffix: '_prod'
  username: 'peertube'
  password: '<your-db-password>'
  pool:
    max: 5

redis:
  hostname: 'localhost'
  port: 6384
  auth: <your-redis-password>
  db: 0

smtp:
  hostname: mail.example.com
  port: 587
  username: admin@example.com
  password: <your-email-password>
  tls: false
  disable_starttls: false
  ca_file: null
  from_address: 'admin@example.com'

email:
  body:
    signature: "PeerTube"
  subject:
    prefix: "[PeerTube]"

storage:
  tmp: '/var/www/video.example.com/storage/tmp/' 
  avatars: '/var/www/video.example.com/storage/avatars/'
  videos: '/var/www/video.example.com/storage/videos/'
  streaming_playlists: '/var/www/video.example.com/storage/streaming-playlists/'
  redundancy: '/var/www/video.example.com/storage/redundancy/'
  logs: '/var/www/video.example.com/storage/logs/'
  previews: '/var/www/video.example.com/storage/previews/'
  thumbnails: '/var/www/video.example.com/storage/thumbnails/'
  torrents: '/var/www/video.example.com/storage/torrents/'
  captions: '/var/www/video.example.com/storage/captions/'
  cache: '/var/www/video.example.com/storage/cache/'
  plugins: '/var/www/video.example.com/storage/plugins/'
  client_overrides: '/var/www/video.example.com/storage/client-overrides/'

log:
  level: 'info'
  rotation:
    enabled : true 
    maxFileSize: 12MB
    maxFiles: 20
  anonymizeIP: false

search:
  remote_uri:
    users: true
    anonymous: false

trending:
  videos:
    interval_days: 7

redundancy:
  videos:
    check_interval: '1 hour'
      -
        size: '10GB'
        min_lifetime: '48 hours'
        strategy: 'most-views'

remote_redundancy:
  videos:
    accept_from: 'anybody'

csp:
  enabled: false
  report_only: true 
  report_uri:

tracker:
  enabled: true
  private: true
  reject_too_many_announces: true

history:
  videos:
    max_age: -1

views:
  videos:
    remote:
      max_age: -1

plugins:
  index:
    enabled: true
    check_latest_versions_interval: '12 hours'
    url: 'https://packages.joinpeertube.org'

###############################################################################
#
# From this point, all the following keys can be overridden by the web interface
# (local-production.json file). If you need to change some values, prefer to
# use the web interface because the configuration will be automatically
# reloaded without any need to restart PeerTube.
#
# /!\ If you already have a local-production.json file, the modification of the
# following keys will have no effect /!\.
#
###############################################################################
...

PeerTube does not support webserver host change. Keep in mind your domain name is definitive after your first PeerTube start.

Certificates

Issue the RSA certificates with:

~/.acme.sh/acme.sh --issue --dns dns_cloudns -d video.example.com --keylength 4096 --key-file /etc/letsencrypt/rsa-certs/video.example.com/privkey.pem --ca-file /etc/letsencrypt/rsa-certs/video.example.com/chain.pem --cert-file /etc/letsencrypt/rsa-certs/video.example.com/cert.pem --fullchain-file /etc/letsencrypt/rsa-certs/video.example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/rsa-certs/video.example.com" --post-hook "find /etc/letsencrypt/rsa-certs/video.example.com/ -name '*.pem' -type f -exec chmod 600 {} \;" --renew-hook "find /etc/letsencrypt/rsa-certs/video.example.com/ -name '*.pem' -type f -exec chmod 600 {} \; -exec service nginx reload \;"

and ECC certificates with:

~/.acme.sh/acme.sh --issue --dns dns_cloudns -d video.example.com --keylength ec-384 --key-file /etc/letsencrypt/ecc-certs/video.example.com/privkey.pem --ca-file /etc/letsencrypt/ecc-certs/video.example.com/chain.pem --cert-file /etc/letsencrypt/ecc-certs/video.example.com/cert.pem --fullchain-file /etc/letsencrypt/ecc-certs/video.example.com/fullchain.pem --pre-hook "mkdir -p /etc/letsencrypt/ecc-certs/video.example.com" --post-hook "find /etc/letsencrypt/ecc-certs/video.example.com/ -name '*.pem' -type f -exec chmod 600 {} \;" --renew-hook "find /etc/letsencrypt/ecc-certs/video.example.com/ -name '*.pem' -type f -exec chmod 600 {} \; -exec service nginx reload \;"

Nginx

Create your server block:

nano /etc/nginx/sites-available/video.example.com

Enter the following:

server {
  listen 80;
  listen [::]:80;
  server_name video.example.com;

  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  location / { return 301 https://$host$request_uri; }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name video.example.com;

  ssl_certificate /etc/letsencrypt/rsa-certs/video.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/rsa-certs/video.example.com/privkey.pem;
  ssl_certificate /etc/letsencrypt/ecc-certs/video.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/ecc-certs/video.example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/ecc-certs/video.example.com/chain.pem;

  include /etc/nginx/custom-config/ssl.conf;

  gzip on;
  gzip_types text/css application/javascript;
  gzip_vary on;

  add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";

  location ~ ^/client/(.*\.(js|css|png|svg|woff2|otf|ttf|woff|eot))$ {
    add_header Cache-Control "public, max-age=31536000, immutable";

    alias /var/www/video.example.com/peertube-latest/client/dist/$1;
  }

  location ~ ^/static/(thumbnails|avatars)/ {
    if ($request_method = 'OPTIONS') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      add_header 'Access-Control-Max-Age' 1728000;
      add_header 'Content-Type' 'text/plain charset=UTF-8';
      add_header 'Content-Length' 0;
      return 204;
    }

    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
    add_header Cache-Control "public, max-age=7200";

    root /var/www/video.example.com/storage;

    rewrite ^/static/(thumbnails|avatars)/(.*)$ /$1/$2 break;
    try_files $uri /;
  }

  location / {
    proxy_pass http://127.0.0.1:9000;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    client_max_body_size 8G;

    proxy_connect_timeout       600;
    proxy_send_timeout          600;
    proxy_read_timeout          600;
    send_timeout                600;
  }

  location ~ ^/static/(webseed|redundancy|streaming-playlists)/ {
    set $peertube_limit_rate 800k;

    if ($request_uri ~ -fragmented.mp4$) {
      set $peertube_limit_rate 5000k;
    }

    limit_rate $peertube_limit_rate;
    limit_rate_after 5000k;

    if ($request_method = 'OPTIONS') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
      add_header 'Access-Control-Max-Age' 1728000;
      add_header 'Content-Type' 'text/plain charset=UTF-8';
      add_header 'Content-Length' 0;
      return 204;
    }

    if ($request_method = 'GET') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';

      access_log off;
    }

    root /var/www/video.example.com/storage;

    rewrite ^/static/webseed/(.*)$ /videos/$1 break;
    rewrite ^/static/redundancy/(.*)$ /redundancy/$1 break;
    rewrite ^/static/streaming-playlists/(.*)$ /streaming-playlists/$1 break;

    try_files $uri /;
  }

  location /tracker/socket {
    proxy_read_timeout 1200s;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_http_version 1.1;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_pass http://127.0.0.1:9000;
  }

  location /socket.io {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;

    proxy_pass http://127.0.0.1:9000;

    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}

Activate the configuration file:

ln -s /etc/nginx/sites-available/video.example.com /etc/nginx/sites-enabled/

Reload Nginx:

service nginx reload

Check config for errors:

nginx -t

Apparmor

Create an apparmor profile:

nano /etc/apparmor.d/peertube

Enter the following:

#include <tunables/global>

profile peertube flags=(complain, attach_disconnected) {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/ssl_keys>
  #include <abstractions/user-tmp>

  /proc/version r,
  /sys/devices/system/node/ r,
  /sys/devices/system/node/node0/meminfo r,
  /sys/fs/cgroup/memory/memory.limit_in_bytes r,
  /usr/bin/dash mrix,
  /usr/bin/ffmpeg mrix,
  /usr/bin/ffprobe mrix,
  /usr/bin/getconf mrix,
  /usr/bin/nice mrix,
  /usr/bin/node mrix,
  /usr/bin/openssl mrix,
  /usr/bin/sh mrix,
  owner /var/www/video.example.com.uk/** r,
  owner /var/www/video.example.com.uk/.config/** rw,
  owner /var/www/video.example.com.uk/.npm/ w,
  owner /var/www/video.example.com.uk/.npm/_logs/ w,
  owner /var/www/video.example.com.uk/.npm/_logs/* w,
  owner /var/www/video.example.com.uk/config/* rw,
  owner /var/www/video.example.com.uk/storage/** w,
  owner /var/www/video.example.com.uk/storage/**/ rw,
  owner /var/www/video.example.com.uk/storage/logs/*.log w,
  owner /var/www/video.example.com.uk/versions/*/node_modules/**.node mr,
  owner /var/www/video.example.com.uk/versions/peertube-v2.4.0/node_modules/youtube-dl/bin/* w,

}

Reload apparmor:

service apparmor reload

It would be advisable to use for a while under complain mode and keep checking with aa-logprof on a daily basis. Once you're happy with the config, enforce with aa-enforce.

It would also be advisable to switch back to complain mode during any upgrade.

Systemd

Create a service file:

nano /etc/systemd/system/peertube.service

Add the following:

[Unit]
Description=PeerTube daemon
After=network.target postgresql.service redis-server.service

[Service]
Type=simple
Environment=NODE_ENV=production
Environment=NODE_CONFIG_DIR=/var/www/video.example.com/config
User=peertube
Group=peertube
AppArmorProfile=peertube
ExecStart=/usr/bin/npm start
WorkingDirectory=/var/www/video.example.com/peertube-latest
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=peertube
Restart=always

; Some security directives.
; Mount /usr, /boot, and /etc as read-only for processes invoked by this service.
ProtectSystem=full
; Sets up a new /dev mount for the process and only adds API pseudo devices
; like /dev/null, /dev/zero or /dev/random but not physical devices. Disabled
; by default because it may not work on devices like the Raspberry Pi.
PrivateDevices=true
; Ensures that the service process and all its children can never gain new
; privileges through execve().
NoNewPrivileges=true
; This makes /home, /root, and /run/user inaccessible and empty for processes invoked
; by this unit. Make sure that you do not depend on data inside these folders.
ProtectHome=true
; Drops the sys admin capability from the daemon.
CapabilityBoundingSet=~CAP_SYS_ADMIN

[Install]
WantedBy=multi-user.target

Enable and start the Peertube service:

systemctl enable peertube.service --now

The Administrator Password

The administrator password is automatically generated and can be found in the logs. You can set another password with:

cd /var/www/video.example.com/peertube-latest && NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run reset-password -- -u root

Alternatively you can set the environment variable PT_INITIAL_ROOT_PASSWORD, to your own administrator password, although it must be 6 characters or more.