Skip to content

Instantly share code, notes, and snippets.

@SamirTalwar
Created October 5, 2025 07:45
Show Gist options
  • Select an option

  • Save SamirTalwar/39588ad591f52cd5abfc130bd2aead05 to your computer and use it in GitHub Desktop.

Select an option

Save SamirTalwar/39588ad591f52cd5abfc130bd2aead05 to your computer and use it in GitHub Desktop.
How I deploy Mastodon on NixOS
{
version,
environmentFiles,
redisPort,
redisPassword,
}:
{ config, pkgs, ... }:
let
image = pkgs.dockerTools.pullImage {
imageName = "ghcr.io/mastodon/mastodon";
imageDigest = "sha256:e8ad3b126c71ed5d7df24d1257754a45338d5158e9dd88255accdf958a676ca6";
hash = "sha256-5BTT1KTve/jQfGCqVjlwQnLt2otOYL944aGn5HRGjmg=";
finalImageName = "ghcr.io/mastodon/mastodon";
finalImageTag = "v4.4.5";
};
streamingImage = pkgs.dockerTools.pullImage {
imageName = "ghcr.io/mastodon/mastodon-streaming";
imageDigest = "sha256:b2d6aefcc160dd4e60b196a814e6a803e6a5e073b17a1f9453b1e42afafad2f8";
hash = "sha256-h4f0UHFNRJjUq4cRyyagpzyn5CewZbgiZk1YWa/zcsQ=";
finalImageName = "ghcr.io/mastodon/mastodon-streaming";
finalImageTag = "v4.4.5";
};
in
{
services.redis.servers.mastodon = {
enable = true;
port = redisPort;
requirePass = redisPassword;
bind = null;
};
# Allow containers to access the databases.
networking.firewall.interfaces."podman1".allowedTCPPorts = [
redisPort
];
networking.firewall.interfaces."podman2".allowedTCPPorts = [
redisPort
];
systemd.services."podman-network-mastodon" = {
description = "Create Podman mastodon network";
wantedBy = [ "multi-user.target" ];
before = [
"podman-mastodon-web.service"
"podman-mastodon-streaming.service"
"podman-mastodon-sidekiq.service"
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Environment = [
"PATH=/run/wrappers/bin:${pkgs.lib.makeBinPath [ pkgs.podman ]}"
];
};
script = ''
if ! podman network exists mastodon; then
podman network create --subnet=10.89.1.0/24 mastodon
fi
'';
};
virtualisation.oci-containers.containers.mastodon-web = {
inherit environmentFiles;
autoStart = true;
image = "${image.imageName}:${image.imageTag}";
imageFile = image;
cmd = [
"bundle"
"exec"
"rails"
"server"
"--port=3000"
];
networks = [
"gateway"
"mastodon"
];
};
virtualisation.oci-containers.containers.mastodon-streaming = {
inherit environmentFiles;
autoStart = true;
image = "${streamingImage.imageName}:${streamingImage.imageTag}";
imageFile = streamingImage;
cmd = [
"node"
"./streaming"
];
networks = [
"gateway"
"mastodon"
];
};
virtualisation.oci-containers.containers.mastodon-sidekiq = {
inherit environmentFiles;
autoStart = true;
image = "${image.imageName}:${image.imageTag}";
imageFile = image;
cmd = [
"bundle"
"exec"
"sidekiq"
];
networks = [ "mastodon" ];
};
}
RAILS_ENV=production
NODE_ENV=production
SINGLE_USER_MODE=true
# Federation
# ----------
# This identifies your server and cannot be changed safely later
# ----------
LOCAL_DOMAIN=test.example
# Redis
# -----
REDIS_HOST=host.containers.internal
REDIS_PORT=6999
REDIS_PASSWORD=B9Fx1oP1c9YXI1SgtZxKCwH3mCjwrJHT
# PostgreSQL
# ----------
DATABASE_URL=postgresql://[email protected]:5432/mastodon
# Elasticsearch (optional)
# ------------------------
# ES_ENABLED=true
# ES_HOST=localhost
# ES_PORT=9200
# Authentication for ES (optional)
# ES_USER=elastic
# ES_PASS=password
# Secrets
# -------
# Make sure to use `bundle exec rails secret` to generate secrets
# -------
SECRET_KEY_BASE=8ce80daefd414e9fc3d939947cc52daa188709b52f595f0d638632bd082a455f029092594fe643b886b89106e52fd9a5a57f6514e6b298a63dd977fb43fffc1d
OTP_SECRET=d4a7f1b9b3195a15ee691c28173445f5193052614fedae56c419cb2f7c633e14185c71e9fab483b803c3afe04e42e5fe04b526573de5da38cb583cf0e524a5b1
# Active Record Encryption
# -------
# Generate with `bundle exec rails db:encryption:init`
# -------
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=yj81XmDT05N1U78pxKjRIXGwlhMbWXno
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=fvMus8CpfcA1tFd37pRJCH94psEuj8yj
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=M4yB0UthqVaBashk5tc0Tzsbr8AL5bTl
# Web Push
# --------
# Generate with `bundle exec rails mastodon:webpush:generate_vapid_key`
# --------
VAPID_PRIVATE_KEY=2QDAtJhwNn7qkfOq3w9ij1sGigqwkNLvNgG3qiLsquw=
VAPID_PUBLIC_KEY=BDvDgI_vBrqPRDJD4O5Mmj3LbO-Ujsw-OQSTWh-rl2BfSx2W2SfyQQtqtGoIQ6GJV7cerK0QIMCv3Z5v4iJDoS8=
# Sending mail
# ------------
# SMTP_SERVER=
# SMTP_PORT=
# SMTP_LOGIN=
# SMTP_PASSWORD=
# SMTP_FROM_ADDRESS=
# File storage (optional)
# -----------------------
S3_ENABLED=false
# S3_ENDPOINT=
# S3_HOSTNAME=
# S3_ALIAS_HOST=
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# IP and session retention
# -----------------------
# Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml
# to be less than daily if you lower IP_RETENTION_PERIOD below two days (172800).
# -----------------------
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952
let
pkgs = import ../../_pkgs;
in
pkgs.testers.runNixOSTest {
name = "mastodon";
nodes = {
machine =
{ ... }:
{
# The default disk image doesn't have enough space to unpack the images.
# We therefore switch to a tmpfs-backed image (using `null`), and
# allocate 4 GB of RAM to ensure it has enough space.
virtualisation.diskImage = null;
virtualisation.memorySize = 4096;
imports = [
../common.nix
../gateway-network.nix
(import ./. {
version = "4.4.5";
environmentFiles = [
(pkgs.writeText "env" (builtins.readFile ./test.env))
];
redisPort = 6999;
redisPassword = "B9Fx1oP1c9YXI1SgtZxKCwH3mCjwrJHT";
})
];
# Make sure the gateway network is created before the app.
systemd.services."podman-network-gateway".before = [
"podman-mastodon-web.service"
];
# Make sure the gateway network is created before the app.
systemd.services."podman-mastodon-web".before = [
"podman-mastodon-streaming.service"
"podman-mastodon-sidekiq.service"
];
virtualisation.oci-containers.containers.mastodon-web = {
# Run migrations first.
cmd = pkgs.lib.mkForce [
"bash"
"-ex"
"-c"
''
bundle exec rake db:migrate
bundle exec rails server --port=3000
''
];
# Open the port directly.
ports = [
"3000:3000/tcp"
];
};
virtualisation.oci-containers.containers.mastodon-streaming = {
# Open the port directly.
ports = [
"4000:4000/tcp"
];
};
# Set up the database, allowing any container to log in.
services.postgresql = {
enable = true;
enableTCPIP = true;
ensureDatabases = [ "mastodon" ];
ensureUsers = [
{
name = "mastodon";
ensureDBOwnership = true;
ensureClauses = {
login = true;
};
}
];
authentication = ''
#type database user origin-address auth-method
host sameuser all 10.88.0.0/14 trust # services running via Podman, over TCP
'';
};
# Allow containers to access the databases.
networking.firewall.interfaces."podman1".allowedTCPPorts = [
5432 # postgresql
];
networking.firewall.interfaces."podman2".allowedTCPPorts = [
5432 # postgresql
];
};
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
machine.wait_for_open_port(3000)
machine.wait_for_open_port(4000)
'';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment