【Docker Compose】コンテナで起動した PostgreSQL に接続する

# はじめに

こんにちは、梅雨です。

バックエンドの開発において、環境の統一やコンテナの隔離などの目的から Docker を採用する事例は多いと思います。

Docker を使用する場合、通常の開発体験とは少し異なるため、あまり慣れていない方だと開発に困ってしまうこともあるでしょう。

そんな方に向けて、この記事では Docker コンテナ内で起動した PostgreSQL のデータベースに接続する方法について解説していきます。

# Docker 環境構築

アプリケーションの構成は以下のようになっています。

```bash
docker-psql-demo
├── backend
│   ├── Dockerfile
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   │   └── server.ts
│   └── tsconfig.json
└── docker-compose.yaml
```

## バックエンドの各ファイル

今回は、バックエンドに Node.js のフレームワークである Express を採用しました。バックエンドに採用する言語やフレームワークには特に制限はないため、お使いのバックエンドがある場合はそちらをお使いいただいて大丈夫です。

```json:backend/package.json
{
  "name": "backend",
  "scripts": {
    "dev": "nodemon ./src/server.ts"
  },
  "dependencies": {
    "express": "^4.21.2"
  },
  "devDependencies": {
    "@types/express": "^5.0.0",
    "nodemon": "^3.1.9",
    "ts-node": "^10.9.2"
  }
}
```

```json:backend/tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}
```

```ts:backend/src/server.ts
import express from "express";

const app = express();

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(8000, () => {
  console.log("Server is running on http://localhost:8000");
});
```

```docker:backend/Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
CMD [ "npm", "run", "dev" ]
```

## `docker-compose.yaml` の記述

続いて、`docker-compose.yaml` を記述していきます。

サービスにはバックエンドとデータベースの二つを定義します。サービス名は何でも大丈夫なので、好きな名前を使用してください。

```yaml:docker-compose.yaml
services:
  backend:
  db:
```

次に、バックエンドのサービスの中身を書いていきます。

`build` には `Dockerfile` のあるパスを記述します。

`volumes` にはコンテナにマウントするパスを記述します。今回の例では、ホスト側の `./backend` をコンテナ側の `/app` にマウントしています。

`ports` にはホスト側のポートとコンテナ側のポートのマッピング関係を指定します。今回の例では Express のサーバを 8000 番ポートで起動するようにしているため、`8000:8000` としています。


```diff_yaml:docker-compose.yaml
services:
  backend:
+     build: ./backend
+     volumes:
+       - ./backend:/app
+     ports:
+       - 8000:8000
  db:
```

次に、データベースのサービスの中身を書いていきます。

`image` にはビルドイメージを記述します。バックエンドでは自分で記述した Dockerfile を指定しましたが、データベースでは DockerHub から直接ビルドイメージを使用します。

`ports` には先程と同様にマッピングしたポート番号を記述します。5432 は PostgreSQL のデフォルトのポート番号です。

`environment` にはコンテナに渡す環境変数を記述します。`POSTGRES_DB` にはデータベース名、`POSTGRES_USER` にはユーザ名、`POSTGRES_PASSWORD` にはパスワードを指定します。これら３つの名前は何でも大丈夫ですが、後でデータベースに接続する時に使用するので、わかりやすい名前にすることをお勧めします。

```diff_yaml:docker-compose.yaml
services:
  backend:
    build: ./backend
    volumes:
      - ./backend:/app
    ports:
      - 8000:8000
  db:
+     image: postgres:16-alpine
+     ports:
+       - 5432:5432
+     environment:
+       POSTGRES_DB: docker_psql_demo
+       POSTGRES_USER: admin
+       POSTGRES_PASSWORD: postgres
```

最後に、このままだとコンテナを終了するたびにデータが揮発してしまうため、永続化用のマウントを記述します。

ボリューム名はサービス名と同じ `db` を指定していますが、これは別に同じでなくてもいいです。各自わかりやすい名前を使用してください。

`volumes` では`db` ボリュームをコンテナ側の `/var/lib/postgresql/data` にマウントしています。この `/var/lib/postgresql/data` は PostgreSQL のデータの保存先のパスです。

```diff_yaml:docker-compose.yaml
services:
  backend:
    build: ./backend
    volumes:
      - ./backend:/app
    ports:
      - 8000:8000
  db:
    image: postgres:16-alpine
+     volumes:
+       - db:/var/lib/postgresql/data
    ports:
      - 5432:5432
    environment:
      POSTGRES_DB: docker_psql_demo
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: postgres

+ volumes:
+   db:
```

以上で環境構築の準備ができました。`docker compose build` でコンテナをビルドしましょう。

```bash
$ docker compose build
```

# ターミナルからの接続

まずはコンテナを起動します。`-d` オプションをつけることで、バックグラウンドでコンテナの起動が可能です。

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

続いて、db コンテナに入り bash を起動しましょう。

```bash
$ docker compose exec db bash
```

:::note
postgres イメージにはデフォルトで bash がインストールされています。
:::

