diff --git a/mastodon/.gitignore b/mastodon/.gitignore new file mode 100644 index 0000000..b75bcae --- /dev/null +++ b/mastodon/.gitignore @@ -0,0 +1,2 @@ +*.sql +.env.production diff --git a/mastodon/README.md b/mastodon/README.md new file mode 100644 index 0000000..6c8bb07 --- /dev/null +++ b/mastodon/README.md @@ -0,0 +1,58 @@ +# pub.solar mastodon +https://mastodon.pub.solar + +### Upgrading +This section assumes you edited `docker-compose.yml` and bumped the mastodon docker +image version tag +``` +# check current trailing number of mastodon containers +# current_container_index +docker ps | grep -E "sidekiq|streaming|web" + +# make a DB backup and copy it to your local machine +docker exec -it matrix-postgres bash + +# in shell in matrix-postgres container: +pg_dump -U mastodon -d mastodon_production -W -f /root/mastodon_db_backup-$(date +%F).sql +exit + +# copy backup to local machine +docker cp matrix-postgres:/root/mastodon_db_backup-$(date +%F).sql . + +# download new mastodon docker images +docker-compose --project-name blue-mastodon pull + +# run pre-update migrations +docker-compose run \ + --rm \ + -e SKIP_POST_DEPLOYMENT_MIGRATIONS=true \ + web \ + rails db:migrate + +# create new containers with new mastodon version +docker-compose --project-name blue-mastodon up \ + --detach \ + --scale web=2 \ + --scale streaming=2 \ + --scale sidekiq=2 \ + --no-recreate + +# stop containers with old mastodon version +docker stop \ + blue-mastodon_web_($current_container_index - 1) \ + blue-mastodon_streaming_($current_container_index - 1) \ + blue-mastodon_sidekiq_($current_container_index - 1) + +# run post-deployment migrations +docker-compose run --rm web rails db:migrate + +# clean up containers with old mastodon version +docker rm \ + blue-mastodon_web_($current_container_index - 1) \ + blue-mastodon_streaming_($current_container_index - 1) \ + blue-mastodon_sidekiq_($current_container_index - 1) +``` + +Todos: +- implement automatic backups, they are only done manually during upgrades at the moment +- switch proxy from nginx-dehydrated to caddy - done diff --git a/mastodon/backups/.gitkeep b/mastodon/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/mastodon/docker-compose.yml b/mastodon/docker-compose.yml new file mode 100644 index 0000000..a44aa65 --- /dev/null +++ b/mastodon/docker-compose.yml @@ -0,0 +1,184 @@ +version: "2.4" + +services: + + # we're using matrix-postgres as DB + #db: + # restart: always + # image: postgres:14-alpine + # shm_size: 256mb + # networks: + # - mastodon-fabric + + # Experimentally replaced by caddy + #nginx: + # image: hub.greenbaum.cloud/nginx-dehydrated:1.19-alpine + # mem_limit: 256m + # restart: always + # environment: + # - LETSENCRYPT_DOMAIN=mastodon.pub.solar + # - UPSTREAM_NAME=mastodon + # - UPSTREAM_CNS_BASE_DOMAIN=svc.e5756d08-36fd-424b-f8bc-acdb92ca7b82.cgn-1.int.greenbaum.zone + # - UPSTREAM_PORT=3000 + # network_mode: mastodon-fabric + # ports: + # - 80 + # - 443 + # labels: + # - triton.cns.services=mastodon-proxy + + caddy: + image: caddy:2.5.1 + mem_limit: 256m + restart: always + environment: + - SITE_DOMAIN=mastodon.pub.solar + - UPSTREAM_APP_DOMAIN=mastodon-web.svc.e5756d08-36fd-424b-f8bc-acdb92ca7b82.cgn-1.int.greenbaum.zone + - UPSTREAM_STREAMING_DOMAIN=mastodon-streaming.svc.e5756d08-36fd-424b-f8bc-acdb92ca7b82.cgn-1.int.greenbaum.zone + - UPSTREAM_APP_PORT=3000 + - UPSTREAM_STREAMING_PORT=4000 + network_mode: mastodon-fabric + ports: + - 80 + - 443 + labels: + - triton.cns.services=mastodon-proxy + entrypoint: /bin/sh + command: >- + -c 'echo " + { + email admins@pub.solar + } + + $$SITE_DOMAIN { + @streaming { + path /api/v1/streaming/* + } + @cache_control { + path_regexp ^/(emoji|packs|/system/accounts/avatars|/system/media_attachments/files) + } + log { + output stderr + } + handle /.well-known/keybase.txt { + root * /srv + file_server + } + reverse_proxy @streaming { + to http://$$UPSTREAM_STREAMING_DOMAIN:$$UPSTREAM_STREAMING_PORT + } + reverse_proxy { + to http://$$UPSTREAM_APP_DOMAIN:$$UPSTREAM_APP_PORT + } + handle_errors { + rewrite 500.html + } + + encode zstd gzip + + header { + Strict-Transport-Security "max-age=31536000" + } + header /sw.js Cache-Control "public, max-age=0" + header @cache_control Cache-Control "public, max-age=31536000, immutable" + } + + files.pub.solar { + handle { + rewrite * /s/jw24ad6l4a6zxsnd32cmf5hp5nsq/pub-solar-mastodon{uri}?download + reverse_proxy { + # backends / upstreams + to https://link.tardigradeshare.io + + # header manipulation + # proxy to an HTTPS endpoint + header_up Host {upstream_hostport} + # copied from mastodon docs for nginx with s3 for files + header_up Connection "" + header_up Authorization "" + # remove these header from the backends response + header_down -content-disposition + header_down -Set-Cookie + header_down -Access-Control-Allow-Origin + header_down -Access-Control-Allow-Methods + header_down -Access-Control-Allow-Headers + header_down -x-amz-id-2 + header_down -x-amz-request-id + header_down -x-amz-meta-server-side-encryption + header_down -x-amz-server-side-encryption + header_down -x-amz-bucket-region + header_down -x-amzn-requestid + # add these header to the backends response + # cache client side for 7 days + header_down Cache-Control "public, max-age=604800" + } + } + handle_errors { + rewrite 500.html + } + } + " | caddy run --adapter caddyfile --config -' + + +# using SmartOS native zone mastodon-redis, lx-brand redis crashes regularly, +# upstream bug: https://github.com/redis/redis/issues/8861 +# redis: +# image: redis:6.2-alpine +# mem_limit: 512m +# restart: always +# network_mode: mastodon-fabric +# labels: +# - triton.cns.services=mastodon-redis + + web: + image: tootsuite/mastodon:v3.5.3 + mem_limit: 1g + restart: always + env_file: .env.production + command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" + network_mode: mastodon-fabric + # see redis service comment + #depends_on: + # - redis + labels: + - triton.cns.services=mastodon-web + + streaming: + image: tootsuite/mastodon:v3.5.3 + mem_limit: 1g + restart: always + env_file: .env.production + command: node ./streaming + network_mode: mastodon-fabric + # see redis service comment + #depends_on: + # - redis + labels: + - triton.cns.services=mastodon-streaming + + sidekiq: + image: tootsuite/mastodon:v3.5.3 + mem_limit: 1g + restart: always + env_file: .env.production + command: bundle exec sidekiq + network_mode: mastodon-fabric + labels: + - triton.cns.services=mastodon-sidekiq + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2 + mem_limit: 512m + restart: always + environment: + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - "cluster.name=es-mastodon" + - "discovery.type=single-node" + - "bootstrap.memory_lock=true" + network_mode: mastodon-fabric + labels: + - triton.cns.services=mastodon-elasticsearch + ulimits: + memlock: + soft: -1 + hard: -1 diff --git a/mastodon/keybase/teutat3s-keybase-verification.txt b/mastodon/keybase/teutat3s-keybase-verification.txt new file mode 100644 index 0000000..d37168c --- /dev/null +++ b/mastodon/keybase/teutat3s-keybase-verification.txt @@ -0,0 +1,56 @@ +================================================================== +https://keybase.io/teutat3s +-------------------------------------------------------------------- + +I hereby claim: + + * I am an admin of https://mastodon.pub.solar + * I am teutat3s (https://keybase.io/teutat3s) on keybase. + * I have a public key ASCk3wTzXKHXwHUoTzp0MOjVgNx3qrRAx21X_rjzqTZJFQo + +To do so, I am signing this object: + +{ + "body": { + "key": { + "eldest_kid": "0120a4df04f35ca1d7c075284f3a7430e8d580dc77aab440c76d57feb8f3a93649150a", + "host": "keybase.io", + "kid": "0120a4df04f35ca1d7c075284f3a7430e8d580dc77aab440c76d57feb8f3a93649150a", + "uid": "5d555cc40616a706ec189ddd24b35019", + "username": "teutat3s" + }, + "merkle_root": { + "ctime": 1618093923, + "hash": "0aa5f6178ad671eda21e4e198202f4ab7f4fefff87760df4da17c78fdaf299ecdf0ae6d8858389123fe360416a4a9471bb4de5cdade3a068bed6a1bf377e76de", + "hash_meta": "883b0d6b47df62fc1566dd8671aac8a34bb93fa7e2d41e8e38bec4f5e19b3721", + "seqno": 19597808 + }, + "service": { + "entropy": "gopmQPbYzUUJHhnB0+YaTF45", + "hostname": "mastodon.pub.solar", + "protocol": "https:" + }, + "type": "web_service_binding", + "version": 2 + }, + "client": { + "name": "keybase.io go client", + "version": "5.6.1" + }, + "ctime": 1618093946, + "expire_in": 504576000, + "prev": "ecc6e3707d269e8fd62ea94d1ee7330fd15206ae3e6d14946c2f2a4b8f12c535", + "seqno": 28, + "tag": "signature" +} + +which yields the signature: + +hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEgpN8E81yh18B1KE86dDDo1YDcd6q0QMdtV/6486k2SRUKp3BheWxvYWTESpcCHMQg7MbjcH0mno/WLqlNHuczD9FSBq4+bRSUbC8qS48SxTXEIJd2HskvMapYSjPnoSSaFz9MJALBipX25kyi5bMvU+yUAgHCo3NpZ8RA+CMgOYpqqk/K0x7tgsH810Qvjxvs8FJbdCQ3SwAiFZ9v323TzWnFxuq1qbl5fAvQq6RInKYpPNALxD0c42miAahzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEIOyhPREWh9nbN6VTbOXs1YJofwkachECGbSqLa/aoyuWo3RhZ80CAqd2ZXJzaW9uAQ== + +And finally, I am proving ownership of this host by posting or +appending to this document. + +View my publicly-auditable identity here: https://keybase.io/teutat3s + +================================================================== diff --git a/mastodon/nginx/nginx-default.conf.template b/mastodon/nginx/nginx-default.conf.template new file mode 100644 index 0000000..a04e711 --- /dev/null +++ b/mastodon/nginx/nginx-default.conf.template @@ -0,0 +1,117 @@ +upstream ${UPSTREAM_NAME} { + server mastodon-web.${UPSTREAM_CNS_BASE_DOMAIN}:${UPSTREAM_PORT}; +} + +upstream streaming { + server mastodon-streaming.${UPSTREAM_CNS_BASE_DOMAIN}:4000 fail_timeout=0; +} + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g; + +# generated 2021-02-28, Mozilla Guideline v5.6, nginx 1.19, OpenSSL 1.1.1d, modern configuration +# https://ssl-config.mozilla.org/#server=nginx&version=1.19&config=modern&openssl=1.1.1d&guideline=5.6 +server { + listen 80 default_server; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + + ssl_certificate /data/dehydrated/certs/${LETSENCRYPT_DOMAIN}/fullchain.pem; + ssl_certificate_key /data/dehydrated/certs/${LETSENCRYPT_DOMAIN}/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + # modern configuration + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + # HSTS (ngx_http_headers_module is required) (63072000 seconds) + add_header Strict-Transport-Security "max-age=63072000" always; + + # OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + + # verify chain of trust of OCSP response using Root CA and Intermediate certs + ssl_trusted_certificate /data/dehydrated/certs/${LETSENCRYPT_DOMAIN}/fullchain.pem; + + # replace with the IP address of your resolver + resolver 85.88.23.13 85.88.23.14 85.88.1.92; + + keepalive_timeout 70; + sendfile on; + client_max_body_size 80m; + + location /.well-known/keybase.txt { + root /var/www/html; + } + + location / { + try_files $uri @proxy; + } + + location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) { + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=31536000"; + try_files $uri @proxy; + } + + location /sw.js { + add_header Cache-Control "public, max-age=0"; + add_header Strict-Transport-Security "max-age=31536000"; + try_files $uri @proxy; + } + + location @proxy { + 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 Proxy ""; + proxy_pass_header Server; + + proxy_pass http://${UPSTREAM_NAME}; + proxy_buffering on; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_cache CACHE; + proxy_cache_valid 200 7d; + proxy_cache_valid 410 24h; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; + add_header X-Cached $upstream_cache_status; + add_header Strict-Transport-Security "max-age=31536000"; + + tcp_nodelay on; + } + + location /api/v1/streaming { + 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 Proxy ""; + + proxy_pass http://streaming; + proxy_buffering off; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + tcp_nodelay on; + } + + error_page 500 501 502 503 504 /500.html; +} diff --git a/mastodon/nginx/nginx-files.conf b/mastodon/nginx/nginx-files.conf new file mode 100644 index 0000000..3dda39e --- /dev/null +++ b/mastodon/nginx/nginx-files.conf @@ -0,0 +1,77 @@ +server { + listen 443 ssl http2; + + server_name files.pub.solar; + + ssl_certificate /data/dehydrated/certs/files.pub.solar/fullchain.pem; + ssl_certificate_key /data/dehydrated/certs/files.pub.solar/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + # modern configuration + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + # HSTS (ngx_http_headers_module is required) (63072000 seconds) + add_header Strict-Transport-Security "max-age=63072000" always; + + # OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + + # verify chain of trust of OCSP response using Root CA and Intermediate certs + ssl_trusted_certificate /data/dehydrated/certs/files.pub.solar/fullchain.pem; + + root /var/www/files; + + keepalive_timeout 30; + + location = / { + index index.html; + } + + location / { + try_files $uri @s3; + } + + set $s3_backend 'https://link.tardigradeshare.io/s/jw24ad6l4a6zxsnd32cmf5hp5nsq/pub-solar-mastodon'; + + location @s3 { + limit_except GET { + deny all; + } + + resolver 85.88.23.13 85.88.23.14 85.88.1.92; + + + proxy_set_header Host link.tardigradeshare.io; + proxy_set_header Connection ''; + proxy_set_header Authorization ''; + proxy_hide_header content-disposition; + proxy_hide_header Set-Cookie; + proxy_hide_header 'Access-Control-Allow-Origin'; + proxy_hide_header 'Access-Control-Allow-Methods'; + proxy_hide_header 'Access-Control-Allow-Headers'; + proxy_hide_header x-amz-id-2; + proxy_hide_header x-amz-request-id; + proxy_hide_header x-amz-meta-server-side-encryption; + proxy_hide_header x-amz-server-side-encryption; + proxy_hide_header x-amz-bucket-region; + proxy_hide_header x-amzn-requestid; + proxy_ignore_headers Set-Cookie; + proxy_pass $s3_backend$uri?download; + proxy_intercept_errors off; + + proxy_cache CACHE; + proxy_cache_valid 200 48h; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; + proxy_cache_lock on; + + expires 1y; + add_header Cache-Control public; + add_header 'Access-Control-Allow-Origin' '*'; + add_header X-Cache-Status $upstream_cache_status; + add_header Content-Disposition 'inline'; + } +}