AWS EC2にsupabaseをdockerでself-hostingする

# はじめに

`Firebase`の代替と謳われている`supabase`をよく使ってwebアプリを構築していますが、
Web版の無料枠では2つのプロジェクトまでしか作成することができないため、
AWSのEC2上に`self-hosting`しました
今回はシングル構成ですが、`docker swarm`などを利用することで冗長構成にすることも可能です

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/6705e4e9-5bae-8323-aa81-3c0b3b67321b.png" width="300" />

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/914f2f3c-48d2-d430-5531-588fa2c5a12f.png" width="103"/>



# 1. EC2の作成

1. AWS management consoleを開く
1. EC2のページを開く
1. 画面右上の「インスタンスを起動」ボタンをクリック
    以下設定項目を設定

    * 名前
        * 自由に設定
    * OSイメージ
        * AmazonLinux
    * インスタンスタイプ
        * 自由に設定
        * 無料枠の`t2.micro`でも動くかもしれませんが、あまりにもストレスが溜まったので`t3a.small`にしました。
    * キーペア
        * 新しいキーペアを生成
            * キーペア名：自由に設定
            * 他はそのままキーペア生成
    * セキュリティグループ
        * あとで設定するため、
        「既存のセキュリティグループを設定」 > 空のまま
    * ストレージ
        * 無料枠の限界30GiBまで拡張

1. 「インスタンスを起動」

# 2. セキュリティーグループを作成する。

1. 左側サイドメニューから、「ネットワーク & セキュリティ」 > 「セキュリティグループ」を選択する
1. 画面右上「セキュリティグループを作成」をクリック
    以下設定項目を設定

    * セキュリティグループ名
        * 自由に設定
    * 説明
        * 自由に設定
    * VPC
        * 先ほどEC2を構築したVPCを選択
    * インバウンドルール
    
    |タイプ|プロトコル|ポート範囲|ソース||説明|
    |----|----|----|----|----|----|
    |カスタムTCP|TCP|8000|Anywhere-IPv4|0.0.0.0/0|自由に設定|
    |ssh|TCP|22|Anywhere-IPv4|0.0.0.0/0|自由に設定|
    |HTTPS|TCP|443|Anywhere-IPv4|0.0.0.0/0|自由に設定|
    |カスタムTCP|TCP|3000|Anywhere-IPv4|0.0.0.0/0|自由に設定|
    |カスタムTCP|TCP|5432|Anywhere-IPv4|0.0.0.0/0|自由に設定|
    
    `0.0.0.0/0`の部分を自社のIPに設定することでIP制限をかけることができます

    * タグ
        * 自由に設定

1. 先ほど作成したEC2インスタンスを選択
1. 「アクション」 > 「セキュリティ」 > 「セキュリティグループを変更」
1. 今作ったセキュリティグループを設定

![スクリーンショット 2023-02-24 23.17.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/47275789-a8cb-9e1a-9da5-14c83740e6f7.png)

このようになっていたらOK

# 3. EC2に接続し、supabaseを起動する

1. `terraterm`などのツールや、`instance connect`を利用してEC2にssh接続をする

    * ファイルを操作するのでVScodeで入っちゃうのがおすすめ
    * 楽なのは`instance connect`で、画面上部の「接続」ボタンから接続したらOK

1. 以下の通りにコマンドを実行

    ```sh
    $ sudo yum update
    ・
    ・
    ・
    Upgrade  4 Packages
    
    Total download size: 34 M
    Is this ok [y/d/N]: y
    ・
    ・
    ・
    Complete!
    
    # docker install
    $ sudo yum install docker -y
    $ sudo service docker start
    
    # docker-compose install
    # version 1.29.2の場合
    # versionは好きなものを入れる
    $ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
    $ sudo chmod +x /usr/local/bin/docker-compose
    $ sudo chmod 666 /var/run/docker.sock
    
    # Gitのinstall
    $ sudo yum install git -y
    
    # supabase リポジトリのcloen
    $ git clone --depth 1 https://github.com/supabase/supabase
    $ cd supabase/docker
    $ cp .env.example .env
    ```

1. .envの修正

    .env内に出てくる`localhost`の値を全てEC2の`publicIP`に置き換える
    私の環境では４箇所ありました。



