😎

WSL2でDevcontainerでBashとPythonのデバッガーが使える環境構築の話

に公開

はじめに

この記事は
https://zenn.dev/tazzae999jp/articles/a8e394f10a6c1f
の続編として書いてる。
上記の記事では、pythonのプログラムをコンテナで動作、デバッガーはホストで動作でattachでのリモートデバッグの話を書いた。「docker compose」の環境。
今回も「docker compose」の環境だが、Devcontainerでの環境構築を行った。
理由は、加えて、bashのシェルスクリプトのデバッガー環境が必要となったため。
bashのシェルスクリプトのデバッガーのVSコードの拡張機能はlaunch方式のものしか
存在しなかったため、このbashのシェルスクリプトも本番環境を想定したコンテナでしか動作できない。

補足
rogalmic.bash-debug 0.3.9 は launch 方式のみで、attach はなし。そのためデバッガもコンテナ内で起動させる必要がある。

そのため、プログラム本体とデバッガーの両方をコンテナで動作させるしかない。
結果、Devcontainerを利用する必要があったのである。

諸事情があり、ポータブル版のVSコード1.63.2を使う必要があった

ポータブル版のVSコード1.63.2の
マイクロソフト公式のダウンロードのURLは

https://update.code.visualstudio.com/1.63.2/win32-x64-archive/stable

である

~/.bashrc
に下記の記述を追加した

# porttable vs code 1.63.2
pt_code () {
  local exe="/mnt/c/Tools/VSCode-1.63.2/Code.exe"
  local target="${1:-$PWD}"
  nohup "$exe" --remote "wsl+${WSL_DISTRO_NAME}" "$target" >/dev/null 2>&1 </dev/null &
  disown
}

これで、ubuntuのターミナルで、
開きたいワークスペースフォルダをカレントディレクトリとした状況で
pt_code
を打ち込むことで、そのフォルダをワークスペースフォルダとして、
ポータブル版のVSコード1.63.2が使える状況となった。

ポータブル版のVSコードのお作法として、
Zipを展開し、例として
C:\Tools\VSCode-1.63.2
などに展開したら、すぐに!!!(まだ、一回も起動してない状況ですぐに)
直下に、dataフォルダを作る のがお作法である。(ポータブル版のVSコードの鉄則!!)

今回はバージョン固定が、必須のため
C:\Tools\VSCode-1.63.2\data\user-data\User\settings.json
をすぐにさっさと作る必要ありました!!
その中身は、

{
  "update.mode": "none",
  "extensions.autoCheckUpdates": false,
  "extensions.autoUpdate": false,
  "telemetry.enableTelemetry": false,
  "telemetry.enableCrashReporter": false,
}

この設定値の意味は、下記のとおりである

update.mode: "none"
VS Code 本体の自動更新を完全停止する。今回の環境は 1.63.2 に固定して運用するため、勝手に 1.7x 系へ上がって互換性が崩れる事故を防ぐ。ポータブル版では 初回起動前に data\user-data\User\settings.json を作っておくのが鉄則(先に作らないと、起動時に既定が反映されてしまう)。

extensions.autoCheckUpdates: false
拡張機能の更新確認そのものを止める。古い署名なし拡張(例:ms-python.python 2021.9.1246542782、rogalmic.bash-debug 0.3.9)を指定バージョンのまま固定で使うため、更新の通知・取得を抑止してブレない状態にする。

extensions.autoUpdate: false
拡張機能の自動更新を無効化する。チェックを止めても、個別に更新を押すと上がってしまうため、自動では絶対に上げない安全弁を二重化する。結果として、VS Code 本体・拡張の両方が“その日の検証済みの組み合わせ”で固定される。

telemetry.enableTelemetry: false
使用状況テレメトリの送信を無効化する。イベントや利用統計を外部に出さない。
再現性重視の検証では、余計な通信を発生させず、設定差による挙動ブレを避けられる。
(※ポータブル版なら C:\Tools\VSCode-1.63.2\data\user-data\User\settings.json に記載)

telemetry.enableCrashReporter: false
クラッシュレポート送信を無効化する。例外発生時の自動送信も止まるため、
ログの純度が上がり、不具合の切り分けが単純になる。

運用ポイント:

  • 初回起動前に適用しておくと確実(起動後でも可)。
  • 併せて 本体/拡張の自動更新は無効化update.mode: "none", extensions.autoUpdate:false, extensions.autoCheckUpdates:false)にして、
    “その日の検証済みの組み合わせ”を固定する。
  • 反映確認は ヘルプ → 開発者ツールNetworkdc.services.visualstudio.com / vortex.data.microsoft.com / insights0件であること。

そこまでやったら、powershellにて、下記のコマンドで
「Remote – Containers」と「Remote – WSL」をインストールしておく必要あります。
Windows側の領域へのインストールです。
まず、これをやっておかなければ、このポータブル版のVSコード1.63.2でWSL2の領域を開けません

Remote – Containers

& "C:\Tools\VSCode-1.63.2\bin\code.cmd" --install-extension ms-vscode-remote.remote-containers

Remote – WSL

& "C:\Tools\VSCode-1.63.2\bin\code.cmd" --install-extension ms-vscode-remote.remote-wsl

古いシステムの保守開発のための本番環境を想定したdockerコンテナでの開発環境だが、モダンな環境構築への学びは十分にあると考えている

古いといってもVSコードのバージョンや、拡張機能を古めのバージョンに
ダウングレードしたものを、バージョン固定して利用すれば、構築可能だった

互換性のあるバージョンを選択するところがメンドイだけであり
基本的な考え方や、やり方は、最新バージョンでも、同様であり、
むしろ、最新のモダンな環境構築のほうがもっと楽であろう
( 環境構築時に互換性から来るイレギュラーケースの特別対応などが要らなく、スンナリできるから )

拡張機能も古めのバージョンで、署名なしの時代でのもののため
下記の「 ふつうの入れ方(code CLI) 」
では、署名がない件のエラーが出てインストールに失敗する。
そのため、署名がなくても無理くりインストールして認識させる
下記の「 今回うまくいった入れ方(server.sh を使う) 」
シェルスクリプトに書いて、無理くり動かしてのインストールの対応にしたのである

ふつうの入れ方(code CLI)
「コンテナ内のターミナルで実施」

~/.vscode-server/bin/*/bin/code --install-extension ms-python.python
~/.vscode-server/bin/*/bin/code --install-extension rogalmic.bash-debug
# 確認
~/.vscode-server/bin/*/bin/code --list-extensions --show-versions \
  | grep -E 'ms-python\.python|bash-debug'

今回うまくいった入れ方(server.sh を使う)
「コンテナ内のターミナルで実施」
古い“未署名”VSIXは code だと失敗するため、こちらで導入。

~/.vscode-server/bin/*/server.sh --install-extension /opt/vsix/ms-python.python-2021.9.1246542782.vsix
~/.vscode-server/bin/*/server.sh --install-extension /opt/vsix/bash-debug-0.3.9.vsix
# 成功判定(実体ディレクトリの存在)
test -d ~/.vscode-server/extensions/ms-python.python-2021.9.1246542782
test -d ~/.vscode-server/extensions/rogalmic.bash-debug-0.3.9

上記のような、そういう環境構築でのイレギュラーケースの対応は、
他の開発案件の環境構築でも、将来的に出てきそうな話題だと予想します
今回の学びによって、それを対応時の思考の引き出しになると考えており
その参考値として、当記事で今回の話題を書いておくのは、意義があると感じております。

devcontainer.json

"extensions": [
]
で、識別子を書いておけば自動で入る
Devcontainerとしての通常のお作法的なやり方では
拡張機能のインストールができなかった。

補足
devcontainer.json の extensions は 拡張IDだけを受け取り、
常にマーケットプレイスの 最新(互換)版 を入れます。バージョン指定は不可です。
devcontainer.json の extensions は 最新版を自動導入する仕組みです。
今回は 旧版 VSIX を 指定バージョンで固定 したいため使いません。
代わりに ~/.vscode-server/bin/*/server.sh --install-extension /path/to.vsix
で ローカル VSIX を前景ブロック導入しています。

こういった環境構築でのイレギュラーケースでの対応への学びがあった
あったのである。

ですから、この記事で学んだことは、
ぜんぜん、新しめの環境構築でも十分役立つノウハウだと判断し、
当記事としてまとめている次第である。

