Upgrade to Pro — share decks privately, control downloads, hide ads and more …

はじめての環境構築!デプロイ〜Docker基礎を学べるワークショップ!

 はじめての環境構築!デプロイ〜Docker基礎を学べるワークショップ!

More Decks by ディップ株式会社

Other Decks in Technology

Transcript

  1. 今回のゴール 「Dockerとは何か」 「なぜ便利なのか」を自分の言葉で説明できる docker run / docker build / docker

    compose up の中で何が起きているかが分 かる Dockerfile と compose.yaml を読み・書きできる ビルドキャッシュを意識して、待ち時間の短いDockerfileを書ける 何か壊れたとき、自分でログを読んで原因を突き止められる Docker入門 / 技育アカデミア 2
  2. 目次 1. 仮想化の基礎 — なぜDockerが生まれたのか 2. Dockerとは何か — 公式定義とアーキテクチャ 3.

    コンテナとイメージ — 3つの登場人物 4. Docker CLI の使い方 5. Dockerfile とイメージビルド 6. ビルドキャッシュを味方につける 7. データとネットワーク 8. Docker Compose で複数コンテナを束ねる 9. まとめ Docker入門 / 技育アカデミア 3
  3. 「環境構築の苦しみ」を想像してみよう PHP 7.4 と 8.3 を 同じPCで共存させたい ローカルでは動くのに、本番(別バージョン)で動かない 新メンバーの環境構築に 3日かかる

    いつの間にか環境設定がズレてテストが当てにならない 共通の原因 → 「実行環境」がコードと一緒に運ばれていない それを解決するために、軽くて運びやすいコンテナ型仮想化が生まれた。 Docker入門 / 技育アカデミア 6
  4. 仮想化の2つの方式 ホスト型仮想化 ホストOSの上に「仮想化ソフト」を 入れる その中でゲストOSを丸ごと動かす 例: VirtualBox, VMware Fusion コンテナ型仮想化

    OSは用意せず、ホストOSを仕切っ て使う カーネルをホストと共有する 例: Docker, Podman Docker入門 / 技育アカデミア 7
  5. ホスト型 vs コンテナ型 観点 ホスト型仮想化 (VM) コンテナ型仮想化 (Docker) OSの持ち方 ゲストOSをまるごと持つ

    ホストOSのカーネルを共有 起動時間 数十秒〜数分 1〜数秒 サイズ 数GB 数十MB〜数百MB 隔離の強さ OSレベルで強い プロセスレベル 主な用途 OSごと検証・OS差吸収 アプリの実行環境を束ねる 「軽い」 「速い」 「環境ごと運べる」これがコンテナの特徴。 Docker入門 / 技育アカデミア 8
  6. Dockerを構成する3つの要素 Dockerfile イメージの設計図。 FROM node:20-alpine COPY . /app CMD ["node",

    "/app/index.js"] Image / Container Image: 設計図から作られた読み取り 専用テンプレート Container: イメージから起動した実 体 Docker入門 / 技育アカデミア 11
  7. 3要素のライフサイクル全体図 出典: GeeksforGeeks 1. Dockerfile をテキストで書く 2. docker build で

    Image を生成 3. docker push で Registry に登録 → 他環境が pull 4. docker run で Container が起動 5. docker stop / rm でコンテナを消 す(Imageは残る) Docker入門 / 技育アカデミア 12
  8. コンテナとは — 公式定義 A container is an isolated process for

    your application's component. (コンテナとは、アプリの構成要素のための 隔離されたプロセス である) 公式が挙げる4つの性質: Self-contained: 動作に必要なものをすべて内包 Isolated: ホスト・他コンテナと隔離されている Independent: 個別に管理・削除できる Portable: 開発PCでもクラウドでも同じように動く Docker入門 / 技育アカデミア 14
  9. レイヤを「見る」コマンド $ docker history node:20-alpine IMAGE CREATED CREATED BY SIZE

    abc123... 2 weeks ago CMD ["node"] 0B def456... 2 weeks ago ENTRYPOINT ["docker-entrypoint.sh"] 0B ... 2 weeks ago RUN addgroup -g 1000 node && adduser ... 87.3kB ... 3 weeks ago /bin/sh -c #(nop) ARG NODE_VERSION=20.11.1 0B ghi789... 3 weeks ago /bin/sh -c apk add --no-cache libstdc++ 3.46MB ... 4 weeks ago /bin/sh -c #(nop) ADD file:abc... in / 7.34MB 各行が 1レイヤ 「 apk add ... でX MB増えた」が見えるので、ダイエットの手がかりになる Docker入門 / 技育アカデミア 17
  10. イメージタグの読み方とベストプラクティス postgres:15.4-alpine │ │ └─ バリアント(alpine / slim など) │

    └───── バージョン └─────────────── イメージ名 パターン 使う場面 nginx / nginx:latest NG(再現性が壊れる) nginx:1.27 マイナー固定。開発で許容 nginx:1.27.0 パッチ固定。本番推奨 Docker入門 / 技育アカデミア 19
  11. 最初の一歩 — docker run hello-world $ docker run hello-world Hello

    from Docker! This message shows that your installation appears to be working correctly. 裏で起きていること: 1. ローカルに hello-world イメージがあるか確認 2. なければ Docker Hub からpull 3. イメージからコンテナを作成 4. コンテナの中でプログラムを実行("Hello from Docker!" を出力) 5. プログラム終了とともにコンテナも停止 Docker入門 / 技育アカデミア 21
  12. 主要コマンド コマンド 役割 docker run <image> イメージからコンテナを起動 docker ps 動作中のコンテナ一覧

    docker ps -a 停止中も含めた全コンテナ docker logs <id> コンテナのログを見る docker exec -it <id> bash 動作中コンテナに入る この5つで「動かす・状態を見る・ログを読む・中に入る」が全部できる。 Docker入門 / 技育アカデミア 22
  13. その他のコマンド コマンド 役割 docker pull <image> レジストリからイメージを取得 docker images ローカルのイメージ一覧

    docker stop <id> コンテナを停止 docker rm <id> コンテナを削除 docker rmi <image> イメージを削除 Docker入門 / 技育アカデミア 23
  14. 実例: nginxを起動してブラウザで見る # バックグラウンドでnginxを起動、ホストの8080をコンテナの80に繋ぐ $ docker run -d --name web

    -p 8080:80 nginx:1.27 # 動いているか確認 $ docker ps CONTAINER ID IMAGE STATUS PORTS NAMES abc123 nginx:1.27 Up 5 seconds 0.0.0.0:8080->80/tcp web # ブラウザで http://localhost:8080 を開く → Welcome to nginx! が見える # 停止して削除 $ docker stop web $ docker rm web -d = バックグラウンド / -p host:container = ポート公開 / --name = 名前付け Docker入門 / 技育アカデミア 24
  15. よく使うオプション オプション 意味 -d デタッチ(バックグラウンド実行) -it 対話的に使う( bash 等を起動したいとき) --rm

    終了時にコンテナを自動削除 --name <name> コンテナに名前を付ける -p HOST:CONTAINER ポートを公開する Docker入門 / 技育アカデミア 25
  16. その他のオプション オプション 意味 -v HOST:CONTAINER ボリュームをマウントする -e KEY=VALUE 環境変数を渡す --env-file

    <path> ファイルから環境変数を読み込む --network <net> 接続するネットワークを指定 --platform=linux/amd64 プラットフォームを明示 --restart=always 落ちたら自動再起動 Docker入門 / 技育アカデミア 26
  17. 環境変数の渡し方 3パターン # ① -e で個別に渡す $ docker run -e

    DB_HOST=db myapp # ② --env-file でファイルから読み込む $ docker run --env-file .env myapp # ③ ホストの環境変数を透過させる(値を書かない) $ docker run -e DB_HOST myapp 機密情報を -e で直書きすると docker inspect で見えてしまう .env は .gitignore に必ず追加(コミット事故が頻発) Docker入門 / 技育アカデミア 27
  18. docker exec でコンテナの中に入る # bashを起動して対話的に入る $ docker exec -it <container>

    bash # 中で1コマンドだけ実行する $ docker exec <container> ls /app # alpineコンテナには bash が無いので sh を使う $ docker exec -it alpine-container sh デバッグの基本コマンド。 「コンテナ起動してるのに繋がらない…」のとき、まずこれ で中を確認する。 Docker入門 / 技育アカデミア 28
  19. Dockerfile 主要命令 命令 意味 FROM ベースイメージを指定(必ず最初) WORKDIR 以降の作業ディレクトリ COPY ホストのファイルをイメージにコピー

    RUN ビルド時にコマンドを実行(レイヤを増やす) CMD コンテナ起動時に実行するデフォルトコマンド 最初はこの5つから。残りは必要になったら覚えればOK。 Docker入門 / 技育アカデミア 30
  20. その他の命令 命令 意味 ENV コンテナ実行時にも残る環境変数 ARG ビルド時だけ使える引数 EXPOSE 公開するポートを宣言(ドキュメント目的) ENTRYPOINT

    コンテナの「中身そのもの」になるコマンド USER 実行ユーザーを切り替える(本番では必須) HEALTHCHECK 死活監視コマンド Docker入門 / 技育アカデミア 31
  21. 実例: Node.jsアプリのDockerfile FROM node:20-alpine WORKDIR /app # 依存ファイルだけ先にコピー(キャッシュ効かせるため) COPY package.json

    package-lock.json ./ RUN npm ci # 残りのソースコードをコピー COPY . . EXPOSE 3000 CMD ["node", "server.js"] Docker入門 / 技育アカデミア 32
  22. ビルドして動かす # Dockerfileのあるディレクトリで実行 $ docker build -t myapp:1.0 . #

    -t : タグ付け(imageName:tag) # . : ビルドコンテキスト(このディレクトリ以下をDockerに渡す) # 出来上がったイメージから起動 $ docker run -d -p 3000:3000 --name app myapp:1.0 # ログを追いかけて確認 $ docker logs -f app docker build の最後の . を忘れがち( 「カレントディレクトリをビルドコンテ キストに渡す」の意味) Docker入門 / 技育アカデミア 33
  23. CMD と ENTRYPOINT の違い 設定 役割 CMD デフォルトで実行するコマンド。 docker run

    image arg で簡単に 上書き ENTRYPOINT コンテナの本体プロセス。 docker run image arg の arg は ENTRYPOINT の引数として渡る 3つの使い分けパターンを次のスライドで見ます。 Docker入門 / 技育アカデミア 34
  24. CMD と ENTRYPOINT — 3つのパターン # パターン1: CMDだけ → 「デフォルト動作」を提供

    CMD ["node", "server.js"] # パターン2: ENTRYPOINTだけ → 「このイメージは常にこのコマンド」 ENTRYPOINT ["python", "main.py"] # パターン3: 両方 → 「ENTRYPOINTがツール本体、CMDがデフォルト引数」 ENTRYPOINT ["python", "main.py"] CMD ["--help"] パターン3の動作: docker run myapp → python main.py --help docker run myapp arg1 arg2 → python main.py arg1 arg2 Docker入門 / 技育アカデミア 35
  25. COPY と ADD の違い 命令 用途 COPY ホストのファイルをコピー(基本これ) ADD COPY

    + URLダウンロード や tar展開(副作用多い、避ける) COPY src/ /app/src/ # 基本はこれ ADD https://example.com/file.tar.gz /tmp/ # ADD のURL機能 ADD archive.tar.gz /app/ # ADD のtar展開機能 公式も「COPYはADDより明示的で予測可能なので、こちらを使え」と推奨。 Docker入門 / 技育アカデミア 36
  26. ARG と ENV の違い 命令 いつ使われるか コンテナに残るか ARG ビルド時のみ 残らない

    ENV ビルド時 + 実行時 残る ARG VERSION=1.0 # --build-arg VERSION=2.0 で上書き可能 ENV APP_VERSION=${VERSION} # コンテナ実行時にも残る シークレットは ARG でも ENV でも渡さない。 docker history でレイヤに残るの で、BuildKit の --mount=type=secret を使う。 Docker入門 / 技育アカデミア 37
  27. マルチステージビルド 「ビルド時に必要なもの」を最終イメージに残さないテクニック。 # ステージ1: ビルド FROM golang:1.22-alpine AS builder WORKDIR

    /src COPY . . RUN CGO_ENABLED=0 go build -o /out/app . # ステージ2: 実行(ビルドツールを含まない) FROM alpine:3.20 COPY --from=builder /out/app /app CMD ["/app"] 最終イメージから Go本体・ソースが消える → 数百MB → 数MB に。 Docker入門 / 技育アカデミア 38
  28. キャッシュ判定のルール 命令 キャッシュ判定の方法 RUN / ENV / WORKDIR 等 Dockerfileに書かれた文字列が前回と同じか

    COPY / ADD コピー対象ファイルの内容(チェックサム)が同じか 上から順に判定し、1つでも「変更あり」と判定されると、それ以降は全部キャッ シュ破棄 だから「変更されやすいもの」ほど Dockerfileの後ろに書く のが鉄則 Docker入門 / 技育アカデミア 41
  29. キャッシュを効かせる書き方 — 悪い例 FROM node:20-alpine WORKDIR /app # ソース全部を先にコピー COPY

    . . # 依存インストール RUN npm ci CMD ["node", "server.js"] 問題: ソースを1行変えただけで npm ci がやり直しになる。 毎ビルド数分待たされる地獄。 Docker入門 / 技育アカデミア 42
  30. キャッシュを効かせる書き方 — 良い例 FROM node:20-alpine WORKDIR /app # 依存ファイルだけ先にコピー COPY

    package.json package-lock.json ./ RUN npm ci # ソースは最後にコピー COPY . . CMD ["node", "server.js"] 効果: package.json が変わらない限り npm ci のキャッシュが効く。 この順番がキャッシュを活かすコツ。 Docker入門 / 技育アカデミア 43
  31. キャッシュのヒット/ミスを判定する流れ 命令を見てハッシュを計算 │ ▼ 同じハッシュのレイヤが ── Yes ──► キャッシュ命中(既存レイヤを再利用) ローカルにある?

    │ No ▼ 新しいレイヤを生成 ──► これ以降の命令は **全部キャッシュ無効** つまり、 「上の方の命令ほどキャッシュの恩恵が大きい」 。 だから「変更頻度の低いもの → 高いもの」の順に Dockerfile を書く。 Docker入門 / 技育アカデミア 44
  32. .dockerignore でビルドコンテキストを軽くする .gitignore の Docker 版。 docker build 時にカレントディレクトリ以下がまるごと Docker

    daemonに送られるので、不要ファイルを除外すると 転送が速くなる & COPY . . でキャッシュが壊れる事故を防げる。 # .dockerignore の例 node_modules .git .env *.log dist/ .DS_Store Docker入門 / 技育アカデミア 45
  33. RUN は && でまとめる # NG: レイヤが増え、apt-get update のキャッシュが古くなる RUN

    apt-get update RUN apt-get install -y curl # OK: 同じRUNにまとめる RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* レイヤ数を減らし、 update と install の整合性も保てる。 Docker入門 / 技育アカデミア 46
  34. BuildKit のキャッシュマウント # syntax=docker/dockerfile:1.7 FROM node:20-alpine COPY package*.json ./ RUN

    --mount=type=cache,target=/root/.npm npm ci /root/.npm を Docker が管理する永続キャッシュとして保持し、 --no-cache でビル ドし直してもダウンロード済みパッケージは再利用される。 npm 以外: pip → /root/.cache/pip 、Go → /root/.cache/go-build 、apt → /var/cache/apt 。 Docker入門 / 技育アカデミア 48
  35. イメージサイズ削減のコツ 1. ベースイメージを slim / alpine に変える 2. マルチステージビルドでビルド用と実行用を分ける 3.

    RUN を && でまとめる、最後に rm -rf で削除物を片付ける 4. .dockerignore で node_modules / .git を必ず除外 5. パッケージマネージャの キャッシュを消す(apt: /var/lib/apt/lists/* ) Docker入門 / 技育アカデミア 49
  36. 2種類のマウント バインドマウント docker run -v $(pwd):/app myapp ホストのディレクトリをそのままマ ウント ホストの変更がコンテナに即反映

    開発時のソースコード共有に最適 名前付きボリューム docker run -v pgdata:/var/lib/postgresql/data postgres Docker が管理する領域にマウント ホストのファイルシステムからは見 えない DBデータの永続化に最適(公式推 奨) Docker入門 / 技育アカデミア 53
  37. docker volume コマンド $ docker volume create mydata # 作成

    $ docker volume ls # 一覧 $ docker volume inspect mydata # 詳細(ホスト上のパスや作成日時など) $ docker volume rm mydata # 削除(中身も消える!) $ docker volume prune # 使われてないボリュームを一括削除 Docker入門 / 技育アカデミア 54
  38. ユーザー定義ネットワークの強み コンテナ名でDNS解決できる。 $ docker network create mynet $ docker run

    -d --name db --network mynet postgres:15.4 $ docker run -d --name app --network mynet myapp $ docker exec app curl http://db:5432 # ← コンテナ名で繋がる Docker の埋め込みDNSサーバ ( 127.0.0.11 ) がコンテナ名→IPを解決してくれる。デ フォルトのbridgeでは名前解決できないので、必ず network create するか Compose を使う。 Docker入門 / 技育アカデミア 55
  39. ポートマッピング — 外の世界とつなぐ docker run -d -p 8080:80 nginx #

    -p <ホスト側>:<コンテナ側> ホスト側で別ポートにぶつかる心配があるなら左を変える -p 127.0.0.1:8080:80 で外部公開を防ぐバインドも可能 Docker入門 / 技育アカデミア 56
  40. 実際のアプリは「複数コンテナ」で動く これを docker run だけで管理すると: 長い docker run ... を何個も叩く必要がある

    起動順序を人間が管理する( sleep 10 で雑に待つ世界) ネットワーク作成も自前 停止も docker stop を全部に対して… チーム共有が現実的に不可能になる。 Docker入門 / 技育アカデミア 58
  41. Docker Composeなら1コマンド $ docker compose up -d これだけで、以下を全自動で実行してくれる: ネットワーク・ボリュームの作成 イメージの

    build / pull depends_on + healthcheck による起動順序の制御 全コンテナの起動 Docker入門 / 技育アカデミア 59
  42. Docker Compose の公式ポジション Compose は、マルチコンテナアプリケーションを1つのYAMLファイルで定義・実 行するためのツール 主な利点: マルチコンテナを1ファイルで定義 → チームで共有可能

    変更のないサービスは既存コンテナを再利用(高速) 変数機能で環境差を吸収 compose.yaml を Git に置けば、 git clone → docker compose up -d で環境が完成 する。 Docker入門 / 技育アカデミア 60
  43. compose.yaml のトップレベル構造 name: myapp # プロジェクト名 (省略可) services: # ←

    コンテナ群の定義(必須) app: ... volumes: # ← 名前付きボリュームの宣言 pgdata: networks: # ← ネットワーク(任意) secrets: # ← シークレット(任意) configs: # ← 設定ファイル(任意) 多くのプロジェクトでは services + volumes だけで十分。 他のキーは「そういうのもある」程度で覚えておけばOK。 Docker入門 / 技育アカデミア 61
  44. compose.yaml のサンプル services: app: build: . ports: ["8080:3000"] depends_on: db:

    { condition: service_healthy } db: image: postgres:15.4 environment: { POSTGRES_PASSWORD: password } volumes: ["pgdata:/var/lib/postgresql/data"] healthcheck: { test: pg_isready, interval: 1s, retries: 60 } volumes: pgdata: Webアプリ + DB の典型構成。 Docker入門 / 技育アカデミア 62
  45. サービスとイメージの指定 services 動かしたいコンテナを「サービス」とし て定義。 services: app: ... db: ... サービス名は

    コンテナ間通信のホスト名 にも使える。 build / image build: — Dockerfileからビルドす る image: — レジストリの既存イメー ジを使う services: app: build: . db: image: postgres:15.4 Docker入門 / 技育アカデミア 63
  46. ポート・環境変数・ボリュームの設定 ports / environment ports: - "8080:3000" # host:container environment:

    NODE_ENV: development DATABASE_URL: postgres://...@db:5432/app DBへの接続は サービス名 db をホ スト名として使える 秘密情報は .env + env_file: に 逃がす volumes volumes: - .:/app # bind mount - pgdata:/var/lib/... # named volume トップレベルでも宣言が必要: volumes: pgdata: Docker入門 / 技育アカデミア 64
  47. build: の詳細 services: app: build: context: . # Dockerfileの探索起点 dockerfile:

    ./docker/Dockerfile # Dockerfileの場所 args: { NODE_VERSION: "20" } # ビルド時の引数 target: production # マルチステージのどこまで作るか platforms: [linux/amd64, linux/arm64] 短く書くなら build: . だけでもOK。 Docker入門 / 技育アカデミア 65
  48. 起動順序の制御 — depends_on + healthcheck services: app: depends_on: db: condition:

    service_healthy # ← DB が healthy になるまで待つ db: image: postgres:15.4 healthcheck: test: pg_isready -U postgres interval: 1s retries: 60 depends_on だけでは「起動した」までしか待たない。 「準備完了」を待つには condition: service_healthy をセットで使う。 Docker入門 / 技育アカデミア 66
  49. .env ファイルと変数展開 # .env (プロジェクトルートに置く) POSTGRES_PASSWORD=supersecret APP_PORT=8080 # compose.yaml services:

    app: ports: - "${APP_PORT:-3000}:3000" db: environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} .env の値が ${VAR} として自動展開される。 Docker入門 / 技育アカデミア 67
  50. .env のハマりどころ .env は .gitignore に必ず追加(コミット事故が頻発) ${VAR:-default} で「未定義時のデフォルト値」を指定できる 展開後のyamlを確認したいときは →

    docker compose config .env の置き場所は compose.yaml と同じディレクトリが基本 # 変数が反映されないときに最初に叩くコマンド $ docker compose config # → 展開後の最終yamlが画面に出る # 値が空・古い値ならファイル位置や名前を確認する Docker入門 / 技育アカデミア 68
  51. 日常で使う Compose コマンド # 起動(バックグラウンド) docker compose up -d #

    イメージを再ビルドしてから起動 docker compose up -d --build # 停止 & 削除(ボリュームは残る) docker compose down # データもまっさらに削除(DBデータも消える!) docker compose down -v Docker入門 / 技育アカデミア 69
  52. 状態確認・デバッグ用の Compose コマンド # 状態を確認 docker compose ps # ログをリアルタイムで見る

    docker compose logs -f app # コンテナに入る docker compose exec app bash # 設定の展開後を確認(デバッグ最強) docker compose config docker compose config は .env を展開した最終yamlを表示する。 変数が反映されないときの確認に使える。 Docker入門 / 技育アカデミア 70
  53. トラブル時はこの順で見る 1. docker compose ps で全コンテナの STATUS を確認 2. docker

    compose logs <service> でログを読む(ここが一番大事) 3. ログから原因がわかったら、設定を直して up -d --build 4. どうしても直らない最終手段: down -v && up -d --build (データもキャッシュもまっさらにしてやり直す) Docker入門 / 技育アカデミア 71
  54. compose.yaml 読解の例(抜粋) services: app: build: { context: ., dockerfile: ./docker/Dockerfile

    } ports: ["8080:8000"] volumes: - .:/app # ① ソースを bind mount - app_cache:/root/.cache # ② キャッシュを named volume depends_on: db: { condition: service_healthy } db: image: postgres:15.4 healthcheck: { test: pg_isready -U postgres, interval: 1s, retries: 60 } volumes: ["pgdata:/var/lib/postgresql/data"] volumes: { pgdata: {}, app_cache: {} } Docker入門 / 技育アカデミア 72
  55. 補足: ファイル名の歴史 ファイル名 状況 compose.yaml 現在の推奨(Compose V2 以降) docker-compose.yml 旧名(今も動作する)

    compose.yml これも動作する 旧: docker-compose up (Compose V1, スタンドアロン) 新: docker compose up (Compose V2, Dockerのサブコマンド) 新規プロジェクトは compose.yaml + docker compose 形式で書きましょう。 Docker入門 / 技育アカデミア 73
  56. 今日の振り返り 1. 仮想化とコンテナの位置づけ 2. 3要素:Dockerfile / Image / Container 3.

    CLIの基本とトラブルシュート 4. Dockerfile とマルチステージ 5. ビルドキャッシュの仕組み 6. Volume と Network 7. Docker Compose の使い方 Docker入門 / 技育アカデミア 75
  57. 学ぶときのコツ 「動かす → 中を覗く → 壊して直す」を繰り返す。 公式イメージを docker run して中に

    exec -it bash で入ってみる Dockerfileを写経して docker build してみる わざとtypoして、エラーメッセージを読む練習をする OSSのリポジトリで compose.yaml を読み漁る Docker入門 / 技育アカデミア 77