【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のさわりの部分は理解できたのでよかったです。
なにぶん、特殊な環境でずっと働いていたもので、開発標準が全然わかってないんですよね。
この調子で、ほかにもいろいろ勉強していきたいと思います。