Mastodon on K3s:
Preparations:
Before we start we need to make sure that you have the following steps done, and I am not going to explain it as if I did, 3 parts won't be enough, so I'll assume that you have:
- You own a domain name.
- Your domain is setup in Cloudflare and that you are using their DNS/Zero Trust service.
- You have setup for an email service provider, like Mailgun or Postmark (I use postmark), any provider which provides SMTP is good.
- You have a paid account with iDrive, and you have created a public bucket with a CDN (check this post here, but you don't need to use AWS CLI to setup the permissions as iDrive team has added it as an option from the UI)

Assuming that you have followed along, and you have all your nodes connected to each other, let's start doing our magic
PS: I'll create a github repo with all the code for reference.
Redis:
This one is the easiest and simplest, we can create it using the following yml which will run redis on the 3rd node (worker-2):
--- apiVersion: v1 kind: Namespace metadata: name: mastodon-server --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: redis-pvc namespace: mastodon-server spec: accessModes: - ReadWriteOnce storageClassName: local-path resources: requests: storage: 5Gi --- apiVersion: v1 kind: Service metadata: name: redis namespace: mastodon-server spec: selector: app: redis ports: - name: redis-port protocol: TCP port: 6379 --- apiVersion: apps/v1 kind: Deployment metadata: name: redis namespace: mastodon-server spec: replicas: 1 selector: matchLabels: app: redis template: metadata: labels: app: redis name: redis spec: nodeSelector: kubernetes.io/hostname: worker-2 containers: - name: redis image: redis:latest imagePullPolicy: Always args: ["--appendonly", "yes"] ports: - name: redis containerPort: 6379 volumeMounts: - name: redis-storage mountPath: /data env: - name: ALLOW_EMPTY_PASSWORD value: "yes" volumes: - name: redis-storage persistentVolumeClaim: claimName: redis-pvc
PostgreSQL:
For the database, we will use ubuntu postgresql docker image, and we will run the instance on one of the instances preferably not the master.
As I mentioned in my post here, we will need we will need to define a storage, a port and a deployment, I don't think we needs the custom configuration since everything will be connected locally.
I'll also define a new thing, which is secrets to hide the sensitive data we are going to use.
--- apiVersion: v1 kind: Namespace metadata: name: mastodon-server --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-pvc namespace: mastodon-server spec: accessModes: - ReadWriteOnce storageClassName: local-path resources: requests: storage: 5Gi --- apiVersion: v1 kind: Service metadata: name: postgres namespace: mastodon-server spec: selector: app: postgres ports: - name: postgres-port protocol: TCP port: 5432 --- apiVersion: v1 kind: Secret metadata: namespace: mastodon-server name: postgres-secret type: Opaque stringData: POSTGRES_USER: postgres POSTGRES_PASSWORD: <CHANGE-ME> POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_DB: mastodon POSTGRES_NON_ROOT_USER: <CHANGE-ME> POSTGRES_NON_ROOT_PASSWORD: <CHANGE-ME> --- apiVersion: v1 kind: ConfigMap metadata: name: postgres-config namespace: mastodon-server data: init-data.sh: | #!/bin/bash set -e; if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}'; GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER}; EOSQL fi --- apiVersion: apps/v1 kind: Deployment metadata: name: postgres namespace: mastodon-server spec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres name: postgres spec: nodeSelector: kubernetes.io/hostname: worker-1 containers: - name: postgres image: ubuntu/postgres:latest imagePullPolicy: Always ports: - name: postgres containerPort: 5432 volumeMounts: - name: postgres-storage mountPath: /var/lib/postgresql/data env: - name: POSTGRES_USER valueFrom: secretKeyRef: name: secrets key: POSTGRES_USER - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: secrets key: POSTGRES_PASSWORD - name: POSTGRES_HOST_AUTH_METHOD valueFrom: secretKeyRef: name: secrets key: POSTGRES_HOST_AUTH_METHOD volumes: - name: secrets secret: secretName: postgres-secret - name: postgres-storage persistentVolumeClaim: claimName: postgres-pvc
Make sure to go over the file and change the value of each <CHANGE-ME>
with something you use, like the password, the user .. etc.
Mastodon:
Before we install Mastodon, you need to go over to Mastodon documentation and get yourself familiar with the environment variables and what they means, you can find them here.
It is important that you do so, as you will need to modify the environment variables to meet your needs.
You can generate the SECRET_KEY_BASE and OTP_SECRET using the following commands
docker run --rm -it -w /app/www --entrypoint rake lscr.io/linuxserver/mastodon secret docker run --rm -it -w /app/www --entrypoint rake lscr.io/linuxserver/mastodon mastodon:webpush:generate_vapid_key
Once you do, you can you can change them in this big yml file:
--- apiVersion: v1 kind: Namespace metadata: name: mastodon-server --- apiVersion: v1 kind: Service metadata: name: mastodon namespace: mastodon-server spec: selector: app: mastodon ports: - name: mastodon-plain protocol: TCP port: 80 - name: mastodon-secure protocol: TCP port: 443 --- apiVersion: v1 kind: Secret metadata: namespace: mastodon-server name: mastodon-secret type: Opaque stringData: SECRET_KEY_BASE: <CHANGE-ME> OTP_SECRET: <CHANGE-ME> VAPID_PRIVATE_KEY: <CHANGE-ME> VAPID_PUBLIC_KEY: <CHANGE-ME> SMTP_LOGIN: <CHANGE-ME> SMTP_PASSWORD: <CHANGE-ME> SMTP_FROM_ADDRESS: <CHANGE-ME> AWS_ACCESS_KEY_ID: <CHANGE-ME> AWS_SECRET_ACCESS_KEY: <CHANGE-ME> S3_ENDPOINT: <CHANGE-ME> SMTP_FROM_ADDRESS: <CHANGE-ME> --- apiVersion: apps/v1 kind: Deployment metadata: name: mastodon namespace: mastodon-server labels: app: mastodon spec: replicas: 3 selector: matchLabels: app: mastodon template: metadata: labels: app: mastodon name: mastodon spec: topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: mastodon containers: - name: mastodon image: lscr.io/linuxserver/mastodon:latest imagePullPolicy: Always ports: - name: plain containerPort: 80 - name: secure containerPort: 443 env: - name: PUID value: "1000" - name: GUID value: "1000" - name: TZ value: Europe/Istanbul - name: LOCAL_DOMAIN value: <CHANGE-ME> # domain.com - name: WEB_DOMAIN value: <CHANGE-ME> # domain.com - name: REDIS_HOST value: redis.redis-server - name: REDIS_PORT value: "6379" - name: DB_NAME value: mastodon - name: DB_HOST value: postgres.mastodon-server - name: DB_USER valueFrom: secretKeyRef: name: db_secrets key: POSTGRES_NON_ROOT_USER - name: DB_PASS valueFrom: secretKeyRef: name: db_secrets key: POSTGRES_NON_ROOT_PASSWORD - name: DB_PORT value: "5432" - name: ES_ENABLED value: "false" - name: SECRET_KEY_BASE valueFrom: secretKeyRef: name: secrets key: SECRET_KEY_BASE - name: OTP_SECRET valueFrom: secretKeyRef: name: secrets key: OTP_SECRET - name: VAPID_PRIVATE_KEY valueFrom: secretKeyRef: name: secrets key: VAPID_PRIVATE_KEY - name: VAPID_PUBLIC_KEY valueFrom: secretKeyRef: name: secrets key: VAPID_PUBLIC_KEY - name: RAILS_SERVE_STATIC_FILES value: "false" - name: SMTP_SERVER valueFrom: secretKeyRef: name: secrets key: SMTP_SERVER - name: SMTP_PORT value: "587" # change this one accordenly - name: SMTP_LOGIN valueFrom: secretKeyRef: name: secrets key: SMTP_LOGIN - name: SMTP_PASSWORD valueFrom: secretKeyRef: name: secrets key: SMTP_PASSWORD - name: SMTP_FROM_ADDRESS valueFrom: secretKeyRef: name: secrets key: SMTP_FROM_ADDRESS - name: S3_ENABLED value: "true" - name: CDN_HOST value: <CHANGE-ME> - name: S3_BUCKET value: "mastodon" - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: name: secrets key: AWS_ACCESS_KEY_ID - name: AWS_SECRET_ACCESS_KEY valueFrom: secretKeyRef: name: secrets key: AWS_SECRET_ACCESS_KEY - name: S3_REGION value: auto - name: S3_ENDPOINT valueFrom: secretKeyRef: name: secrets key: S3_ENDPOINT - name: S3_HOSTNAME value: <CHANGE-ME> # assets.domain.com - name: S3_FORCE_SINGLE_REQUEST value: "true" - name: S3_READ_TIMEOUT value: "15" - name: S3_OPEN_TIMEOUT value: "15" resources: limits: cpu: "1.0" memory: "2560Mi" requests: cpu: "0.5" memory: "2560Mi" volumes: - name: db_secrets secret: secretName: postgres-secret - name: secrets secret: secretName: mastodon-secret
I am not going to go over each and everything as most of them are self explained, the name of each variable explain exactly what it is, all you have to do is to find the one I marked as
Cloudflare Tunnels:
It is the same thing as you read here so I won't explain it, I'll paste the yml content for reference
--- apiVersion: v1 kind: Namespace metadata: name: cloudflare-server --- apiVersion: apps/v1 kind: Deployment metadata: name: cloudflare namespace: cloudflare-server spec: replicas: 1 selector: matchLabels: app: cloudflare template: metadata: labels: app: cloudflare name: cloudflare spec: topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: cloudflare containers: - name: cloudflare image: cloudflare/cloudflared:latest imagePullPolicy: Always args: - "tunnel" - "--no-autoupdate" - "run" - "--token" - <ADD_YOUR_TOKEN_HERE>
Accessing Your Instance
Now that you have your instance up and running, we need to configure Cloudflare tunnel to let you access it, but you need to be careful about one simple point, we used LinuxServer mastodon image, which generate a selfcertificate which is not 100% secure and will cause some problems when talking with Cloudflare tunnels, so we will need to ask Cloudflare to not validate it, this is a simple thing

Our service URL should be <app>.<namespace>
and since our app is called mastodon
and the namespace is mastodon-server
the url is mastodon.mastodon-server
Accessing the Terminal
There is still something you need to do, after registering and creating your account on mastodon, you need to mark it as the owner of the instance, you can do that using the following command:
app/www/bin/tootctl accounts modify <CHANGE-ME> --role Owner
but how can you access the terminal? since you have lens/openlens installed you can click on any running pod named mastodon-<random>
and in the top right you will choose the Pod Shell icon, which will give you access to the docker image shell, this way now you can run the command without any problem.
Remember to change <CHANGE-ME>
to your username.
Daily Database Backup
We have everything in place, and we only need to make sure we don't loose our data, so we will create a cronjob that will backup our databases and put it on a different S3 bucket that we created on iDrive.
--- apiVersion: v1 kind: Namespace metadata: name: mastodon-server --- apiVersion: v1 kind: Secret metadata: namespace: mastodon-server name: backup-secret type: Opaque stringData: S3_ACCESS_KEY_ID: <CHANGE-ME> S3_SECRET_ACCESS_KEY: <CHANGE-ME> S3_BUCKET: <CHANGE-ME> S3_ENDPOINT: <CHANGE-ME> --- apiVersion: batch/v1 kind: CronJob metadata: name: postgresql-backup-cron-job namespace: mastodon-server spec: schedule: "0 */12 * * *" successfulJobsHistoryLimit: 1 failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: containers: - name: postgresql-backup-job image: ghcr.io/zaherg/postgres_backup:latest imagePullPolicy: Always env: - name: POSTGRES_DATABASE value: "all" - name: POSTGRES_HOST value: "postgres.mastodon-server" - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: db_secrets key: POSTGRES_USER - name: POSTGRES_USER valueFrom: secretKeyRef: name: db_secrets key: POSTGRES_USER - name: S3_ACCESS_KEY_ID valueFrom: secretKeyRef: name: backup_secrets key: S3_ACCESS_KEY_ID - name: S3_SECRET_ACCESS_KEY valueFrom: secretKeyRef: name: db_sbackup_secretsecrets key: S3_SECRET_ACCESS_KEY - name: S3_BUCKET valueFrom: secretKeyRef: name: backup_secrets key: S3_BUCKET - name: S3_ENDPOINT valueFrom: secretKeyRef: name: backup_secrets key: S3_ENDPOINT restartPolicy: OnFailure volumes: - name: db_secrets secret: secretName: postgres-secret - name: backup_secrets secret: secretName: backup-secret backoffLimit: 3
Remember to change <CHANGE-ME>
to its accurate value.