Macユーザー必見！Docker Composeで始めるSupabaseローカル開発

# 概要
- この記事で記載すること：`supabase`をローカルマシン(Apple Sillicon Mac)上のDockerで構築し、python使って、テーブルの作成とテーブルへのレコードインサートを行う。
- デフォルトのままのdocker-composeの設定ファイルだと、筆者の手元ではエラーを吐きまくったので、備忘としてメモ。


## 構成イメージ

### ざっくり

![supabaseのコンテナイメージ-Page-1.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/cbfea1c2-fa43-41d6-b78e-eab8835a26a7.png)


### 詳細に

![supabaseのコンテナイメージ-Page-2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/13b5cf5e-3239-4734-aa9f-28c6edee3940.png)


# supabaseの利用方法

- 利用方法はいくつかある。
    - supabaseがホストしているサーバ（freeプランあるけど、基本は有償）を使う方法
    - ローカルで利用する方法
        - `supabase CLI` を使うケース
        - `docker` を使うケース
        - supabase のpythonパッケージを使うケース
- 今回は、『`docker` を使うケース』で設定し、利用してみる。

## 設定

- 基本的に公式のインストラクションに従っていけば良いが、（多分）Macだと権限エラーが起きてしまうので、`docker-compose.yaml`を編集する必要がある。
- インストラクション
    - `git clone --depth 1 https://github.com/supabase/supabase`
    - `mkdir supabase-project`
        
        ```python
        # Tree should look like this
        # .
        # ├── supabase
        # └── supabase-project
        
        ```
        
    - `cp -rf supabase/docker/* supabase-project`
        - 後述するが、`supabase-project/docker-compose.yml` を編集する。※この編集が必要なのは、macだけかもしれない。windowsやlinuxで未検証。
    - `cp supabase/docker/.env.example supabase-project/.env`
    - `cd supabase-project`
- 修正すべき箇所が2箇所
    - ①`supabase-db` コンテナの設定
    - ② `volumes` の設定

### ①`supabase-db` コンテナの設定

※この編集が必要なのは、macだけかもしれない。windowsやlinuxでの修正必要有無は未検証。

```yaml
db:
    container_name: supabase-db
    image: supabase/postgres:15.8.1.060
    restart: unless-stopped
    volumes:
      - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro,Z
      - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:ro,Z
      - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro,Z
      - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro,Z
      - ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:ro,Z
      - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:ro,Z
      - ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:ro,Z
      - postgres-data:/var/lib/postgresql/data
      - db-config:/etc/postgresql-custom
    healthcheck:
      test:
        [
        "CMD",
        "pg_isready",
        "-U",
        "postgres",
        "-h",
        "localhost"
        ]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s  # 起動時間の余裕を追加
```

### ② `volumes` の設定

```yaml
volumes:
  db-config:
  postgres-data:  # 新しいnamed volume
```

### dockerを起動する。

`docker compose up -d` 

## アクセスする

- ダッシュボードにアクセスする場合：http://localhost:8000/
    - `http://localhost:8000`


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/9fae5460-0271-4c41-9c16-1302289051d4.png)




## ライブラリのインストール

```
supabase
python-dotenv
```

## supabaseのDB（postgreSQL）を使う

- 2つやることがある。
    - ①クライアントとなるpythonファイルを作成する
    - ②supabase上で、postgreRPC関数を作成する

### ファイル構成

ローカルマシンまたはローカル上のDocker上で以下の構成にする。

```python
.
├── .env
├── Taskfile.yaml
├── context/
│   ├── cpu/
│   │   └── docker-compose.yaml # pythonサーバとUnstructuredのAPIサーバの2つを設定するファイル
│   └── Dockerfile # pythonサーバ
└── experimentation/
    ├── my-func.py
    └── .env
```

### .env

```
POETRY_VERSION=2.1.2
POETRY_HOME=/opt/poetry
PROJECT_NAME=test-supabase-from-python-client
CONTAINER_NAME=test-supabase-from-python-client
```

### Taskfile.yaml

```yaml
version: '3'

dotenv: ['.env']

tasks:
  build:
    desc: Dockerイメージのビルド
    cmds:
      - docker compose -f ./context/cpu/docker-compose.yaml build

  up:
    desc: コンテナを起動
    cmds:
      - docker compose -f ./context/cpu/docker-compose.yaml up -d

  down:
    desc: コンテナの停止と削除
    cmds:
      - docker compose -f ./context/cpu/docker-compose.yaml down

  write_uid_onto_env:
    desc: ホストマシンのUIDとUSERNAMEを.envに書き込む
    cmds:
      - |
        TEMP_FILE=$(mktemp)
        grep -v "^USER_UID=" .env | grep -v "^USERNAME=" > $$TEMP_FILE
        echo "USER_UID=$(id -u $(whoami))" >> $$TEMP_FILE
        echo "USERNAME=$USER" >> $$TEMP_FILE
        mv $$TEMP_FILE .env

  shell:
    desc: UID書き込み -> build -> up -> コンテナに入る
    cmds:
      - task: write_uid_onto_env
      - task: build
      - task: up
      - docker exec -it "{{.container_name}}" /bin/bash
    vars:
      container_name:
        sh: 'echo ${CONTAINER_NAME}'

```

