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

# Install Umami Analytics on a VPS

> Self-host Umami Analytics on an Arct Cloud Linux VPS with Docker, PostgreSQL, HTTPS, protected secrets, backups, and safe updates.

Arct Cloud provides an unmanaged Linux VPS. Umami is not preinstalled or managed by Arct Cloud; you are responsible for installation, privacy configuration, database operations, security, backups, and updates.

This guide installs the current Umami release on Ubuntu 24.04 using Umami's official Docker Compose architecture: the Umami application plus PostgreSQL.

## Requirements

| Basis                             | CPU and memory                                      | Software and storage                                                                                                                        |
| --------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Umami requirements**            | Umami does not publish a numeric CPU or RAM minimum | For source installs: Node.js 18.18 or newer and PostgreSQL 12.14 or newer; the official Compose stack bundles the runtime and PostgreSQL 15 |
| **Practical Arct recommendation** | 2 vCPUs and 4 GB RAM (`cvm.micro`)                  | 40 GB NVMe for a small site or low-traffic portfolio; use more storage and memory as traffic and retention grow                             |

Analytics volume, retention, and concurrent dashboard queries determine real resource use. Monitor PostgreSQL growth rather than treating the starting plan as a fixed production limit.

## Install Umami

<Steps>
  <Step title="Deploy Ubuntu 24.04">
    [Deploy a server](/compute/virtual-machines/deploy), select a plan that meets your traffic and retention needs, and choose Ubuntu 24.04.
  </Step>

  <Step title="Connect over SSH">
    Find the server IP address in the Arct Cloud console, then connect:

    ```bash theme={null}
    ssh ubuntu@YOUR_SERVER_IP
    ```

    See [Connect via SSH](/compute/virtual-machines/connect-ssh) for help with keys or usernames.
  </Step>

  <Step title="Point a domain to the server">
    Create an `A` record such as `analytics.example.com` pointing to the VPS's public IPv4 address. Add an `AAAA` record only if IPv6 is configured. Confirm the record resolves before requesting a certificate:

    ```bash theme={null}
    getent ahostsv4 analytics.example.com
    ```
  </Step>

  <Step title="Install Docker Engine">
    Install Docker Engine and the Compose plugin from Docker's official Ubuntu repository:

    ```bash theme={null}
    sudo apt update
    sudo apt install -y ca-certificates curl openssl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
      -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc

    sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
    Types: deb
    URIs: https://download.docker.com/linux/ubuntu
    Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
    Components: stable
    Architectures: $(dpkg --print-architecture)
    Signed-By: /etc/apt/keyrings/docker.asc
    EOF

    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io \
      docker-buildx-plugin docker-compose-plugin
    sudo systemctl enable --now docker
    sudo docker run --rm hello-world
    ```
  </Step>

  <Step title="Generate the application secrets">
    Create the project directory and generate a unique database password and `APP_SECRET`. Hexadecimal values avoid connection-string escaping problems.

    ```bash theme={null}
    sudo install -d -m 750 -o "$USER" -g "$USER" /opt/umami
    cd /opt/umami
    umask 077
    printf 'POSTGRES_PASSWORD=%s\nAPP_SECRET=%s\n' \
      "$(openssl rand -hex 24)" \
      "$(openssl rand -hex 32)" > .env
    chmod 600 .env
    ```

    <Warning>Do not commit `.env`, paste it into support tickets, or regenerate it during routine updates. Preserve it with your encrypted backups.</Warning>
  </Step>

  <Step title="Create the official-style Compose stack">
    The configuration follows Umami's current upstream Compose file, with secrets moved to `.env` and application port `3000` restricted to loopback. PostgreSQL has no published host port.

    ```bash theme={null}
    cd /opt/umami

    tee compose.yaml >/dev/null <<'EOF'
    services:
      umami:
        image: ghcr.io/umami-software/umami:latest
        restart: always
        init: true
        ports:
          - "127.0.0.1:3000:3000"
        environment:
          DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami
          APP_SECRET: ${APP_SECRET}
        depends_on:
          db:
            condition: service_healthy
        healthcheck:
          test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
          interval: 5s
          timeout: 5s
          retries: 5

      db:
        image: postgres:15-alpine
        restart: always
        environment:
          POSTGRES_DB: umami
          POSTGRES_USER: umami
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
        volumes:
          - umami-db-data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
          interval: 5s
          timeout: 5s
          retries: 5

    volumes:
      umami-db-data:
    EOF

    sudo docker compose config --quiet
    ```
  </Step>

  <Step title="Start Umami and secure the default account privately">
    Start Umami while port `3000` is still reachable only on server loopback:

    ```bash theme={null}
    cd /opt/umami
    sudo docker compose up -d
    ```

    From a **second terminal on your local computer**, open an SSH tunnel and leave it running:

    ```bash theme={null}
    ssh -N -L 3000:127.0.0.1:3000 ubuntu@YOUR_SERVER_IP
    ```

    Open `http://localhost:3000` and sign in with Umami's initial credentials:

    | Username | Password |
    | -------- | -------- |
    | `admin`  | `umami`  |

    Change the default password immediately, then create a separate account for routine use if your team needs shared access. Never embed an administrator credential in a website's tracking code. Close the tunnel with `Ctrl+C` only after the default password has been changed.
  </Step>

  <Step title="Configure the public reverse proxy and HTTPS">
    Replace `analytics.example.com` with your domain:

    ```bash theme={null}
    sudo apt install -y nginx certbot python3-certbot-nginx

    sudo tee /etc/nginx/sites-available/umami >/dev/null <<'EOF'
    server {
        listen 80;
        listen [::]:80;
        server_name analytics.example.com;

        location / {
            proxy_pass http://127.0.0.1:3000;
            proxy_http_version 1.1;
            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 $scheme;
        }
    }
    EOF

    sudo ln -s /etc/nginx/sites-available/umami \
      /etc/nginx/sites-enabled/umami
    sudo nginx -t
    sudo systemctl reload nginx
    sudo certbot --nginx -d analytics.example.com
    ```

    If UFW is enabled, allow SSH and the reverse proxy:

    ```bash theme={null}
    sudo ufw allow OpenSSH
    sudo ufw allow 'Nginx Full'
    sudo ufw enable
    ```
  </Step>

  <Step title="Verify the deployment">
    Confirm both containers are healthy and the local and public endpoints respond:

    ```bash theme={null}
    cd /opt/umami
    sudo docker compose ps
    curl -fsS http://127.0.0.1:3000/api/heartbeat
    curl -I https://analytics.example.com
    sudo docker compose logs --tail 50 umami db
    ```

    Add a website in Umami, copy its tracking snippet into that site's `<head>`, visit the site, and confirm that a pageview appears in the realtime view.
  </Step>