bash が起動できたら、まずは `psql` コマンドを実行してみましょう。引数には先程 `docker-compose.yaml` 内で環境変数として渡したデータベース名を指定します。

```bash
\# psql docker_psql_demo
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL:  role "root" does not exist
```

すると、"root" ロールがないと怒られてしまいます。

PostgreSQL はデフォルトで "root" というユーザを使用するため、先程環境変数として渡したユーザ名を `-U` オプションで指定しましょう。

```bash
/# psql docker_psql_demo -U admin
psql (16.6)
Type "help" for help.

docker_psql_demo=\# 
```

これでターミナルから PostgreSQL のデータベースに接続ができました。

せっかくなので、このままテーブルを作成してしまいましょう。

```bash
docker_psql_demo=\# CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, age INTEGER);
CREATE TABLE
```

現在あるテーブルは `\dt` コマンドで確認できます。

```bash
docker_psql_demo=\# \dt;
       List of relations
 Schema | Name  | Type  | Owner 
--------+-------+-------+-------
 public | users | table | admin
(1 row)
```

# アプリケーションからの接続

今度はバックエンドのアプリケーションからデータベースに接続する方法を解説します。

まずは npm の `postgres` パッケージをインストールします。

:::note warn
自身のバックエンドがある方は飛ばしていただいて大丈夫です。
:::

```
$ docker compose run --rm backend npm i postgres
```

続いて `backend/src/utils/sql.ts` で PostgreSQL に接続するためのインスタンスを作成します。

```ts:backend/src/utils/sql.ts
import postgres from "postgres";

const sql = postgres("postgres://admin:postgres@db:5432/docker_psql_demo");

export default sql;
```

ここで重要となってくるのが、`postgres://` から始まる URI です。

この URI は以下のページで詳細に説明されています。

https://www.postgresql.jp/docs/9.4/libpq-connect.html#AEN41133

基本的な構造は

postgres:// `ユーザ名` : `パスワード` @ `ホスト名` : `ポート番号` / `テーブル名`

のようになっており、今回の例では

- ユーザ名: admin
- パスワード: postgres
- ホスト名: db
- ポート番号: 5432
- テーブル名: docker_psql_demo

となっています。ホスト名を `localhost` ではなくサービス名の `db` とすることに注意してください。

インスタンスの準備ができたら、`/users` の CRUD 操作を行えるルーティングを記述してみましょう。

```ts
// Read users
app.get("/users", async (req, res) => {
  const users = await sql`SELECT * FROM users`;
  res.send(users);
});

// Read user
app.get("/users/:id", async (req, res) => {
  const { id } = req.params;
  const [user] = await sql`SELECT * FROM users WHERE id = ${id}`;

  if (!user) {
    res.sendStatus(404);
  }

  res.send(user);
});

// Create user
app.post("/users", async (req, res) => {
  const { name, age } = req.body;

  if (!name || !age) {
    res.sendStatus(400);
  }

  const [user] =
    await sql`INSERT INTO users (name, age) VALUES (${name}, ${age}) RETURNING id`;

  res.setHeader("Location", `http://localhost:8000/${user.id}`).sendStatus(201);
});

// Update user
app.patch("/users/:id", async (req, res) => {
  const { id } = req.params;
  const { name, age } = req.body;

  if (!name || !age) {
    res.sendStatus(400);
  }

  const [user] =
    await sql`UPDATE users SET name = ${name}, age = ${age} WHERE id = ${id} RETURNING id`;

  if (!user) {
    res.sendStatus(404);
  }

  res.sendStatus(204);
});

// Delete user
app.delete("/users/:id", async (req, res) => {
  const { id } = req.params;

  const [user] = await sql`DELETE FROM users WHERE id = ${id} RETURNING id`;

  if (!user) {
    res.sendStatus(404);
  }

  res.sendStatus(204);
});
```

curl コマンドを使って確認します。

### Get users

```bash
$ curl localhost:8000/users -X GET
[]
```

### Create user

```bash
$ curl localhost:8000/users -X POST -H 'Content-Type:application/json' -d '{"name":"meiyu","age":23}'
Created
```

### Get user

```bash
$ curl localhost:8000/users/1 -X GET
{"id":1,"name":"meiyu","age":23}

$ curl localhost:8000/users/2 -X GET
Not Found
```

### Update user

```bash
$ curl localhost:8000/users/1 -X PATCH -H 'Content-Type:application/json' -d '{"name":"tsuyuni","age":23}'

$ curl localhost:8000/users/2 -X PATCH -H 'Content-Type:application/json' -d '{"name":"tsuyuni","age":23}'
Not Found
```

### Delete User

```bash
$ curl localhost:8000/users/1 -X DELETE

$ curl localhost:8000/users/2 -X DELETE
Not Found
```

以上が Docker コンテナで起動した PostgreSQL のデータベースにアプリケーションから接続する方法となります。

# おわりに

今回は Docker コンテナ内で起動した PostgreSQL のデータベースに、ターミナルとアプリケーションのそれぞれから接続する方法について解説しました。

Docker と PostgreSQL を用いて開発を行う際は、ぜひ参考にしてみてください。