発端となった環境構築は下記のとおり

2025/09の下旬の話である。
本番環境が古めのLinuxでpython 2.7.5のシステムの保守開発のため、
ローカルにその環境を再現する環境を作った

開発環境としてdocker compose環境を構築した
本番はRed HatだったがDockerHub公開イメージがないため、
ベースになってるCentOSの対応するバージョンでのイメージを使った

dockerコンテナにpython2.7.5の古めのバージョンが入ってる環境で、
WSL2にRemote WSLで接続しているVSコードをホスト側とした環境

この古い環境でのbashやpythonのデバッガーを機能させるための拡張機能は
署名がない時期の古いもののため通常の方法ではVSコードにインストールできない
また、
理由があり、VSコード1.63.2を使わざるを得なかった。
その理由とは、
実際にエラーがでて環境構築ができなかったからです。

エラーの詳細は忘れましたが、Rebuild Without cache reopen のプロセス中で
拡張機能のインストールが絡んだあたりでのエラーだったと記憶してます。

そこで、
ポータブル版のVSコード1.63.2を使うとエラーが解消されうまく環境構築できました。

なぜ、ポータブル版のVSコード1.63.2でうまくいったかというと

互換性維持:本番相当(Python 2.7.5)+当時の拡張(ms-python.python 2021.9.1246542782/rogalmic.bash-debug 0.3.9)が、VS Code 1.63.2 世代で安定。

拡張の署名/仕様差:古い未署名期の VSIX は、後年の VS Code だと導入時に弾かれる/不安定になりやすい。1.63.2 なら運用実績がある。

再現性最優先:他案件(最新版)と完全分離し、毎回同じ組み合わせ(本体1.63.2+固定拡張)で検証できる。

運用方針:自動更新を全停止し、server.sh --install-extension で前景ブロック導入→確実に“その日の組み合わせ”を固定できる。

補足
「1.63.2 の VS Code Server 環境では server.sh 経由でローカル VSIX を導入できた(code CLI だと失敗した)。検証はこのバージョン前提
別バージョンだと、別の方法が必要かもしれません。

当環境の検証では 1.63.2 以外で Rebuild/拡張導入フェーズで失敗が再現したため、1.63.2 に固定した。

上記の理由があり、VSコード1.63.2を使わざるを得なかった。
他の開発作業では最新のVSコードを使いたく
拡張機能も最新のものを使いたい

この環境のために、拡張機能をダウングレードしたバージョンで固定したのが
他の開発作業で使うメインのVSコードに影響を与えないために、
ポータブル版のVSコード1.63.2をこの環境のために利用した。

ここに記載した多くの事柄は、
最新のモダンな開発環境でも、ほとんど役に立つ
むしろ、最新のモダンな開発環境のほうが上記の環境構築上の苦戦項目が少ないため
もっと、すんなりと簡単に構築できるだろう。
この記事でしてることを身に着けておくと、イレギュラーな環境構築への対応への学びになるため
当記事として残しておくことにした。

Devcontainerのお作法でUID=1000、GID=1000のユーザをコンテナに作ってる状況で要るコマンド

devcontainer.jsonでUID=1000、GID=1000のユーザでログインする設定にしている状況で、
Devcontainer環境で開きなおしたVSコード上で、ターミナルを開くと
UID=1000、GID=1000で起動したターミナルなので、そこで作業する分にはいいんですけど。

もし、ホスト側の
ubuntuのターミナルからUID=1000、GID=1000のユーザでログインし
そのとき、「bash -l」で~/.bashrcなどのログインシェルも適用してログインする
コマンドは下記のとおりです。

docker compose -p devc_rhel exec --user 1000:1000 app bash -l

なお、このコマンドの
-p devc_rhel
は、後続で記載の行方不明問題を回避のためのプロジェクト名で
ホスト側でのワークスペースフォルダ名に指定してのやり方にしてるので、
ホスト側でのワークスペースフォルダ名を指定。
今回の例では「devc_rhel」

app
は、docker-compose.ymlに書いてる
ログインしたいコンテナに対するサービス名を指定。
今回の例では「app」

ファイル構成

WSL2の「Ubuntu-24.04」にて
/myDocker/devc_rhel
のディレクトリ配下に下記のファイル構成をとった。
なお、debug_smoke.pyや、aaa.sh、bbb.sh、ccc.sh、func.sh
は、環境調査のためのデバッガーが機能するかを確認のための適当なファイルである
( 本チャンの環境の開発対象のものは掲載しない )
同様に、data/aaa、data/aaa222.datや、logs/aaaa.log、logs/aaaa2.logも環境調査の適当値

devc_rhel
├── .devcontainer
│   ├── devcontainer.json
├── Dockerfile
├── bash-4.2.tar.gz
├── bash-debug-0.3.9.vsix
├── bin
│   ├── .vscode
│   │   ├── launch.json
│   │   └── settings.json
│   ├── project-multiroot.code-workspace
│   ├── pydir
│   │   └── debug_smoke.py
│   └── shdir
│       ├── aaa.sh
│       ├── bbb.sh
│       ├── ccc.sh
│       └── func.sh
├── data
│   ├── aaa
│   └── aaa222.dat
├── debugpy-1.5.1-py2.py3-none-any.whl
├── docker
│   ├── entrypoint.sh
│   └── setup-extensions.sh
├── docker-compose.yml
├── get-pip.py
├── logs
│   ├── aaaa.log
│   └── aaaa2.log
└── ms-python.python-2021.9.1246542782.vsix

なぜ「.vscode」を直下においてないのか。(マルチルート対応)

通常、VSコードで開くフォルダの直下に、「.vscode」置くものである
今回は、ホスト側で
/myDocker/devc_rhel
を開いたのだから、
/myDocker/devc_rhel/.vscode
にしておくべきと一見すると思うだろうが、理由があって、そのようにしていない
実際にした構成は、ホスト側では、
/myDocker/devc_rhel/bin/.vscode
の位置にて「.vscode」フォルダがある。

なぜ、そうしたかというと、
ホスト側で最初に開いたときの構造よりも、最終的にDevcontainerに入った状況で
VSコードを開いた時の構造を重視しているからである。
docker-compose.ymlでは、

    volumes:
      - ./bin:/usr/local/bin
      - ./data:/data
      - ./logs:/var/log/myapp

のような形でバインドマウントしている
DevcontainerでVSコードを開いた時に、ホスト側のバインドマウントしている領域は
VSコードで見れるようにしたいのであるが、
複数のルートがあるときに、どうすべきかの問題解決を今回はする必要があった

複数(今回は3つ)のルート、つまり、マルチルートで、VSコードを開く必要があった

devcontainer.json では、

  "workspaceFolder": "/usr/local/bin",

の記載があるため、
「Rebuild Without cache Reopen」や「Reopen in Container」した結果
コンテナ側でVSコードが開いた時は、
/usr/local/bin の一つのルートフォルダでVSコードが開いた状況になる
そこを起点とした直下に、「.vscode」があるのである。

まとめると、
バインドマウントしている状況に応じて、
コンテナ側でVSコードをReopenしたルートフォルダの直下に「.vscode」フォルダが
来ることが大事であり、
そのためには、ホスト側でVSコードを開いてる段階では、
直下に「.vscode」フォルダが来る、通常の想定を崩してもよい
という考え方である。

★ まず、この考え方を、知る ところに、私には、カベがあり、気づきがあった。 ★

さらに加えて、今回は、
コンテナ側でReopenした後、再度、マルチルートとしてReopenして
上記の、複数(今回は3つ)のルートで
VSコードが開いてる状況として、
ホスト側へバインドマウントしている領域に関しては、VSコードで
参照や、編集/保存ができる状況での構成としたかったのである。

そのため、
最初に、コンテナ側でReopenした後、唯一、VSコードで開いてる
/usr/local/bin のルートフォルダ に、
project-multiroot.code-workspace
があり、VSコードで見えていなければならないんです。

project-multiroot.code-workspace
には

{
  "folders": [
    { "path": "/usr/local/bin" },
    { "path": "/data" },
    { "path": "/var/log/myapp" }
  ],
  "settings": {}
}

という記述があります。

コンテナ側でReopenした後
VSコードのメニューから
「ファイル」-「ファイルでワークスペースを開く」 で、
この project-multiroot.code-workspace
を指定すると
/usr/local/bin
/data
/var/log/myapp
の3つのルートで開く状況になります。

