# Docker入門 Dockerで環境構築を行う前に,pythonの仮想環境について,さらにDockerとは何かを説明します. ## 前置き ### 注意 今日の内容は.コンピュータに関する一定の前提知識が必要になってきます.理解が困難にならないよう,適宜説明を入れるつもりですが,全てを説明しきるのは分量的にも,技術の複雑さからも困難な部分があります. 今回はプロダクト開発関連知識を扱うということなので,講義内容を広く浅くすることで,最低限の知識,技術をつけていただくことと,講習会で扱う内容に関して,皆さんが自分で調査してみるきっかけとなればと考えています. より一層の理解をしていただくために,今回扱った内容で分からない言葉があれば,実際に自分で調べてみてください. ### 前回の復習 pythonの仮想環境 前回はanacondaを使った環境を構築しました. > パッケージによる互換性や依存性に左右されることなく,各プロジェクトで独立に実行環境を用意するため,プロジェクトを始める際は「仮想環境」を導入する必要があります. ### 仮想環境導入のもう1つの意味 仮想環境を作成していない状態で,「`pip isntall`」を用いてパッケージのインストールを行うと,システム全体で使うpython環境に影響があります. パッケージ管理を「`conda`」や「`venv`」,「`vertualenv`」などを使うことによって,プロジェクト単位に留めることにより,これを防ぐことができます. ### 「仮想化技術」との差異 これらはあえていうならば「アプリケーション」レベルで仮想化を行っています.後で登場する「マシンの仮想化」との違いで重要になるポイントですので覚えておいてください. ### プロダクト開発における実行環境 開発したアプリケーションは,開発時に用いた実行環境の上で動作するものです. 従って,アプリケーションを誰かに共有したい場合,共有する相手も同じ実行環境を構築する必要があります. この時,アプリケーションの実行ファイルのみならず,実行環境の共有とその再構築が容易に行うことができれば,非常に便利です. 環境構築とその共有を簡単に実現できるものが,「Docker」(ドッカー)です. ## 仮想化技術とDocker 以下は非常に役に立つDockerのドキュメントです.本講習会もこちらをもとに作成しています.より詳しい情報はこちらから手に入れることができます. Docker Docs: https://docs.docker.com/ Docker Docs 日本語化プロジェクト:https://docs.docker.jp/index.html Dockerは**コンテナ型の仮想環境を構築するための軽量な仮想化プラットフォーム**です. まず,「コンテナとは何か」という疑問が出てきたかと思います. それを説明するために知っておかなければならない「仮想化技術」の変遷を見ていこうと思います.  引用(https://kubernetes.io/ja/docs/concepts/overview/what-is-kubernetes/) ### 仮想化技術が登場する前 従来は,物理的に1つのサーバの上でアプリケーションを実行していました. このとき,複数のアプリケーションを同時に実行したいとします. 1つのマシンの上で.各アプリケーションが使用可能な計算資源(メモリ,CPUコア等々)を効率よく割り当てる方法がなく,「**リソースの割り当て問題**」がありました. 例えば、複数のアプリケーションを実行させた場合、ひとつのアプリケーションがリソースの大半を消費してしまうと、他のアプリケーションのパフォーマンスが低下してしまうことがありました。 この解決策として,アプリケーションを別々のサーバ,物理的に異なるサーバで動かすことが行われていました. しかし,複数のサーバを維持することは費用が嵩みます. また,マシンがオーバースペックにも関わらず.1つのアプリケーションしか動かしていないのであれば,余った計算リソースがもったいないですね. 故に,「**1つのマシンの上で複数のアプリケーションを効率的に同時に動かす**」必要がありました. ### 仮想マシンの登場 上記問題の解決策として,「仮想化」が登場します.「仮想化」とは,物理的には1つのサーバのCPU,メモリなどのリソースを論理的に分割・統合する技術です.(これを実現するソフトウェアをHypervisorと呼びます.) この仮想化によって,物理的に1つのサーバを,あたかも複数のサーバが存在しているかのように扱うことができます.この仮想化によって分割されたマシンを「仮想マシン」と呼びます. 仮想マシンを動かす方法はいくつかありますが,ホストのOSの上に仮想化ソフトウェアを用いて仮想マシンを立ち上げる方法があります. 上図にあるように,仮想マシン環境には新たに別のOSがインストールされて稼働します.仮想マシン上で動くOSのことをゲストOSと呼びます. 仮想化では,アプリケーションが仮想マシンごとに隔離されます.これによってリソースの効率的な運用がなされるようになります. 先述したように,仮想化技術が生まれる前は,物理的に複数の独立したサーバでアプリケーションを複数動かしていました. ここに仮想化技術が導入されたことで,効率的にアプリケーションを動かすことが可能になりましたが,それだけではありません. **仮想マシン自体は物理的に存在せず,またアプリケーションごとに環境が隔離されます.これにより実行環境の使い捨て,交換,更新が容易になりました.** ### コンテナ技術の登場 ここではDockerで取り扱うことが可能になる「コンテナ技術」について導入します. 先ほど説明した「仮想化」では,立ち上げた仮想マシンごとに各自のOSを持っていました. 一方でコンテナ技術は,ホストOSの「カーネル」は共有した上で,CPUやメモリ等のリソースを隔離し,「コンテナ」と呼ばれる仮想環境を作成します.(上図参照) 各コンテナごとにゲストOSを稼働させるわけではないため,その分仮想マシンよりも処理が軽量です. また,OSそのものを立ち上げるわけではないため,仮想マシンと比較して起動が速いです. #### カーネルとは何か  OSには中核部分としてアプリケーションとハードウェアでの実際の情報処理との間の掛橋のことです. ### Docker イメージとコンテナ Dockerにはイメージとコンテナという概念があります. 大まかに説明すると, コンテナのベースとなる雛形としてあるものがイメージであり,コンテナは実際にホストマシンから隔離されて動く環境です. コンテナはイメージを実行することにより作成されます. コンテナにはイメージは大きなサイズ(100MB~2GB程度)のファイルになりますがコンテナは非常に軽量(数十KB程度)です. 厳密な説明ではありませんが,pythonではクラスをインスタンス化して作成しますよね.イメージとコンテナの関係はこれに似ていると言えるでしょう. 以上のことから,イメージを共有することで,環境を共有することができるのがわかるかと思います. ### Dockerを使った環境構築の意義 * 撤去が容易.コンテナを削除するだけ. * ホストマシンの環境を一切変更しない.コンテナはホストマシンの環境とほぼ完全に独立しており,隔離が可能. * 環境の共有が容易.Dockerを導入してあるマシン間であれば,OSの違いに縛られることなく環境を共有できる. プロダクト開発において,アプリケーションの実行環境を`Docker`を使って構築することには以上のようなメリットがあります. これらはプロダクト開発を行うにあたってはいずれも非常に重要なことです. 自分のマシンのローカルに実行環境を作成して失敗してしまった際,元の状態を復元することは容易ではありません.Dockerであればコンテナを撤去してイメージを作り直せばいいだけです.コンテナに対しては,よく「ephemeral; はかない」という言葉が使われます. また,実行環境を共有したいのであれば,イメージを共有し,Dockerが動く環境を用意すれば動作環境が簡単に再現できます. ## UbuntuでDockerを体験しよう ### 自分が作りたい環境のイメージを作成する Dockerで環境を構築する際,自分が構築したい環境のイメージを作成する必要があります. 一からイメージを作ることも可能ですが,多くの場合,既に作成されて公開されているイメージをベースとして,自分作りたい環境に合わせた内容を付け足すことによってイメージを作成します. ## Dockerfile DockerイメージはDockerfileというテキストファイルに書かれた命令をDockerが読み込みことで自動的に作成されます. 従って,自分がベースとしたいDocker イメージが何なのかや,追加するパッケージや処理を命令として記述してDockerfileとし,これをビルドすることでDockerイメージが作成できます. ### Dockerで環境構築を行う流れ 1. `Dockerfile`を書く.どのイメージをベースとしてどんなイメージを新しく構築するかを記述する. 2. イメージを`Dockerfile`からビルドする. 3. イメージからコンテナを作成し,コンテナを起動する. 4. コンテナにアクセスする. ## Dockerfileでイメージ構築 https://docs.docker.com/engine/reference/builder/ http://docs.docker.jp/v1.9/engine/reference/builder.html 以下にpython環境を構築するDockerfileを示しました. 上から順に処理することで指定されたイメージが構築されます. ```Dockerfile FROM python@sha256:1962807996bcd816a82a7bc4d567bd973b149f1b05e05ad982129fe5d2787822 RUN apt-get update \ && apt-get upgrade -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && pip install -upgrade pip WORKDIR /usr/app CMD ["bash"] ``` ### DockerHub https://hub.docker.com/ `FROM`はDockerHub等にアップロードされている公開リポジトリから自分がビルドするイメージのベースにするイメージを取得できます.DockerHubにはさまざまな環境のイメージが公開されています.自分が作成したい環境に適したイメージをベースとすることで効率よく環境構築が可能です.通常の記述形式は`FROM BaseImageName:tag`です. 上記Dockerfileでは,DockerHub上の`python:3.8`の`arm64`用を用います.Jetson NanoのCPUアーキテクチャは`Arm64`ですから,これに適したイメージを選択しなければなりません.今回は`@`以降にdigestとよばれるイメージのシリアルナンバーのようなものを用いて,pythonの3.8タグの中のfor `arm64`を指定しました. `RUN`命令はイメージを構築する過程で実行するコマンドを記述します.OSの更新,キャッシュの削除,pipのバージョン更新を行いました. `WORKDIR`命令はワーキングディレクトリを指定します.これ以降に記述された命令はここで指定されたディレクトリで行われることになります.これは複数使うことが可能です. 以下のようにすると,最後の`pwd`コマンドは`/a/b/c`という出力結果を返します. ```Dockerfile WORKDIR /a WORKDIR b WORKDIR c RUN pwd ``` `CMD`ではコンテナを立ち上げる際に実行するコマンドを指定します. 今回はbashを立ち上げています. その他コンテナ起動までになされるべき処理をここに書き込み,イメージを構築します. ## イメージのビルド 以下のコマンドでDockerfileからイメージを構築します. ``` $ docker build -t [作成するイメージ名]:[タグ名] [Dockerfileのあるディレクトリ] ``` ``` $ docker build -t python3_8:test . ``` 以下のコマンドでイメージの一覧を確認できます. ``` $ docker images ``` ## コンテナの起動 以下のコマンドでコンテナを起動します. ``` $ docker run -it -p [ホストのポート:コンテナのポート] -v [ホストのディレクトリ:コンテナのディレクトリ] -w [作業ディレクトリ] --name [コンテナ名] [イメージ名]:[タグ名] ``` ``` $ docker run -it -p 8000:8000 -v $PWD:/usr/app --name python3_8_con python3_8:test ``` これで,コンテナが起動しているはずです.今回は,`Dockerfile`にて`CMD`で"bash"を実行しています. そのため,最初に「コンテナ内」のbashが立ち上がるはずです. それぞれのオプションを説明します. ### `-it` 疑似 TTY(pseudo-TTY)をコンテナの標準入力に接続するよう、 Docker に対して命令します。つまり、コンテナ内でインタラクティブな bash シェルを作成します。 ### `-p` 上のコマンドでは`{ホストマシンのポート番号}:{コンテナのポート番号}`という順番で並んでおり,`8000:8000`はコンテナのポート`8000`をホストマシンのポート`8000`にバインド(割り当て)するという意味になります. ### `-v` {ホストのディレクトリ}:{コンテナのディレクトリ}という順で並んでおり,これはホスト側のディレクトリをコンテナの指定したディレクトリにマウントできます.このマウントに関しては,後ほど詳しく説明します. ### `--name` こちらは作成するコンテナの名前を指定します. ### [イメージ名]:[タグ名] 最後に,用いるイメージとそのタグを指定しましょう. ## コンテナはホストから隔離された環境 少しこのコンテナの中を覗いてみましょう. ``` # cat /etc/os-release ``` を実行することで大抵のLinuxディストリビューションは環境情報をみることができます. ``` PRETTY_NAME="Debian GNU/Linux 10 (buster)" NAME="Debian GNU/Linux" VERSION_ID="10" VERSION="10 (buster)" VERSION_CODENAME=buster ID=debian HOME_URL="https://www.debian.org/" SUPPORT_URL="https://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" ``` 先ほどまでのJetsonの環境で動いていたのはUbuntuでしたが, こちら`Debian GNU/Linux`と呼ばれるOSが動いていることがわかります. コンテナ内は異なる環境であることが理解できたかと思います. 他にもみてみましょう. ターミナルをもう1つ開きましょう. コンテナに入った状態のターミナルとホストマシンのままのターミナルの両方でルートディレクトリ(階層構造に唯一存在する最上位 (根; root) のディレクトリ)に移りましょう. ``` $ cd / ``` そこから`cd`でディレクトリをみていってください. 内容が異なっていることがわかりますか?このように,コンテナとはホストマシンとは完全に独立したものなのです. 通常、コンテナはそのコンテナの中にあるデータだけを利用できます。あるコンテナから、別のコンテナの中にあるデータや、Dockerホストのデータを利用することはできません。 ## コンテナの状態を確認, コンテナの起動,停止 docker コンテナの状態をみてみましょう. 以下のコマンドをコンテナのターミナルで実行してください. コンテナのbashが閉じてホストマシンに移行します. ``` # exit ``` 続いてホストマシンのbashで以下のコマンドを実行しましょう.現在存在しているコンテナの一覧を見ることができます. ``` $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ``` 上の各情報名の下にコンテナの情報が表示されます.先ほど`docker run`した際に指定した名前がついていることが確認できます.ここで,`PORTS`を見ると割り当てた内容が書かれていますね.`STATUS`を見ると,`exited`となっていることが確認できると思います. ここで,以下のコマンドを実行してください. ``` $ docker start python3_8_con $ docker ps -a ``` `STATUS`が`UP`に変更されているのがわかると思います.コンテナから`docker run -it `で作成して起動して入ったコンテナで`exit`するとコンテナは停止します. `docker start`は停止状態のコンテナを起動します.`Up`は稼働しているコンテナを指します.コンテナはバックグラウンドで稼働しています. `docker start`で起動したコンテナに,もう一度入ります.以下コマンドを実行しましょう. ``` $ docker exec -it python3_8_con /bin/bash ``` これでもう一度コンテナにはいることができました. ここで`exit`してみましょう. ``` # exit ``` `docker exec`でコンテナに入り,`exit`した状態です.ここで`docker ps -a`してコンテナを確認すると,`STATUS`が`Up`であることが確認できます. つまり,`docker exec`でコンテナに入ると,`exit`してもコンテナは停止せず,バックグラウンドで動き続けます. 停止したい時は以下で停止できます. ``` $ docker stop python3_8_con ``` もう1つコンテナに入る方法として,`docker attach`があります. それではコンテナをもう1度起動したら,以下コマンドでコンテナに入りましょう. ``` $ docker attach <コンテナ名> ``` そして`exit`してください.続いてコンテナの状態を確認しましょう. コンテナが停止していることが確認できます.`exec`と`attach`では抜けた後のコンテナの状態が異なるのです. ## コンテナの削除 コンテナを以下コマンドで削除しましょう.その後,コンテナの一覧で削除されたことを確認してください. ``` $ docker rm <コンテナ名> ``` ### `docker run -rm` 先ほどの`docker run`コマンドのオプションに以下のように`-rm`を追加して実行してみましょう. ``` $ docker run -it -rm -p 8000:8000 -v $PWD:/usr/app --name python3_8_con python3_8:test ``` そして`exit`で抜けてみてください.その後,コンテナの一覧を確認すると,コンテナが削除されていることがわかるかと思います.`-rm`オプションは,コンテナを停止するとコンテナが自動で削除される設定にするときに使います. ## ホスト上のディレクトリをコンテナにマウント 今回は`-v`オプションを使ってホストのディレクトリをコンテナの指定したディレクトリにマウントしました. それでは,現在のディレクトリ`/usr/app`の中を見てます. ``` # ls -a Dockerfile # cat Dockerfile ``` ```dockerfile= FROM python@sha256:1962807996bcd816a82a7bc4d567bd973b149f1b05e05ad982129fe5d2787822 RUN apt-get update \ && apt-get upgrade -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && pip install -U pip WORKDIR /usr/app/ CMD ["bash"] ``` 以上から,こちらの`Dockerfile`は先ほど作成したものと同じであることがわかります.これは`docker run`を実行する際に`-v`オプションでホストのカレントディレクトリをコンテナの`/usr/app`にマウントするよう指定したからです. ### 3種のマウント Dockerにおける「マウント」には3つの種類が存在します.「バインドマウント」,「ボリュームマウント」,「一時ファイルシステムのマウント」の3つです. #### まずは,マウントしない時 マウントしないとどうなるのでしょうか? 一旦コンテナを全て削除し,`-v`オプションを付けない以下のコマンドを実行しましょう. ``` $ docker run -it -rm -p 8000:8000 --name python3_8_con python3_8:test ``` コンテナに入ったら,新しくテキストファイルを作成し,"hello"と書き込みましょう. ``` # touch hello_con.txt # vi hello_con.txt ``` コンテナを抜けます.`-rm`オプションのおかげでコンテナは削除されました. コンテナとホストマシンは通常隔離されていますから,コンテナ内部にアクセスするにはコンテナにはいる必要があります.しかしコンテナは削除されたので,もはやアクセスできません.このように,マウントを行わなければ,コンテナ内で編集したものは,コンテナが削除された時点で失われてしまいます. (コンテナを削除さえしなければ,停止,再稼働しても編集した内容は残っています.しかし 安心はできないですね.) #### バインドマウント Dockerホストのファイルやディレクトリをコンテナ上にマウントする機能です。 バインドマウントを行うと、コンテナの外にあるファイルを、コンテナの中から読み書き可能になります。 先ほどオプションに付けた`-v`がバインドマウントです. 以下を実行してください.(コンテナから出たり入ったりするので`-rm`は今回使いません.) ``` $ docker run -it -p 8000:8000 -v $PWD:/usr/app --name python3_8_con python3_8:test ``` コンテナ内でファイルを作成しましょう. ``` # touch test.txt # vi test.txt ``` exitしてカレントディレクトリを確認してください.新しいファイルが確認できます. 次にホストマシン上でファイルを作成して編集しましょう. そしてコンテナを起動してコンテナに入り,作業ディレクトリを確認しましょう.これもホストマシン上で作ったファイルが確認できます. #### ボリュームマウント ボリュームマウントはDockerが管理している領域上のデータをコンテナへマウントする機能です。ホストマシンのディレクトリではなく,`docker volume create `でdockerの管理下に作ったボリュームをマウントします. #### 一時ファイルシステム Dockerホストにファイルとして保存したくないデータを一時的に利用できるようにする(データ領域をメモリ上に置く)機能です。 ### `docker run`を.shにまとめる コンテナの起動にて,`docker run`コマンドでオプションが多く,とても長い1行のコマンドを使いました.より実践的なコンテナを立てようとすると,さらにオプションが増えてくるので,長いコマンドを自動化することが必要になってきます. そこで,まずは`シェルスクリプト`を使って自動化します. これにより,コマンドを1つのファイルにまとめて保存し,編集,実行が行えます. それでは,ホスト側で以下のファイル`docker_run.sh`を作成しましょう. ``` #! /bin/bash CUR_DIR=`pwd` echo "$CUR_DIR" docker run -it \ -p 8000:8000 \ -v "$CUR_DIR":/usr/app \ --name python3_8_con \ python3_8:test ``` 保存したら,以下コマンドを実行しましょう. ``` $ bash docker_run.sh ``` これでコンテナのbashが立ち上がれば成功です. ## NGCを利用する. https://ngc.nvidia.com/ >NGC はディープラーニング、機械学習、HPC のための GPU 対応ソフトウェア総合カタログを提供しています。NGC コンテナーは、高性能かつ導入が容易なソフトウェアを提供します。既に NGC コンテナーを使い、最速の結果を出したという実績もあります。インフラの構築から開放されるので、NGC を利用して、ユーザーは効率の良いモデルの構築、最適なソリューションの実現、短時間でのインサイトの収集に集中できます。 (https://www.nvidia.com/ja-jp/gpu-cloud/containers/ より) 先ほどはDocker Hubに共有されているイメージをベースとして,Dockerfileを作成することで自分のイメージを作成し,それを元にコンテナを起動する方法を説明しました. しかしながら,GPUを使った処理を行う場合,作成するコンテナをGPUに対応させる必要が出てきます.NVIDIAドライバのインストールなど, 実行する環境によってさまざまなソフトウェアをインストールするなどしてインフラ構築を行う必要があります. これはJetson Nanoにおいても同様です.NGCにはNVIDIA製品で開発を行うために必要となる環境を少ない手順で作成できるよう,それらを使うためのコンテナが公開されています. ここでは,NGCに公開されている, Jetson Nano用Jetpackがベースのコンテナを`docker pull`し,自分のJetson Nano上に深層学習,機械学習用のコンテナを作成しましょう. ### pullするコンテナを選択 https://ngc.nvidia.com/ にアクセスし,検索によりJetson 関連のコンテナを絞り込みます. 今回は深層学習,機械学習用のフレームワークを動かすことのできる`NVIDIA L4T-ML`(https://ngc.nvidia.com/catalog/containers/nvidia:l4t-ml) というコンテナをカタログから使用します. ### pullする カタログのoverviewの下部にコンテナを起動するまでのコマンドが書かれているので,公開されている環境に手を加えず,そのままの環境で扱いたい場合は,そのコマンドを実行します.この時,イメージのタグをしっかりとみて,ホストOSに対応しているか確認してください. ``` sudo docker pull nvcr.io/nvidia/l4t-ml:r32.5.0-py3 ``` ``` sudo docker run -it --rm --runtime nvidia --network host nvcr.io/nvidia/l4t-ml:r32.5.0-py3 ``` ### 演習 #### ステップ1 Dockerfileを自分で作成し,Jetson上にpytorchの環境を構築してください. #### ステップ2 コンテナにアクセスし,インタラクティブモードからtorchのバージョンを表示し,pytorhがインストールされた環境を確認してください. #### ヒント NGCを使いましょう. ### Headless modeについて Deep Lerning InstituteではヘッドレスデバイスモードでJetsonを扱いました.この方法で扱いたい方は, >Unless a user-provided run command overrides the default, a JupyterLab server instance is automatically started along with the container. You can connect to it by navigating your browser to http://localhost:8888 (or substitute the IP address of your Jetson device if you wish to connect from a remote host, i.e. with the Jetson in headless mode). Note that the default password used to login to JupyterLab is nvidia. Overviewにこう書かれているため,使うことができます. ## remote-Containerを使ってVScodeからコンテナ内を操作する 参考資料:https://code.visualstudio.com/docs/containers/ssh ### 前準備 作業PC側の設定を行います.(Jetsonではない) #### windows10の場合 管理者権限でコマンドプロンプトを開いてください. ``` $ sc config ssh-agent start=auto $ net start ssh-agent ``` 以上を実行したのち, 以下のコマンドを実行して下さい. ``` $ ssh-add <keyfile> # <keyfile>は秘密鍵のパス ``` #### macOSの場合 VScodeで現在開いている「作業PC」のシェルで以下を実行してください. ``` $ ssh-add <keyfile> # <keyfile>は秘密鍵のパス ``` を行ってください.このコマンドはシェル起動ごとに実行する必要があります. シェル起動時に実行するコードを指定する`.bashrc`や`.zshrc`といったファイルに書き込んでおくと便利です.ぜひ自分で調べてやってみてください. ## VScodeの設定 新しいVScodeのウインドウを用意してください. まずは拡張機能の"Docker"を「ローカル環境」にインストールしてください.(Jetson側ではない.) VScodeの`settings.json`を開きましょう. 下の一行を加えましょう. ``` "docker.host": "ssh://{username}@{jetsonのIP}", ``` Remote Explorer を開きましょう.Containersを選んでください. 現在アクセス可能なコンテナが表示されます.通常はローカル環境に存在するコンテナが表示されますが,先ほど`settings.json`に書き込んだおかげで,Jetson上のコンテナにもアクセスが可能になります! ## 最後に Dockerは他にも様々な機能,手技があります. この教材でDockerについて全て語ることは不可能ですので,何かわからないことがあれば,以下リンクがDockerの日本語ドキュメントですので,こちらぜひ参考にしてください. https://matsuand.github.io/docs.docker.jp.onthefly/ ## VScodeに関して役立つ資料 - VScode Remote-sshについて https://code.visualstudio.com/docs/remote/ssh - VScode Remote-Containerについて https://code.visualstudio.com/docs/remote/containers - VScode拡張機能 "Docker" について https://code.visualstudio.com/docs/containers/overview
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up