はじめに
Dockerの理解を目的に、フルスタックアプリをコンテナで動かしてみました!
その過程で気づいたことや困ったことを紹介します。
↓今回作成したアプリのリポジトリ
作ったもの
学習内容と学習時間を記録できるシンプルなアプリです。
学習記録アプリ

学習内容、学習時間を入力し、登録ボタンを押すとDBに学習記録が登録されます。
技術スタック
- React(TypeScript)
- Hono(TypeScript)
- PostgreSQL
- Docker
アーキテクチャ
フロントエンド、バックエンド、データベースのコンテナをそれぞれ立ち上げています。
ディレクトリ構造
project/
├── docker-compose.yml
├── backend/
│ ├── Dockerfile
│ └── src/
└── frontend/
├── Dockerfile
└── src/
フロントエンド、バックエンドはそれぞれDockerfileを書き、データベースは配布されているPostgreSQLのイメージを使用しています。
そして、docker-compose.ymlで三つのコンテナを立ち上げるための設定を記述しています。
Dockerfile, docker-compose.yml
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
フロントエンドのコードをコンテナ内にコピーし、コンテナ内の5173番ポートでアプリを立ち上げる設定を書いています。
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["sh", "-c", "npx drizzle-kit generate && npx drizzle-kit migrate && npm run dev"]
こちらも同じようにバックエンドのコードをコピーして、コンテナ内の3000番ポートで立ち上げる設定です。
また、立ち上げる前にマイグレーションをおこなっています。後ほど記載しますが、このマイグレーションで詰まりました。
version: "3.8"
services:
client:
build: ./frontend
container_name: my-container
ports:
- "5173:5173"
depends_on:
- api
volumes:
- ./frontend:/app
- /app/node_modules
api:
build: ./backend
environment:
DATABASE_URL: postgresql://user:password@db:5432/mydatabase
container_name: api
ports:
- "3000:3000"
depends_on:
- db
volumes:
- ./backend:/app
- /app/node_modules
db:
image: postgres:16-alpine
container_name: my-postgre-db
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydatabase
volumes:
- db_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
db_data:
Dockerfileと配布されているPostgreSQLのイメージを元にコンテナを三つ作成する設定が書かれています。
Dockerfileにはコンテナ内のどのポートを使うかという設定のみでしたが、ここではそのコンテナ内のポートと自分のPCのローカルホストを繋ぐためのポートマッピングをおこなっています。
また、正しい順番で立ち上がって欲しいので、depends_onでコンテナの依存関係を指定しています。これは、データベースよりも先にapiコンテナが作成されてしまった場合、マイグレーションに失敗してしまうためです。
難しかったところ
マイグレーションに失敗した
マイグレーションがうまくいかず、データベースが使えませんでした。結論から言うと、使っていたDATABASE_URLが原因でした。
エラーが発生したURL
DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase
修正後のURL
DATABASE_URL=postgresql://user:password@db:5432/mydatabase
どういうことかというと、マイグレーションはこのURLを元にバックエンドから実行されます。(backend/Dockerfileに書いた設定)
なので、localhost:5432とはバックエンドのlocalhost:5432を指します。ですが、下の図を見たらわかるように参照したいのはdbコンテナのlocalhost:5432です。
よって、DATABASE_URLではdb:5432と指定する必要があります。(docker-compose.ymlで決めた名前)
学び・気づき
コンテナごとにlocalhost:5432がある
「難しかったところ」で述べたようにコンテナは一つのPCみたいなものなので、それぞれにlocalhostが存在します。それを踏まえた宛先の指定が必要であることを学びました。
なぜDockerが便利なのかがわかった
今までは実際に触ったことがなかったので、「環境を統一できるから便利」と何となくの認識でした。
しかし、今回実際にコンテナを立ててみて、Node.jsのバージョンはDockerfileで指定されているし、パッケージのバージョンはもちろんpackage-lock.jsonで指定されているのでバージョンずれが起きようがないことを知り、便利さを実感しました。
これで別PCやサーバーで使うとしてもgit cloneしてdocker compose upするだけで同じ開発環境を再現することができます。
Dockerfile, Docker Imageを理解できた
こちらも実際に触るまでは何となく設計図みたいなイメージはあったのですが、DockerfileとDocker Imageの違いをわかっていませんでした。
実際にDockerfile, Docker Imageの中身を見てみたことでどんな違いがあるのかはっきり理解することができました。どちらも設計図ですが、Docker Imageの方はより具体的な内容になっていて、jsonで書かれています。
今度の展望
デプロイ
今回作成したアプリを実際にEC2などにデプロイすることでDockerコンテナのデプロイを体験してみたいです。
Docker Hub
PostgreSQLのイメージを使うことにしかDocker Hubを使ったことがないので、今回作成したイメージをDocker Hubに保存してみたいです。
マルチステージビルド
実務を想定してマルチステージビルドを採用したデプロイをおこなってみたいです。開発環境にしか必要ないファイルを実行用コンテナに含めないことで軽量にするという考え方らしいです。
↓マルチステージビルドの説明
参考文献
おわりに
今回のアプリはLTで発表したのですが、誰かに発表することを目的にするとものすごく理解が深まるように感じました。
今後も積極的にLTに参加して知識を増やしていきたいです!