コンテナの / より以下全部をVSコードで開くのは、現実的ではありません
コンテナはLinuxのシステム全体がありますが

そのうち、バインドマウントしてホスト側にある永続化対象の
ファイルだけVSコードで開いて、参照や、編集/保存がしたいでしょう

バインドマウントの設定は、
docker-compose.ymlでの

    volumes:
      - ./bin:/usr/local/bin
      - ./data:/data
      - ./logs:/var/log/myapp

の記述のように、一部分だけを局所的に行って
その各々について、マルチルート形式でVSコードで開ける状況にしたいでしょう

今回、Devcontainer環境で、
コンテナでReopen時の、初期で開くルートフォルダを
ソースコードが詰まってるフォルダとして、
そこに、「.vscode」のフォルダを配置 ( ホスト側ではワークスペースフォルダの直下でなくてもよい )
さらに、初期で開く、ルートフォルダに、
project-multiroot.code-workspace
を配置して
マルチルート形式でVSコードで開きなおせる構成とすることが

★ 一部を局所的に複数、バインドマウントしたdocker compose環境における
★ DevcontainerでのVSコードの構成の仕方として
★ 有効なやり方だな!!
★ この気づきを得ることができた。というのが学びであった。
★ という気付きを手に入れたというのが、1つ自分の中での学びだったので
★ これについて、記述させていただいた次第であります。

なお、この project-multiroot.code-workspace
のファイル名ですが、 xxx.code-workspace であれば
「xxx」の部分はなんでも好きな名前をつけれます
( .code-workspace の拡張子のファイル名であればよい )
今回は、マルチルートで開きなおしたい
という意味を込めたファイル名にしました。

devcontainer.json

devcontainer.json
は、下記のとおりである

{
  "name": "devc_rhel",
  "dockerComposeFile": "../docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/usr/local/bin",
  "overrideCommand": false,
  "shutdownAction": "none",
  "remoteUser": "vscode",
  "forwardPorts": [8000],

  // **************************
  // devcontainerの書き方
  // **************************
  // "customizations": {
  //   "vscode": {
  //     "extensions": [
  //     // 書いたら最新版を取ってきてしまうので書かない
  //     // 今回はダウングレード済みのバージョンを維持するため空にしておく
  //     // "ms-python.python",
  //     // "rogalmic.bash-debug"
  //     ],
  //     "settings": {
  //       "extensions.autoUpdate": false,
  //       "extensions.autoCheckUpdates": false,
  //       "extensions.ignoreRecommendations": true,
  //       "update.mode": "none",
  //       "python.experiments.enabled": false,
  //       "python.languageServer": "Jedi"
  //     }
  //   }
  // },
  // **************************

  // **************************
  // 旧Remote-Containers用の書き方
  // **************************
  "settings": {
    "extensions.autoUpdate": false,
    "extensions.autoCheckUpdates": false,
    "extensions.ignoreRecommendations": true,
    "update.mode": "none",
    "python.experiments.enabled": false,
    "python.languageServer": "Jedi"
  },
  "extensions": [
    // 書いたら、最新版になっちゃうから書かない
    // 今回は、ダウングレードしたバージョンに固定すべきだから書かない
    // "ms-python.python",
    // "rogalmic.bash-debug"
  ],
  // **************************
  
  "postCreateCommand": "",
  "postStartCommand":  "bash -lc '/opt/setup-extensions.sh --block'",
  "postAttachCommand": "",
  
  "features": {}
}

Devcontainerは、docker compose -p プロジェクト名 なにがし
で、プロジェクト名として、今いるディレクトリ名を指定した形で
docker側の環境を作ろうとする動きがあるらしい。
今回の例では、ホスト側では
/myDocker/devc_rhel
をワークスペースフォルダとしてる状況のため
プロジェクト名が「devc_rhel」として動く

実は、今回、事情があって、ポータブル版のVSCode-1.63.2を使ってる関係で
Devcontainerでなく、Remote-containersと称されてた時代のもので
環境構築したことになっている
Remote-containersの場合は、Devcontainerに比べ、
devcontainer.jsonのnameの値など、より、しっかりと、
devc_rhelの値を指定しておかないと、
VSコードで「Rebuild Without cache Reopen」などしたときに、
エラーになってしまうことがあった
新しめのDevcontainerでも、当記事で説明している設定値の指定方法でも
問題なく動くため、この指定方法を身に着け実践するようにしておけばよろしいだろう

"name": "devc_rhel",
としているのは、
ホスト側でのワークスペースフォルダの /myDocker/devc_rhel の
devc_rhel と同じ値を指定した
こうしておけば、
https://zenn.dev/tazzae999jp/articles/b60d353fd9329d
で記載した行方不明問題を回避し、うまいこと動くのである。

補足
未指定でも動くが、命名の整合を取ることで行方不明問題の回避や、エラーなどのトラブルの回避、となることがあり。個人的には、ワークスペースフォルダのディレクトリにあわせておけば、
わかりやすく、トラブルも少ないと考えています。

"dockerComposeFile": "../docker-compose.yml",
は、ホスト側で見たときに、devcontainer.jsonからの相対パスで、
docker-compose.yml を指定する必要があるからだ。

"service": "app",
は、docker-compose.ymlでのサービス名のうち、どのサービスで
DevcontainerとしてVSコードを開きなおしをするかである

"workspaceFolder": "/usr/local/bin",
は、バインドマウントしてるもののうち、
DevcontainerとしてVSコードの開きなおしをしたときに、
どれを初期のワークスペースフォルダとするかを、dockerコンテナ側のパスで指定する

補足)
ホストではリポジトリをワークスペースとして開きますが、
Devcontainer はマルチルート構成(複数バインド)にしているため、
コンテナ内の初期カレントだけ作業導線を優先して
/usr/local/bin に固定しています
(ホスト側の「リポジトリ=ワークスペース」という前提をコンテナ内には持ち込まない)

"overrideCommand": false,
は、
Devcontainer 起動時に、イメージ/compose の CMDENTRYPOINT を上書きしない → コンテナの既定起動(本番の CMD)がそのまま PID1 で実行される。
本番 CMD をそのまま動かす一方で、VS Code(Remote-Containers 1.63.2)は
docker exec 相当でコンテナに入り**、
postStartCommand(例:bash -lc '/opt/setup-extensions.sh --block')を起動直後に実行する。
→ **CMDpostStart が“同時成立”**する。

  • いつ使う設定か

    • 本番互換を守りたい(CMD を変えない)。
    • Reopen in Container 前提で、postStart を毎回確実に走らせたい
    • 拡張導入や初期化を postStart でブロック完了させたい。
  • 注意(最低限)

    1. .devcontainer/devcontainer.json"service"compose のサービス名と一致させる。
    2. compose 側に tty: truestdin_open: true を付け、exec 可能にしておく。
    3. postStartCommand のスクリプトは 冪等かつ 実体ディレクトリ存在で成功判定(失敗時は非0終了)。
  • 要するに
    **「本番 CMD はそのまま」+「Devcontainer の初期化は毎回完走」**を両立させるための基本設定が
    "overrideCommand": false

"shutdownAction": "none",
は、

  • なにをする設定か
    VS Code(Remote-Containers/Dev Containers)を閉じたときに、コンテナをどう扱うかを決める。
    "none"何もしないコンテナを起動したまま残す

  • 挙動(初心者向けに一言で)
    エディタを閉じても、コンテナは止まらない
    docker ps に残り続ける/バックグラウンドで動く。

  • いつ "none" を選ぶか

    • 本番 CMD を動かしっぱなしにしたい(今回の方針)。
    • VS Code を閉じても、サービスや待受(debugpy など)を維持したい。
    • 次回の Reopen in Container を高速にしたい(再起動・再セットアップを避ける)。
  • 他の選択肢(参考)

    • "stopContainer"今のコンテナだけ止める(compose 連携なし)。
    • "stopCompose"compose で起動した関連コンテナをまとめて止める
      ※今回の「本番 CMD を保ったまま Devcontainer の初期化を回す」運用では "none" が適合
  • 注意(最低限)

    1. 残しっぱなし=リソース(メモリ/ポート)が使われ続ける。不要になったら docker compose down で明示停止。
    2. 複数プロジェクトを並行するときは、ポート衝突に注意(命名整合と compose の project 名で分離)。
  • 要するに
    エディタを閉じてもコンテナは落とさない
    本番 CMD はそのまま次回 Reopen を速く」という、今回の運用方針に "shutdownAction": "none" がちょうど良い

