> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy Mattermost Team Edition on Upsun

> Learn how to deploy a self-hosted Mattermost Team Edition server on Upsun, with PostgreSQL 16 and OpenSearch 2 configured automatically through a single infrastructure-as-code file.


export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["jared-wright"], date: "2026-02-27T07:00:00.000Z", image: "/images/posts/tutorials/deploy-mattermost-on-upsun/deploy-mattermost-on-upsun.webp" }} />

[Mattermost Team Edition](https://mattermost.com/) is the open-source, self-hosted version of Mattermost's team messaging platform. If you want full control over your team's communication data without relying on a third-party SaaS service, it's a solid choice. On Upsun, the infrastructure is defined entirely in a single YAML file, and the deployment lifecycle is automated through build, deploy, and post-deploy hooks.

This article walks through the template that deploys **Mattermost Team Edition 11.4.0** on Upsun, backed by **PostgreSQL 16** for storage and **OpenSearch 2** for full-text message search.

<Warning>
  The template used in this article is a community-maintained deployment template, not an official Upsun-supported project.
</Warning>

## What you'll deploy

The template sets up the following stack:

| Component               | Version       |
| ----------------------- | ------------- |
| Mattermost Team Edition | 11.4.0        |
| Runtime                 | Go 1.24       |
| Database                | PostgreSQL 16 |
| Search                  | OpenSearch 2  |

## Prerequisites

* An [Upsun account](https://auth.upsun.com/register)
* The [Upsun CLI](https://docs.upsun.com/administration/cli.html) installed and authenticated
* Git installed locally

## Clone the template

```bash {filename="Terminal"} theme={null}
git clone https://github.com/jww-sh/mattermost.git my-mattermost
cd my-mattermost
```

The repository already contains all the configuration you need. No application code is written by you — the build hook downloads the Mattermost binary directly.

## How the configuration works

### The infrastructure file

Everything Upsun needs to provision your environment lives in `.upsun/config.yaml`:

```yaml {filename=".upsun/config.yaml"} theme={null}
applications:
    app:
        type: 'golang:1.24'
        hooks:
            build: !include
                type: string
                path: ../build.sh
            deploy: !include
                type: string
                path: ../deploy.sh
            post_deploy: !include
                type: string
                path: ../postdeploy.sh
        relationships:
            database: { service: db, endpoint: postgresql }
            essearch: { service: searchelastic, endpoint: opensearch }
        web:
            upstream:
                socket_family: tcp
                protocol: http
            commands:
                start: ./bin/mattermost
            locations:
                '/':
                    root: 'client'
                    index:
                        - "root.html"
                    allow: true
                    rules:
                        \.(css|js|gif|jpe?g|png|ttf|eot|woff2?|otf|html|ico|svg|map|json|woff?)$:
                            allow: true
                        ^/robots\.txt$:
                            allow: true
        mounts:
            '/.config': { source: instance, source_path: config-hidden }
            'config': { source: instance, source_path: config-files }
            'logs': { source: instance, source_path: log-files }
            'data': { source: instance, source_path: data-files }
            'dist/files': { source: instance, source_path: dist-files }
            'plugins': { source: instance, source_path: plugins }
            'client/plugins': { source: instance, source_path: client_plugins }
services:
    db:
        type: postgresql:16
    searchelastic:
        type: opensearch:2
routes:
    "https://www.{default}/":
        type: upstream
        upstream: "app:http"
        cache:
            enabled: false
    "https://{default}/":
        type: redirect
        to: "https://www.{default}/"
```

A few things worth noting:

* **Seven persistent mounts** keep Mattermost's config, logs, data, and plugin directories intact across deployments. These use `instance` storage, so they're local to each container rather than shared.
* **Routes** send all traffic to `www.{default}` and redirect the bare domain to it. Caching is disabled because Mattermost serves dynamic content.
* The `start` command runs the Mattermost binary directly — there's no additional web server in front of it.

### The build hook

The `build.sh` script runs during the build phase, before the container is deployed:

```bash {filename="build.sh"} theme={null}
#!/usr/bin/env bash

export MATTERMOST_VERSION=$(cat mattermost_version)

download_mattermost() {
    local tarball="mattermost-team-${MATTERMOST_VERSION}-linux-amd64.tar.gz"
    local cache_file="${PLATFORM_CACHE_DIR}/${tarball}"

    if [ -f "$cache_file" ]; then
        printf "\n  ✔ \033[1mUsing cached Mattermost archive\033[0m ($MATTERMOST_VERSION)\n\n"
    else
        printf "\n  ✔ \033[1mDownloading Mattermost...\033[0m ($MATTERMOST_VERSION)\n\n"
        wget --quiet -c "https://releases.mattermost.com/${MATTERMOST_VERSION}/${tarball}" -O "$cache_file"
    fi

    tar -xzf "$cache_file"
    cp -a mattermost/* .
    chmod +x bin/mattermost
}

set_config() {
    cp config/config.json config.default
}

set -e
download_mattermost
set_config
```

The version is read from the `mattermost_version` file (currently `11.4.0`). The tarball is cached in `PLATFORM_CACHE_DIR` so subsequent builds skip the download if the version hasn't changed. After extraction, a copy of the default `config.json` is saved as `config.default` for the deploy hook to restore if needed.

### The deploy hook

`deploy.sh` runs after each build before the app starts:

```bash {filename="deploy.sh"} theme={null}
#!/usr/bin/env bash

set_config() {
    if [ ! -f config/config.json ] || [ ! -s config/config.json ]; then
        cp config.default config/config.json
    fi
}

set_local(){
    mkdir -p .config/local/
    if [ -e ".config/local/mattermost_local.socket" ] && [ ! -S ".config/local/mattermost_local.socket" ]; then
        rm -f .config/local/mattermost_local.socket
    fi
}

set_config
set_local
```

It restores `config.json` from the backup if the file is missing or empty, and ensures the local socket directory exists. Any stale non-socket file at the socket path is removed to prevent Mattermost from failing to bind on startup.

### The post-deploy hook

`postdeploy.sh` runs once after the app has started, but only on the **first deployment**:

```bash {filename="postdeploy.sh"} theme={null}
#!/usr/bin/env bash

wait_for_socket() {
    local socket=".config/local/mattermost_local.socket"
    local max_wait=30
    local waited=0
    while [ $waited -lt $max_wait ]; do
        [ -S "$socket" ] && return 0
        sleep 1
        waited=$((waited + 1))
    done
    echo "Warning: Mattermost socket not ready after ${max_wait}s" >&2
}

first_deploy() {
    wait_for_socket

    local admin_password
    admin_password="$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 20)!9Aa"

    ./bin/mmctl user create --local --username "$PSH_INITADMIN_USERNAME" --email "$PSH_INITADMIN_EMAIL" --password "$admin_password"
    printf "%s" "$admin_password" > .config/admin_credentials

    ./bin/mmctl team create --local --name $PSH_FIRSTTEAM_NAME --display-name $PSH_FIRSTTEAM_DISPLAYNAME --private
    ./bin/mmctl team users add --local $PSH_FIRSTTEAM_NAME $PSH_INITADMIN_USERNAME

    ./bin/mmctl channel create --local --team $PSH_FIRSTTEAM_NAME --name $PSH_FIRSTCHANNEL_NAME --display-name $PSH_FIRSTCHANNEL_DISPLAYNAME
    ./bin/mmctl channel users add --local $PSH_FIRSTTEAM_NAME:$PSH_FIRSTCHANNEL_NAME $PSH_INITADMIN_USERNAME

    ./bin/mmctl auth login "$MM_SERVICESETTINGS_SITEURL" --name deploy-session --username "$PSH_INITADMIN_USERNAME" --password-file .config/admin_credentials
    ./bin/mmctl post create $PSH_FIRSTTEAM_NAME:$PSH_FIRSTCHANNEL_NAME --message "$PSH_WELCOME_MESSAGE"
    ./bin/mmctl post create $PSH_FIRSTTEAM_NAME:$PSH_FIRSTCHANNEL_NAME --message "$PSH_WARNING_MESSAGE1"
    ./bin/mmctl post create $PSH_FIRSTTEAM_NAME:$PSH_FIRSTCHANNEL_NAME --message "$PSH_WARNING_MESSAGE2"
    ./bin/mmctl auth clean

    touch .config/upsun.installed
}

if [ ! -f .config/upsun.installed ]; then
    first_deploy
fi
```

The script uses `mmctl` (the Mattermost CLI bundled with the binary) in local socket mode to:

1. Wait up to 30 seconds for the Unix socket to be ready
2. Generate a cryptographically random admin password (20 alphanumeric characters plus `!9Aa` to satisfy complexity rules)
3. Create the admin user (`admin` / `admin@example.com`) and save the password to `.config/admin_credentials`
4. Create a private team (`team-admin`) and a channel (`setup`)
5. Post welcome and warning messages into the channel
6. Write a `.config/upsun.installed` marker so this block never runs again

### Environment variables

The `.environment` file is sourced on every container start and runtime event. It translates Upsun's platform variables into Mattermost's `MM_*` configuration format:

```bash {filename=".environment"} theme={null}
# Basics.
export MM_SERVICESETTINGS_LISTENADDRESS=":$PORT"
export MM_SERVICESETTINGS_SITEURL=$(echo "$PLATFORM_ROUTES" | base64 --decode | jq -r 'to_entries[] | select(.value.primary) | .key')
export MM_SERVICESETTINGS_ALLOWCORSFROM=$(echo "$PLATFORM_ROUTES" | base64 --decode | jq -r 'to_entries[] | select(.value.primary) | .key')
# Customization.
export MM_TEAMSETTINGS_ENABLECUSTOMBRAND=true
# PostgreSQL.
export DATABASE_HOST=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].host")
export DATABASE_PORT=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].port")
export DATABASE_NAME=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].path")
export DATABASE_USER=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].username")
export DATABASE_PASSWORD=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].password")
export MM_SQLSETTINGS_DRIVERNAME="postgres"
export MM_SQLSETTINGS_DATASOURCE="postgres://$DATABASE_USER:$DATABASE_PASSWORD@$DATABASE_HOST:$DATABASE_PORT/$DATABASE_NAME?sslmode=disable&connect_timeout=10"
# Elasticsearch.
export ELASTIC_HOST=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".essearch[0].host")
export ELASTIC_PORT=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".essearch[0].port")
export MM_ELASTICSEARCHSETTINGS_CONNECTIONURL="http://$ELASTIC_HOST:$ELASTIC_PORT"
export MM_ELASTICSEARCHSETTINGS_USERNAME=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".essearch[0].username")
export MM_ELASTICSEARCHSETTINGS_PASSWORD=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".essearch[0].password")
# Logging.
export MM_LOGSETTINGS_ENABLEFILE=true
export MM_LOGSETTINGS_FILELOCATION="./logs/"
# Notifications.
export MM_NOTIFICATIONLOGSETTINGS_ENABLEFILE=false
export MM_NOTIFICATIONLOGSETTINGS_FILELOCATION="./logs/"
# Plugins.
export MM_PLUGINSETTINGS_ENABLEUPLOADS=true
# Email.
export MM_EMAILSETTINGS_SMTPSERVER=$PLATFORM_SMTP_HOST
export MM_EMAILSETTINGS_SMTPPORT=25

# Local mode, for setting up the first user on first deploy.
export MM_SERVICESETTINGS_ENABLELOCALMODE=true
export MM_SERVICESETTINGS_LOCALMODESOCKETLOCATION="/app/.config/local/mattermost_local.socket"
export MMCTL_LOCAL_SOCKET_PATH="/app/.config/local/mattermost_local.socket"
# First deploy details.
export PSH_INITADMIN_USERNAME=admin
export PSH_INITADMIN_EMAIL=admin@example.com
export PSH_FIRSTTEAM_NAME=team-admin
export PSH_FIRSTTEAM_DISPLAYNAME=team-admin
export PSH_FIRSTCHANNEL_NAME=setup
export PSH_FIRSTCHANNEL_DISPLAYNAME=Setup
export PSH_WELCOME_MESSAGE="Congrats @admin! You have successfully deployed your own Mattermost server on Upsun!"
export PSH_WARNING_MESSAGE1="**WARNING:** A randomly generated password was created for this System Admin account. Retrieve it by running: \`cat /app/.config/admin_credentials\` via SSH"
export PSH_WARNING_MESSAGE2="**WARNING:** Once logged in, go to **Account Settings** in the top right of the toolbar and update your credentials immediately."
```

`PLATFORM_RELATIONSHIPS` and `PLATFORM_ROUTES` are base64-encoded JSON blobs that Upsun injects at runtime. The `.environment` file decodes them with `jq` to extract service hostnames, ports, and credentials without you needing to manage secrets manually. The `PSH_*` variables at the bottom define the default admin account, first team, first channel, and the welcome messages posted on first deploy.

## Deploy to Upsun

Create a new Upsun project from the repository root:

```bash {filename="Terminal"} theme={null}
upsun project:create
```

Follow the CLI prompts to name your project and select a region. Then push to deploy:

```bash {filename="Terminal"} theme={null}
upsun push
```

The first deployment takes a few minutes. The build hook downloads the Mattermost binary (\~200 MB), and the post-deploy hook initializes the admin account after the app starts.

## Retrieve your admin credentials

After the first deployment completes, SSH into the app container and read the credentials file:

```bash {filename="Terminal"} theme={null}
upsun ssh -- cat /app/.config/admin_credentials
```

This prints the randomly generated password for the `admin` account. Open your site URL to log in:

```bash {filename="Terminal"} theme={null}
upsun environment:url --primary
```

<Warning>
  Change your admin password immediately after first login. Go to **Account Settings** in the top-right toolbar and update your credentials.
</Warning>

## Updating Mattermost Team Edition

To upgrade to a newer version, update the version number in `mattermost_version`, commit the change, and push:

```bash {filename="Terminal"} theme={null}
echo "11.5.0" > mattermost_version
git add mattermost_version
git commit -m "Upgrade Mattermost to 11.5.0"
upsun push
```

The build hook will download the new tarball (or use a cached copy if it already downloaded it), and Upsun's zero-downtime deployment will replace the running instance.

## Plugins

The `plugins` and `client/plugins` mounts persist across deployments, so any plugins you install through the Mattermost marketplace or by uploading a plugin bundle survive redeployments. Plugin uploads are enabled by default via `MM_PLUGINSETTINGS_ENABLEUPLOADS=true` in `.environment`.

## Conclusion

This template gives you a fully functional Mattermost Team Edition instance on Upsun with no manual infrastructure setup. PostgreSQL and OpenSearch are provisioned automatically, the admin account is created on first deploy, and all configuration is driven by environment variables — so there are no secrets in your repository.

Get started with a free Upsun account at [upsun.com](https://upsun.com).
