Add Mastodon.

This commit is contained in:
2026-01-04 19:36:31 +00:00
parent f17fea6910
commit d756126a34
13 changed files with 849 additions and 0 deletions

81
mastodon/README.md Normal file
View File

@@ -0,0 +1,81 @@
# Mastodon
Mastodon is a free, open-source social network server based on ActivityPub. It allows you to run your own instance of a decentralized social media platform.
## Version
This package deploys Mastodon v4.5.3 (released July 8, 2025).
## Dependencies
- **PostgreSQL**: Database for storing application data
- **Redis**: Used for caching and background job queuing
## Configuration
### VAPID Keys
Mastodon requires VAPID (Voluntary Application Server Identification) keys for Web Push notifications. These keys use Elliptic Curve P-256 cryptography.
**The Wild Cloud API automatically generates proper VAPID keys when you add the Mastodon app.** No manual configuration is required!
### Database
The database is automatically initialized with:
- Database: `mastodon_production`
- User: `mastodon` with auto-generated password
- All necessary privileges granted
The db-init job handles creating the database and user, and automatically updates the user password if it changes.
### Storage
Mastodon uses two persistent volumes:
- **Assets** (10Gi): Stores compiled assets and static files
- **System** (100Gi): Stores user uploads, media files, and other system data
Both volumes use ReadWriteMany access mode to allow multiple pods to access them simultaneously.
## Components
Mastodon runs three separate services:
- **Web (Puma)**: Main web server for the Mastodon web interface
- **Streaming (Node.js)**: Real-time streaming API for live updates
- **Sidekiq**: Background job processor for async tasks
## Access
After deployment, Mastodon will be available at:
- https://mastodon.{your-cloud-domain}
The ingress automatically routes:
- `/api/v1/streaming` → Streaming service
- All other paths → Web service
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add mastodon
wild app deploy mastodon
```
2. Generate and configure VAPID keys (see above)
3. Access your instance in a browser and create the first admin user account
4. Configure additional settings through the Mastodon admin interface
## Security
All containers run as non-root user (UID 991) with:
- No privilege escalation
- All capabilities dropped
- Compliant with Pod Security Standards
## Notes
- SMTP configuration is inherited from your Wild Cloud instance settings
- Database credentials are auto-generated and stored in your instance's `secrets.yaml`
- The Active Record Encryption keys are auto-generated for Rails 8.0.3 compatibility

185
mastodon/db-init-job.yaml Normal file
View File

@@ -0,0 +1,185 @@
apiVersion: batch/v1
kind: Job
metadata:
name: mastodon-db-init
namespace: {{ .namespace }}
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
component: db-init
spec:
restartPolicy: OnFailure
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: db-init
image: postgres:16-alpine
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
env:
- name: PGHOST
value: "{{ .dbHostname }}"
- name: PGPORT
value: "{{ .dbPort }}"
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: postgres.password
- name: MASTODON_DB
value: "{{ .dbName }}"
- name: MASTODON_USER
value: "{{ .dbUsername }}"
- name: MASTODON_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
command:
- sh
- -c
- |
set -e
echo "Waiting for PostgreSQL to be ready..."
until pg_isready -h $PGHOST -p $PGPORT -U $PGUSER; do
echo "PostgreSQL is unavailable - sleeping"
sleep 2
done
echo "PostgreSQL is ready"
echo "Creating database if it doesn't exist..."
psql -v ON_ERROR_STOP=1 <<-EOSQL
SELECT 'CREATE DATABASE $MASTODON_DB'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$MASTODON_DB')\gexec
EOSQL
echo "Creating/updating user..."
psql -v ON_ERROR_STOP=1 <<-EOSQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '$MASTODON_USER') THEN
CREATE USER $MASTODON_USER WITH PASSWORD '$MASTODON_PASSWORD';
ELSE
ALTER USER $MASTODON_USER WITH PASSWORD '$MASTODON_PASSWORD';
END IF;
END
\$\$;
EOSQL
echo "Granting privileges..."
psql -v ON_ERROR_STOP=1 <<-EOSQL
GRANT ALL PRIVILEGES ON DATABASE $MASTODON_DB TO $MASTODON_USER;
\c $MASTODON_DB
GRANT ALL ON SCHEMA public TO $MASTODON_USER;
EOSQL
echo "Database initialization complete"
---
apiVersion: batch/v1
kind: Job
metadata:
name: mastodon-db-migrate
namespace: {{ .namespace }}
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
component: db-migrate
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: db-migrate
image: {{ .image }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
command:
- bundle
- exec
- rails
- db:migrate
env:
- name: LOCAL_DOMAIN
value: "{{ .domain }}"
- name: RAILS_ENV
value: production
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: secretKeyBase
- name: OTP_SECRET
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: otpSecret
- name: ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordPrimaryKey
- name: ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordDeterministicKey
- name: ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordKeyDerivationSalt
- name: DB_HOST
value: "{{ .dbHostname }}"
- name: DB_PORT
value: "{{ .dbPort }}"
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUsername }}"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
- name: REDIS_HOST
value: "{{ .redisHostname }}"
- name: REDIS_PORT
value: "{{ .redisPort }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: redis.password
volumeMounts:
- name: assets
mountPath: /opt/mastodon/public/assets
- name: system
mountPath: /opt/mastodon/public/system
volumes:
- name: assets
persistentVolumeClaim:
claimName: mastodon-assets
- name: system
persistentVolumeClaim:
claimName: mastodon-system

View File

@@ -0,0 +1,156 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mastodon-sidekiq
namespace: {{ .namespace }}
spec:
replicas: {{ .sidekiq.replicas }}
selector:
matchLabels:
component: sidekiq
template:
metadata:
labels:
component: sidekiq
spec:
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: sidekiq
image: {{ .image }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
command:
- bundle
- exec
- sidekiq
- -c
- "{{ .sidekiq.concurrency }}"
- -q
- default,8
- -q
- push,6
- -q
- ingress,4
- -q
- mailers,2
- -q
- pull
- -q
- scheduler
env:
- name: LOCAL_DOMAIN
value: "{{ .domain }}"
- name: RAILS_ENV
value: production
- name: RAILS_LOG_LEVEL
value: info
- name: DEFAULT_LOCALE
value: "{{ .locale }}"
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: secretKeyBase
- name: OTP_SECRET
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: otpSecret
- name: VAPID_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPrivateKey
- name: VAPID_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPublicKey
- name: ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordPrimaryKey
- name: ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordDeterministicKey
- name: ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordKeyDerivationSalt
- name: DB_HOST
value: "{{ .dbHostname }}"
- name: DB_PORT
value: "{{ .dbPort }}"
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUsername }}"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: postgres.password
- name: REDIS_HOST
value: "{{ .redisHostname }}"
- name: REDIS_PORT
value: "{{ .redisPort }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: redis.password
- name: SMTP_SERVER
value: "{{ .smtp.server }}"
- name: SMTP_PORT
value: "{{ .smtp.port }}"
- name: SMTP_LOGIN
value: "{{ .smtp.user }}"
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: smtpPassword
- name: SMTP_FROM_ADDRESS
value: "{{ .smtp.from }}"
- name: SMTP_AUTH_METHOD
value: "{{ .smtp.authMethod }}"
- name: SMTP_ENABLE_STARTTLS
value: "{{ .smtp.enableStarttls }}"
- name: SMTP_TLS
value: "{{ .smtp.tls }}"
volumeMounts:
- name: assets
mountPath: /opt/mastodon/public/assets
- name: system
mountPath: /opt/mastodon/public/system
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 768Mi
volumes:
- name: assets
persistentVolumeClaim:
claimName: mastodon-assets
- name: system
persistentVolumeClaim:
claimName: mastodon-system

View File

@@ -0,0 +1,83 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mastodon-streaming
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: streaming
template:
metadata:
labels:
component: streaming
spec:
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: streaming
image: {{ .streamingImage }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
ports:
- name: streaming
containerPort: {{ .streamingPort }}
protocol: TCP
env:
- name: NODE_ENV
value: production
- name: PORT
value: "{{ .streamingPort }}"
- name: STREAMING_CLUSTER_NUM
value: "1"
- name: DB_HOST
value: "{{ .dbHostname }}"
- name: DB_PORT
value: "{{ .dbPort }}"
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUsername }}"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
- name: REDIS_HOST
value: "{{ .redisHostname }}"
- name: REDIS_PORT
value: "{{ .redisPort }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: redis.password
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
memory: 512Mi
livenessProbe:
httpGet:
path: /api/v1/streaming/health
port: streaming
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /api/v1/streaming/health
port: streaming
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 3

View File

@@ -0,0 +1,170 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mastodon-web
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: web
template:
metadata:
labels:
component: web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: {{ .image }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
command:
- bundle
- exec
- puma
- -C
- config/puma.rb
ports:
- name: http
containerPort: {{ .webPort }}
protocol: TCP
env:
- name: LOCAL_DOMAIN
value: "{{ .domain }}"
- name: RAILS_ENV
value: production
- name: RAILS_LOG_LEVEL
value: info
- name: DEFAULT_LOCALE
value: "{{ .locale }}"
- name: SINGLE_USER_MODE
value: "{{ .singleUserMode }}"
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: secretKeyBase
- name: OTP_SECRET
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: otpSecret
- name: VAPID_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPrivateKey
- name: VAPID_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPublicKey
- name: ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordPrimaryKey
- name: ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordDeterministicKey
- name: ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordKeyDerivationSalt
- name: DB_HOST
value: "{{ .dbHostname }}"
- name: DB_PORT
value: "{{ .dbPort }}"
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUsername }}"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: postgres.password
- name: REDIS_HOST
value: "{{ .redisHostname }}"
- name: REDIS_PORT
value: "{{ .redisPort }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: redis.password
- name: SMTP_SERVER
value: "{{ .smtp.server }}"
- name: SMTP_PORT
value: "{{ .smtp.port }}"
- name: SMTP_LOGIN
value: "{{ .smtp.user }}"
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: smtpPassword
- name: SMTP_FROM_ADDRESS
value: "{{ .smtp.from }}"
- name: SMTP_AUTH_METHOD
value: "{{ .smtp.authMethod }}"
- name: SMTP_ENABLE_STARTTLS
value: "{{ .smtp.enableStarttls }}"
- name: SMTP_TLS
value: "{{ .smtp.tls }}"
- name: STREAMING_API_BASE_URL
value: "wss://{{ .domain }}"
- name: WEB_CONCURRENCY
value: "2"
- name: MAX_THREADS
value: "5"
volumeMounts:
- name: assets
mountPath: /opt/mastodon/public/assets
- name: system
mountPath: /opt/mastodon/public/system
resources:
requests:
cpu: 250m
memory: 768Mi
limits:
memory: 1280Mi
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 3
volumes:
- name: assets
persistentVolumeClaim:
claimName: mastodon-assets
- name: system
persistentVolumeClaim:
claimName: mastodon-system

33
mastodon/ingress.yaml Normal file
View File

@@ -0,0 +1,33 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mastodon
namespace: {{ .namespace }}
annotations:
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}
rules:
- host: {{ .domain }}
http:
paths:
- path: /api/v1/streaming
pathType: Prefix
backend:
service:
name: mastodon-streaming
port:
number: {{ .streamingPort }}
- path: /
pathType: Prefix
backend:
service:
name: mastodon-web
port:
number: {{ .webPort }}

View File

@@ -0,0 +1,20 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: {{ .namespace }}
labels:
- includeSelectors: true
pairs:
app: mastodon
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- pvc-assets.yaml
- pvc-system.yaml
- db-init-job.yaml
- deployment-web.yaml
- deployment-sidekiq.yaml
- deployment-streaming.yaml
- service-web.yaml
- service-streaming.yaml
- ingress.yaml

67
mastodon/manifest.yaml Normal file
View File

@@ -0,0 +1,67 @@
name: mastodon
is: mastodon
description: Mastodon is a free, open-source social network server based on ActivityPub.
version: 4.5.3
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mastodon.svg
requires:
- name: postgres
- name: redis
defaultConfig:
namespace: mastodon
externalDnsDomain: "{{ .cloud.domain }}"
timezone: UTC
image: ghcr.io/mastodon/mastodon:v4.5.3
streamingImage: ghcr.io/mastodon/mastodon-streaming:v4.5.3
domain: mastodon.{{ .cloud.domain }}
locale: en
singleUserMode: false
# Database configuration
dbHostname: "{{ .apps.postgres.host }}"
dbPort: "{{ .apps.postgres.port }}"
dbName: mastodon_production
dbUsername: mastodon
# Redis configuration
redisHostname: "{{ .apps.redis.host }}"
redisPort: "{{ .apps.redis.port }}"
# Ports
webPort: 3000
streamingPort: 4000
# Storage
assetsStorage: 10Gi
systemStorage: 100Gi
# SMTP configuration
smtp:
enabled: "{{ .cloud.smtp.host | ternary true false }}"
server: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
from: notifications@{{ .cloud.domain }}
user: "{{ .cloud.smtp.user }}"
authMethod: plain
enableStarttls: auto
tls: "{{ .cloud.smtp.tls }}"
# TLS
tlsSecretName: wildcard-wild-cloud-tls
# Sidekiq configuration
sidekiq:
replicas: 1
concurrency: 25
defaultSecrets:
- key: secretKeyBase
default: "{{ random.AlphaNum 128 }}"
- key: otpSecret
default: "{{ random.AlphaNum 128 }}"
- key: vapidPrivateKey
# Must be generated with: bundle exec rake mastodon:webpush:generate_vapid_key
- key: vapidPublicKey
# Must be generated with: bundle exec rake mastodon:webpush:generate_vapid_key
- key: activeRecordPrimaryKey
default: "{{ random.AlphaNum 32 }}"
- key: activeRecordDeterministicKey
default: "{{ random.AlphaNum 32 }}"
- key: activeRecordKeyDerivationSalt
default: "{{ random.AlphaNum 32 }}"
- key: dbPassword
- key: smtpPassword
requiredSecrets:
- postgres.password
- redis.password

4
mastodon/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .namespace }}

11
mastodon/pvc-assets.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mastodon-assets
namespace: {{ .namespace }}
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .assetsStorage }}

11
mastodon/pvc-system.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mastodon-system
namespace: {{ .namespace }}
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .systemStorage }}

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mastodon-streaming
namespace: {{ .namespace }}
spec:
type: ClusterIP
ports:
- port: {{ .streamingPort }}
targetPort: streaming
protocol: TCP
name: streaming
selector:
component: streaming

14
mastodon/service-web.yaml Normal file
View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mastodon-web
namespace: {{ .namespace }}
spec:
type: ClusterIP
ports:
- port: {{ .webPort }}
targetPort: http
protocol: TCP
name: http
selector:
component: web