"remoteUser": "vscode",
は、DevcontainerでVSコードを開きなおしの際に
dockerコンテナ側のどのユーザでログインするかを指定します。
このUID=1000、GID=1000のユーザは、
https://zenn.dev/tazzae999jp/articles/b60d353fd9329d
にて、
ghcr.io/devcontainers/features/common-utils
のfeatureを指定し
"username": "vscode",
"userUid": 1000,
"userGid": 1000
を指定する方法もあります。
これは、特にMacユーザが準備してるDockerfileなどで、彼らは
Docker for MacがvirtioFSを活用したバインドマウントをすることで
権限問題が発生しない特性があり、Dockerfileにユーザ作成の記述を書かずに、
rootユーザで作業する構成のDockerfileのままにしてケースがあります
その状況ではsudoもありません
そんなDockerfileを使った、docker compose環境であっても
後から、UID=1000、GID=1000のユーザの作成、および、sudoコマンドの準備などを
ghcr.io/devcontainers/features/common-utilsのfeatureを通じてできます。
WSL2のubuntuは、初期の既定ユーザがUID=1000、GID=1000であることが多い。
UID=1000、GID=1000でコンテナ側も作業しておれば、
ホストとコンテナでファイルやフォルダの権限が一致するため、権限問題おきません
ただし、今回は、
このfeatureを利用せず、"features": {}としています。
ユーザ名vscodeでUID=1000、GID=1000のユーザの作成や、sudoが使える状況にするのは
Dockerfileの記述で行うことにしました
理由は、本番環境ではシェルの実行が、UID=1000、GID=1000のユーザだからです
また、VSコードのターミナル以外でも、WSL2のubuntuのターミナルより
docker compose -p devc_rhel exec --user 1000:1000 app bash -l
でコンテナにログインし、UID=1000、GID=1000での作業が行えるようにしたいからです。

"forwardPorts": [8000],
は、基本的に、docker-compose.ymlの
ports:
- "127.0.0.1:8000:8000"
の記述にあわせておけばよいと思います
https://zenn.dev/tazzae999jp/articles/a8e394f10a6c1f
では、ホスト-コンテナ間のリモートデバッグのため、attach時に
5678ポートでのホストからコンテナへの接続の必要性があったので
5678もportsに指定してました。その場合は、[8000,5678]の値指定になります。
今回は、Devcontainerでコンテナ内で単独での動作とデバッグ実行となるため
この転送が不要です。
今回のファイル構成の例にはありませんでしたが、httpのサービスを8000ポートで
公開し、外部PCより接続したいニーズがあるため、8000だけの指定になってます。

補足
後で知ったのですが、docker-compose.ymlで指定してる場合は、
devcontainer.jsonの forwardPortsは省略可とのことです。
compose で 127.0.0.1:8000:8000 を公開している場合、forwardPorts は省略しても到達可能(VS Code のポート自動検出に頼らないなら)

// **************************
// devcontainerの書き方
// **************************

// **************************
// 旧Remote-Containers用の書き方
// **************************

2種類示しました。書式が違うだけで中身は同じです
"settings": {
"extensions.autoUpdate": false,
"extensions.autoCheckUpdates": false,
"extensions.ignoreRecommendations": true,
"update.mode": "none",
"python.experiments.enabled": false,
"python.languageServer": "Jedi"
},
の部分の意味は、
各々、

  • extensions.autoUpdate: false
    拡張機能の自動更新を無効化。勝手に新版へ上がって挙動が変わるのを防ぐ(今回の固定運用の前提)。

  • extensions.autoCheckUpdates: false
    更新の有無のチェック自体を止める。通知や取得を抑止し、検証済みの組み合わせを固定。

  • extensions.ignoreRecommendations: true
    拡張のおすすめ表示を無視。誤って別系統の拡張を入れてしまう事故を防ぐ(固定方針を揺らさない)。

  • update.mode: "none"
    VS Code 本体の自動更新を完全停止。1.63.2に固定し、後年版での仕様差に巻き込まれないようにする。

  • python.experiments.enabled: false
    Python拡張の実験的機能(A/Bや段階的ロールアウト)を無効化。古い環境で挙動が変わらないよう固定。

  • python.languageServer: "Jedi"
    Jedi を言語サーバに指定。今回の世代(Python 2.7 系)と相性がよく、補完/解析を安定化。

です。
"extensions": [
// 書いたら、最新版になっちゃうから書かない
// 今回は、ダウングレードしたバージョンに固定すべきだから書かない
// "ms-python.python",
// "rogalmic.bash-debug"
],
の部分は、各拡張機能の識別子をここに並べれば
Devcontainerは、コンテナ側の ~/.vscode-server 配下に
識別子指定された拡張機能をインストールする機能があります
ただし、今回は、これらの拡張機能は古めのバージョンのものをインストールしなければならない
諸事情があり、署名がない時代のバージョンであるため
通常のインストールはできません。
そのため、別途、setup-extensions.sh を通じての専用のやり方での
インストールが必要です。
この事情により、今回は、ここは、コメントアウトし何も値を指定しませんでした。

"postCreateCommand": "",
"postStartCommand": "bash -lc '/opt/setup-extensions.sh --block'",
"postAttachCommand": "",
こうしている理由は、

  • 毎回、必ず走らせたい処理postStartCommand に置く。
    • postStartコンテナ起動直後に毎回1回だけ走る(Reopen in Container のたびに確実)。
    • 今回の処理は VS Code Server の server.sh --install-extension を使うため、VS Code Server が立ち上がった後である postStart最も安定
  • postCreateCommand は使わない
    • これは**“初回(作成直後)だけ”**走るフック。
    • 一度コンテナを作った後の Reopen では発火しない再現性が崩れる
    • 「毎回同じ状態(拡張が入った状態)にそろえる」目的に合わない
  • postAttachCommand は使わない
    • これは VS Code の再接続ごとに何度でも走る。
    • 今回は postStart で完結しており、postAttach にも同じコマンドを置くと二重実行のノイズ(ログ重複・再検査の待ち)が出る。
    • スクリプトは冪等だが、最小限のフックで運用する方が読みやすく安全

ここで保証していること

  • postStart拡張の“実体ディレクトリ”が揃うまでブロック導入できていないまま進まない
  • overrideCommand: false のままでも、CMD は PID1 でそのまま動くpostStart は別プロセスで毎回動く両立

要するに:「初回だけ」でも「何度でも」でもなく、
“起動のたびに1回だけ確実に”が欲しいので、postStartCommand にだけ置く

です。

"features": {}
は、特に、Devcontainer側のfeatureを今回、利用していないので空にしてます。

docker-compose.yml

# Dev Containers が --project-name devc_rhel で起動するため、人間の混乱回避で合わせる
name: devc_rhel

services:
  app:
    # ★ ビルド後に参照されるイメージ名を固定(inspect の不一致防止)
    image: devc_rhel_app
    build:
      context: .
      dockerfile: Dockerfile
    working_dir: /usr/local/bin
    tty: true

    volumes:
      - ./bin:/usr/local/bin
      - ./data:/data
      - ./logs:/var/log/myapp
      # 必要になったら増やせばOK

    environment:
      - TZ=Asia/Tokyo
      - LANG=ja_JP.UTF-8
      - LC_ALL=ja_JP.UTF-8
      - LC_CTYPE=ja_JP.UTF-8
      - LC_TIME=ja_JP.UTF-8
      - PYTHONIOENCODING=UTF-8

    entrypoint: ["/opt/entrypoint.sh"]
    command: ["bash"]

    # ループバックのみ公開(外部露出を避ける)
    ports:
      - "127.0.0.1:8000:8000"

name: devc_rhel
にしている理由は、devcontainer.jsonのnameがそうしてるのと同じ
当記事で理由はそこに記載しました。
ホスト側でのワークスペースフォルダの /myDocker/devc_rhel の
devc_rhel と同じ値にしとくのがよい

image: devc_rhel_app
にしている理由は、
ホスト側でのワークスペースフォルダの /myDocker/devc_rhel の
devc_rhel に対して
services:
app:
におけるサービス名の app を _ で連結した
devc_rhel_app
のイメージ名にしておく
Remote-containersはこのイメージ名でイメージを作るようである
それにあわしておけば、エラーにならないです
Devcontainerの場合は、ここまでしなくてもいいですが
別に、こうしておけば、Devcontainerでも動きますので、
この指定方法でよいのではないかと思います