1. supabaseのセキュリティセットアップ

    1. 公式のガイドに従い[JWT Generator](https://supabase.com/docs/guides/self-hosting#api-keys)を利用して
`jwt secret`と、`anon key`と`service key`を作成する
    1. 次の箇所を書き換える
        
        ```
        .env:
            JWT_SECRET - jwt secret
            ANON_KEY - anon key
            SERVICE_ROLE_KEY - service key
        
        volumes/api/kong.yml
            anon - anon key
            service_role - service key
        ```
1. docker-compose.ymlの微修正(いらないかも)

    私のPC上ではこれまでのセットアップで動いたのですが、`Amazon linux`上ではコンテナがうまく立ち上がらなかったので、脳死気味になるが、以下の設定を全てのサービスに追加する

    ```yaml
     privileged: true
    ```
    
    例) 
    
    ```yaml
      rest:
        container_name: supabase-rest
        image: postgrest/postgrest:v10.1.2
        depends_on:
          db: # Disable this if you are using an external Postgres database
            condition: service_healthy
        restart: unless-stopped
        environment:
          PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
          PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
          PGRST_DB_ANON_ROLE: anon
          PGRST_JWT_SECRET: ${JWT_SECRET}
          PGRST_DB_USE_LEGACY_GUCS: "false"
        privileged: true   # 追加
    ```


    <font color="red">最後にdocker-compose.ymlファイルを載せます</font>

1. お待ちかねのサーバー起動(初回はimageのpullに時間がかかります。)

    ```bash
    $ docker-compose up -d
    ```

