diff --git a/lib/mix/tasks/deactivate_user.ex b/lib/mix/tasks/deactivate_user.ex
deleted file mode 100644
index 96b3db6e4..000000000
--- a/lib/mix/tasks/deactivate_user.ex
+++ /dev/null
@@ -1,13 +0,0 @@
-defmodule Mix.Tasks.DeactivateUser do
-  use Mix.Task
-  alias Pleroma.User
-
-  @shortdoc "Toggle deactivation status for a user"
-  def run([nickname]) do
-    Mix.Task.run("app.start")
-
-    with user <- User.get_by_nickname(nickname) do
-      User.deactivate(user)
-    end
-  end
-end
diff --git a/lib/mix/tasks/fix_ap_users.ex b/lib/mix/tasks/fix_ap_users.ex
deleted file mode 100644
index 7e970850e..000000000
--- a/lib/mix/tasks/fix_ap_users.ex
+++ /dev/null
@@ -1,28 +0,0 @@
-defmodule Mix.Tasks.FixApUsers do
-  use Mix.Task
-  import Ecto.Query
-  alias Pleroma.{Repo, User}
-
-  @shortdoc "Grab all ap users again"
-  def run([]) do
-    Mix.Task.run("app.start")
-
-    q =
-      from(
-        u in User,
-        where: fragment("? @> ?", u.info, ^%{"ap_enabled" => true}),
-        where: u.local == false
-      )
-
-    users = Repo.all(q)
-
-    Enum.each(users, fn user ->
-      try do
-        IO.puts("Fetching #{user.nickname}")
-        Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(user.ap_id, false)
-      rescue
-        e -> IO.inspect(e)
-      end
-    end)
-  end
-end
diff --git a/lib/mix/tasks/generate_config.ex b/lib/mix/tasks/generate_config.ex
deleted file mode 100644
index 70a110561..000000000
--- a/lib/mix/tasks/generate_config.ex
+++ /dev/null
@@ -1,39 +0,0 @@
-defmodule Mix.Tasks.GenerateConfig do
-  use Mix.Task
-
-  @shortdoc "Generates a new config"
-  def run(_) do
-    IO.puts("Answer a few questions to generate a new config\n")
-    IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n")
-    domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim()
-    name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim()
-    email = IO.gets("What's your admin email address: ") |> String.trim()
-
-    secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
-    dbpass = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
-
-    resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", dbpass: dbpass)
-
-    result =
-      EEx.eval_file(
-        "lib/mix/tasks/sample_config.eex",
-        domain: domain,
-        email: email,
-        name: name,
-        secret: secret,
-        dbpass: dbpass
-      )
-
-    IO.puts(
-      "\nWriting config to config/generated_config.exs.\n\nCheck it and configure your database, then copy it to either config/dev.secret.exs or config/prod.secret.exs"
-    )
-
-    File.write("config/generated_config.exs", result)
-
-    IO.puts(
-      "\nWriting setup_db.psql, please run it as postgre superuser, i.e.: sudo su postgres -c 'psql -f config/setup_db.psql'"
-    )
-
-    File.write("config/setup_db.psql", resultSql)
-  end
-end
diff --git a/lib/mix/tasks/generate_password_reset.ex b/lib/mix/tasks/generate_password_reset.ex
deleted file mode 100644
index 6bf640150..000000000
--- a/lib/mix/tasks/generate_password_reset.ex
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Mix.Tasks.GeneratePasswordReset do
-  use Mix.Task
-  alias Pleroma.User
-
-  @shortdoc "Generate password reset link for user"
-  def run([nickname]) do
-    Mix.Task.run("app.start")
-
-    with %User{local: true} = user <- User.get_by_nickname(nickname),
-         {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
-      IO.puts("Generated password reset token for #{user.nickname}")
-
-      IO.puts(
-        "Url: #{
-          Pleroma.Web.Router.Helpers.util_url(
-            Pleroma.Web.Endpoint,
-            :show_password_reset,
-            token.token
-          )
-        }"
-      )
-    else
-      _ ->
-        IO.puts("No local user #{nickname}")
-    end
-  end
-end
diff --git a/lib/mix/tasks/make_moderator.ex b/lib/mix/tasks/make_moderator.ex
deleted file mode 100644
index a454a958e..000000000
--- a/lib/mix/tasks/make_moderator.ex
+++ /dev/null
@@ -1,30 +0,0 @@
-defmodule Mix.Tasks.SetModerator do
-  use Mix.Task
-  import Mix.Ecto
-  alias Pleroma.{Repo, User}
-
-  @shortdoc "Set moderator status"
-  def run([nickname | rest]) do
-    Application.ensure_all_started(:pleroma)
-
-    moderator =
-      case rest do
-        [moderator] -> moderator == "true"
-        _ -> true
-      end
-
-    with %User{local: true} = user <- User.get_by_nickname(nickname) do
-      info =
-        user.info
-        |> Map.put("is_moderator", !!moderator)
-
-      cng = User.info_changeset(user, %{info: info})
-      {:ok, user} = User.update_and_set_cache(cng)
-
-      IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}")
-    else
-      _ ->
-        IO.puts("No local user #{nickname}")
-    end
-  end
-end
diff --git a/lib/mix/tasks/pleroma/gen_instance.ex b/lib/mix/tasks/pleroma/gen_instance.ex
new file mode 100644
index 000000000..94f2220b1
--- /dev/null
+++ b/lib/mix/tasks/pleroma/gen_instance.ex
@@ -0,0 +1,161 @@
+defmodule Mix.Tasks.Pleroma.Gen.Instance do
+  use Mix.Task
+
+  @shortdoc "Generates the configuration for a new instance"
+  @moduledoc """
+  Generates the configuration for a new instance.
+
+  If any options are left unspecified, you will be prompted interactively. This
+  means the simplest invocation would be
+
+      mix pleroma.gen.instance
+
+  ## Options
+
+  - `-f`, `--force` - overwrite any output files
+  - `-o PATH`, `--output PATH` - the output file for the generated configuration
+  - `--output-psql PATH` - the output file for the generated PostgreSQL setup
+  - `--domain DOMAIN` - the domain of your instance
+  - `--instance-name INSTANCE_NAME` - the name of your instance
+  - `--admin-email ADMIN_EMAIL` - the email address of the instance admin
+  - `--dbhost HOSTNAME` - the hostname of the PostgreSQL database to use
+  - `--dbname DBNAME` - the name of the database to use
+  - `--dbuser DBUSER` - the user (aka role) to use for the database connection
+  - `--dbpass DBPASS` - the password to use for the database connection
+  """
+
+  def run(rest) do
+    {options, [], []} =
+      OptionParser.parse(
+        rest,
+        strict: [
+          force: :boolean,
+          output: :string,
+          output_psql: :string,
+          domain: :string,
+          instance_name: :string,
+          admin_email: :string,
+          dbhost: :string,
+          dbname: :string,
+          dbuser: :string,
+          dbpass: :string
+        ],
+        aliases: [
+          o: :output,
+          f: :force
+        ]
+      )
+
+    paths =
+      [config_path, psql_path] = [
+        Keyword.get(options, :output, "config/generated_config.exs"),
+        Keyword.get(options, :output_psql, "config/setup_db.psql")
+      ]
+
+    will_overwrite = Enum.filter(paths, &File.exists?/1)
+    proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false)
+
+    unless not proceed? do
+      domain =
+        Keyword.get(options, :domain) ||
+          Mix.shell().prompt("What domain will your instance use? (e.g. pleroma.soykaf.com)")
+          |> String.trim()
+
+      name =
+        Keyword.get(options, :name) ||
+          Mix.shell().prompt("What is the name of your instance? (e.g. Pleroma/Soykaf)")
+          |> String.trim()
+
+      email =
+        Keyword.get(options, :admin_email) ||
+          Mix.shell().prompt("What is your admin email address?")
+          |> String.trim()
+
+      dbhost =
+        Keyword.get(options, :dbhost) ||
+          case Mix.shell().prompt("What is the hostname of your database? [localhost]") do
+            "\n" -> "localhost"
+            dbhost -> dbhost |> String.trim()
+          end
+
+      dbname =
+        Keyword.get(options, :dbname) ||
+          case Mix.shell().prompt("What is the name of your database? [pleroma_dev]") do
+            "\n" -> "pleroma_dev"
+            dbname -> dbname |> String.trim()
+          end
+
+      dbuser =
+        Keyword.get(options, :dbuser) ||
+          case Mix.shell().prompt("What is the user used to connect to your database? [pleroma]") do
+            "\n" -> "pleroma"
+            dbuser -> dbuser |> String.trim()
+          end
+
+      dbpass =
+        Keyword.get(options, :dbpass) ||
+          case Mix.shell().prompt(
+                 "What is the password used to connect to your database? [autogenerated]"
+               ) do
+            "\n" -> :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
+            dbpass -> dbpass |> String.trim()
+          end
+
+      secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
+
+      result_config =
+        EEx.eval_file(
+          "sample_config.eex" |> Path.expand(__DIR__),
+          domain: domain,
+          email: email,
+          name: name,
+          dbhost: dbhost,
+          dbname: dbname,
+          dbuser: dbuser,
+          dbpass: dbpass,
+          version: Pleroma.Mixfile.project() |> Keyword.get(:version),
+          secret: secret
+        )
+
+      result_psql =
+        EEx.eval_file(
+          "sample_psql.eex" |> Path.expand(__DIR__),
+          dbname: dbname,
+          dbuser: dbuser,
+          dbpass: dbpass
+        )
+
+      Mix.shell().info(
+        "Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs."
+      )
+
+      File.write(config_path, result_config)
+      Mix.shell().info("Writing #{psql_path}.")
+      File.write(psql_path, result_psql)
+
+      Mix.shell().info(
+        "\n" <>
+          """
+          To get started:
+          1. Verify the contents of the generated files.
+          2. Run `sudo -u postgres psql -f #{escape_sh_path(psql_path)}`.
+          """ <>
+          if config_path in ["config/dev.secret.exs", "config/prod.secret.exs"] do
+            ""
+          else
+            "3. Run `mv #{escape_sh_path(config_path)} 'config/prod.secret.exs'`."
+          end
+      )
+    else
+      Mix.shell().error(
+        "The task would have overwritten the following files:\n" <>
+          (Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>
+          "Rerun with `--force` to overwrite them."
+      )
+    end
+  end
+
+  defp escape_sh_path(path) do
+    ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
+  end
+end
diff --git a/lib/mix/tasks/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex
similarity index 68%
rename from lib/mix/tasks/sample_config.eex
rename to lib/mix/tasks/pleroma/sample_config.eex
index 6db36fa09..066939981 100644
--- a/lib/mix/tasks/sample_config.eex
+++ b/lib/mix/tasks/pleroma/sample_config.eex
@@ -1,3 +1,8 @@
+# Pleroma instance configuration
+
+# NOTE: This file should not be committed to a repo or otherwise made public
+# without removing sensitive information.
+
 use Mix.Config
 
 config :pleroma, Pleroma.Web.Endpoint,
@@ -16,11 +21,10 @@ config :pleroma, :media_proxy,
   redirect_on_failure: true
   #base_url: "https://cache.pleroma.social"
 
-# Configure your database
 config :pleroma, Pleroma.Repo,
   adapter: Ecto.Adapters.Postgres,
-  username: "pleroma",
+  username: "<%= dbuser %>",
   password: "<%= dbpass %>",
-  database: "pleroma_dev",
-  hostname: "localhost",
+  database: "<%= dbname %>",
+  hostname: "<%= dbhost %>",
   pool_size: 10
diff --git a/lib/mix/tasks/pleroma/sample_psql.eex b/lib/mix/tasks/pleroma/sample_psql.eex
new file mode 100644
index 000000000..66f76752f
--- /dev/null
+++ b/lib/mix/tasks/pleroma/sample_psql.eex
@@ -0,0 +1,9 @@
+CREATE USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
+-- in case someone runs this second time accidentally
+ALTER USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
+CREATE DATABASE <%= dbname %>;
+ALTER DATABASE <%= dbname %> OWNER TO <%= dbuser %>;
+\c <%= dbname %>;
+--Extensions made by ecto.migrate that need superuser access
+CREATE EXTENSION IF NOT EXISTS citext;
+CREATE EXTENSION IF NOT EXISTS pg_trgm;
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
new file mode 100644
index 000000000..c20fecaa1
--- /dev/null
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -0,0 +1,207 @@
+defmodule Mix.Tasks.Pleroma.User do
+  use Mix.Task
+  alias Pleroma.{Repo, User}
+
+  @shortdoc "Manages Pleroma users"
+  @moduledoc """
+  Manages Pleroma users.
+
+  ## Create a new user.
+
+      mix pleroma.user new NICKNAME EMAIL [OPTION...]
+
+  Options:
+  - `--name NAME` - the user's name (i.e., "Lain Iwakura")
+  - `--bio BIO` - the user's bio
+  - `--password PASSWORD` - the user's password
+  - `--moderator`/`--no-moderator` - whether the user is a moderator
+
+  ## Delete the user's account.
+
+      mix pleroma.user rm NICKNAME
+
+  ## Deactivate or activate the user's account.
+
+      mix pleroma.user toggle_activated NICKNAME
+
+  ## Create a password reset link.
+
+      mix pleroma.user reset_password NICKNAME
+
+  ## Set the value of the given user's settings.
+
+      mix pleroma.user set NICKNAME [OPTION...]
+
+  Options:
+  - `--locked`/`--no-locked` - whether the user's account is locked
+  - `--moderator`/`--no-moderator` - whether the user is a moderator
+  """
+
+  def run(["new", nickname, email | rest]) do
+    {options, [], []} =
+      OptionParser.parse(
+        rest,
+        strict: [
+          name: :string,
+          bio: :string,
+          password: :string,
+          moderator: :boolean
+        ]
+      )
+
+    name = Keyword.get(options, :name, nickname)
+    bio = Keyword.get(options, :bio, "")
+
+    {password, generated_password?} =
+      case Keyword.get(options, :password) do
+        nil ->
+          {:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
+
+        password ->
+          {password, false}
+      end
+
+    moderator? = Keyword.get(options, :moderator, false)
+
+    Mix.shell().info("""
+    A user will be created with the following information:
+      - nickname: #{nickname}
+      - email: #{email}
+      - password: #{
+      if(generated_password?, do: "[generated; a reset link will be created]", else: password)
+    }
+      - name: #{name}
+      - bio: #{bio}
+      - moderator: #{if(moderator?, do: "true", else: "false")}
+    """)
+
+    proceed? = Mix.shell().yes?("Continue?")
+
+    unless not proceed? do
+      Mix.Task.run("app.start")
+
+      params =
+        %{
+          nickname: nickname,
+          email: email,
+          password: password,
+          password_confirmation: password,
+          name: name,
+          bio: bio
+        }
+        |> IO.inspect()
+
+      user = User.register_changeset(%User{}, params)
+      Repo.insert!(user)
+
+      Mix.shell().info("User #{nickname} created")
+
+      if moderator? do
+        run(["set", nickname, "--moderator"])
+      end
+
+      if generated_password? do
+        run(["reset_password", nickname])
+      end
+    else
+      Mix.shell().info("User will not be created.")
+    end
+  end
+
+  def run(["rm", nickname]) do
+    Mix.Task.run("app.start")
+
+    with %User{local: true} = user <- User.get_by_nickname(nickname) do
+      User.delete(user)
+    end
+
+    Mix.shell().info("User #{nickname} deleted.")
+  end
+
+  def run(["toggle_activated", nickname]) do
+    Mix.Task.run("app.start")
+
+    with user <- User.get_by_nickname(nickname) do
+      User.deactivate(user)
+    end
+  end
+
+  def run(["reset_password", nickname]) do
+    Mix.Task.run("app.start")
+
+    with %User{local: true} = user <- User.get_by_nickname(nickname),
+         {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
+      Mix.shell().info("Generated password reset token for #{user.nickname}")
+
+      IO.puts(
+        "URL: #{
+          Pleroma.Web.Router.Helpers.util_url(
+            Pleroma.Web.Endpoint,
+            :show_password_reset,
+            token.token
+          )
+        }"
+      )
+    else
+      _ ->
+        Mix.shell().error("No local user #{nickname}")
+    end
+  end
+
+  def run(["set", nickname | rest]) do
+    {options, [], []} =
+      OptionParser.parse(
+        rest,
+        strict: [
+          moderator: :boolean,
+          locked: :boolean
+        ]
+      )
+
+    case Keyword.get(options, :moderator) do
+      nil -> nil
+      value -> set_moderator(nickname, value)
+    end
+
+    case Keyword.get(options, :locked) do
+      nil -> nil
+      value -> set_locked(nickname, value)
+    end
+  end
+
+  defp set_moderator(nickname, value) do
+    Application.ensure_all_started(:pleroma)
+
+    with %User{local: true} = user <- User.get_by_nickname(nickname) do
+      info =
+        user.info
+        |> Map.put("is_moderator", value)
+
+      cng = User.info_changeset(user, %{info: info})
+      {:ok, user} = User.update_and_set_cache(cng)
+
+      Mix.shell().info("Moderator status of #{nickname}: #{user.info["is_moderator"]}")
+    else
+      _ ->
+        Mix.shell().error("No local user #{nickname}")
+    end
+  end
+
+  defp set_locked(nickname, value) do
+    Mix.Ecto.ensure_started(Repo, [])
+
+    with %User{local: true} = user <- User.get_by_nickname(nickname) do
+      info =
+        user.info
+        |> Map.put("locked", value)
+
+      cng = User.info_changeset(user, %{info: info})
+      user = Repo.update!(cng)
+
+      IO.puts("Locked status of #{nickname}: #{user.info["locked"]}")
+    else
+      _ ->
+        IO.puts("No local user #{nickname}")
+    end
+  end
+end
diff --git a/lib/mix/tasks/register_user.ex b/lib/mix/tasks/register_user.ex
deleted file mode 100644
index e74721c49..000000000
--- a/lib/mix/tasks/register_user.ex
+++ /dev/null
@@ -1,22 +0,0 @@
-defmodule Mix.Tasks.RegisterUser do
-  use Mix.Task
-  alias Pleroma.{Repo, User}
-
-  @shortdoc "Register user"
-  def run([name, nickname, email, bio, password]) do
-    Mix.Task.run("app.start")
-
-    params = %{
-      name: name,
-      nickname: nickname,
-      email: email,
-      password: password,
-      password_confirmation: password,
-      bio: bio
-    }
-
-    user = User.register_changeset(%User{}, params)
-
-    Repo.insert!(user)
-  end
-end
diff --git a/lib/mix/tasks/rm_user.ex b/lib/mix/tasks/rm_user.ex
deleted file mode 100644
index 27521b745..000000000
--- a/lib/mix/tasks/rm_user.ex
+++ /dev/null
@@ -1,13 +0,0 @@
-defmodule Mix.Tasks.RmUser do
-  use Mix.Task
-  alias Pleroma.User
-
-  @shortdoc "Permanently delete a user"
-  def run([nickname]) do
-    Mix.Task.run("app.start")
-
-    with %User{local: true} = user <- User.get_by_nickname(nickname) do
-      User.delete(user)
-    end
-  end
-end
diff --git a/lib/mix/tasks/sample_psql.eex b/lib/mix/tasks/sample_psql.eex
deleted file mode 100644
index bc22f166c..000000000
--- a/lib/mix/tasks/sample_psql.eex
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
--- in case someone runs this second time accidentally
-ALTER USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
-CREATE DATABASE pleroma_dev;
-ALTER DATABASE pleroma_dev OWNER TO pleroma;
-\c pleroma_dev;
---Extensions made by ecto.migrate that need superuser access
-CREATE EXTENSION IF NOT EXISTS citext;
-CREATE EXTENSION IF NOT EXISTS pg_trgm;
diff --git a/lib/mix/tasks/set_locked.ex b/lib/mix/tasks/set_locked.ex
deleted file mode 100644
index 2b3b18b09..000000000
--- a/lib/mix/tasks/set_locked.ex
+++ /dev/null
@@ -1,30 +0,0 @@
-defmodule Mix.Tasks.SetLocked do
-  use Mix.Task
-  import Mix.Ecto
-  alias Pleroma.{Repo, User}
-
-  @shortdoc "Set locked status"
-  def run([nickname | rest]) do
-    ensure_started(Repo, [])
-
-    locked =
-      case rest do
-        [locked] -> locked == "true"
-        _ -> true
-      end
-
-    with %User{local: true} = user <- User.get_by_nickname(nickname) do
-      info =
-        user.info
-        |> Map.put("locked", !!locked)
-
-      cng = User.info_changeset(user, %{info: info})
-      user = Repo.update!(cng)
-
-      IO.puts("locked status of #{nickname}: #{user.info["locked"]}")
-    else
-      _ ->
-        IO.puts("No local user #{nickname}")
-    end
-  end
-end