補足)
そこまでしなくても、動く環境はあることは知ってます
ただ、今回のバージョンでは、この命名を設定値に書き込むことでエラー回避できました
同じ考えて、はじめから、設定値に書き込んでおくと、多くの環境で問題なくエラー回避
できることが多いのではないかと考えています
ですので、個人的には、今後、Devcontainerの環境を自分が主体で作ろうとするとき
一旦、当記事のノウハウでしてみようと思ってます。

Dockerfile

# ベース:CentOS 7.2(本番が RHEL 7.2 相当のため Vault を使用)
FROM centos:centos7.2.1511

# --- Vault リポジトリ設定(mirrorlist を全削除し Vault 固定)-------------------
RUN set -eux; \
    rm -f /etc/yum.repos.d/*.repo; \
    cat > /etc/yum.repos.d/CentOS-Vault-7.2.1511.repo <<'EOF'
[base]
name=CentOS-7.2.1511 - Base
baseurl=http://vault.centos.org/7.2.1511/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

[updates]
name=CentOS-7.2.1511 - Updates
baseurl=http://vault.centos.org/7.2.1511/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

[extras]
name=CentOS-7.2.1511 - Extras
baseurl=http://vault.centos.org/7.2.1511/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
EOF

# ベース必須ツール(Vault環境で最低限)
RUN set -eux; \
    yum -y makecache; \
    yum -y install \
        tzdata \
        ca-certificates \
        openssl \
        curl \
        tar \
        gzip \
        bzip2 \
        unzip; \
    update-ca-trust force-enable || true; \
    yum clean all

# 1) ロケール実体を導入 & 生成
RUN set -eux; \
    yum -y install glibc-common; \
    localedef -i ja_JP -f UTF-8 ja_JP.UTF-8 || true; \
    # 予備ロケール(英語)も作っておくと安心
    localedef -i en_US -f UTF-8 en_US.UTF-8 || true

# 2) システム既定ロケールを固定
RUN printf 'LANG=ja_JP.UTF-8\n' > /etc/locale.conf

# 3) Python I/O / 4) LC_CTYPE をENVで固定(全プロセスに継承)
ENV LANG=ja_JP.UTF-8 \
    LC_ALL=ja_JP.UTF-8 \
    LC_CTYPE=ja_JP.UTF-8 \
    PYTHONIOENCODING=UTF-8

# ------------------------------------------------------------------
# Python 2.7.5 にて互換性の問題が発生しにくい
# pipのバージョンを導入しておく
# curl -fsSL https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py
# でダウンロードしたものを
# Dockerfileと同じ場所に置いておくこと
# ------------------------------------------------------------------
# pip 20.3.4(Py2.7 最終)導入
COPY get-pip.py /tmp/get-pip.py
RUN set -eux; \
    python /tmp/get-pip.py "pip==20.3.4"; \
    rm -f /tmp/get-pip.py

# ======================================================================
# VS Code(コンテナ側サーバ)に展開する VSIX を同梱
# 期待するビルドコンテキスト:
#   - ms-python.python-2021.9.1246542782.vsix
#   - bash-debug-0.3.9.vsix
# ======================================================================
RUN mkdir -p /opt/vsix
COPY ms-python.python-2021.9.1246542782.vsix /opt/vsix/
COPY bash-debug-0.3.9.vsix                /opt/vsix/

# ======================================================================
# Dev Containers の実行ユーザ(UID/GID=1000)を作成
# ======================================================================
RUN set -eux; \
  yum -y install sudo; \
  groupadd -g 1000 vscode || true; \
  useradd  -m -u 1000 -g 1000 -s /bin/bash vscode || true; \
  mkdir -p /etc/sudoers.d; \
  echo "vscode ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-vscode; \
  chmod 0440 /etc/sudoers.d/90-vscode

# ======================================================================
# ★ vscodeユーザの ${HOME}/password/password をビルド時に配置
#    - ビルドコンテキストに `password/password` を用意してください
#    - 所有権: vscode:vscode、権限: 700(ディレクトリ)/600(ファイル)
# ======================================================================
RUN set -eux; \
  mkdir -p /home/vscode/password
COPY password/password /home/vscode/password/password
RUN set -eux; \
  chown -R vscode:vscode /home/vscode/password; \
  chmod 700 /home/vscode/password; \
  chmod 600 /home/vscode/password/password

# ======================================================================
# 拡張の自動セットアップ & 既存のエントリポイントを同梱
# 期待するビルドコンテキスト:
#   - docker/setup-extensions.sh
#   - devcontainer-poststart.sh
#   - docker/entrypoint.sh
# ======================================================================
COPY docker/setup-extensions.sh /opt/setup-extensions.sh
RUN chmod +x /opt/setup-extensions.sh

# COPY docker/devcontainer-poststart.sh /opt/devcontainer-poststart.sh
# RUN chmod +x /opt/devcontainer-poststart.sh

COPY docker/entrypoint.sh /opt/entrypoint.sh
RUN chmod +x /opt/entrypoint.sh

# ======================================================================
# (追加①)bash-4.2 デバッガ有効ビルドに必要な開発ツールだけ追加
# ======================================================================
RUN set -eux; \
  yum -y install \
    gcc make bison flex texinfo \
    ncurses-devel readline-devel \
    gettext wget xz patch which \
    ksh procps-ng findutils git openssh-clients \
  && yum clean all

# ======================================================================
# (追加②)開発専用 bash-4.2(--enable-debugger)を /opt に導入
#          ※ 本番は従来の /bin/bash 4.2 を使用。コードは無改変。
# ======================================================================
COPY bash-4.2.tar.gz /tmp/bash-4.2.tar.gz
RUN set -eux; \
  cd /tmp; tar xf bash-4.2.tar.gz; \
  cd bash-4.2; \
  ./configure --prefix=/opt/bash-4.2-dbg --enable-debugger --with-installed-readline; \
  make -j"$(getconf _NPROCESSORS_ONLN)"; \
  make install; \
  /opt/bash-4.2-dbg/bin/bash --version; \
  cd /; rm -rf /tmp/bash-4.2 /tmp/bash-4.2.tar.gz

# ======================================================================
# (追加③)BASH_ENV 用 startup.sh を配置(読むのは devcontainer 起動後)
# ======================================================================
RUN set -eux; \
  install -d -m 0755 /usr/local/share/bash-debug; \
  printf '%s\n' \
    '#!/bin/bash' \
    'shopt -s extdebug 2>/dev/null || true' \
    'set -o functrace 2>/dev/null || true' \
    'if [ -r /usr/share/bashdb/bashdb-trap.sh ]; then' \
    '  . /usr/share/bashdb/bashdb-trap.sh' \
    'fi' \
    > /usr/local/share/bash-debug/startup.sh; \
  chmod 0755 /usr/local/share/bash-debug/startup.sh

# ------------------------------------------------------------------
# debugpy 1.5.1 をオフラインインストール
# 
# https://pypi.org/project/debugpy/1.5.1/#files
# で、debugpy-1.5.1-py2.py3-none-any.whl をダウンロードして
# Dockerfileと同じ場所に置いておくこと
# debugpy-1.5.1-py2.py3-none-any.whl はプラットフォーム非依存なファイル
# のためWindows端末でダウンロードしたものをWSL2側において
# それがdockerコンテナ側へのインストールで使われる状況でも問題ありません
# ------------------------------------------------------------------
# debugpy 1.5.1 をオフラインインストール
COPY debugpy-1.5.1-py2.py3-none-any.whl /tmp/debugpy-1.5.1-py2.py3-none-any.whl
RUN set -eux; \
    pip install --no-index /tmp/debugpy-1.5.1-py2.py3-none-any.whl; \
    rm -f /tmp/debugpy-1.5.1-py2.py3-none-any.whl

# ======================================================================
# ★★ cron(cronie)を導入して crontab/cron 機能を有効化可能にする
#     - CentOS 7 系では cronie パッケージに crond / crontab を含む
#     - ログ用に /var/log/cron を作成
# ======================================================================
RUN set -eux; \
  yum -y install cronie; \
  touch /var/log/cron; \
  chmod 0644 /var/log/cron; \
  yum clean all

# 任意:作業ディレクトリとエントリポイント(あなたの既存スクリプトを利用)
COPY docker/entrypoint.sh /opt/entrypoint.sh
RUN chmod +x /opt/entrypoint.sh
ENTRYPOINT ["/opt/entrypoint.sh"]
CMD ["bash"]

なお、

ms-python.python-2021.9.1246542782.vsix
は、

https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2021.9.1246542782/vspackage

よりダウンロード可能です

bash-debug-0.3.9.vsix
は、

https://github.com/rogalmic/vscode-bash-debug/releases

よりダウンロード可能です

bash-4.2.tar.gz
は、
ダウンロード元が不安定でダウンロードに時間がかかりますが
下記のコマンドで、なんとか、ダウンロード可能です
(たしか、1回、途中で失敗し、再度、やり直したら成功しました)

# /tmp に保存(IPv4、3回リトライ、タイムアウト30秒)
wget -4 -t 3 --timeout=30 -O /tmp/bash-4.2.tar.gz http://ftp.gnu.org/gnu/bash/bash-4.2.tar.gz

抜粋その1

# ======================================================================
# (追加①)bash-4.2 デバッガ有効ビルドに必要な開発ツールだけ追加
# ======================================================================
RUN set -eux; \
  yum -y install \
    gcc make bison flex texinfo \
    ncurses-devel readline-devel \
    gettext wget xz patch which \
    ksh procps-ng findutils git openssh-clients \
  && yum clean all

# ======================================================================
# (追加②)開発専用 bash-4.2(--enable-debugger)を /opt に導入
#          ※ 本番は従来の /bin/bash 4.2 を使用。コードは無改変。
# ======================================================================
COPY bash-4.2.tar.gz /tmp/bash-4.2.tar.gz
RUN set -eux; \
  cd /tmp; tar xf bash-4.2.tar.gz; \
  cd bash-4.2; \
  ./configure --prefix=/opt/bash-4.2-dbg --enable-debugger --with-installed-readline; \
  make -j"$(getconf _NPROCESSORS_ONLN)"; \
  make install; \
  /opt/bash-4.2-dbg/bin/bash --version; \
  cd /; rm -rf /tmp/bash-4.2 /tmp/bash-4.2.tar.gz

# ======================================================================
# (追加③)BASH_ENV 用 startup.sh を配置(読むのは devcontainer 起動後)
# ======================================================================
RUN set -eux; \
  install -d -m 0755 /usr/local/share/bash-debug; \
  printf '%s\n' \
    '#!/bin/bash' \
    'shopt -s extdebug 2>/dev/null || true' \
    'set -o functrace 2>/dev/null || true' \
    'if [ -r /usr/share/bashdb/bashdb-trap.sh ]; then' \
    '  . /usr/share/bashdb/bashdb-trap.sh' \
    'fi' \
    > /usr/local/share/bash-debug/startup.sh; \
  chmod 0755 /usr/local/share/bash-debug/startup.sh

この部分がbashのデバッガーが使えるようにするために必要なものらしいです。
簡単に言うと、
まず、(追加①)で、(追加②)に必要なものをそろえる
bash-4.2.tar.gzにbash4.2のソースコードがあり、
(追加②)は、--enable-debugger のオプションをつけてビルドを行う。
結果として、/opt/bash-4.2-dbg/bin/bash を利用して
launch方式でbashのシェルスクリプトを起動するのでデバッグ実行できます
普通の/usr/bin/bashを指定すると、デバッガーが起動せず、エラーになります
デバッガー起動できるための --enable-debugger を付けてビルドしたものを
作っているのです。
また、動作については、/usr/bin/bashと同じ動作が保証されています。

補足
「/usr/bin/bashと同じ動作が保証されています。」は強すぎる表現でした
公開されているソースコードからビルドしなおしたため、基本的には同じ動作のはずですが
デバッガ対応ビルド --enable-debugger 付きのビルドしたことでの差異が完全にゼロとは
言い切れない。が、基本的には同じ動作をするはずである。

(追加③)は、startup.shの作成をしています。
今回は、特にしていませんが、
Dockerfile に、
ENV BASH_ENV=/usr/local/share/bash-debug/startup.sh
に書くか、
devcontainer の postStart
で、
echo 'export BASH_ENV=/usr/local/share/bash-debug/startup.sh' >> /home/vscode/.bashrc
として、UID=1000,GID=1000のユーザのログインシェルで
export BASH_ENV=/usr/local/share/bash-debug/startup.sh
する構成にしておくなどすると
より、bashでのデバッガーが安定するとのことです
詳しく説明すると

目的:非対話シェルでの Bash デバッグを安定させるため、
Bash の“デバッグ用スイッチ”を入れてから、bashdb(Bash用デバッガ)の罠(trap)を読み込む

shopt -s extdebug 2>/dev/null || true

何をしてる? → Bash の拡張デバッグをONにするスイッチ。
効果 → 関数呼び出しなどで追加情報が取れるようになり、デバッガが細かい位置を把握しやすくなる。
2>/dev/null || true の意味 → もし古い/ビルド違いの Bash でこのオプションが無くてもエラーにしない(落ちない)。

set -o functrace 2>/dev/null || true

何をしてる? → DEBUG や RETURN の **trap(罠)を“関数やサブシェルにも伝播”**させる設定。
効果 → 関数の中に入った時も、ステップ(1行ずつ)やブレークが拾いやすくなる。
補足 → ない環境でも失敗で止まらないようにしてある。

[ -r /usr/share/bashdb/bashdb-trap.sh ] && . /usr/share/bashdb/bashdb-trap.sh

bashdb-trap.sh って何?
bashdb(Bashのデバッガ)が提供する**“罠(trap)定義のスクリプト”**。
読み込むと、Bashが行ごとに実行するときにデバッガ側に通知するような仕掛け(DEBUG/RETURN などの trap 設定)が一括で入る。
典型的な場所は /usr/share/bashdb/(ディストリやパッケージで場所は多少違うことがある)。
何をしてる? → 存在して“読める”なら、読み込む(&& . file)。
効果 → bashdb のフックが有効になり、ブレーク・ステップが安定。
安全策 → ファイルが無い環境では何もしない(エラーで止めない)。

まとめ
extdebug … 拡張デバッグON(細かい情報を出す下地づくり)。
functrace … trap を関数/サブシェルにも伝える(中に入っても止めやすい)。
bashdb-trap.sh … bashdb の“罠”を一括ロード(デバッガ連携の実体)。
どれも無くても落ちないようにガードしてあり、あれば安定する——という“安全第一”の書き方。

です

.vscode/launch.json

{
    "version": "0.2.0",
    "configurations": [
      {
        "type": "bashdb",
        "request": "launch",
        "name": "Bash Debug: Current File",
        "program": "${file}",
        "args": [],
        "pathBash": "/opt/bash-4.2-dbg/bin/bash",
        "cwd": "${fileDirname}",
        "env": {
          "LANG": "ja_JP.UTF-8",
          "LC_ALL": "ja_JP.UTF-8",
          "LC_CTYPE": "ja_JP.UTF-8",
          "PYTHONIOENCODING": "UTF-8"
        },
        "terminalKind": "integrated",
      },
      {
        "name": "Py27 (attach:5678)",
        "type": "python",
        "request": "attach",
        "connect": { "host": "127.0.0.1", "port": 5678 },
        "justMyCode": false
      },
    ]
  }

1つ目の項目「 "name": "Bash Debug: Current File", 」

"program": "${file}",
にて、今、VSコードでコードエディタをアクティブで開いてる
bashのシェルスクリプトをターゲットとしてデバッグ起動しますが、
"pathBash": "/opt/bash-4.2-dbg/bin/bash",
で、--enable-debugger を付けてビルドしたbashを使ってデバッグ起動します
( ここを /usr/bin/bash にしたら、デバッガーが起動しません )
子プロセスへのステップインは、残念ながらできません
ただし、シェルスクリプトの中で
. child.sh
のように「. 」を前につけたり
source child.sh
のように「source 」を前につけたり
して、別シェルスクリプトを起動した場合は、同一プロセスとして
取り込む形で、その別のシェルスクリプトを実行することになるため
その場合は、その別のシェルスクリプトの内部実装に対して
デバッガーでステップインしていくことが可能です
その他、シェルの実装の仕方で、注意点があります
$0は、デバッガーで起動したときと、通常で起動した時では値がかわってしまうため

  • $0 は“どう起動したか”で値が変わる(直実行/bash script.shsource/デバッガ起動 など)。
  • 絶対に BASH_SOURCE[0] を使う。さらに 実体パスへ解決してから使う。

例としての最小パターンのコードを下記に示す

#!/usr/bin/env bash
set -euo pipefail

# スクリプト自身の絶対パスとディレクトリ(シンボリックも解決)
SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname -- "$SCRIPT_PATH")"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"

main() {
  # 相対パス前提の処理は、必ず起点を固定する
  cd "$PROJECT_ROOT"
  # ここに本処理…
}

# 「直実行のときだけ main」を呼ぶ(source された時は呼ばない)
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi

という実装方針に変えていけば、デバッガーを活用した開発と、本番運用で同期がとれます

他にも、同じコードで、開発でデバッガーを活用し、本番で同期をとるための
実装上の注意事項は以下のとおりです

  1. プロセス設計

    • source/関数化を基本にして、子プロセスに逃がさないbash child.shbash -c を乱用しない)。
    • 最後だけ exec(必要なら最終移譲時のみ)。途中での exec はデバッグを切る。
  2. エラーハンドリングを固定

    • set -euo pipefail最初に宣言。
    • trap 'echo "[ERROR] line:$LINENO" >&2' ERR失敗地点を可視化
    • パイプは cmdA | cmdB でも 中段失敗を検知pipefail)。
  3. パスとカレントディレクトリ

    • 絶対パス運用に統一(SCRIPT_DIRPROJECT_ROOT を起点に)。
    • cd の成否を必ず前提にする(例:cd "$PROJECT_ROOT" の直後に pwd -P)。
  4. BASH_ENV(任意で安定化)

    • 非対話シェルにも extdebug / functrace / bashdb-trap を適用してブレークの効きムラを減らす
    • 例:ENV BASH_ENV=/usr/local/share/bash-debug/startup.sh(Dockerfile)
      または echo 'export BASH_ENV=/usr/local/share/bash-debug/startup.sh' >> /home/vscode/.bashrc(postStart)
  5. ログと再現性

    • 標準出力/標準エラーを分ける。エラーは stderr>&2)。
    • タイムスタンプで追えるように:exec > >(ts '[%Y-%m-%d %H:%M:%S] ' ) 2>&1moreutilsts 等)。
    • ロケールは固定:LANG=ja_JP.UTF-8LC_ALL=ja_JP.UTF-8
  6. 外部コマンドの存在確認

    • 依存ツールは起動前チェックcommand -v jq >/dev/null || { echo "jq not found" >&2; exit 127; }
  7. 設定と実行の分離

    • .env/設定ファイルを読み込む関数を分け、本体ロジックから I/O を分離
    • これでデバッガでも本番でも**同じ入口(main)**で動かせる。
  8. 終了コードの厳格化

    • 成功=0、失敗≠0 を徹底。分岐の最後は return/exit明示
    • || true は「落ちてはいけない所だけ」に限定して使う。

2つ目の項目「 "name": "Py27 (attach:5678)", 」

python 2.7.5の環境で、コンテナ側であらかじめ起動したプロセスに
attachする方式でのデバッグ実行の項目です。
VSコードがDevcontainer環境にログインしていたら
そのまま、VSコード上のターミナルから、コマンドをたたいて起動します
もしくは、
ホスト側のubuntuのターミナルで
docker compose -p devc_rhel exec --user 1000:1000 app bash -l
を実行し、コンテナに入った後、
コマンドをたたいて起動します。
たとえば、debug_smoke.pyを起動したい場合は、
debug_smoke.pyがあるフォルダをカレントディレクトリとして、
python -m debugpy --listen 0.0.0.0:5678 --wait-for-client debug_smoke.py
の形式で、debugpyを通じた起動をする
すると、一旦、固まる
この固まる現象は正常動作である
VSコードのデバッガーを起動し、このプロセスにアタッチすると、
この固まりが解放され処理が続行する
この時、VSコード側で該当のソースコードにブレイクポイントを置いていなければ
そのまま、最後まで処理が完了するだろう
ブレイクポイントを置いていれば、そこで止まって
リモートデバッグとして、ステップ実行や、各種変数値の確認や、コードのイミディエイト実行など
通常のVSコードのデバッガー機能を活用した実行が行える

補足)
python -m debugpy --listen ... [--wait-for-client] script.py
は公式推奨パターンです。
https://github.com/microsoft/debugpy
を参照してください。

残念ながら、pythonのdebugpyは、このバージョン、この環境では
launch方式は動作しませんでした。
その理由は、下記のとおり

  • 前提(固定値)

    • OS:CentOS 7.2、Python:2.7.5
    • VS Code:1.63.2(Remote-Containers 世代)
    • Python 拡張:2021.9.1246542782
    • デバッガ:debugpy 1.5.1(コンテナに導入済み、python.debugpyPath は固定)
  • launch と attach の違い(要点だけ)

    • launch:VS Code の拡張がターゲットプロセスを起動し、内部で -m debugpy … を仕込む。
    • attach自分で -m debugpy を起動(待受)してから、VS Code が後から接続する。
  • 起きていた事実(この環境での再現)

    1. launch を実行しても、ターゲット側に debugpy の待受が立たない
      • /tmp/debugpy-logs/サーバ起動の痕跡が出ない(「Waiting for client」行が現れない)。
    2. VS Code 側は接続待ちのままになり、Python プロセスが生成されない/すぐ終了する。
    3. 設定(python.pythonPath=/usr/bin/python, python.debugpyPath=/usr/lib/python2.7/site-packages/debugpy)は正しく固定済み
      拡張が“Python 2.7 を launch で起こす”経路が働いていないことが分かる。
  • なぜそうなるか(理由を噛み砕く)

    • この世代の拡張(2021.9)では、Python 2 系の launch 起動が安定していない
      • Python 2.7 は既に EoL(サポート終了)で、拡張側の launcher(ターゲット生成処理)が現役想定から外れている
      • そのため 「拡張がプロセスを起こす」部分が発火せずdebugpy サーバ自体が立たない
    • 一方で attach は拡張の“起動器”を使わない
      • 自分で python -m debugpy --listen 5678 --wait-for-client script.py を起動するので、
        Python 2.7 上の debugpy 本体が動く限り、VS Code は確実に接続できる。
  • 検証の仕方(10秒で事実確認)

    • launch 直後に:ls /tmp/debugpy-logs && grep -i 'Waiting for client' /tmp/debugpy-logs/* || echo "起動痕跡なし"痕跡が無いはず。
    • attach では:
      python -m debugpy --log-to /tmp/debugpy-logs \
        --listen 5678 --wait-for-client /usr/local/bin/debug_smoke.py
      
      → ログに Waiting for clientConnected が出る(= attach は動く)。
  • 結論

    • launch は拡張側の“プロセス起動”が成立せず不発
    • attach は debugpy 本体をこちらで確実に起こせるので、この環境ではattach 一択が安定。
    • 以降は .vscode/launch.json は “Attach” のみを残し、実行は python -m debugpy … で待受→接続、の運用にする。

一旦、あきらめて、この環境では、attach方式でのデバッグ実行をすることにしました。

.vscode/settings.json

{
    "python.logging.level": "Trace",
    "python.trace.server": "verbose",
    "python.debugpyPath": "/usr/lib/python2.7/site-packages/debugpy",
    "python.defaultInterpreterPath": "/usr/bin/python",
    "python.pythonPath": "/usr/local/bin/python",
}

上記の各項目の説明は。
python.logging.level: Python拡張のログ詳細度。"Trace"にすると拡張の内部ログを最大まで出します(Output パネルで“Python”を開く)。launch が不発/attach は成功などの切り分けに使うため最大化しています。

python.trace.server: (言語サーバ)LSP 通信の詳細ログ。"verbose"で、VS Code ↔ 解析エンジン間のやり取りを詳細に記録します。補完や定義ジャンプ等のトラブルを追いやすくするため。

python.debugpyPath: デバッガ本体(debugpy)への絶対パス。ここでは
/usr/lib/python2.7/site-packages/debugpy(debugpy 1.5.1 をコンテナに導入済)を明示し、自分で python -m debugpy … を起動して待ち受け → VS Code が attachの運用を確実にします。

python.defaultInterpreterPath: ワークスペースの既定 Python 実行ファイル。ここでは /usr/bin/python(システムの Python 2.7)を指し、拡張がツール実行時に参照する“既定の Python”を固定します。古い環境で検証済みの 2.7 を常に使わせる狙い。

python.pythonPath: 旧式の“Python の場所”設定(互換目的)。この世代の拡張(2021.9)はまだ参照する経路があるため、後方互換の保険として入れています(記事中でも“設定でインタープリタと debugpy を固定”の方針)。※二重指定は「どのコードパスでも 2.7 を選ばせる」ためで、実運用では両方を同じ 2.7 に合わせるのが安全です。

entrypoint.sh

#!/bin/bash
# /opt/entrypoint.sh
# No-op shim: do nothing except exec the passed command.
# Safe for CentOS 7.2 / bash 4.2. No filesystem or env mutations.

set -e  # fail fast on errors

# If no command was provided, open an interactive login shell.
if [ "$#" -eq 0 ]; then
  exec bash -l
else
  exec "$@"
fi

この実験環境では、上記だけで十分です。
上記のコードの意味は、下記のとおり

  • #!/bin/bash
    CentOS 7.2 の bash は /bin/bash が正置き。ここを直接指定して確実に bash を起動する。

  • set -e
    エラーで即終了(fail-fast)。不完全な状態で処理が続かないようにする。

  • if [ "$#" -eq 0 ]; then exec bash -l; else exec "$@"; fi

    • 引数なし(= CMD/command が空):ログインシェルで開く(bash -l)。
      → コンテナを手動で触る実験に使える。
    • 引数あり:渡されたコマンドにそのまま exec で置き換える("$@")。
      PID1 を本番アプリに譲る余計なプロセスを1つも増やさない

これに加えて、本番想定の本チャンの開発環境ではもっとたくさん実装書きます(それは掲載しません)

setup-extensions.sh

#!/usr/bin/env bash
set -euo pipefail

mode="${1:---block}"

VSIX_DIR="/opt/vsix"
EXT_DIR="${HOME}/.vscode-server/extensions"
REQ=(
  "ms-python.python-2021.9.1246542782.vsix:ms-python.python-2021.9.1246542782"
  "bash-debug-0.3.9.vsix:rogalmic.bash-debug-0.3.9"
)

# server.sh が出るまで待つ(VS Code Server 展開待ち)
wait_server_sh() {
  while :; do
    local s
    s=$(echo "${HOME}"/.vscode-server/bin/*/server.sh 2>/dev/null || true)
    [ -n "$s" ] && [ -x "$s" ] && { echo "$s"; return 0; }
    sleep 1
  done
}

already_ok() {
  for item in "${REQ[@]}"; do
    [ -d "${EXT_DIR}/${item##*:}" ] || return 1
  done
  return 0
}

install_once() {
  local server_sh="$1"
  mkdir -p "${EXT_DIR}"
  for item in "${REQ[@]}"; do
    "${server_sh}" --install-extension "${VSIX_DIR}/${item%%:*}" --force --extensions-dir "${EXT_DIR}" || true
  done
  chown -R "$(id -u)":"$(id -g)" "${EXT_DIR}" || true
}

case "$mode" in
  --block)
    # 既に入っていれば即終了(冪等)
    already_ok && exit 0
    server_sh="$(wait_server_sh)"
    # 導入→検証が通るまで待つ(=Reopen 完了時点で必ず揃う)
    while :; do
      install_once "$server_sh"
      already_ok && break
      sleep 2
    done
    exit 0
    ;;
  *)
    echo "usage: $0 [--block]" >&2; exit 2 ;;
esac

このコードは、署名なしの時期での古いバージョンの拡張機能について
通常の方法ではインストールできなかったため
devcontainer.jsonの
"postCreateCommand": "",
"postStartCommand": "bash -lc '/opt/setup-extensions.sh --block'",
"postAttachCommand": "",
この記述をトリガーにして起動されるコードです。
これにより、
「Rebuild Witchout cache Reopen」でVSコードをDevcontainer環境で
開きなおしたときには、該当の拡張機能がインストールされてる状況となりました。
devcontainer.jsonのトリガー部分の記述と、setup-extensions.shの
コードの意味の詳細は、下記のとおりです。

