Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Mastodon Ubuntu
GUIDEInstall Guides

How to Install Mastodon on Ubuntu 24.04 — Self-Host a Federated Social Server

24 min read

How to Install Mastodon on Ubuntu 24.04 — Self-Host a Federated Social Server

Running your own Mastodon server means owning your social identity, your moderation policies, and your data -- with no corporate algorithm deciding what your audience sees and no central authority able to suspend you without recourse. This guide walks you through deploying a production-grade Mastodon instance on an Ubuntu 24.04 VPS using Docker Compose, from first SSH connection to a federated server handing out @you@yourdomain handles across the fediverse.

Recommended Plan: Self-hosting Mastodon needs real resources -- Postgres, Redis, the Rails web container, a streaming Node process, and Sidekiq workers all run side by side. We recommend the CloudCore Professional VPS plan for single-admin instances with up to a few hundred active users.

Table of Contents

  • What is Mastodon?
  • Why Self-Host Federated Social?
  • Prerequisites
  • Step 1: Update the System and Install Docker
  • Step 2: Create the Mastodon Directory and User
  • Step 3: Write the Docker Compose File
  • Step 4: Generate the .env.production File
  • Step 5: Bootstrap the Database and Admin
  • Step 6: Start Mastodon
  • Step 7: Configure Nginx as a TLS Reverse Proxy
  • Step 8: Configure S3-Compatible Media Storage
  • Step 9: Configure SMTP for Confirmation Emails
  • Step 10: Media Cache Cleanup Cron
  • Post-Install: First Login and Federation Check
  • Troubleshooting
  • FAQ
  • Next Steps
  • What is Mastodon?

    Mastodon is a free, open-source, federated microblogging server. It speaks the ActivityPub protocol, which means any Mastodon server can exchange posts, follows, likes, and replies with any other ActivityPub server -- not just other Mastodon instances, but also Pleroma, Friendica, Misskey, Pixelfed, PeerTube, and every other piece of federated software in the fediverse. A user registered on your server can follow someone on mastodon.social, who can in turn follow a video creator on a PeerTube instance, who can boost a photo from a Pixelfed user -- all transparently, from whichever app they happen to use.

    The architecture is deliberately decentralised. Each server (commonly called an instance) is independently administered. The admin controls moderation, federation rules, custom emoji, character limits, and privacy policy. Users get a handle that looks like an email address: @[email protected]. The combination of username plus domain makes every fediverse identity globally unique without needing a central registry.

    Under the hood Mastodon is a Ruby on Rails application with a Node.js streaming service, backed by PostgreSQL for durable storage and Redis for feeds and background-job queues. Background work -- delivering posts out to thousands of followers across the network, pulling in remote media, sending email, generating preview cards -- runs in Sidekiq worker processes. Elasticsearch is optional and enables full-text search. For a single-admin server, the whole stack fits comfortably on one VPS.

    Why Self-Host Federated Social?

    Running your own Mastodon instance gives you things that no commercial social network can offer:

    • No corporate control -- No shareholders, no ad inventory, no growth-hacked algorithm deciding your reach. Your timeline is strictly chronological. Your posts never get demoted for linking externally or for not hitting an engagement threshold.
    • Your moderation, your rules -- You decide what's allowed, what gets boosted, and which remote servers your instance federates with. If an instance is full of spam or abuse, you can defederate it with a single admin action. If you want a stricter code of conduct than the default, write it.
    • Own your identity -- Your handle is @[email protected]. Nobody can revoke it, nobody can squat it, nobody can sell your name to someone else. If you ever migrate to different software (Pleroma, GoToSocial, Akkoma), you keep the domain and your followers can come with you.
    • Data sovereignty -- Every post, every follow, every DM lives on a database you control. You can back it up, encrypt it, inspect it, export it. No third party can mine your social graph or train a model on your DMs without your knowledge.
    • No advertising, no tracking -- Mastodon has no ad system. There are no trackers, no pixels, no third-party scripts injected into your timeline. Your users' attention is not a product.
    • Federation without lock-in -- Even though you host it yourself, your users are not isolated. They can follow and be followed by millions of fediverse users on thousands of other servers.
    • Cost scales with you, not the platform -- A single VPS at a predictable monthly rate can host a personal, family, or small-community instance indefinitely. No "free tier" that changes terms, no sudden price hikes.

    Self-Hosted vs. Hosted Mastodon vs. Commercial Social

    FactorSelf-Hosted Mastodonmasto.host / Hosted MastodonTwitter / Threads
    Admin controlFullLimitedNone
    Moderation rulesYou write themProvider terms applyPlatform decides
    Custom domainYesYesNo
    Ads / trackingNoneNoneYes
    Data exportFull DB accessExport toolLimited
    Monthly costEUR ~19.99 VPSEUR 9-90/mo hosted"Free" (attention)
    FederationFull fediverseFull fediverseWalled garden

    Prerequisites

    Before you start, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access and at least 6 vCPU, 12 GB RAM, 200 GB SSD
    • A registered domain name pointed at your VPS IP (A record for social.yourdomain.com, or whichever subdomain you pick)
    • Ports 80 and 443 open on your firewall
    • Basic familiarity with SSH, Docker, and editing config files
    • An SMTP provider for transactional email (Postmark, Amazon SES, Mailgun, Resend, or similar)
    • Optional but strongly recommended: an S3-compatible object storage bucket (Contabo Object Storage, Wasabi, Backblaze B2, or AWS S3) for media
    Recommended Plan: CloudCore Professional
    >
    A moderately active single-admin instance with a few hundred users needs enough RAM for Postgres, Redis, the Rails web container, a Node streaming process, and two to three Sidekiq workers. The CloudCore Professional plan gives you:
    >
    - 6 vCPU cores
    - 12 GB RAM
    - 200 GB NVMe SSD
    - Unmetered bandwidth
    - Ideal for single-admin or small-community Mastodon instances

    Pick a subdomain before you start and set its A record to your VPS IP. In this guide we'll use social.example.com -- substitute your own throughout.

    Connect to your server:

    bash
    ssh root@your-server-ip

    Step 1: Update the System and Install Docker

    Update the base system first:

    bash
    sudo apt update && sudo apt upgrade -y

    Install Docker Engine and the Compose plugin from Docker's official repository (the Ubuntu-packaged Docker is often outdated):

    bash
    sudo apt install -y ca-certificates curl gnupg
    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
      sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    sudo chmod a+r /etc/apt/keyrings/docker.gpg

    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

    sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    Verify both are working:

    bash
    docker --version
    docker compose version

    Expected output:

    text
    Docker version 27.3.1, build ce12230
    Docker Compose version v2.29.7

    Enable Docker at boot:

    bash
    sudo systemctl enable --now docker

    Step 2: Create the Mastodon Directory and User

    Mastodon containers run as UID 991 internally, so we need a matching directory layout with correct ownership to avoid permission issues.

    bash
    sudo mkdir -p /opt/mastodon/{postgres,redis,public/system}
    sudo chown -R 991:991 /opt/mastodon/public
    cd /opt/mastodon

    The public/system directory is where uploaded media lives if you're not using S3. Even if you plan to use S3 later, having it correctly owned prevents the container from failing on startup.

    Step 3: Write the Docker Compose File

    Create /opt/mastodon/docker-compose.yml with the core five services: Postgres, Redis, the Rails web app, the Node streaming service, and a Sidekiq worker.

    bash
    sudo tee /opt/mastodon/docker-compose.yml > /dev/null <<'EOF'
    services:
      db:
        image: postgres:15-alpine
        restart: always
        shm_size: 256mb
        networks:
          - internal_network
        healthcheck:
          test: ['CMD', 'pg_isready', '-U', 'mastodon']
        volumes:
          - ./postgres:/var/lib/postgresql/data
        environment:
          - POSTGRES_USER=mastodon
          - POSTGRES_DB=mastodon_production
          - POSTGRES_PASSWORD=CHANGE_ME_STRONG_DB_PASSWORD

    redis: image: redis:7-alpine restart: always networks: - internal_network healthcheck: test: ['CMD', 'redis-cli', 'ping'] volumes: - ./redis:/data

    web: image: ghcr.io/mastodon/mastodon:v4.3 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb networks: - external_network - internal_network healthcheck: test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1'] ports: - '127.0.0.1:3000:3000' depends_on: - db - redis volumes: - ./public/system:/mastodon/public/system

    streaming: image: ghcr.io/mastodon/mastodon-streaming:v4.3 restart: always env_file: .env.production command: node ./streaming/index.js networks: - external_network - internal_network healthcheck: test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1'] ports: - '127.0.0.1:4000:4000' depends_on: - db - redis

    sidekiq: image: ghcr.io/mastodon/mastodon:v4.3 restart: always env_file: .env.production command: bundle exec sidekiq depends_on: - db - redis networks: - external_network - internal_network volumes: - ./public/system:/mastodon/public/system healthcheck: test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\\ 6' || false"]

    networks: external_network: internal_network: internal: true EOF

    A few notes on this layout:

    • shm_size: 256mb on Postgres -- the default /dev/shm of 64 MB is too small for Postgres parallel workers on a moderately busy instance.
    • networks: internal_network on db and redis with internal: true means they have no outbound internet access and cannot be reached from outside the compose project. This is important defence-in-depth.
    • ports: '127.0.0.1:3000' -- the web container is bound only to the loopback interface. Nginx (running on the host) will reverse-proxy to it over TLS. We never expose port 3000 or 4000 publicly.
    • Pinned tag v4.3 rather than latest -- upgrades are a deliberate act.
    Change CHANGE_ME_STRONG_DB_PASSWORD now and remember what you set it to; you'll repeat it in .env.production.

    Step 4: Generate the .env.production File

    Mastodon needs several cryptographic secrets before it will start. Use the official image to generate them -- this saves you from installing Ruby on the host.

    Generate three secrets:

    bash
    cd /opt/mastodon

    docker run --rm -e OTP_SECRET= -e SECRET_KEY_BASE= \ ghcr.io/mastodon/mastodon:v4.3 bundle exec rake secret

    copy output -> SECRET_KEY_BASE

    docker run --rm -e OTP_SECRET= -e SECRET_KEY_BASE= \ ghcr.io/mastodon/mastodon:v4.3 bundle exec rake secret

    copy output -> OTP_SECRET

    docker run --rm ghcr.io/mastodon/mastodon:v4.3 \ bundle exec rake mastodon:webpush:generate_vapid_key

    copy output -> VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY

    Now write the environment file:

    bash
    sudo tee /opt/mastodon/.env.production > /dev/null <<'EOF'
    

    Federation

    LOCAL_DOMAIN=social.example.com SINGLE_USER_MODE=false ALTERNATE_DOMAINS=

    Secrets (paste the values you generated above)

    SECRET_KEY_BASE=PASTE_SECRET_KEY_BASE_HERE OTP_SECRET=PASTE_OTP_SECRET_HERE VAPID_PRIVATE_KEY=PASTE_VAPID_PRIVATE_KEY_HERE VAPID_PUBLIC_KEY=PASTE_VAPID_PUBLIC_KEY_HERE

    Deployment

    RAILS_ENV=production NODE_ENV=production RAILS_SERVE_STATIC_FILES=true RAILS_LOG_LEVEL=warn

    Database

    DB_HOST=db DB_PORT=5432 DB_USER=mastodon DB_NAME=mastodon_production DB_PASS=CHANGE_ME_STRONG_DB_PASSWORD

    Redis

    REDIS_HOST=redis REDIS_PORT=6379

    Limits

    MAX_TOOT_CHARS=500 MAX_POLL_OPTIONS=4 MAX_POLL_OPTION_CHARS=50

    SMTP (filled in Step 9)

    SMTP_SERVER= SMTP_PORT=587 SMTP_LOGIN= SMTP_PASSWORD= [email protected] SMTP_AUTH_METHOD=plain SMTP_OPENSSL_VERIFY_MODE=peer

    S3 media (filled in Step 8)

    S3_ENABLED=false EOF

    Tighten permissions -- this file holds every secret your instance has:

    bash
    sudo chmod 600 /opt/mastodon/.env.production

    The three keys you generated do specific things:

    • SECRET_KEY_BASE is used by Rails to sign cookies and session tokens. Rotating it forcibly logs out every user and invalidates password-reset tokens.
    • OTP_SECRET encrypts two-factor authentication secrets in the database. If you lose this, every user loses their 2FA device.
    • VAPID_PRIVATE_KEY / VAPID_PUBLIC_KEY sign Web Push notifications so that browsers can verify they came from your server. Rotate them and existing push subscriptions break.
    Back up .env.production to somewhere safe (encrypted password manager, offline storage) immediately.

    Step 5: Bootstrap the Database and Admin

    With the compose file and secrets in place, run the Mastodon setup rake tasks. Start just Postgres and Redis first so the web container can reach them:

    bash
    cd /opt/mastodon
    sudo docker compose up -d db redis
    sleep 10

    Run the schema creation task:

    bash
    sudo docker compose run --rm web bundle exec rake db:setup

    This creates the schema, loads the initial seed data, and prepares the database. Expect output like:

    text
    Created database 'mastodon_production'
    ... (many migrations) ...
    Seeded Mastodon defaults

    If you see a FATAL: password authentication failed message, double-check that DB_PASS in .env.production matches POSTGRES_PASSWORD in docker-compose.yml.

    Now create the first admin user. Replace the username and email with your own:

    bash
    sudo docker compose run --rm --no-deps web \
      bin/tootctl accounts create admin \
        --email [email protected] \
        --confirmed \
        --role Owner

    Expected output:

    text
    OK
    New password: 4d8f2a6b3c9e7f1a0b5c8d2e4f6a9b3c

    Copy that password immediately -- it's shown once, and logging in with it is how you claim the account. You'll change it at first login.

    Step 6: Start Mastodon

    Bring up the whole stack:

    bash
    sudo docker compose up -d

    Check that every container is healthy:

    bash
    sudo docker compose ps

    Expected output:

    text
    NAME                  IMAGE                                  STATUS
    mastodon-db-1         postgres:15-alpine                     Up (healthy)
    mastodon-redis-1      redis:7-alpine                         Up (healthy)
    mastodon-web-1        ghcr.io/mastodon/mastodon:v4.3         Up (healthy)
    mastodon-streaming-1  ghcr.io/mastodon/mastodon-streaming:v4.3  Up (healthy)
    mastodon-sidekiq-1    ghcr.io/mastodon/mastodon:v4.3         Up (healthy)

    Tail the logs to confirm there are no startup errors:

    bash
    sudo docker compose logs -f web

    Press Ctrl+C to stop tailing. If the web container is restarting in a loop, sudo docker compose logs web | tail -100 is your friend.

    The web app is now listening on 127.0.0.1:3000 and streaming on 127.0.0.1:4000. It is not yet reachable from the internet -- that's Nginx's job.

    Step 7: Configure Nginx as a TLS Reverse Proxy

    Install Nginx and Certbot:

    bash
    sudo apt install -y nginx certbot python3-certbot-nginx

    Create the site configuration:

    bash
    sudo tee /etc/nginx/sites-available/mastodon > /dev/null <<'EOF'
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    upstream backend { server 127.0.0.1:3000 fail_timeout=0; }

    upstream streaming { server 127.0.0.1:4000 fail_timeout=0; }

    proxy_cache_path /var/cache/nginx/mastodon levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g;

    server { listen 80; listen [::]:80; server_name social.example.com; root /opt/mastodon/public; location /.well-known/acme-challenge/ { allow all; } location / { return 301 https://$host$request_uri; } }

    server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name social.example.com;

    ssl_certificate /etc/letsencrypt/live/social.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/social.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header Referrer-Policy same-origin;

    keepalive_timeout 70; sendfile on; client_max_body_size 99m;

    root /opt/mastodon/public; gzip on; gzip_disable "msie6"; gzip_vary on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon;

    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=63072000; includeSubDomains"; try_files $uri @proxy; }

    location /sw.js { add_header Cache-Control "public, max-age=604800, must-revalidate"; 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 https; proxy_set_header Proxy "";

    proxy_pass_header Server; proxy_pass http://backend; 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;

    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 https; 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; } EOF

    sudo ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/ sudo rm -f /etc/nginx/sites-enabled/default

    Get a free Let's Encrypt certificate. Certbot will edit the HTTP block to solve the ACME challenge:

    bash
    sudo certbot --nginx -d social.example.com \
      --non-interactive --agree-tos -m [email protected]
    sudo nginx -t && sudo systemctl reload nginx

    Certbot installs a systemd timer that renews certificates automatically every 60 days. Check it with sudo systemctl list-timers | grep certbot.

    Now visit https://social.example.com in a browser. You should see the Mastodon landing page. Log in as admin with the one-time password from Step 5 and change it immediately from Settings -> Account.

    Step 8: Configure S3-Compatible Media Storage

    Storing every avatar, header, video, and preview card on your VPS disk is fine for a handful of users but becomes unmanageable at scale. Mastodon has first-class support for S3-compatible object storage through the Paperclip attachment library.

    The configuration works with any S3-compatible provider: AWS S3, Contabo Object Storage, Wasabi, Backblaze B2, DigitalOcean Spaces, MinIO, and others.

    Create a bucket on your provider, then create an access key with read/write permissions scoped to that bucket. Edit /opt/mastodon/.env.production:

    bash
    sudo nano /opt/mastodon/.env.production

    Replace the S3_ENABLED=false line with:

    env
    S3_ENABLED=true
    S3_BUCKET=your-mastodon-bucket
    S3_REGION=eu-central
    S3_PROTOCOL=https
    S3_HOSTNAME=eu2.contabostorage.com
    S3_ENDPOINT=https://eu2.contabostorage.com
    AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
    AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY

    Serve uploads directly from the bucket via its own domain

    S3_ALIAS_HOST=media.social.example.com

    Notes on each variable:

    • S3_HOSTNAME and S3_ENDPOINT -- the provider's API endpoint. Values above are Contabo Object Storage (EU region). For AWS it would be s3.eu-central-1.amazonaws.com.
    • S3_REGION -- provider-specific; match exactly what the provider displays.
    • S3_ALIAS_HOST -- a CNAME on your domain that points to the bucket's public URL. Using an alias host means URLs in federated posts look like https://media.social.example.com/... instead of exposing the provider. Set a CNAME in DNS pointing media.social.example.com to the bucket's public hostname, then add it to your Nginx config or the provider's custom-domain settings.
    Restart the stack to pick up the new configuration:

    bash
    cd /opt/mastodon
    sudo docker compose restart web sidekiq

    From now on every new upload lands in the S3 bucket. Existing local uploads continue to be served from public/system -- to migrate them, use tootctl media sync-storage inside the web container.

    Step 9: Configure SMTP for Confirmation Emails

    Mastodon sends email for account confirmation, password resets, notifications, and direct-message alerts. Deliverability matters -- messages that land in spam translate directly into signup drop-off. Use a reputable transactional email provider rather than trying to run your own MTA on the same VPS.

    Popular choices:

    • Postmark -- excellent deliverability, simple pricing
    • Amazon SES -- cheapest at scale, requires sandbox-removal request
    • Mailgun -- mature, good EU region
    • Resend -- developer-friendly, modern dashboard
    Get an SMTP username and password from your provider, verify your sending domain (add SPF, DKIM, and DMARC records to your DNS), then edit .env.production:

    env
    SMTP_SERVER=smtp.postmarkapp.com
    SMTP_PORT=587
    SMTP_LOGIN=your-postmark-server-token
    SMTP_PASSWORD=your-postmark-server-token
    SMTP_FROM_ADDRESS='Mastodon <[email protected]>'
    SMTP_AUTH_METHOD=plain
    SMTP_OPENSSL_VERIFY_MODE=peer
    SMTP_ENABLE_STARTTLS=auto

    Restart the containers that send mail:

    bash
    sudo docker compose restart web sidekiq

    Test delivery by triggering a password-reset email for a test user, or use the built-in rake task:

    bash
    sudo docker compose run --rm web bin/tootctl email_domain_blocks list
    sudo docker compose run --rm web bundle exec rails console
    

    inside the console:

    UserMailer.confirmation_instructions(User.first, 'test').deliver_now

    If email doesn't arrive, check your provider's message log first -- most show rejected messages with a reason (missing DKIM, bad From address, unverified domain).

    Step 10: Media Cache Cleanup Cron

    Every time someone on your instance views a remote post with an image, Mastodon downloads and caches that media locally (or in your S3 bucket). Over months this cache grows without bound -- hundreds of gigabytes is typical for an instance following active accounts on large servers. Mastodon ships a cleanup command that removes remote media older than N days.

    Create a cron job that runs nightly:

    bash
    sudo tee /etc/cron.d/mastodon-cleanup > /dev/null <<'EOF'
    

    Mastodon cache maintenance -- run at 04:00 UTC daily

    0 4 * root cd /opt/mastodon && /usr/bin/docker compose run --rm web bin/tootctl media remove --days=14 >> /var/log/mastodon-cleanup.log 2>&1 30 4 * root cd /opt/mastodon && /usr/bin/docker compose run --rm web bin/tootctl preview_cards remove --days=30 >> /var/log/mastodon-cleanup.log 2>&1 0 5 0 root cd /opt/mastodon && /usr/bin/docker compose run --rm web bin/tootctl statuses remove --days=90 >> /var/log/mastodon-cleanup.log 2>&1 EOF

    sudo chmod 644 /etc/cron.d/mastodon-cleanup sudo touch /var/log/mastodon-cleanup.log

    What each line does:

    • media remove --days=14 -- deletes cached copies of remote media older than 14 days. If someone scrolls back in an old thread, the media is re-fetched on demand.
    • preview_cards remove --days=30 -- removes Open Graph preview images for link cards older than 30 days.
    • statuses remove --days=90 -- removes cached copies of remote posts older than 90 days that no local user has interacted with. Runs weekly on Sundays because it's expensive.
    Tune the retention windows to match your disk budget. A busy instance may need --days=7 for media; a quiet one can keep 30 days without trouble.

    Post-Install: First Login and Federation Check

    With everything running, give your instance a quick end-to-end test.

    Finish admin setup: log in at https://social.example.com as admin, change the password, enable 2FA (Settings -> Account -> Two-factor authentication), and fill in the site metadata (Preferences -> Administration -> Server Settings: name, description, contact, rules, privacy policy).

    Verify federation: from your admin account, follow @[email protected] (Eugen Rochko, Mastodon's founder). Within a few seconds his profile should load and his posts should start appearing in your home timeline. If federation is broken, posts never arrive.

    Check Sidekiq health: go to https://social.example.com/sidekiq (logged in as Owner) and confirm the default, push, pull, mailers, ingress, and scheduler queues all have recent processed jobs and no stuck retries.

    Run the built-in self-check:

    bash
    sudo docker compose run --rm web bin/tootctl self-destruct --help
    

    just to verify the binary runs; don't actually self-destruct

    sudo docker compose run --rm web bin/tootctl feeds build

    Troubleshooting

    ProblemCauseSolution
    Web container crashes on startup with SECRET_KEY_BASE must be setSecrets not generated or .env.production not readableRe-run the rake secret commands in Step 4, paste into .env.production, chmod 600, restart
    502 Bad Gateway from Nginxweb or streaming container not runningsudo docker compose ps to check, then sudo docker compose logs web
    Posts from other servers never appearFirewall blocking outbound 443, or DNS misconfiguredTest: sudo docker compose exec sidekiq wget -qO- https://mastodon.social/.well-known/nodeinfo. Check Sidekiq pull queue for retries
    Media uploads return 500 errorS3 credentials wrong, bucket policy blocks uploads, or S3_HOSTNAME wrongTest credentials with aws s3 ls s3://bucket using the same keys. Check web container logs during upload
    Emails never arriveSPF/DKIM/DMARC not set, domain unverified with providerCheck provider message log. Verify DNS records with dig TXT social.example.com. Send from notifications@your-verified-domain, not gmail.com
    Sidekiq queue builds up, posts slow to federateNot enough Sidekiq worker processesScale: sudo docker compose up -d --scale sidekiq=3. Ensure RAM headroom
    Disk fills upMedia cache not being prunedRun bin/tootctl media remove --days=7 manually, verify cron job in /etc/cron.d/mastodon-cleanup is present
    Your connection is not private at first visitCertbot hasn't run, or DNS not propagatedConfirm A record resolves to VPS IP, re-run sudo certbot --nginx -d social.example.com

    Viewing Logs

    Follow all container logs:

    bash
    cd /opt/mastodon
    sudo docker compose logs -f

    Follow one service:

    bash
    sudo docker compose logs -f sidekiq

    Nginx access and error logs:

    bash
    sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log

    FAQ

    How much RAM and disk does a Mastodon instance really need?

    A personal single-user instance runs comfortably on 4 GB RAM and 40 GB disk, but you'll hit memory pressure fast once federation picks up -- a handful of follows to busy accounts pulls in thousands of daily posts and their cached media. For a single-admin server with up to ~200 active users, plan for 12 GB RAM and 200 GB disk with S3 offload, or 500 GB+ if you keep media on local storage. Postgres and Redis are the memory-hungry services; Sidekiq workers scale horizontally with traffic.

    Is Mastodon the only option, or should I look at Pleroma, Misskey, or GoToSocial?

    Mastodon is the most polished and widely-adopted ActivityPub server, but it's not the only choice. Misskey and its forks (Sharkey, Firefish) have richer UIs with emoji reactions and more customisation, at the cost of higher resource usage. Pleroma and Akkoma are much lighter on RAM (~1 GB) and work well on small VPS plans but have smaller communities and fewer admin tools. GoToSocial is a single-binary Go implementation aimed at tiny instances -- perfect for a personal-use server. All of them federate with each other; your choice affects your experience, not who you can follow. Mastodon is the safest default for anyone hosting for others.

    Can I federate with Pixelfed and PeerTube from my Mastodon server?

    Yes -- that's the point of the fediverse. Any account on any ActivityPub server can be followed from Mastodon. Someone posting photos on Pixelfed, videos on PeerTube, or long-form writing on WriteFreely appears in your home timeline like any other Mastodon user. Replies and boosts flow in both directions. The UX varies -- Pixelfed videos play inline in Mastodon, PeerTube videos embed a player -- but the underlying follow-graph is shared.

    Do I need Elasticsearch?

    No. Elasticsearch enables full-text search of your own toots and toots from accounts that opt into discoverability. Without it, search is limited to usernames, hashtags, and URLs. Running Elasticsearch adds 1-2 GB of RAM usage and non-trivial ops burden. Most single-admin instances skip it until users specifically request full-text search.

    How do I back up my instance?

    Two things matter: the Postgres database and media storage. For the database, run pg_dump nightly from outside the container: sudo docker compose exec -T db pg_dump -U mastodon mastodon_production | gzip > /backups/mastodon-$(date +%F).sql.gz. Keep 30 days, encrypted, off the server. For media, if you use S3 the provider handles durability; if you use local storage, rsync /opt/mastodon/public/system to another machine or object storage. Also back up .env.production -- losing it means losing every user's 2FA seed. Test restores quarterly.

    How do I upgrade Mastodon to a new version?

    First, read the release notes at github.com/mastodon/mastodon/releases -- some versions require a db:migrate, and major versions sometimes add new secrets or environment variables. Back up the database. Update the image tags in docker-compose.yml from, say, v4.3 to v4.4, then:

    bash
    cd /opt/mastodon
    sudo docker compose pull
    sudo docker compose run --rm web bundle exec rake db:migrate
    sudo docker compose up -d

    Always pin to a specific minor version rather than latest. If the upgrade fails, roll back by restoring the previous database dump and changing the image tag back.

    How do I migrate from another Mastodon instance without losing followers?

    Mastodon supports account migration natively. On your old account, set an "alias" pointing at your new @[email protected] handle (Preferences -> Account -> Account aliases). Then on your new account, trigger the move (Preferences -> Account -> Move to a different account). Your followers are notified and followed automatically on the new account within a few hours. Your old posts don't move -- ActivityPub has no mechanism for that -- but your follow graph does.

    Next Steps

    Now that your Mastodon server is running, here are common follow-up tasks:

    • Add custom emoji -- Preferences -> Administration -> Custom Emojis. Upload PNGs or animated GIFs. Instance-specific emoji are one of the most loved features of fediverse culture.
    • Write server rules and a privacy policy -- Preferences -> Administration -> Server Settings. Good rules reduce moderation load later. Link to your plan and Acceptable Use Policy.
    • Configure federation filters -- block known-bad servers proactively. The oliphant.social blocklist aggregates widely-accepted bans.
    • Deploy Grafana + Prometheus monitoring -- track Sidekiq queue depth, Postgres connection count, Redis memory, and HTTP latency. A stuck push queue is the earliest sign of federation trouble.
    • Install a backup agent -- Restic or Borgbackup with encrypted off-site storage. Schedule nightly full database dumps and weekly media sync.
    • Deploy Misskey or Pixelfed alongside -- run a Pixelfed instance on a different subdomain for photos, or a PeerTube instance for video. They all federate with each other and your Mastodon server.
    • Read the official admin docs -- docs.joinmastodon.org is comprehensive and updated with every release. Bookmark the Admin CLI reference.

    Self-Hosting the Fediverse
    >
    A Professional VPS gives you the headroom for Mastodon, plus room to add Pixelfed or PeerTube on the same server later. Full root, predictable billing, EU-hosted -- everything you need to own your social presence.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket