diff --git a/.env.sample b/.env.sample index 2a979e2..b09a97a 100644 --- a/.env.sample +++ b/.env.sample @@ -1,8 +1,165 @@ TYPE=funkwhale - -DOMAIN=funkwhale.example.com - -## Domain aliases -#EXTRA_DOMAINS=', `www.funkwhale.example.com`' - +DOMAIN={{ .Domain }} LETS_ENCRYPT_ENV=production + +# If you have any doubts about what a setting does, +# check https://docs.funkwhale.audio/configuration.html#configuration-reference + +# Set this variables to bind the API server to another interface/port +# example: FUNKWHALE_API_IP=0.0.0.0 +# example: FUNKWHALE_API_PORT=5678 +FUNKWHALE_API_IP=127.0.0.1 +FUNKWHALE_API_PORT=5000 + +# The number of web workers to start in parallel. Higher means you can handle +# more concurrent requests, but also leads to higher CPU/Memory usage +FUNKWHALE_WEB_WORKERS=4 + +# Replace this by the definitive, public domain you will use for +# your instance. It cannot be changed after initial deployment +# without breaking your instance. +FUNKWHALE_HOSTNAME={{ .Domain }} +FUNKWHALE_PROTOCOL=https + +# Log level (debug, info, warning, error, critical) +LOGLEVEL=error + +# Configure e-mail sending using this variale +# By default, funkwhale will output e-mails sent to stdout +# here are a few examples for this setting +# EMAIL_CONFIG=consolemail:// # output e-mails to console (the default) +# EMAIL_CONFIG=dummymail:// # disable e-mail sending completely +# On a production instance, you'll usually want to use an external SMTP server: +# If `user` or `password` contain special characters (eg. +# `noreply@youremail.host` as `user`), be sure to urlencode them, using +# for example the command: +# `python3 -c 'import urllib.parse; print(urllib.parse.quote_plus +# ("noreply@youremail.host"))'` +# (returns `noreply%40youremail.host`) +# EMAIL_CONFIG=smtp://user:password@youremail.host:25 +# EMAIL_CONFIG=smtp+ssl://user:password@youremail.host:465 +# EMAIL_CONFIG=smtp+tls://user:password@youremail.host:587 + +# Make e-mail verification mandatory before using the service +# Doesn't apply to admins. +# ACCOUNT_EMAIL_VERIFICATION_ENFORCE=false + +# The e-mail address to use to send system e-mails. +# DEFAULT_FROM_EMAIL=noreply@yourdomain + +# Depending on the reverse proxy used in front of your funkwhale instance, +# the API will use different kind of headers to serve audio files +# Allowed values: nginx, apache2 +REVERSE_PROXY_TYPE=nginx + +# API/Django configuration + +# Cache configuration +# Examples: +# CACHE_URL=redis://:/ +# CACHE_URL=redis://localhost:6379/0c +# With a password: +# CACHE_URL=redis://:password@localhost:6379/0 +# (the extra semicolon is important) +# Use the next one if you followed Debian installation guide +# +# CACHE_URL=redis://127.0.0.1:6379/0 +# +# If you want to use Redis over unix sockets, you'll actually need two variables: +# For the cache part: +# CACHE_URL=redis:///run/redis/redis.sock?db=0 +# For the Celery/asynchronous tasks part: +# CELERY_BROKER_URL=redis+socket:///run/redis/redis.sock?virtual_host=0 + +# Number of worker processes to execute. Defaults to 0, in which case it uses your number of CPUs +# Celery workers handle background tasks (such file imports or federation +# messaging). The more processes a worker gets, the more tasks +# can be processed in parallel. However, more processes also means +# a bigger memory footprint. +# CELERYD_CONCURRENCY=0 + +# Where media files (such as album covers or audio tracks) should be stored +# on your system? +# (Ensure this directory actually exists) +MEDIA_ROOT=/srv/funkwhale/data/media + +# Where static files (such as API css or icons) should be compiled +# on your system? +# (Ensure this directory actually exists) +STATIC_ROOT=/srv/funkwhale/data/static + +# which settings module should django use? +# You don't have to touch this unless you really know what you're doing +DJANGO_SETTINGS_MODULE=config.settings.production + +# You don't have to edit this, but you can put the admin on another URL if you +# want to +# DJANGO_ADMIN_URL=^api/admin/ + +# In-place import settings +# You can safely leave those settings uncommented if you don't plan to use +# in place imports. +# Typical docker setup: +# MUSIC_DIRECTORY_PATH=/music # docker-only +# MUSIC_DIRECTORY_SERVE_PATH=/srv/funkwhale/data/music +# Typical non-docker setup: +# MUSIC_DIRECTORY_PATH=/srv/funkwhale/data/music +# # MUSIC_DIRECTORY_SERVE_PATH= # stays commented, not needed + +MUSIC_DIRECTORY_PATH=/srv/funkwhale/data/music +MUSIC_DIRECTORY_SERVE_PATH=/srv/funkwhale/data/music + +# LDAP settings +# Use the following options to allow authentication on your Funkwhale instance +# using a LDAP directory. +# Have a look at https://docs.funkwhale.audio/installation/ldap.html for +# detailed instructions. + +# LDAP_ENABLED=False +# LDAP_SERVER_URI=ldap://your.server:389 +# LDAP_BIND_DN=cn=admin,dc=domain,dc=com +# LDAP_BIND_PASSWORD=bindpassword +# LDAP_SEARCH_FILTER=(|(cn={0})(mail={0})) +# LDAP_START_TLS=False +# LDAP_ROOT_DN=dc=domain,dc=com + +FUNKWHALE_FRONTEND_PATH=/srv/funkwhale/front/dist + +# Nginx related configuration +NGINX_MAX_BODY_SIZE=100M + +## External storages configuration +# Funkwhale can store uploaded files on Amazon S3 and S3-compatible storages (such as Minio) +# Uncomment and fill the variables below + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_STORAGE_BUCKET_NAME= +# An optional bucket subdirectory were you want to store the files. This is especially useful +# if you plan to use share the bucket with other services +# AWS_LOCATION= + +# If you use a S3-compatible storage such as minio, set the following variable +# the full URL to the storage server. Example: +# AWS_S3_ENDPOINT_URL=https://minio.mydomain.com +# AWS_S3_ENDPOINT_URL= + +# If you want to serve media directly from your S3 bucket rather than through a proxy, +# set this to false +# PROXY_MEDIA=false + +# If you are using Amazon S3 to serve media directly, you will need to specify your region +# name in order to access files. Example: +# AWS_S3_REGION_NAME=eu-west-2 +# AWS_S3_REGION_NAME= + +# If you are using Amazon S3, use this setting to configure how long generated URLs should stay +# valid. The default value is 3600 (60 minutes). The maximum accepted value is 604800 (7 days) +# AWS_QUERYSTRING_EXPIRE= + +# If you are using an S3-compatible object storage provider, and need to provide a default +# ACL for object uploads that is different from the default applied by boto3, you may +# override it here. Example: +# AWS_DEFAULT_ACL=public-read +# Available options can be found here: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl +# AWS_DEFAULT_ACL= diff --git a/README.md b/README.md index fc14267..8c7c108 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # funkwhale -> One line description of the recipe +> Funkwhale is a self-hosted, modern free and open-source music server, heavily inspired by Grooveshark. diff --git a/abra.sh b/abra.sh new file mode 100644 index 0000000..63e8f1a --- /dev/null +++ b/abra.sh @@ -0,0 +1,8 @@ +export NGINX_CONFIG_VERSION=v1 +export APP_ENTRYPOINT_VERSION=v1 + +secrets() { + docker context use default > /dev/null 2>&1 + DJANGO_SECRET_KEY=$(openssl rand -base64 45) + abra app secret insert "$APP_NAME" django_secret_key v1 "$DJANGO_SECRET_KEY" +} diff --git a/compose.yml b/compose.yml index a2c3805..f5e2e78 100644 --- a/compose.yml +++ b/compose.yml @@ -1,11 +1,44 @@ --- version: "3.8" +x-environment: + &default-env: + - DOMAIN + - LETS_ENCRYPT_ENV + - FUNKWHALE_API_IP + - FUNKWHALE_API_PORT + - FUNKWHALE_WEB_WORKERS + - FUNKWHALE_HOSTNAME + - FUNKWHALE_PROTOCOL + - LOGLEVEL + - ACCOUNT_EMAIL_VERIFICATION_ENFORCE + - DEFAULT_FROM_EMAIL + - REVERSE_PROXY_TYPE + - DATABASE_PASSWORD_FILE=/run/secrets/db_password + - CACHE_URL + - CELERYD_CONCURRENCY + - MEDIA_ROOT + - STATIC_ROOT + - DJANGO_SETTINGS_MODULE + - DJANGO_ADMIN_URL + - MUSIC_DIRECTORY_PATH + - MUSIC_DIRECTORY_SERVE_PATH + - FUNKWHALE_FRONTEND_PATH + - NGINX_MAX_BODY_SIZE + - C_FORCE_ROOT=true + services: app: image: nginx:1.20.0 + environment: *default-env networks: - proxy + - internal + volumes: + - music-data:/srv/funkwhale/data/music:ro + - media-data:/srv/funkwhale/data/media + - static-data:/srv/funkwhale/data/static + - frontend-data:/src/funkwhale/front/dist:ro deploy: restart_policy: condition: on-failure @@ -15,18 +48,95 @@ services: - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`${EXTRA_DOMAINS})" - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" - ## Redirect from EXTRA_DOMAINS to DOMAIN - #- "traefik.http.routers.${STACK_NAME}.middlewares=${STACK_NAME}-redirect" - #- "traefik.http.middlewares.${STACK_NAME}-redirect.headers.SSLForceHost=true" - #- "traefik.http.middlewares.${STACK_NAME}-redirect.headers.SSLHost=${DOMAIN}" - "coop-cloud.${STACK_NAME}.version=" - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost"] - interval: 30s - timeout: 10s - retries: 10 - start_period: 1m + + celeryworker: + image: funkwhale/funkwhale:1.2 + depends_on: + - postgres + - redis + command: celery -A funkwhale_api.taskapp worker -l INFO + environment: *default-env + volumes: + - music-data:/srv/funkwhale/data/music:ro + - media-data:/srv/funkwhale/data/media + networks: + - internal + + celerybeat: + image: funkwhale/funkwhale:1.2 + environment: *default-env + depends_on: + - postgres + - redis + command: celery -A funkwhale_api.taskapp beat --pidfile= -l INFO + networks: + - internal + + api: + image: funkwhale/funkwhale:1.2 + environment: *default-env + depends_on: + - postgres + - redis + secrets: + - django_secret_key + - db_password + volumes: + - music-data:/srv/funkwhale/data/music:ro + - media-data:/srv/funkwhale/data/media + - static-data:/srv/funkwhale/data/static + - frontend-data:/src/funkwhale/front/dist + networks: + - internal + + db: + image: postgres:10-alpine + environment: + - POSTGRES_USER=funkwhale + - POSTGRES_PASSWORD_FILE=/run/secrets/db_password + - POSTGRES_DB=funkwhale + secrets: + - db_password + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - internal + + cache: + image: redis:4-alpine + volumes: + - redis-data:/data + networks: + - internal + +volumes: + frontend-data: + media-data: + music-data: + postgres-data: + redis-data: + static-data: networks: + internal: proxy: external: true + +configs: + nginx_config: + name: ${STACK_NAME}_nginx_config_${NGINX_CONFIG_VERSION} + file: nginx.conf.tmpl + template_driver: golang + app_entrypoint: + name: ${STACK_NAME}_app_entrypoint_${APP_ENTRYPOINT_VERSION} + file: entrypoint.sh.tmpl + template_driver: golang + +secrets: + db_password: + external: true + name: ${STACK_NAME}_db_password_${SECRET_DB_PASSWORD_VERSION} + django_secret_key: + external: true + name: ${STACK_NAME}_django_secret_key_${SECRET_DJANGO_SECRET_KEY_VERSION} diff --git a/entrypoint.sh.tmpl b/entrypoint.sh.tmpl new file mode 100644 index 0000000..b9a6746 --- /dev/null +++ b/entrypoint.sh.tmpl @@ -0,0 +1,33 @@ + +#!/bin/bash + +set -e + +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + + local val="$def" + + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + + export "$var"="$val" + unset "$fileVar" +} + +file_env "DATABASE_PASSWORD" +export DATABASE_URL=postgres://funkwhale:$DATABASE_PASSWORD@db:5432/funkwhale + +# upstream entrypoint +# https://dev.funkwhale.audio/funkwhale/funkwhale/-/blob/develop/api/Dockerfile +./compose/django/entrypoint.sh "$@" diff --git a/nginx.conf.tmpl b/nginx.conf.tmpl new file mode 100644 index 0000000..e2da0e2 --- /dev/null +++ b/nginx.conf.tmpl @@ -0,0 +1,134 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +http { + upstream funkwhale-api { + server {{ env "FUNKWHALE_API_IP" }}:{{ env "FUNKWHALE_API_PORT" }}; + } + + server { + listen 80; + listen [::]:80; + server_name {{ env "FUNKWHALE_HOSTNAME" }}; + location / { return 301 https://$host$request_uri; } + + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:; worker-src 'self'"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + add_header X-Frame-Options "SAMEORIGIN" always; + + root {{ env "FUNKWHALE_FRONTEND_PATH" }}; + + gzip on; + gzip_comp_level 5; + gzip_min_length 256; + gzip_proxied any; + gzip_vary on; + + gzip_types + application/javascript + application/vnd.geo+json + application/vnd.ms-fontobject + application/x-font-ttf + application/x-web-app-manifest+json + font/opentype + image/bmp + image/svg+xml + image/x-icon + text/cache-manifest + text/css + text/plain + text/vcard + text/vnd.rim.location.xloc + text/vtt + text/x-component + text/x-cross-domain-policy; + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Port $server_port; + proxy_redirect off; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + client_max_body_size {{ env "NGINX_MAX_BODY_SIZE" }}; + proxy_pass http://funkwhale-api/; + } + + location /front/ { + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:; worker-src 'self'"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + add_header Service-Worker-Allowed "/"; + alias {{ env "FUNKWHALE_FRONTEND_PATH" }}/; + expires 30d; + add_header Pragma public; + add_header Cache-Control "public, must-revalidate, proxy-revalidate"; + } + location = /front/embed.html { + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:; worker-src 'self'"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + + add_header X-Frame-Options "" always; + alias {{ env "FUNKWHALE_FRONTEND_PATH" }}/embed.html; + expires 30d; + add_header Pragma public; + add_header Cache-Control "public, must-revalidate, proxy-revalidate"; + } + + location /federation/ { + include /etc/nginx/funkwhale_proxy.conf; + proxy_pass http://funkwhale-api/federation/; + } + + location /rest/ { + include /etc/nginx/funkwhale_proxy.conf; + proxy_pass http://funkwhale-api/api/subsonic/rest/; + } + + location /.well-known/ { + include /etc/nginx/funkwhale_proxy.conf; + proxy_pass http://funkwhale-api/.well-known/; + } + + location /media/ { + alias {{ env "MEDIA_ROOT" }}/; + } + + location /_protected/media/ { + # this is an internal location that is used to serve + # audio files once correct permission / authentication + # has been checked on API side + internal; + alias {{ env "MEDIA_ROOT" }}; + } + + # Comment the previous location and uncomment this one if you're storing + # media files in a S3 bucket + # location ~ /_protected/media/(.+) { + # internal; + # # Needed to ensure DSub auth isn't forwarded to S3/Minio, see #932 + # proxy_set_header Authorization ""; + # proxy_pass $1; + # } + + location /_protected/music/ { + # this is an internal location that is used to serve + # audio files once correct permission / authentication + # has been checked on API side + # Set this to the same value as your MUSIC_DIRECTORY_PATH setting + internal; + alias {{ env "MUSIC_DIRECTORY_SERVE_PATH" }}; + } + + location /staticfiles/ { + alias {{ env "STATIC_ROOT" }}/; + } + } +}