0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【学習記録】今までまったくDockerに触ったことない人の初学記録③

Posted at

【Docker超入門③】docker-composeでPython + PostgreSQLを連携させる

はじめに

前回はイメージ・コンテナ・Dockerfileについて学びました。

今回はいよいよ最終回!複数のコンテナを連携させる方法を学びます。

実際に「Python + PostgreSQL」の構成を作って、CSVファイルをDBに投入するところまでやってみます!

シリーズ構成:

  • 第1弾:Dockerって何がすごいの?
  • 第2弾:イメージ・コンテナ・Dockerfileを完全理解する
  • 第3弾(この記事):docker-composeでPython + PostgreSQLを連携させる

1. docker-composeとは

なぜ必要?

コンテナとは、独立した環境を作る仕組みでした。

でも、世の中にはPythonとPostgreSQLとか複数のシステムを連携させたい! とかの要件もあるわけで。

1つのコンテナにいっぱいシステムを突っ込んじゃうのは大変ですよね。

そこで、docker-composeの出番。

docker-composeがやってくれること

機能 説明
① どのコンテナを使うか PostgreSQL?FastAPI?Redis?
② どんな設定で起動するか ポート、環境変数、ボリューム...
③ どの順番で起動するか DB → API などの依存関係を定義
④ コンテナ間の通信 自動でネットワークを構築
⑤ まとめて管理 複数コンテナを1つの単位として起動・停止

2. docker-compose.ymlの書き方

composeを使うための設計図をYAML形式で記述します。

基本構造

version: "3.9"

services:
  app:
    build: ./app
    container_name: compose_app
    depends_on:
      - db
    environment:
      DATABASE_URL: "postgresql://user:password@db:5432/mydb"
    command: python hello.py
    volumes:
      - ./app:/app

  db:
    image: postgres:15
    container_name: compose_db
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

各項目の意味

version: "3.9"

composeのバージョン指定

services:

「これから、この配下にコンテナを定義しますよ!」という宣言

appサービス

app:
  build: ./app
  container_name: compose_app
  • build: ./app:app配下のDockerfileを使ってビルド
  • container_name:コンテナの名前
depends_on:
  - db
  • dbを作った後にこのコンテナを作ってねという依存関係
environment:
  DATABASE_URL: "postgresql://user:password@db:5432/mydb"
  • 環境変数の設定
command: python hello.py
  • 起動時に実行するコマンド(DockerfileのCMDを上書き)
volumes:
  - ./app:/app
  • バインドマウント(後述)

dbサービス

db:
  image: postgres:15
  container_name: compose_db
  • image:Docker Hubのイメージを直接使用(ビルド不要)
ports:
  - "5432:5432"
  • ポートマッピング(ホスト:コンテナ)

3. ボリューム:データを永続化する技術

前回、docker rmするとデータが消えることを学びました。

Volumeを使えば、コンテナを削除してもデータを残せます!

2種類のボリューム

① 名前付きVolume

volumes:
  - db_data:/var/lib/postgresql/data

volumes:
  db_data:

コンテナ外に、名前を指定してデータを保持してもらう場所を作る技術です。

PostgreSQLのデータを/var/lib/postgresql/dataに置いてね!という意味。

これで、コンテナを立ち上げ直しても、テーブルにデータが残ります!

② バインドマウント

volumes:
  - ./app:/app

特定のフォルダの変更を即座にコンテナ内に反映させる設定です。

「docker-compose.ymlがあるパス配下のappフォルダ」と、コンテナ内の「appフォルダ」がつながってるイメージ。

バインドマウントのメリット

app配下のフォルダって、実態としてはpyファイルとか、Reactのファイルとかになりますよね。

でもこれ、開発中だとバンバン作り変えます。

作り変えるたびに、毎回コンテナを作り直すの、面倒じゃないですか?

そこで、バインドマウントを指定しておくわけです。

これで、開発中に変えたファイルはすぐにコンテナに反映してくれるようになります!

ちなみに、Reactだとホットリロードなので、書くだけでOKです。

比較まとめ

種類 用途
名前付きVolume DBなど消えてほしくないデータ PostgreSQLのデータ
バインドマウント 開発中に頻繁に変更するファイル ソースコード

4. コンテナ間通信の仕組み

サービス名がそのままDNSになる!

docker-composeで作ったコンテナは、サービス名でお互いを呼び出せます

environment:
  DB_HOST: db  # ← "db"というサービス名で接続できる!

IPアドレスを調べる必要なし!便利!

Pythonから接続する例

import os
import psycopg2

conn_info = {
    "host": os.environ.get("DB_HOST"),      # "db"
    "dbname": os.environ.get("DB_NAME"),    # "mydb"
    "user": os.environ.get("DB_USER"),      # "user"
    "password": os.environ.get("DB_PASSWORD"), # "password"
    "port": 5432,
}

conn = psycopg2.connect(**conn_info)

docker-compose.ymlで環境変数を設定して、Python側でos.environ.get()で取得するパターンが王道です。


5. depends_onの罠と対処法

罠:起動順序 ≠ 起動完了

depends_on:
  - db

これ、「dbを起動 → appを起動」という順序にしてくれます。

ただし!起動完了までは見てくれないんです。

特にPostgreSQLは起動がちょっと遅いので、起動中にappが動いちゃうことがあります。

対処法:リトライ処理を入れる

import time
import psycopg2

for _ in range(10):
    try:
        conn = psycopg2.connect(**conn_info)
        break
    except Exception:
        print("DB 起動待ち…")
        time.sleep(2)

