Skip to content

Instantly share code, notes, and snippets.

@alansikora
Last active July 30, 2024 15:00
Show Gist options
  • Save alansikora/30781082da4f09fe4392eef062b47058 to your computer and use it in GitHub Desktop.
Save alansikora/30781082da4f09fe4392eef062b47058 to your computer and use it in GitHub Desktop.
Rocket.Chat Matrix Federation
# !/bin/bash
# Rocket.Chat Matrix Federation Setup
# Author: Alan Sikora <[email protected]>
# https://github.com/alansikora
PWD=$(pwd)
[[ $1 = "-dev" ]] && DEV=true || DEV=false
# Set image versions
ROCKETCHAT_IMAGE_TAG="5.4.0"
SYNAPSE_IMAGE_TAG="v1.71.0"
TRAEFIK_IMAGE_TAG="v2.9.4"
REDIS_IMAGE_TAG="6.2.7"
NGINX_IMAGE_TAG="1.23.2"
ELEMENT_IMAGE_TAG="v1.11.14"
# Set config
RC_BRIDGE_PORT=3300
updateEnvFile () {
echo "" > .env
cat << EOF > .env
# Rocket.Chat CLI
RC_IMAGE=$RC_IMAGE
ROCKETCHAT_IMAGE_TAG=$ROCKETCHAT_IMAGE_TAG
SYNAPSE_IMAGE_TAG=$SYNAPSE_IMAGE_TAG
TRAEFIK_IMAGE_TAG=$TRAEFIK_IMAGE_TAG
REDIS_IMAGE_TAG=$REDIS_IMAGE_TAG
NGINX_IMAGE_TAG=$NGINX_IMAGE_TAG
ELEMENT_IMAGE_TAG=$ELEMENT_IMAGE_TAG
# Setup
SERVER_HOSTNAME=$SERVER_HOSTNAME
RC_BRIDGE_PORT=$RC_BRIDGE_PORT
# Let's Encrypt
LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL
# Matrix
AS_UNIQUE_ID=$AS_UNIQUE_ID
AS_HS_TOKEN=$AS_HS_TOKEN
AS_AS_TOKEN=$AS_AS_TOKEN
EOF
}
alpineImageOrNot () {
read -p "Do you with to use the alpine version (y/n) [n]: "
if [ "$REPLY" = "y" ]; then
RC_IMAGE="$RC_IMAGE-alpine"
fi;
}
imageLatestStable () {
RC_IMAGE="rocket.chat:$ROCKETCHAT_IMAGE_TAG"
if [ $DEV = true ]; then
alpineImageOrNot
fi
}
echo ""
echo "Rocket.Chat Matrix Federation Setup"
if [ $DEV = true ]; then
echo ">>>>>>>> DEV MODE ENABLED <<<<<<<<<"
fi
echo ""
# Check if docker is installed
DOCKER_INFO=$(docker info &> /dev/null)
RESULT=$?
if [ $RESULT -ne 0 ]; then
if [ $RESULT -eq 127 ]; then
echo "Please, install the latest docker version then run this script again:"
echo "https://docs.docker.com/engine/install/"
echo ""
exit 1
else
echo "Looks like you might have docker installed, but the wrong permissions, try this:"
echo ""
echo "> sudo groupadd docker"
echo "> sudo usermod -aG docker $USER"
echo "> newgrp docker"
echo ""
echo "And run this command again."
echo ""
exit 1
fi
fi
# Import env vars
if [ -f "$PWD/.env" ]; then
printf "Loading env vars...\n"
export $(grep -v '^#' .env | xargs)
echo ""
fi
# Generate application service data
if [ -z "$AS_UNIQUE_ID" ]; then
CURRENT=$(date +%s)
AS_UNIQUE_ID=${AS_UNIQUE_ID:=$(echo "unique_$CURRENT" | sha1sum | head -c 40)}
AS_HS_TOKEN=${AS_HS_TOKEN:=$(echo "hs_$CURRENT" | sha256sum | head -c 64)}
AS_AS_TOKEN=${AS_AS_TOKEN:=$(echo "as_$CURRENT" | sha256sum | head -c 64)}
fi
if [ -z "$SERVER_HOSTNAME" ]; then
read -p "Type your server's hostname (without https or trailing slashes): "
SERVER_HOSTNAME="$REPLY"
updateEnvFile
printf "\nPlease, create the following records pointing to your server's IP address:\n"
printf " - $SERVER_HOSTNAME\n"
printf " - synapse.$SERVER_HOSTNAME\n"
printf " - element.$SERVER_HOSTNAME\n"
printf " - traefik.$SERVER_HOSTNAME\n"
printf "\n"
fi
if [ -z "$LETSENCRYPT_EMAIL" ]; then
read -p "Type your email address, will be used to issue certificates: "
LETSENCRYPT_EMAIL="$REPLY"
updateEnvFile
printf "\n"
fi
if [ $DEV = true ]; then
printf "Which Rocket.Chat image you want to use? (current: ";
if [ -z "${RC_IMAGE}" ]; then
printf "none"
else
printf "$RC_IMAGE"
fi
printf ")\n"
RC_IMAGE_DEFAULT_OPTION=1
if ! [ -z "${RC_IMAGE}" ]; then
printf "0) keep current\n"
RC_IMAGE_DEFAULT_OPTION=0
fi;
printf "1) latest stable\n"
printf "2) latest dev\n"
printf "3) specific version\n"
printf "4) specific PR\n"
read -p "Pick an option [$RC_IMAGE_DEFAULT_OPTION]: "
case $REPLY in
1)
imageLatestStable
;;
2)
RC_IMAGE="rocket.chat:develop"
ROCKETCHAT_IMAGE_TAG="develop"
alpineImageOrNot
;;
3)
read -p "Type the version: "
RC_IMAGE="rocket.chat:$REPLY"
ROCKETCHAT_IMAGE_TAG="$REPLY"
;;
4)
read -p "Type the PR number: "
RC_IMAGE="ghcr.io/rocketchat/rocket.chat:pr-$REPLY"
ROCKETCHAT_IMAGE_TAG="pr-$REPLY"
;;
*)
if [ "$RC_IMAGE_DEFAULT_OPTION" = 1 ]; then
imageLatestStable
fi
esac
echo ""
else
imageLatestStable
fi
updateEnvFile
# Setup synapse
printf "Setting up Synapse...\n"
mkdir -p data/matrix/synapse
if [ ! -f "$PWD/data/matrix/synapse/homeserver.yaml" ]; then
docker run -it --rm \
-v $(pwd)/data/matrix/synapse:/data \
-e SYNAPSE_SERVER_NAME="$SERVER_HOSTNAME" \
-e SYNAPSE_REPORT_STATS=yes \
-e UID=1000 \
-e GID=1000 \
matrixdotorg/synapse:$SYNAPSE_IMAGE_TAG generate
fi
if ! grep -q "enable_registration: true" data/matrix/synapse/homeserver.yaml; then
sed -i '$ d' data/matrix/synapse/homeserver.yaml
tee -a data/matrix/synapse/homeserver.yaml > /dev/null <<EOF
app_service_config_files:
- /data/registration.yaml
retention:
enabled: true
enable_registration: true
enable_registration_without_verification: true
suppress_key_server_warning: true
federation_ip_range_blacklist:
- '127.0.0.0/8'
- '10.0.0.0/8'
- '172.16.0.0/12'
- '192.168.0.0/16'
- '100.64.0.0/10'
- '169.254.0.0/16'
- '::1/128'
- 'fe80::/64'
- 'fc00::/7'
database:
name: psycopg2
args:
user: synapse
password: itsasecret
database: synapse
host: postgres
cp_min: 5
cp_max: 10
redis:
enabled: true
host: redis
port: 6379
allow_public_rooms_without_auth: true
allow_public_rooms_over_federation: true
# vim:ft=yaml
EOF
fi
tee data/matrix/synapse/registration.yaml > /dev/null <<EOF
id: rocketchat_$AS_UNIQUE_ID
hs_token: $AS_HS_TOKEN
as_token: $AS_AS_TOKEN
url: http://172.17.0.1:$RC_BRIDGE_PORT
sender_localpart: rocket.cat
de.sorunome.msc2409.push_ephemeral: true
namespaces:
users:
- exclusive: false
regex: .*
rooms:
- exclusive: false
regex: .*
aliases:
- exclusive: false
regex: .*
rocketchat:
homeserver_url: http://synapse:8008
homeserver_domain: $SERVER_HOSTNAME
EOF
# Setup Nginx
printf "\nSetting up Nginx...\n"
mkdir -p data/matrix/nginx
> data/matrix/nginx/matrix.conf
tee -a data/matrix/nginx/matrix.conf > /dev/null <<EOF
server {
listen 80;
listen $RC_BRIDGE_PORT;
server_name $SERVER_HOSTNAME;
location /.well-known/matrix/server {
access_log off;
add_header Access-Control-Allow-Origin *;
default_type application/json;
return 200 '{"m.server": "synapse.$SERVER_HOSTNAME:443"}';
}
location /.well-known/matrix/client {
access_log off;
add_header Access-Control-Allow-Origin *;
default_type application/json;
return 200 '{"m.homeserver": {"base_url": "https://synapse.$SERVER_HOSTNAME"}}';
}
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_pass http://rocketchat:3000;
proxy_read_timeout 90;
}
}
EOF
# Setup Traefik
printf "\nSetting up Traefik...\n"
mkdir -p data/traefik/config
# - Setup ACME
if [ ! -f "$PWD/data/traefik/acme.json" ]; then
echo "{}" > data/traefik/acme.json
chmod 0600 data/traefik/acme.json
fi
# - Setup config: middlewares
tee data/traefik/config/middlewares.yml > /dev/null <<'EOF'
http:
middlewares:
httpsredirect:
redirectScheme:
scheme: https
permanent: true
EOF
# - Setup config: routers
tee data/traefik/config/routers.yml > /dev/null <<'EOF'
http:
routers:
redirecttohttps:
entryPoints:
- "web"
middlewares:
- "httpsredirect"
rule: "HostRegexp(`{host:.+}`)"
service: "noop@internal"
EOF
# - Setup traefik.yml
tee data/traefik/traefik.yml > /dev/null <<EOF
entryPoints:
web:
address: ":80"
web-secure:
address: ":443"
api:
dashboard: true
insecure: true
providers:
file:
directory: "/config"
watch: true
docker:
endpoint: "unix:///var/run/docker.sock"
network: "federation"
watch: true
exposedByDefault: false
certificatesResolvers:
letsencrypt:
acme:
caServer: https://acme-v02.api.letsencrypt.org/directory
email: "$LETSENCRYPT_EMAIL"
storage: "/acme.json"
httpChallenge:
entryPoint: "web"
EOF
# Setup Element
printf "\nSetting up Element...\n"
mkdir -p data/element
tee data/element/config.json > /dev/null <<EOF
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://synapse.$SERVER_HOSTNAME",
"server_name": "$SERVER_HOSTNAME"
},
"m.identity_server": {
"base_url": "https://vector.im"
}
},
"brand": "Element",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"integrations_widgets_urls": [
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
],
"hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",
"showLabsSettings": true,
"piwik": {
"url": "https://piwik.riot.im/",
"siteId": 1,
"policyUrl": "https://element.io/cookie-policy"
},
"roomDirectory": {
"servers": [
"matrix.org",
"gitter.im",
"libera.chat"
]
},
"enable_presence_by_hs_url": {
"https://matrix.org": false,
"https://matrix-client.matrix.org": false
},
"terms_and_conditions_links": [
{
"url": "https://element.io/privacy",
"text": "Privacy Policy"
},
{
"url": "https://element.io/cookie-policy",
"text": "Cookie Policy"
}
],
"hostSignup": {
"brand": "Element Home",
"cookiePolicyUrl": "https://element.io/cookie-policy",
"domains": [
"matrix.org"
],
"privacyPolicyUrl": "https://element.io/privacy",
"termsOfServiceUrl": "https://element.io/terms-of-service",
"url": "https://ems.element.io/element-home/in-app-loader"
},
"sentry": {
"dsn": "https://[email protected]/6",
"environment": "develop"
},
"posthog": {
"projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
"apiHost": "https://posthog.element.io"
},
"features": {
"feature_spotlight": true
},
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
}
EOF
# Setup PostgresSQL
printf "\nSetting up PostgreSQL...\n"
mkdir -p data/postgres/data
# Setup Docker Compose
printf "\nSetting up Docker Compose...\n"
tee docker-compose.yml > /dev/null <<'EOF'
services:
traefik:
image: "traefik:${TRAEFIK_IMAGE_TAG}"
restart: "unless-stopped"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./data/traefik/traefik.yml:/etc/traefik/traefik.yml:ro"
- "./data/traefik/config:/config:ro"
- "./data/traefik/acme.json:/acme.json"
labels:
- "traefik.enable=true"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
- "traefik.http.routers.traefik.rule=Host(`traefik.$SERVER_HOSTNAME`)"
- "traefik.http.routers.traefik.entrypoints=web-secure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
networks:
- internal
postgres:
image: "postgres:14"
restart: "unless-stopped"
environment:
POSTGRES_PASSWORD: itsasecret
POSTGRES_USER: synapse
POSTGRES_DB: synapse
POSTGRES_INITDB_ARGS: "--encoding='UTF8' --lc-collate='C' --lc-ctype='C'"
volumes:
- "./data/postgres/data:/var/lib/postgresql/data"
networks:
- internal
redis:
image: "redis:${REDIS_IMAGE_TAG}"
restart: "unless-stopped"
networks:
- internal
synapse:
image: "matrixdotorg/synapse:${SYNAPSE_IMAGE_TAG}"
restart: "unless-stopped"
environment:
SYNAPSE_CONFIG_DIR: "/data"
SYNAPSE_CONFIG_PATH: "/data/homeserver.yaml"
UID: "1000"
GID: "1000"
TZ: "America/New_York"
volumes:
- "./data/matrix/synapse:/data"
ports:
- 8008:8008
- 8448:8448
labels:
- "traefik.enable=true"
- "traefik.http.services.synapse.loadbalancer.server.port=8008"
- "traefik.http.routers.synapse.rule=Host(`synapse.$SERVER_HOSTNAME`)"
- "traefik.http.routers.synapse.entrypoints=web-secure"
- "traefik.http.routers.synapse.tls=true"
- "traefik.http.routers.synapse.tls.certresolver=letsencrypt"
networks:
- internal
nginx:
image: "nginx:${NGINX_IMAGE_TAG}"
restart: "unless-stopped"
volumes:
- "./data/matrix/nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf"
labels:
- "traefik.enable=true"
- "traefik.http.services.nginx.loadbalancer.server.port=80"
- "traefik.http.routers.nginx.rule=Host(`$SERVER_HOSTNAME`)"
- "traefik.http.routers.nginx.entrypoints=web-secure"
- "traefik.http.routers.nginx.tls=true"
- "traefik.http.routers.nginx.tls.certresolver=letsencrypt"
networks:
- internal
element:
image: vectorim/element-web:${ELEMENT_IMAGE_TAG}
restart: unless-stopped
volumes:
- ./data/element/config.json:/app/config.json
labels:
- "traefik.enable=true"
- "traefik.http.services.element.loadbalancer.server.port=80"
- "traefik.http.routers.element.rule=Host(`element.$SERVER_HOSTNAME`)"
- "traefik.http.routers.element.entrypoints=web-secure"
- "traefik.http.routers.element.tls=true"
- "traefik.http.routers.element.tls.certresolver=letsencrypt"
networks:
- internal
rocketchat:
image: "${RC_IMAGE}"
command: >
bash -c
"for i in `seq 1 30`; do
node main.js &&
s=$$? && break || s=$$?;
echo \"Tried $$i times. Waiting 5 secs...\";
sleep 5;
done; (exit $$s)"
restart: unless-stopped
volumes:
- ./uploads:/app/uploads
- ./data/matrix/synapse/registration.yaml:/app/matrix-federation-config/registration.yaml
environment:
- PORT=3000
- ROOT_URL=https://localhost:3000
- MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true
- MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true
- NODE_ENV=production
# - REG_TOKEN=${REG_TOKEN}
depends_on:
- mongodb
ports:
- 3000:3000
- 3300:3300
networks:
- internal
# labels:
# - "traefik.enable=true"
# - "traefik.http.services.rc.loadbalancer.server.port=3000"
# - "traefik.http.routers.rc.rule=Host(`$SERVER_HOSTNAME`)"
# - "traefik.http.routers.rc.entrypoints=web-secure"
# - "traefik.http.routers.rc.tls=true"
# - "traefik.http.routers.rc.tls.certresolver=letsencrypt"
# - "traefik.http.services.bridge.loadbalancer.server.port=$RC_BRIDGE_PORT"
# - "traefik.http.routers.bridge.rule=Host(`$SERVER_HOSTNAME`)"
# - "traefik.http.routers.bridge.entrypoints=web-secure"
# - "traefik.http.routers.bridge.tls=true"
# - "traefik.http.routers.bridge.tls.certresolver=letsencrypt"
mongodb:
image: mongo:5.0
restart: unless-stopped
volumes:
- ./data/mongo/db:/data/db
command: mongod --oplogSize 128 --replSet rs0
labels:
- "traefik.enable=false"
networks:
- internal
# this container's job is just run the command to initialize the replica set.
# it will run the command and remove himself (it will not stay running)
mongo-init-replica:
image: mongo:5.0
command: >
bash -c
"for i in `seq 1 30`; do
mongo mongodb/rocketchat --eval \"
rs.initiate({
_id: 'rs0',
members: [ { _id: 0, host: 'localhost:27017' } ]})\" &&
s=$$? && break || s=$$?;
echo \"Tried $$i times. Waiting 5 secs...\";
sleep 5;
done; (exit $$s)"
depends_on:
- mongodb
networks:
- internal
networks:
internal:
attachable: true
EOF
# Rocket.Chat Matrix Federation Tester
# Author: Alan Sikora <[email protected]>
# https://github.com/alansikora
echo ""
echo "Rocket.Chat Matrix Federation Tester"
echo ""
checkURL () {
echo ""
printf "Checking $URL_TO_TEST..."
RESPONSE=$(curl -s -L $URL_TO_TEST)
if [[ "$RESPONSE" == *"$EXPECTED_URL_SUBSTRING"* ]]; then
echo "OK!"
else
echo "$URL_TO_TEST is not as expected"
exit 1
fi
}
# Import env vars
if [ -f "$PWD/.env" ]; then
printf "Loading env vars...\n"
export $(grep -v '^#' .env | xargs)
fi
if [ -z "${SERVER_HOSTNAME}" ]; then
echo "You must run the setup script first and have a .env file on your directory"
exit 1
fi
# Check if synapse URL is correct
URL_TO_TEST="https://synapse.$SERVER_HOSTNAME"
EXPECTED_URL_SUBSTRING="<h1>It works! Synapse is running</h1>"
checkURL
# Check if client's well known is correct
URL_TO_TEST="https://$SERVER_HOSTNAME/.well-known/matrix/client"
EXPECTED_URL_SUBSTRING="\"base_url\": \"https://synapse.$SERVER_HOSTNAME\""
checkURL
# Check if servers's well known is correct
URL_TO_TEST="https://$SERVER_HOSTNAME/.well-known/matrix/server"
EXPECTED_URL_SUBSTRING="\"synapse.$SERVER_HOSTNAME:443\""
checkURL
# Check if Element's URL is correct
URL_TO_TEST="https://element.$SERVER_HOSTNAME/"
EXPECTED_URL_SUBSTRING="<noscript>Sorry, Element requires JavaScript to be enabled.</noscript>"
checkURL
# Check if Rocket.Chat's URL is correct
URL_TO_TEST="https://$SERVER_HOSTNAME/"
EXPECTED_URL_SUBSTRING="<title>Rocket.Chat</title>"
checkURL
@Denperidge
Copy link

To use the script, I had to add version: '3.3' to the top of the docker-compose file. Otherwise some options were not recognised. If this issue happens with multiple people, perhaps automate that in the script! Either way, thanks for the setup!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment