code-server 構築の茨の道と、その近道

ながらく[Codeanywhere](https://codeanywhere.com)を住処にしてきたものの、実はここ半年ほどフラフラしていました。VS Code Remote を使ったり、ほかに浮気したりと。しかし、最近になってようやくエディタ環境がCoder (code-server)に固まってきたので、構築方法をまとめておきたいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/5007102b-afb2-b95f-5b53-176fa9240605.png)

## Coder と code-server

[VS Code](https://code.visualstudio.com) が非常に人気ですが、これをそのままオンラインで使えるようにしたのが[Coder](https://coder.com)です。VS Code同様にオープンソースで開発は進められていて、[github.com/cdr/code-server](https://github.com/cdr/code-server)にホストされています。なお、2019年10月現在でスターが23k超えでした。構成としては、最新版のVS Codeを取り込みつつ、一部にパッチをあて、Electronなしで動くように調整してある、といった感じです。拡張機能にも対応しています。~~v2のリリースが間近です。~~ 2019年10月下旬にv2がリリースされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/2b7f607c-a421-0c1e-2eb6-f41d2c13bf1b.png)

現在のところ、オープンソース版の認証機構はシンプルな固定パスワード(`auth=password`)のみで、マルチユーザ対応などもありません。個人の開発用サーバにインストールして使う、あるいはChromebookのLinux環境に入れて使う、などの用途に限定されるかと思います。

### Coderの良い点、注意点

言うまでもなく、メリットはクラウドで完結する点です。ローカル環境に何もインストールする必要がありません。サーバ上に開発環境を構築しておけば、MacでもWinでも、携帯端末だろうと、ブラウザさえあればアクセスできます。スペックも選び放題です。一度慣れると、ローカル環境に縛られるのが鬱陶しくなります。

一方で、クラウドはデメリットでもあります。当然ネット接続が必須です。最低でも数Mbpsは確保できないと、使用にストレスがたまるでしょう。出先では電波状況に左右されます。

ただ、この点は、一時的にローカル端末のDockerで起動するという手もあります。Windowsでも、[WSL2](https://docs.microsoft.com/en-us/windows/wsl/wsl2-install)の登場でそれが現実的になってきました。例えば、移動の多いときや休暇中、ネットワークに不安のあるときはローカル環境にpullしておいて、普段それ以外のシーンでは基本、クラウドで作業するなど。

### Alternatives?

オンラインエディタの現実的な選択肢があまりなかった数年前に比べると、状況は一変しています。オープンソースで公開されている[VS Codeのエディタ部分](https://github.com/microsoft/monaco-editor)の出来が良いせいか、新規参入も増えました。いかにもExlipseだったCheも、すっかりVS Code風になっていたり古参にも変化が見えます。

一方、セルフホスティングするなら、Coder(code-server)一択になるでしょう。VS Codeの使い勝手と、余計な機能を足さない潔さと、GitHub上での支持、開発母体であるCoder Inc.が昨年末に資金調達に成功している点も、安心材料です。

製品名 | 提供方法 | 特徴 | 備考
:-- | :-- | :-- | :--
[Coder](https://coder.com) | SaaS | ⛅💲 | code-serverのエンタープライズ向け
[code-server](https://github.com/cdr/code-server) | Docker他 | ⛅⭐23k | VS Codeのブラウザ版
[Codeanywhere](https://codeanywhere.com) | SaaS | ⛅💲📱 | オンラインエディタの草分け
[Cloud9](https://aws.amazon.com/cloud9/?origin=c9io) | AWS | ⛅💲 | いまやすっかりawsの一部
[Eclipse Che](https://www.eclipse.org/che/) | SaaS | ⛅⭐6k | Java系
[CodeTasty](https://codetasty.com) | SaaS | ⛅💲 | スロバキア産
[VS Code](https://github.com/microsoft/vscode) | Electron | ⭐84k | Remote拡張機能あり

- ⛅ オンライン利用
- 📱 スマートフォン対応
- 💲 有償

その他、表に含めませんでしが、[GitLabのIDE機能](https://docs.gitlab.com/ee/user/project/web_ide/)がいつの間にか充実していたり、[StackBlitz](https://stackblitz.com/)みたいにインフラをまったく考えずコーディングするツールが増えてたり、改めて普及期に入ってきた感があります。

> 追記(2019年12月) - VS Code本家のリポジトリも、`yarn web`でブラウザから起動できるようになりました。ただし認証機構などはないので、今のところ開発時のテスト用という位置づけ。
> https://code.visualstudio.com/updates/v1_40#_test-vs-code-running-in-a-browser

## Dockerでインストール

いくつか入れ方がありますが、特に理由がなければ、Dockerコンテナとして動かすのが扱いやすいでしょう。まずは、dockerのバージョンを確認しておきます。

```bash
$ docker -v
Docker version 19.03.1, build 74b1e89
```

もし、入っていなければ[ここを参照して](https://docs.docker.com/install/)導入しておきます。次に、以下のコマンドを実行してみます。ディレクトリはどこからでも構いません。

```bash
$ docker run -it -p 8080:8080 codercom/code-server:v2
```

次のような表示が出て、待機状態になったら起動成功です。

```bash
info  Server listening on http://0.0.0.0:8080
info    - No authentication
info    - Not serving HTTPS
```

さっそく、ブラウザから`http://localhost:8080`にアクセスしてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/c5bab3bd-3788-48c2-4d9f-0c466d8bf077.png)

見慣れたVS Codeにしか見えない画面が、ブラウザのなかに収まっています。すごい。

一通り遊んだらターミナルに戻り、<kbd>Ctr</kbd>+<kbd>C</kbd> で終了しておきましょう。

### ホスト側のファイルを開く

前節のコンテナは作成が簡単ですが、残念ながら、そのままではあまり役に立ちません。

先ほど遊んた際、ファイルを保存しましたか? 残念ながら、それらはコンテナの停止とともに消えてしまいます。また、コンテナ内のファイルしか編集できないのでは、あまりに不便です。

そこで、次は手元にフォルダ`projects`を作り、コンテナ内に共有(bind)する形で起動してみましょう。

```bash
$ mkdir -p ~/projects # ソースコード置き場
$ mkdir -p ~/.local/share/code-server # 設定ファイル置き場
$ docker run \
  --interactive --tty \
  --publish 8080:8080 \
  --mount type=bind,source="$HOME/projects",target="/home/coder/project" \
  --mount type=bind,source="$HOME/.local/share/code-server",target="/home/coder/.local/share/code-server" \ 
  codercom/code-server:v2
```

`docker run`の`--mount`オプションでは、`source`にホスト側のパス、`target`にコンテナ側のパスを指定します。上記、ふたつあるのは、「ソースコード置き場」と「設定ファイル置き場」を別々に指定しているためです。

### フォルダを開く

改めて、`http://localhost:8080`にアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/0471e44f-fc2b-558a-b1f0-d1e1a75f6360.png)

Open Folder ボタンをクリックして、`/home/coder/project`にアクセスします。このとき、パスはコンテナ内の絶対パスになっているので、その点注意が必要かもしれません。コンテナ内のユーザは`coder`です。

フォルダが開いたら、URLを確認します。

```
http://localhost:8080/?folder=/home/coder/project
```

となっているでしょうか? `folder`に絶対パスが指定されています。

試しに、適当なフォルダ`hello-world`を作成してみます。使い勝手はVS Codeと同じです。その上で、URLの最後に作成したフォルダ名を追加してEnterします。

```
http://localhost:8080/?folder=/home/coder/project/hello-world
```

今度は、`hello-world`がルートになった画面が表示されたはずです。頻繁に使うのであれば、このフォルダ名付きのURLをブックマークしておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/764e952e-0355-ad77-6a86-579bd429a14d.png)


## 本格的に使うには

さて、ここまではDockerを触ったことさえあれば簡単だったと思います。ただ、お試しならいいのですが、ここからさらに本格的に使おうとすると、地雷が多いので注意が必要です。

お急ぎのひとは、もろもろ対策済みのDockerfileと起動スクリプトを作ってあります。下記URLへどうぞ。

- https://github.com/cognitom/coder2-docker

以下、ひっかかりそうなポイントをまとめます。Coder特有のものもありますが、Dockerで開発する場合に共通のもの🐳もあります。

項目 | 面倒度 | 症状
:-- | :-- | :--
パスワード認証 | 🌋🗻🗻 | デフォルトで認証なし
SSL | 🌋🗻🗻 | 自己認証局だと、PWAにしたとき警告が地味に邪魔
Git | 🌋🗻🗻 | ホスト側のGit設定が引き継がれない
SSH 🐳 | 🌋🌋🗻 | Coderに統合されたGit機能で、pushする際のSSH鍵が必要
ファイル権限 🐳 | 🌋🌋🌋 | コンテナ内のユーザとuid:gidが一致しないと、<br>共有してもホスト側で使えない

> **メモ** ここからの説明は、Linuxサーバをホストとしてdockerが動作している想定で進めます。ローカルで試してみるのも面白いですが、やはり本領を発揮するのはクラウドでの利用です。また、ファイル権限の問題はDocker for Macでは挙動が異なります。

### パスワード認証

`docker run`する際に認証方式を指定する必要があります。今のところ、選択肢は"password"だけです。

```bash
$ docker run -it -p 8080:8080 codercom/code-server:v2 --auth=password

info  Server listening on http://0.0.0.0:8080
info    - Password is f92f17b864a64110aedf1870
info    - To use your own password, set the PASSWORD environment variable
info    - Not serving HTTPS
```

上記のdockerコマンドを実行すると、起動のたびにランダムなパスワードが設定されます。

ただ、パスワードが毎回変わるのも不便なので、dockerの環境変数として指定します。

```bash
$ docker run -it -p 8080:8080 \
  --env PASSWORD="my-strong-password" \
  codercom/code-server:v2 --auth=password

info  Server listening on http://0.0.0.0:8080
info    - Using custom password for authentication
info    - Not serving HTTPS
```

ブラウザからアクセスすると、認証画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/3bab83d7-798b-724b-006c-461d3b651fc5.png)

### SSL

自分専用の環境なので、自己認証でも構わないのですが、最近はLet's Encryptで簡単に証明書が作れるようになってしまったので、むしろきちんとSSL対応したほうが簡単かもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/5edf7987-029a-63d4-7f15-c0997047a158.png)

実際には、GCPやAWSなどのクラウド環境に、インストールすることになると思います。開発用に使っているドメインがあれば、それを流用しましょう。

- **必要なもの**: 自由に使えるドメイン名 (例: example.com)

すでに証明書を取得済みであれば、そのファイルを起動時に指定するだけです。仮に`~/certs`に証明書が入っているとすると、次のように指定します。パスなどは適宜自分の環境に合わせてください。

```bash
$ docker run -it -p 8080:8080 \
  --mount type=bind,source="$HOME/certs",target="/certs" \
  codercom/code-server:v2 \
    --cert="/certs/my-cert.crt" \
    --cert-key="/certs/my-cert.key"
```

開発環境でSSLを未導入であれば、これを機に入れてしまいましょう。[lego](https://github.com/go-acme/lego)というgoで書かれたツールが簡単です。毎日使うものでもないので、インストールの必要もありません。Docker経由で利用します。

```bash
$ docker run -it -v "${HOME}/.lego:/.lego" goacme/lego \
    --email="yourname@example.com" \
    --domains="example.com" \
    --dns=manual \
    run
```

画面の指示に従っていくと、`~/.lego`内に証明書が作成されます。上記、`--dns=manual`としましたが、各社のDNSサービスに対応しているので自動化することもできます。詳しくは、[legoのドキュメント](https://go-acme.github.io/lego/dns/)を参照してください。

あらためて、Coderを起動します。

```bash
$ docker run -it -p 8080:8080 \
  --mount type=bind,source="$HOME/.lego",target="/.lego" \
  codercom/code-server:v2 \
    --cert="/.lego/certificates/example.com.crt" \
    --cert-key="/.lego/certificates/example.com.key"
```

ブラウザからアクセスすると、鍵マークが表示されたはずです。
![2019-10-06 (2)_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1858/4033f24a-165c-51ce-0e18-fc85fab49857.jpeg)

> **メモ** 証明書の有効期限は3ヶ月間です。更新方法については説明を省きますか、cronを仕掛けて自動化しておきましょう。

### Git

普段の環境であれば、おそらく次のようにユーザ名とメールアドレスが設定済みだと思います。

```bash
$ git config --global user.name "John Doh"
$ git config --global user.email "john.doh@example.com"
```

一方、コンテナ内では未設定なので、内部から`git commit`ができません。そこで、起動時の`--mount`オプションで、ホストとコンテナの`.giconfig`をバインドします。

```bash
$ docker run -it -p 8080:8080 
  --mount type=bind,source="$HOME/.gitconfig",target="/home/coder/.gitconfig" \
  codercom/code-server:v2
```

これで、Coder内でGUIからコミットできるようになりました。

### SSH 🐳

ただ、このままだと、コミットはできてもpushやpullができません。何故なら、コンテナ内にSSHの設定と秘密鍵がないからです。`.gitconfig`同様に、`.ssh`フォルダをバインドしてしまいましょう。

```bash
$ docker run -it -p 8080:8080 \
  --mount type=bind,source="$HOME/.ssh",target="/home/coder/.ssh" \
  codercom/code-server:v2
```

手元のマシン内で実行している場合は、これで良いのですが、環境がリモートにあって、SSH Agent Forwardingしている場合はどうでしょうか? その場合は、Unixソケット(`$SSH_AUTH_SOCK`)を共有する必要があります。

```bash
$ docker run -it -p 8080:8080 \
  --env SSH_AUTH_SOCK=/ssh-agent \
  --mount type=bind,source="$SSH_AUTH_SOCK",target="/ssh-agent" \
  --mount type=bind,source="$HOME/.ssh",target="/home/coder/.ssh" \
  codercom/code-server:v2
```

実は、Unixソケットの共有のためには、もういち手順必要で、ホストとコンテナでuidが一致していなくてはなりません。この点については、次節に書きます。

> **メモ** 上記の`$SSH_AUTH_SOCK`は、ターミナルのセッションを切った時点で消えてしまいます。あくまでも、Coderを手動で起動したときのみ有効な手だと思ってください。デーモン化している場合もこの手は使えません。筆者は、push/pullだけはターミナルから実行するようにしています。

### ファイル権限 → Fixuid 🐳

ここまで、スルーしてきましたが、Coderで作成したファイルは**ホスト側から編集できません**。どういうことかは、ホスト側でlsコマンドを叩けばはっきりします。

```bash
$ cd ~/projects && ls -al
drwxr-xr-x  9 cognitom cognitom 4096 Oct  2 13:07 .
drwxr-xr-x  9 cognitom cognitom 4096 Oct  2 13:07 ..
drwxr-xr-x  3 1000 1000 4096 Oct  2 13:07 hello-world
```

親フォルダが私`cognitom`の所有になっている一方で、なぞの`1000`というのが見えます。`id`を確認しておきましょう。まず、ホスト側。

```bash
$ id
uid=5000(cognitom) gid=5000(cognitom) groups=5000(cognitom),412(docker),20141(google-sudoers)
```

続けて、コンテナ側からも確認します。

```bash
$ id
uid=1000(coder) gid=1000(coder) groups=1000(coder)
```

`1000`番の正体はこいつです。ホスト側には存在しないユーザだったので、ユーザ名ではなく`1000`とだけ`uid`と`gid`が表示されていたのです。これが、次のような結果だったらすべて丸く収まったのですが...

```bash
$ id
uid=5000(coder) gid=5000(coder) groups=5000(coder)
```

[fixuid](https://github.com/boxboat/fixuid)はこの状況に対する解決策です。上のように、コンテナ起動時に`uid:gid`をホスト側に揃えてくれます。いかにも苦肉の策ですが、このツールが出て2年経っても進展はあまりなさそうです。経緯については[moby/moby#7198](https://github.com/moby/moby/issues/7198)を参照。

ちなみに、[cdr/code-server#439](https://github.com/cdr/code-server/issues/439)の議論を見ていると、Out-of-the-boxでFixuid対応するかもしれませんが、ハックくさいので、本家でやらなくて良い気もします。

その代わり、用意されている本家のDockerfileを拡張しましょう。

```bash
$ mkdir coder2 && cd coder2
```

適当な空ディレクトリを作り、その中に次のDockerfileを作成します。

```dockerfile
FROM codercom/code-server:v2

RUN sudo bash -c '\
    curl -SsL https://github.com/boxboat/fixuid/releases/download/v0.4/fixuid-0.4-linux-amd64.tar.gz | tar -C /usr/local/bin -xzf - && \
    chmod 4755 /usr/local/bin/fixuid && \
    mkdir -p /etc/fixuid && \
    printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml'

ENTRYPOINT ["fixuid", "dumb-init", "code-server", "--host", "0.0.0.0"]
```

ビルドして実行します。その際に、`--user`指定するのがポイントです。

```bash
$ docker build -t coder2 .
$ docker run -it -p 8080:8080 --user $(id -u):$(id -g) coder2
```

> **メモ** fixuidは、ここで指定されたユーザ(uid)と、ビルド時に`/etc/fixuid/config.yml`に記録したユーザがのid異なると、作動します。

## まとめ

ここまでの対策をすべて取り込んだ起動コマンドが、次のものです。長いので、シェルスクリプトにしてあります。なお、前節までは、対話モードで起動していましたが、普段使うには不便なのでデーモンとして起動し、フリーズしたら自動的に再起動するようなオプションも設定しています。

※`docker-compose`でも構いませんが、開発環境においてはシェルスクリプトの方が小回りが効いてお薦めです。

```bash
#/bin/bash

# Dirs and files
mkdir -p "$HOME/projects"                 # Root dir shared with Coder
mkdir -p "$HOME/.local/share/code-server" # Coder's config dir
mkdir -p "$HOME/.lego"                    # SSL certificates
mkdir -p "$HOME/.ssh"                     # SSH keys, config and known_hosts
touch "$HOME/.gitconfig"                  # Git configs

# Stop and delete the container if exists
docker stop coder2 > /dev/null 2>&1
docker rm coder2 > /dev/null 2>&1

# Build and run
docker build -t coder2 .
docker run \
  --name coder2 \
  --detach \
  --restart unless-stopped \
  --publish ${CODER_PORT:=8080}:8080 \
  --user $(id -u):$(id -g) \
  --env PASSWORD="$CODER_PASS" \
  --env SSH_AUTH_SOCK=/ssh-agent \
  --mount type=bind,source="$SSH_AUTH_SOCK",target="/ssh-agent" \
  --mount type=bind,source="$HOME/projects",target="/home/coder/project" \
  --mount type=bind,source="$HOME/.local/share/code-server",target="/home/coder/.local/share/code-server" \
  --mount type=bind,source="$HOME/.lego",target="/.lego" \
  --mount type=bind,source="$HOME/.ssh",target="/home/coder/.ssh" \
  --mount type=bind,source="$HOME/.gitconfig",target="/home/coder/.gitconfig" \
  coder2 \
    --auth=password \
    --cert="/.lego/certificates/$CODER_HOST.crt" \
    --cert-key="/.lego/certificates/$CODER_HOST.key"
```


```dockerfile
FROM codercom/code-server:v2

RUN sudo bash -c '\
    curl -SsL https://github.com/boxboat/fixuid/releases/download/v0.4/fixuid-0.4-linux-amd64.tar.gz | tar -C /usr/local/bin -xzf - && \
    chmod 4755 /usr/local/bin/fixuid && \
    mkdir -p /etc/fixuid && \
    printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml'

ENTRYPOINT ["fixuid", "dumb-init", "code-server", "--host", "0.0.0.0"]
```

上記ファイルを適当なディレクトリに作成します。Dockerfileは前節のものと同一です。

- coder2-docker/
    - Dockerfile
    - up --- 起動用のシェルスクリプト ※ファイル名は何でも構いません

事前に、ホスト側で環境変数をセットしておきます。

```bash
$ echo "export CODER_HOST='example.com'" >> ~/.bashrc
$ echo "export CODER_PASS='your-perfect-strong-password'" >> ~/.bashrc
$ echo "export CODER_PORT=8080" >> ~/.bashrc
$ source ~/.bashrc
```

実行します。

```bash
$ bash up
```

## さいごに

長い道のりでした。苦労せずとも、手元でVS Codeを使うなり、[VS Code Remote](https://code.visualstudio.com/docs/remote/remote-overview)を使えば良いじゃないかと言われそうですが、開発環境をクラウド化するメリットは大きいです。また、VS Code Remoteもクセが強く別の苦労があります。Coderの方が筋は良さそうです。

今まで、少なくとも数年前までは、CodeanywhereなどSaaSに頼る必要があったオンラインエディタですが、Coderの登場で自前の環境に用意できるようになりました。機能拡張もVS Code互換なので、カスタマイズも自由にできます。2019年現在、最強のオンラインエディタといって良さそうです。

一方、Coderに限った話ではないのですが、Dockerを開発環境として使う場合の知見が少なく、そちらを解決する方に苦労した感があります。もちろん、デプロイやテストでは使っているのだと思いますが、開発環境自体のコンテナ化はそれほど一般化していないのかもしれません。本稿では、SSHやFixuidなど、Coder自体の説明ととしては少しスコープから外れるものも含めました。参考になれば幸いです。

---

## 補足

### コードについて

前述の通り、コードについては下記に公開しています。

- https://github.com/cognitom/coder2-docker

こちらをクローンしてきて、自分用に調整してもらうのが良いかもしれません。

### セキュリティ対策

code-serverは認証がパスワードのみで、簡易な作りになっています。VPNの内側に入れてしまうか、Firewallで制限をかけるなど、追加の対策は必要でしょう。(SaaS版はこのあたりに追加機能がありそうな気がします。未確認)

> **メモ**: クッキーにパスワードが残る点が、issueに上がっていますね...[cdr/code-server#931](https://github.com/cdr/code-server/pull/931)。v2の正式リリースが落ち着いたら、改善すると期待。

Coder特有の問題ではありませんが、開発環境で使う以上、SSHの鍵の扱いも気をつける必要があります。

- 共有サーバで使用しない (root権限を他人に持たせない)
- 他の人に使わせない
- 不使用時は仮想マシンをシャットダウンする (お金も節約)
- パスワードを長く推測不可能なものに
- SSL暗号化
- Firewall:
    - 接続元のIPアドレスを限定する
    - 開放するポートを限定する

上記を基本として、必要に応じて以下のいずれかで補強する形になるでしょうか。

- VPN
- SSH port forwarding
- SSH dynamic port forwarding (SOCKS5 proxy)

ただ、毎日使うものなので、あまり手順が多いと辛くなります。

- モバイル回線の不安定さ
- VPN回線の不安定さ
- ノートPCがスリープから復帰した際の不安定さ

など加味すると、筆者としてはVPNがちょっと辛くなってきました。SSLとFirewallできっちり守っておけば、常時起動しているわけでもないので、大丈夫だろうとは思います。

まったく別のベクトルとして、[COS(Container Optimized OS)](https://cloud.google.com/container-optimized-os/)など、Dockerに特化したLinuxディストリビューションを使うことも、長期的なサーバメンテナンスの視点では選択肢に入ります。これについては、本稿のスコープから外れるのでまたの機会に。