</Steps>

## Persistent Data, Secrets, and Ports

The `umami-db-data` Docker volume contains all analytics and account data. `/opt/umami/.env` contains the PostgreSQL password and `APP_SECRET`; changing `APP_SECRET` invalidates existing authentication tokens. Keep both the database backup and `.env` encrypted and stored outside the VPS.

Only ports `80` and `443` need inbound public access. Port `3000` is loopback-only, and PostgreSQL port `5432` is not published at all. Do not add a public `5432` mapping. Website tracking requests arrive through the same HTTPS endpoint as the dashboard.

<Note>Self-hosting gives you control of the analytics database, but you remain responsible for your privacy notice, retention policy, access controls, and any laws that apply to your visitors.</Note>

## Back Up and Restore

Create a consistent [logical PostgreSQL backup](https://www.postgresql.org/docs/current/app-pgdump.html) and preserve the matching secrets:

```bash theme={null}
cd /opt/umami
install -d -m 700 backups
umami_backup_stamp=$(date +%F-%H%M%S)
sudo docker compose exec -T db \
  pg_dump -U umami -d umami -Fc \
  > "backups/umami-${umami_backup_stamp}.dump"
cp -p .env "backups/secrets-${umami_backup_stamp}.env"
sudo docker image inspect ghcr.io/umami-software/umami:latest \
  --format '{{index .RepoDigests 0}}' \
  > "backups/app-image-${umami_backup_stamp}.txt"
sudo docker compose exec -T db postgres --version \
  > "backups/postgres-version-${umami_backup_stamp}.txt"
chmod 600 backups/*
```

Copy the dump, `.env`, application digest, and PostgreSQL version record to encrypted storage outside the VPS. Test restoration before relying on the backup.

<Warning>Never test a restore over the live database. Use a replacement VPS or an isolated Compose project with a new, empty PostgreSQL volume.</Warning>

On the replacement stack, restore the saved `.env`, set the Umami image to the recorded immutable digest, and use the same PostgreSQL major version. Start only the empty database, then restore the dump into it:

```bash theme={null}
cd /opt/umami
sudo docker compose up -d --wait db
sudo docker compose exec -T db \
  pg_restore -U umami -d umami --exit-on-error \
  < /path/to/umami-backup.dump
sudo docker compose up -d --wait umami
sudo docker compose ps
curl -fsS http://127.0.0.1:3000/api/heartbeat
sudo docker compose logs --tail 100 umami
```

Verify login, historical reports, and new realtime events through an SSH tunnel to the replacement VPS. Switch DNS or the reverse-proxy upstream only after those checks pass. Keep the original stack intact until the replacement is serving traffic correctly; update it only after the restore has been proven.

## Update Safely

Umami's official Compose file follows its current `latest` application channel. Treat each pull as a deployment:

1. Read the [Umami releases](https://github.com/umami-software/umami/releases) and migration notes.

2. Create a PostgreSQL dump and copy `.env` off the server.

3. Record the current application image digest:

   ```bash theme={null}
   sudo docker image inspect ghcr.io/umami-software/umami:latest \
     --format '{{index .RepoDigests 0}}'
   ```

4. Pull and recreate the containers:

   ```bash theme={null}
   cd /opt/umami
   sudo docker compose pull
   sudo docker compose up -d
   sudo docker compose ps
   sudo docker compose logs --tail 100 umami db
   ```

5. After a major Umami upgrade, refresh PostgreSQL planner statistics as recommended upstream:

   ```bash theme={null}
   sudo docker compose exec -T db \
     psql -U umami -d umami -c 'ANALYZE;'
   ```

6. Verify login, realtime collection, and historical reports before pruning old images.

Application startup can migrate the database. Do not run an older Umami image against a database already migrated by a newer release. For rollback, build a replacement stack with the previous image digest, the same PostgreSQL major version, the saved `.env`, and a new empty volume. Restore the pre-update dump, verify it through an SSH tunnel, then switch traffic; keep the migrated stack available until rollback is confirmed.

## Troubleshooting

<AccordionGroup>
  <Accordion title="The Umami container is unhealthy">
    Run `sudo docker compose -f /opt/umami/compose.yaml ps` and inspect `sudo docker compose -f /opt/umami/compose.yaml logs --tail 100 umami db`. The database must become healthy before Umami starts; also check disk space with `df -h`.
  </Accordion>

  <Accordion title="Umami cannot authenticate to PostgreSQL">
    Confirm `POSTGRES_PASSWORD` exists in `/opt/umami/.env` and that `sudo docker compose config` resolves the same value for the app and database. Changing the Compose environment does not change the password inside an already initialized PostgreSQL volume; update the database role deliberately or restore the original `.env`.
  </Accordion>

  <Accordion title="The dashboard loads but no pageviews appear">
    Confirm the snippet uses this Umami domain and the correct website ID. In the browser network panel, look for blocked script or collection requests; content blockers commonly block analytics. Test without a blocker and verify the public URL uses a valid HTTPS certificate.
  </Accordion>

  <Accordion title="Nginx returns 502 Bad Gateway">
    Confirm `curl -fsS http://127.0.0.1:3000/api/heartbeat` works, then run `sudo nginx -t`. If the app is still starting migrations, follow its logs instead of repeatedly restarting it.
  </Accordion>
</AccordionGroup>

## Official Resources

<CardGroup cols={3}>
  <Card title="Umami Documentation" icon="book-open" href="https://docs.umami.is/docs/install">
    Official installation, configuration, and update guidance.
  </Card>

  <Card title="Umami on GitHub" icon="github" href="https://github.com/umami-software/umami">
    Source code, Compose file, security policy, and issue tracker.
  </Card>

  <Card title="Umami Releases" icon="tag" href="https://github.com/umami-software/umami/releases">
    Current releases, changes, and upgrade context.
  </Card>
</CardGroup>

<Note>Umami is developed by Umami Software and its contributors. Arct Cloud is an independent infrastructure provider and is not affiliated with, sponsored by, or endorsed by Umami Software.</Note>