### context/cpu/docker-compose.yaml

```yaml
services:

  python-client:
    build:
      context: ../../context/
      dockerfile: Dockerfile
      args:
        POETRY_VERSION: ${POETRY_VERSION}
        POETRY_HOME: ${POETRY_HOME}
        USER_UID: ${USER_UID}
        USERNAME: ${USERNAME}
        PROJECT_NAME: ${PROJECT_NAME}
    container_name: ${CONTAINER_NAME}
    volumes:
      - type: bind
        source: ../../
        target: /home/${USERNAME}/${PROJECT_NAME}
    working_dir: /home/${USERNAME}/${PROJECT_NAME}
    tty: true 
    restart: always # コンテナが停止した場合に常に再起動する
```

### context/Dockerfile

```docker
FROM python:3.11-slim

# Build-time arguments
ARG POETRY_VERSION
ARG POETRY_HOME
ARG USER_UID
ARG USERNAME
ARG PROJECT_NAME

# 必要なパッケージをインストール（curl等）
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    build-essential \
    libmagic-dev \
    poppler-utils \
    tesseract-ocr \
    tesseract-ocr-jpn \
    libreoffice \
    pandoc \
    fonts-noto-cjk \
    libgl1 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# ==========================================
# Create user in the container to avoid permission matter 
# incompatible between host and container user
# ==========================================
RUN useradd --uid ${USER_UID} -m ${USERNAME} 

USER $USERNAME
ENV PATH="/usr/local/bin:$PATH"

WORKDIR /home/${USERNAME}/${PROJECT_NAME}
```


### ①クライアントとなるpythonファイルを作成する｜`my-func.py`

```python
import os
from supabase import create_client, Client
from dotenv import load_dotenv
import socket

# .envファイルの内容を読み込見込む
load_dotenv()

# Docker内外の判定関数
def is_running_in_docker():
    """
    スクリプトがDockerコンテナ内で実行されているかどうかを判定します。
    複数の方法で検証し、より信頼性の高い判定を行います。

    Returns:
        bool: Docker内ならTrue、それ以外はFalse
    """
    # 方法1: /.dockerenv ファイルの存在確認
    if os.path.exists("/.dockerenv"):
        return True

    # 方法2: cgroup情報の確認 (Linux環境)
    try:
        with open("/proc/1/cgroup", "r") as f:
            return "docker" in f.read()
    except:
        pass

    # 方法3: ホスト名がコンテナIDっぽいかチェック
    try:
        hostname = socket.gethostname()
        if len(hostname) == 12 and all(c in "0123456789abcdef" for c in hostname):
            return True
    except:
        pass

    # すべての方法が失敗したら、Docker外と判断
    return False

# 実行環境に応じてAPIのURLを設定
if is_running_in_docker():
    url = "http://host.docker.internal:8000/"  # Docker内部から接続する場合（厳密には、Docker DesktopまたはRancherDesktopを使っているケースでこれは機能する）
else:
    url = "http://localhost:8000/"  # ホストマシンから接続する場合

# INFO:
# service_roleキーは、サービス内のさまざまなタスクを実行するためのキー。こちらは RLS を迂回できるため、取り扱いには十分に注意する必要があります。
# アプリからデータベースに接続してCRUD処理を行う場合はこちらのAPI鍵を設定する必要があります。
# =========================
# SERVICE ROLE キーの取得方法
# -------------------------
# Supabase ダッシュボードにログインします。
# 対象のプロジェクトを選択します。
# 左側のサイドバーから「Project Settings（プロジェクト設定）」をクリックします。
# 次に「API」をクリックします。
# この「API」ページに、anon key（現在は publishable key に名称変更される予定）と service_role key（現在は secret key に名称変更される予定）が表示されています。
# =========================
SERVICE_ROLE_KEY = "<SERVICE ROLE キー>"
key = SERVICE_ROLE_KEY

supabase: Client = create_client(url, key)

def edge_function_test() -> None:
    """
    疎通テストの目的でデフォルトで作成されているEdge Functionのテスト
    """
    response = supabase.functions.invoke(
        "hello",
        invoke_options={
            "body": {"name": "Functions"},
        },
    )

    return response

def create_table(table_name: str = "messages") -> bool:
    """
    SupabaseのPostgreSQLデータベースにテーブルを作成する
    """
    # テーブル作成のためのSQL文を定義
    query = f"""
    CREATE TABLE IF NOT EXISTS {table_name} (
        id SERIAL PRIMARY KEY,
        content TEXT NOT NULL,
        created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc', now())
    );
    """

    try:
        response = supabase.rpc("execute_sql", {"query": query}).execute()
        print("テーブル作成に成功しました")
        print(response.data)  # Should be just the success message text
        return True
    except Exception as e:
        print(f"エラー: {e}")
        return False

def insert_message(content: str, table_name: str = "messages") -> None:
    """
    メッセージをデータベースに追加する
    """
    # メッセージをデータベースに挿入する
    data = {
        "content": content,
    }
    response = supabase.table(table_name).insert(data).execute()

    # メッセージの一覧を取得
    messages = supabase.table(table_name).select("*").execute()

    # メッセージ内容のみをリストに格納
    message_list = [msg["content"] for msg in messages.data]

    return "\n".join(message_list)

def main() -> None:
    # テーブルを作成する
    create_table()

    # テストメッセージを挿入する
    insert_message(
        "Hello, Supabase! from docker",
        table_name="messages",
    )

if __name__ == "__main__":
    main()

```