1. [http://public ip:3000]()を開くと、、、

![スクリーンショット 2023-02-25 0.23.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/c39ca2a5-f6de-630e-c6a0-319d42be5d76.png)

やったー！

# 4. storageをs3に変更する

* せっかくAWS上で動かしているので、storageはs3を使いましょう〜
* EBSも圧迫しますしね

1. s3を作成する

    名前は自由。他はデフォルトのままでOKです。

1. docker-compose.ymlファイルのstorageの環境変数を変更

    ```yaml
          FILE_SIZE_LIMIT: 52428800
          STORAGE_BACKEND: s3       # 変更
          FILE_STORAGE_BACKEND_PATH: /var/lib/storage
          TENANT_ID: supabase       # 変更(なんでもいいです。変えなくてもいいです。)
          REGION: ap-northeast-1    # s3のリージョン指定
          GLOBAL_S3_BUCKET: *****   # s3バケット名を指定
          ENABLE_IMAGE_TRANSFORMATION: "true"
          IMGPROXY_URL: http://imgproxy:5001
    ```

1. 環境再起動
    
    ```bash
    docker-compose restart
    ```

これでstorage機能の保存先がs3になります。

# 5. 参考

<details><summary>開く: docker-compose.yaml</summary>

```yaml:docker-compose.yml
# Usage
#   Start:          docker compose up
#   With helpers:   docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up
#   Stop:           docker compose down
#   Destroy:        docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans

version: "3.8"

services:
  studio:
    container_name: supabase-studio
    image: supabase/studio:20230216-e731b77
    restart: unless-stopped
    healthcheck:
      test: [ "CMD", "node", "-e", "require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" ]
      timeout: 5s
      interval: 5s
      retries: 3
    ports:
      - ${STUDIO_PORT}:3000/tcp
    environment:
      STUDIO_PG_META_URL: http://meta:8080
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

      DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
      DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}

      SUPABASE_URL: http://kong:8000
      SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
      SUPABASE_ANON_KEY: ${ANON_KEY}
      SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
    privileged: true

  kong:
    container_name: supabase-kong
    image: kong:2.8.1
    restart: unless-stopped
    ports:
      - ${KONG_HTTP_PORT}:8000/tcp
      - ${KONG_HTTPS_PORT}:8443/tcp
    environment:
      KONG_DATABASE: "off"
      KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml
      # https://github.com/supabase/cli/issues/14
      KONG_DNS_ORDER: LAST,A,CNAME
      KONG_PLUGINS: request-transformer,cors,key-auth,acl
      KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
      KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
    volumes:
      - ./volumes/api:/var/lib/kong:ro
    privileged: true

  auth:
    container_name: supabase-auth
    image: supabase/gotrue:v2.47.0
    depends_on:
      db: # Disable this if you are using an external Postgres database
        condition: service_healthy
    healthcheck:
      test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health" ]
      timeout: 5s
      interval: 5s
      retries: 3
    restart: unless-stopped
    environment:
      GOTRUE_API_HOST: 0.0.0.0
      GOTRUE_API_PORT: 9999
      API_EXTERNAL_URL: ${API_EXTERNAL_URL}

      GOTRUE_DB_DRIVER: postgres
      GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}

      GOTRUE_SITE_URL: ${SITE_URL}
      GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
      GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}

      GOTRUE_JWT_ADMIN_ROLES: service_role
      GOTRUE_JWT_AUD: authenticated
      GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
      GOTRUE_JWT_EXP: ${JWT_EXPIRY}
      GOTRUE_JWT_SECRET: ${JWT_SECRET}

      GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
      GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
      # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true
      # GOTRUE_SMTP_MAX_FREQUENCY: 1s
      GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
      GOTRUE_SMTP_HOST: ${SMTP_HOST}
      GOTRUE_SMTP_PORT: ${SMTP_PORT}
      GOTRUE_SMTP_USER: ${SMTP_USER}
      GOTRUE_SMTP_PASS: ${SMTP_PASS}
      GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
      GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
      GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
      GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
      GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}

      GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
      GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
      MFA_ENABLED: ${MFA_ENABLED}
    privileged: true

  rest:
    container_name: supabase-rest
    image: postgrest/postgrest:v10.1.2
    depends_on:
      db: # Disable this if you are using an external Postgres database
        condition: service_healthy
    restart: unless-stopped
    environment:
      PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
      PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
      PGRST_DB_ANON_ROLE: anon
      PGRST_JWT_SECRET: ${JWT_SECRET}
      PGRST_DB_USE_LEGACY_GUCS: "false"
    privileged: true

  realtime:
    container_name: realtime-dev.supabase-realtime
    image: supabase/realtime:v2.5.1
    depends_on:
      db: # Disable this if you are using an external Postgres database
        condition: service_healthy
    healthcheck:
      test: [ "CMD", "bash", "-c", "printf \\0 > /dev/tcp/localhost/4000" ]
      timeout: 5s
      interval: 5s
      retries: 3
    restart: unless-stopped
    environment:
      PORT: 4000
      DB_HOST: ${POSTGRES_HOST}
      DB_PORT: ${POSTGRES_PORT}
      DB_USER: supabase_admin
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_NAME: ${POSTGRES_DB}
      DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
      DB_ENC_KEY: supabaserealtime
      API_JWT_SECRET: ${JWT_SECRET}
      FLY_ALLOC_ID: fly123
      FLY_APP_NAME: realtime
      SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
      ERL_AFLAGS: -proto_dist inet_tcp
      ENABLE_TAILSCALE: "false"
      DNS_NODES: "''"
    command: >
      sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server"
    privileged: true

  storage:
    container_name: supabase-storage
    image: supabase/storage-api:v0.28.2
    depends_on:
      db: # Disable this if you are using an external Postgres database
        condition: service_healthy
      rest:
        condition: service_started
      imgproxy:
        condition: service_started
    healthcheck:
      test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/status" ]
      timeout: 5s
      interval: 5s
      retries: 3
    restart: unless-stopped
    environment:
      ANON_KEY: ${ANON_KEY}
      SERVICE_KEY: ${SERVICE_ROLE_KEY}
      POSTGREST_URL: http://rest:3000
      PGRST_JWT_SECRET: ${JWT_SECRET}
      DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
      FILE_SIZE_LIMIT: 52428800
      STORAGE_BACKEND: s3
      FILE_STORAGE_BACKEND_PATH: /var/lib/storage
      TENANT_ID: supabase
      # TODO: https://github.com/supabase/storage-api/issues/55
      REGION: ap-northeast-1
      GLOBAL_S3_BUCKET: supabase-sample-storage
      ENABLE_IMAGE_TRANSFORMATION: "true"
      IMGPROXY_URL: http://imgproxy:5001
    volumes:
      - ./volumes/storage:/var/lib/storage
    privileged: true

  imgproxy:
    container_name: supabase-imgproxy
    image: darthsim/imgproxy:v3.11
    healthcheck:
      test: [ "CMD", "imgproxy", "health" ]
      timeout: 5s
      interval: 5s
      retries: 3
    environment:
      IMGPROXY_BIND: ":5001"
      IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
      IMGPROXY_USE_ETAG: "true"
    volumes:
      - ./volumes/storage:/var/lib/storage
    privileged: true

  meta:
    container_name: supabase-meta
    image: supabase/postgres-meta:v0.60.7
    depends_on:
      db: # Disable this if you are using an external Postgres database
        condition: service_healthy
    restart: unless-stopped
    environment:
      PG_META_PORT: 8080
      PG_META_DB_HOST: ${POSTGRES_HOST}
      PG_META_DB_PORT: ${POSTGRES_PORT}
      PG_META_DB_NAME: ${POSTGRES_DB}
      PG_META_DB_USER: supabase_admin
      PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
    privileged: true

  # Comment out everything below this point if you are using an external Postgres database
  db:
    container_name: supabase-db
    image: supabase/postgres:15.1.0.42-rc2
    healthcheck:
      test: pg_isready -U postgres -h localhost
      interval: 5s
      timeout: 5s
      retries: 10
    command:
      - postgres
      - -c
      - config_file=/etc/postgresql/postgresql.conf
      - -c
      - log_min_messages=fatal # prevents Realtime polling queries from appearing in logs
    restart: unless-stopped
    ports:
      # Pass down internal port because it's set dynamically by other services
      - ${POSTGRES_PORT}:${POSTGRES_PORT}
    environment:
      POSTGRES_HOST: /var/run/postgresql
      PGPORT: ${POSTGRES_PORT}
      POSTGRES_PORT: ${POSTGRES_PORT}
      PGPASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATABASE: ${POSTGRES_DB}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/realtime.sql
      - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/roles.sql
      # Must be superuser to enable pg_net extension
      - ./volumes/db/webhooks.sql:/etc/postgresql.schema.sql
      - ./volumes/db/data:/var/lib/postgresql/data
    privileged: true
```

<details>