特には意味がないですが、一応

実験用のサンプルコードなので
特には意味がないですが、一応、
aaa.sh、bbb.sh、ccc.sh、func.sh、debug_smoke.py
も示しておきます
aaa.sh、bbb.sh、ccc.sh、このあたりは、子プロセスへのステップインを実験したかったため
用意したものですが、結果、できないことがわかりました。
ただ、実行自体は、ちゃんと動いてます。デバッガーが子プロセスへのステップインできないだけです

aaa.sh

#!/bin/bash
set -euxo pipefail

# 標準出力の色をシアンにする
# printf '\e[36m'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/func.sh"

echo_blue "hello from bash (aaa)"
foo=42
inc foo
inc foo
echo_blue "aaa foo=$foo"

# --- bbb は “子プロセス”として実行(source ではない)
#     ↓↓↓ ここにブレークを置けば、以降 子→孫まで追える
bash "${SCRIPT_DIR}/bbb.sh"

read -p "press Enter to continue..." _
echo_blue "bye"

# 標準出力の色を元の色へ戻す
# printf '\e[0m'

★ 特記事項 ★
launch.jsonで、
"terminalKind": "integrated",
を指定しない状況だと、デバッグ―コンソールに
カラフルな色付きで表示され、
echo した結果の文字列も、青色で示され見やすかったのですが
read -p "press Enter to continue..." _
で、キー入力を受け付けることができませんでした
launch.jsonで、
"terminalKind": "integrated",
を指定すると、
ターミナル上で、デバッグ実行がなされて
echoの出力もそこにでてくれました。
そして、
read -p "press Enter to continue..." _
で、キー入力を受け付けることができました
しかし、全部同じ色で
echoの出力がどれなのか、わかりづらい状況となりました。
そこで、