接続に失敗したら2秒待って再トライ。これで安心!


6. 実践:CSVをPostgreSQLに投入する

いよいよ総仕上げ!データパイプラインを作ります。

フォルダ構成

project/
├── docker-compose.yml
└── app/
    ├── Dockerfile
    ├── requirements.txt
    ├── load-csv.py
    └── sample.csv

① docker-compose.yml

version: "3.9"

services:
  app:
    build: ./app
    container_name: compose_app
    depends_on:
      - db
    command: python load-csv.py
    volumes:
      - ./app:/app
    environment:
      DB_HOST: db
      DB_NAME: mydb
      DB_USER: user
      DB_PASSWORD: password

  db:
    image: postgres:15
    container_name: compose_db
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

② Dockerfile

FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "load-csv.py"]

③ requirements.txt

psycopg2-binary==2.9.9

④ sample.csv

Date,Open,High,Low,Close,Volume
"Dec 30, 2021",146.45,147.06,145.76,146.00,"648,851"
"Dec 31, 2021",145.54,146.37,144.68,144.68,"864,885"
"Jan 3, 2022",144.48,145.55,143.50,145.07,"1,261,225"
"Jan 4, 2022",145.55,146.61,143.82,144.42,"1,146,389"

⑤ load-csv.py

import csv
import os
import time
from pathlib import Path
import psycopg2
from psycopg2.extras import execute_values

# 接続設定
conn_info = {
    "host": os.environ.get("DB_HOST"),
    "dbname": os.environ.get("DB_NAME"),
    "user": os.environ.get("DB_USER"),
    "password": os.environ.get("DB_PASSWORD"),
    "port": 5432,
}
TARGET_TABLE = "sample_table"
CSV_PATH = Path("sample.csv")

def load_csv_to_postgres(csv_path: Path, table_name: str):
    if not csv_path.exists():
        raise FileNotFoundError(f"CSVファイルが見つかりません: {csv_path}")

    # CSV読み込み
    with csv_path.open("r", encoding="utf-8", newline="") as f:
        reader = csv.reader(f)
        rows = list(reader)

    if not rows:
        print("CSVにデータ行がありません。")
        return

    columns = rows[0]
    data_rows = rows[1:]

    # CREATE TABLE文を生成(すべてTEXT型)
    col_defs = ", ".join(f'"{c}" TEXT' for c in columns)
    create_table_sql = f"""
        CREATE TABLE IF NOT EXISTS {table_name} (
            {col_defs}
        );
    """

    # INSERT文を生成
    quoted_columns = [f'"{c}"' for c in columns]
    col_str = ", ".join(quoted_columns)
    values = [tuple(r) for r in data_rows]
    insert_sql = f"INSERT INTO {table_name} ({col_str}) VALUES %s"

    # DB接続(リトライあり)
    conn = None
    for _ in range(10):
        try:
            conn = psycopg2.connect(**conn_info)
            break
        except Exception:
            print("DB 起動待ち…")
            time.sleep(2)

    if conn is None:
        raise Exception("DBに接続できませんでした")

    try:
        with conn:
            with conn.cursor() as cur:
                print("CREATE TABLE IF NOT EXISTS を実行します。")
                cur.execute(create_table_sql)

                print(f"{len(values)} 行を挿入します。")
                execute_values(cur, insert_sql, values)

        print("テーブル作成およびデータ挿入が完了しました。")
    finally:
        conn.close()

if __name__ == "__main__":
    load_csv_to_postgres(CSV_PATH, TARGET_TABLE)

実行!

docker compose up --build
compose_app  | DB 起動待ち…
compose_app  | CREATE TABLE IF NOT EXISTS を実行します。
compose_app  | 4 行を挿入します。
compose_app  | テーブル作成およびデータ挿入が完了しました。

完璧!

DBを確認してみる

別のターミナルで:

docker exec -it compose_db psql -U user -d mydb
SELECT * FROM sample_table;

サンプルデータが投入されていれば成功です!

後始末

docker compose down

これで、すべてのコンテナが停止・削除されます。

名前付きVolumeはdocker compose downでは消えません。データを完全に消したい場合は docker compose down -v を使います。


7. 補足:ネットワークを確認してみる

docker-composeは自動でネットワークを作ってくれます。

docker network ls
NETWORK ID     NAME                    DRIVER    SCOPE
xxxxxxxxxxxx   project_default         bridge    local

詳細を見てみる:

docker network inspect project_default

Containersセクションに、同じネットワーク内のコンテナが表示されます。


シリーズまとめ

3回にわたってDockerの基礎を学んできました!

第1弾で学んだこと

  • 仮想マシンとコンテナの違い
  • Dockerを使う理由(環境差異の解消)
  • docker run hello-world

第2弾で学んだこと

  • イメージ = 設計図、コンテナ = 実体
  • 基本コマンド(pull, run, ps, stop, rm)
  • Dockerfileの書き方

第3弾で学んだこと

  • docker-composeで複数コンテナを管理
  • ボリューム(名前付き / バインドマウント)
  • コンテナ間通信(サービス名 = DNS)
  • depends_onの罠とリトライ処理
  • 実践:CSVをDBに投入するパイプライン

おわりに

とりあえず、何とかコンテナ間通信の実装までこぎつけまして。。。
かなり大変でしたが、Doclkerのさわりの部分は理解できたのでよかったです。

なにぶん、特殊な環境でずっと働いていたもので、開発標準が全然わかってないんですよね。
この調子で、ほかにもいろいろ勉強していきたいと思います。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?