### ①クライアントとなるpythonファイルを作成する｜`.env`

`supabase-project/.env` ↑で設定した、.envファイルをローカルマシンにコピーする。

### ②supabase上で、postgreRPC関数を作成する

RPC関数を定義する必要性：supabaseは、psycog2などから直接SQLでテーブルを作成することを禁止している。そのため、テーブルを作りたい場合は、(A)テーブルをsupabaseのGUI上から作成する、(B)今回解説しているように、RPC関数を作成して、それをpythonで利用する、という形にするしかない。

1. supabaseのダッシュボードのナビゲーションから「SQL Editor」をクリックする。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/298cedf4-1581-42d7-9546-836a9e1b94af.png" width=30%>


1. RPC関数を入力して、「Run」する ※ RPC関数には注意点があるので、記事最下部に記載する。
    
    ```sql
    CREATE OR REPLACE FUNCTION execute_sql(query text)
    RETURNS text AS $$
    BEGIN
      EXECUTE query;
      RETURN 'SQL executed successfully';  -- Just return the message directly, not wrapped in JSON
    EXCEPTION WHEN OTHERS THEN
      RAISE EXCEPTION '%', SQLERRM;  -- Let actual errors be proper errors
    END;
    $$ LANGUAGE plpgsql SECURITY DEFINER;
    
    -- Grant permissions
    GRANT EXECUTE ON FUNCTION execute_sql(text) TO authenticated;
    GRANT EXECUTE ON FUNCTION execute_sql(text) TO service_role;
    ```
    

### 準備完了したので、`my-func.py` を実行する

問題なく実行できると、supabaseのダッシュボード画面上に以下のようにテーブルが表示される（以下の添付の場合、何個かテーブルを作ってしまったので、`hello`や`test`というテーブルも見えてしまっているが,,,）

![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/0e9c3323-2f82-42b7-b935-eaec05677dde.png)


また、Table Editorを開くと以下のようにデータがちゃんと挿入されていることがわかる（以下の画像の場合、テストで2回pythonを実行したので、2レコードあるが、一回だけであれば1レコードになっているはず）。

![image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687015/6b3a4093-1ded-4858-8e7f-1cb840be8872.png)


# RPC関数の注意点

以下のものを、supabaseのSQL Editorで実行させておくと、関数が登録される。登録された関数をpython側で呼び出す（`response = supabase.rpc("execute_sql", {"query": query}).execute()`の中の`"execute_sql"`の部分）ことで、SQLを実行させることができる。

```sql
CREATE OR REPLACE FUNCTION execute_sql(query text)
RETURNS text AS $$
BEGIN
  EXECUTE query;
  RETURN 'SQL executed successfully';  -- Just return the message directly, not wrapped in JSON
EXCEPTION WHEN OTHERS THEN
  RAISE EXCEPTION '%', SQLERRM;  -- Let actual errors be proper errors
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Grant permissions
GRANT EXECUTE ON FUNCTION execute_sql(text) TO authenticated;
GRANT EXECUTE ON FUNCTION execute_sql(text) TO service_role;
```

注意点として、**この実装をした場合は、SQLインジェクションされる可能性が高いので、
本番で実装する場合は、pythonで定義したSQLを自由に実行できないようにする必要がある。**

例えば、実行すべきSQLが決まっているなら、RPC関数の中にそのSQLを書いておき、実行はpythonに任せる、みたいなやり方がGOOD。その場合は、以下のようなRCP関数を定義しておく。

```sql
-- Create custom function to create our messages table
CREATE OR REPLACE FUNCTION create_messages_table()
RETURNS VOID AS $$
BEGIN
    -- Create the messages table if it doesn't exist
    CREATE TABLE IF NOT EXISTS messages (
        id SERIAL PRIMARY KEY,
        content TEXT NOT NULL,
        created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc', now())
    );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Grant execute permissions
GRANT EXECUTE ON FUNCTION create_messages_table() TO anon;
GRANT EXECUTE ON FUNCTION create_messages_table() TO authenticated;
GRANT EXECUTE ON FUNCTION create_messages_table() TO service_role;
```

これを実行させたいときは、RCP関数の登録後に、pythonから、`response = supabase.rpc("create_messages_table").execute()`とすれば良い。


# まとめ
- `supabase`をローカルマシン（Apple Sillicon Mac）のDocker環境で動作させるための設定を行った。
- さらに、pythonでsupabaseに対してテーブル作成、テーブルの更新（レコードインサート）を行えるようにした。
- そのためのRPCを作成しt