# 標準出力の色をシアンにする
printf '\e[36m'

で、シアンの色にして

# 標準出力の色を元の色へ戻す
printf '\e[0m'

で、元の色に戻す
方法を試しました。
ところが、コンソール全体の文字色がシアンになってしまいました
echo の出力だけ、期待したのに
そこで、func.shに

echo_blue() {
  # 引数をそのまま1行として出力(スペース/日本語も安全)
  printf '\033[36m%s\033[0m\n' "$*"
}

の関数定義をして、
echo_blue "hello from bash (aaa)"
の実行をすると、
echoの出力の文字色だけ、シアンにできました。

bbb.sh

#!/bin/bash
set -euxo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/func.sh"

echo "bbb start"
say_hello
foo=100
inc foo
echo "bbb foo=$foo"

# ★ 孫プロセスを “bash ccc.sh …” で起動(=別プロセス)
#    ↓↓↓ ここにブレークを置けば、孫で止まることを確認しやすい
bash "${SCRIPT_DIR}/ccc.sh" "$foo"

echo "bbb end"

ccc.sh

#!/bin/bash
set -euxo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/func.sh"

log2tty "ccc start"
say_hello
bar="${1:-0}"
inc bar
log2tty "ccc bar(after inc)=$bar"
echo "ccc end"

func.sh

#!/bin/bash
set -Eeuo pipefail

# 10進で +1(未定義/空/先頭0にも安全)
inc() {
  local var="$1"
  local cur="${!var:-0}"
  printf -v "$var" '%d' "$((10#${cur} + 1))"
}

say_hello() {
  echo "hello from func"
}

# ★ デバッグ実行でも VS Code の Debug Console だけでなく
#    統合ターミナルにも確実に流したい時のロガー
log2tty() {
  # /dev/tty があれば両方へ、なければ通常の stdout へ
  if [[ -t 1 ]] && [[ -e /dev/tty ]]; then
    echo "[TTY] $*" | tee /dev/tty
  else
    echo "[STDOUT] $*"
  fi
}

echo_blue() {
  # 引数をそのまま1行として出力(スペース/日本語も安全)
  printf '\033[36m%s\033[0m\n' "$*"
}

debug_smoke.py

# -*- coding: utf-8 -*-
import sys, time
print("DEBUG_SMOKE: Python:", sys.version)
for i in range(5):
    print("DEBUG_SMOKE: tick", i)
    time.sleep(1)
print("DEBUG_SMOKE: done")

Discussion