プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

QNAP NAS 上の複数の docker コンテナに外部から https でアクセスできるようにする

いま、 Roudcube を https 接続で使っています。

最近いろんなこと(Pleasanter だとか RAG だとか)を docker で試しているんですが、こういう試しに作ったサイトも、デモなどのため外部からアクセスできるようにしたいと感じてます。いままでは、とりあえず http 接続で乗り切ったのですが、やっぱり https でつなぎたいなと。

で、 Roundcube コンテナを https 化するのに、 https-portal というコンテナを利用しているのですが、これは内部で Let's Encript を使っているので、 80 番ポートにアクセスできる必要があります。

そうすると、どうやって NAS の1つの 80 番ポートで複数の docker コンテナへの https リクエストをさばけばいいんだろうか?となります。最初は、無理なのかな?と思ったのですが、 https-portal の Github の説明を見ていると、

とあるので、同じポート番号であってもマルチドメインに対応して振り分けをやってくれるようです。

ということで、この機能をうまく自分の環境で使えないかと試したので、そのあたりのことをメモっておきます。

なお、 docker は QNAP の NAS (TS-262) 上の Container Station のものを利用しています。

docker network について

https-portal 自体はマルチドメインに対応していそうですが、ひとつの docker アプリケーション(ここでは、 docker-compose.yml で生成される、通常は複数の docker コンテナからなる一連のコンテナスタックをアプリケーションと呼んでいます)に全然関係ないコンテナを詰め込むのは避けたいところです。

どうしたものかと思いつつ、 docker compose は1つのアプリケーション内であれば、コンテナ間で通信ができることから、 docker の network について理解すればヒントがありそうかな?となりました。

いろいろと読んでみて自分なりに理解したつもりなのですが、まあ、中途半端に書くよりも詳しいことはググれば(ChatGPT にでも聞けば)いろいろと情報が得られるので、ここでは必要そうなことをまとめておくと

  • docker compose でアプリケーションを作成すると、そのアプリケーション用のネットワークが自動的に作られます
  • この network 内ではコンテナ間で通信することができます(コンテナは docker-compose.yml のサービス名で指定できます)
  • 一つのアプリケーションに複数の network を持たせることができます(また、サービス単位で利用する network を指定することもできます)
  • 同じ network につながっているコンテナ同士は通信が可能です

ということでした。

つまり、どういうことかというと

  • https-portal 用の network を用意
  • https 化したいアプリケーション(コンテナ)に https-portal 用の network を追加する

としておけば、 https-portal からの通信がアプリケーション側に届くということになり、さきほどのマルチドメインを実現できそうです。いろんなことできるんですね docker 。

なお、 network については、下記の記事などを参考にしました。

準備

では、早速やってみようと思います。

いま、 https-portal を動かしている Roundcube アプリケーションの docker-compose.yml を確認すると、

version: '3'

services:
  https-portal:
    image: steveltn/https-portal:1.23.1
    ports:
      - 'xxxx:80'
      - 'yyyy:443'
    restart: always
    volumes:
      - rcmail_ssl_certs:/var/lib/https-portal
    environment:
      DOMAINS: 'xxxx.yyyy.zzzz -> http://roundcubemail:80'
      STAGE: 'production'
      # FORCE_RENEW: 'true'
      CLIENT_MAX_BODY_SIZE: 0
  
  roundcubemail:
    image: myroundcube:0.3
    container_name: myrc
#    restart: unless-stopped
    volumes:
      - rcmail_www:/var/www/html
      - rcmail_sqlite:/var/roundcube/db
#    ports:
#      - 9001:80
    environment:
      - ROUNDCUBEMAIL_DB_TYPE=sqlite
      - ROUNDCUBEMAIL_SKIN=elastic
      - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://サーバー名
      - ROUNDCUBEMAIL_DEFAULT_PORT=993
      - ROUNDCUBEMAIL_SMTP_SERVER=ssl://サーバー名
      - ROUNDCUBEMAIL_SMTP_PORT=465
      - ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE=30M
      - ROUNDCUBEMAIL_PLUGINS=archive,zipdownload,attachment_reminder

volumes:
    rcmail_ssl_certs:
        external: true
    rcmail_www:
        external: true
    rcmail_sqlite:
        external: true

となっています。まずは、これを https-portal の部分と Roundcube の部分に分けたいと思います。

ボリュームをコピー

さて、上記の docker-compose.yml をみればわかりますが、 https-portal に関連する証明書等の保存先をボリュームにしています。rcmail_ssl_certs というやつですね。ですが、複数のアプリケーションで利用しようとすると、名前の先頭に rcmail_ とついているので、ちょっといけてません。

ということで、このボリュームをリネームしたいところなんですが、 docker はボリュームのリネームができないので、

【図解付き】Docker Data Volumeのバックアップ・リストア方法 #MySQL - Qiita

などを参考にして、新しいボリュームを作成し、既存のボリュームをこれにコピーすることにします。

一応、作業前に、このボリューム(https-portal が使っているボリューム)を含んでいる Roundcube のアプリケーションを停止しておきます。次に、コピー先となるボリュームを作成します。

[ユーザ名@NAS名 application]$ docker volume create ssl_certs
ssl_certs
[ユーザ名@NAS名 application]$ docker volume ls
DRIVER    VOLUME NAME
local     21ce479cdc9ce0e65fffc4a071bf46312c6c089cb54c0cf964344276b6630b25
(略)
local     ssl_certs
[ユーザ名@NAS名 application]$ 

既存のボリュームの内容をバックアップします。

[ユーザ名@NAS名 tmp]$ docker run --rm --volumes-from rcmail-https-portal-1 -v `pwd`:/backup busybox tar cvf /backup/ssl_certs_backup.tar /var/lib/https-portal
tar: removing leading '/' from member names
var/lib/https-portal/
(略)
var/lib/https-portal/xxxx.yyyy.zzzz/production/signed.crt
[ユーザ名@NAS名 tmp]$ 

ここで、 rcmail-https-portal-1 というのは https-portal コンテナの名前です。これを実行した結果、作業したディレクトリに tar ファイルができました。

[ユーザ名@NAS名 tmp]$ ls
shared-net/  ssl_certs_backup.tar
[ユーザ名@NAS名 tmp]$ 

この tar ファイルをさきほど作成した新しいボリュームにコピーします。

[ユーザ名@NAS名 tmp]$ docker run --rm -v ssl_certs:/var/lib/https-portal -v `pwd`:/backup busybox tar xvf /backup/ssl_certs_backup.tar
var/lib/https-portal/
(略)
var/lib/https-portal/xxxx.yyyy.zzzz/production/signed.crt
[ユーザ名@NAS名 tmp]$ 

ここの -v ssl_certs:/var/lib/https-portal で新しいボリューム ssl_certs を busybox の一時コンテナの /var/lib/https-portal に割り当てて、 tar xvf で解凍することで、このフォルダ(ひいては新しいボリューム)にファイルを書き込んでいるということです。うまくいったっぽいですね。

https-portal 用の network を作成

次の準備として、 https-portal で利用する network をコマンドラインから作っておきます(QNAP の Container Station には network 用のGUI がないため)。

[ユーザ名@NAS名 application]$ docker network create https-portal-nw
d5bdd0e55eed2468eaff67a216ba98197692c075a44bdbea34eadcfad13c3be5
[ユーザ名@NAS名 application]$ 

これで準備は完了です。

既存の docker compose を2つのアプリケーションに分離

さて、準備ができたら、いよいよ既存の docker-compose.yml を https-portal と Roundcube の2つに分離します。

https-portal 用の docker-compose.yml は

services:
  https-portal:
    image: steveltn/https-portal:1.25.2
    ports:
      - 'xxxx:80'
      - 'yyyy:443'
    restart: always
    volumes:
      - ssl_certs:/var/lib/https-portal
    environment:
      DOMAINS: 'xxxx.yyyy.zzzz -> http://roundcubemail:80'
      #STAGE: 'production'
      # FORCE_RENEW: 'true'
      CLIENT_MAX_BODY_SIZE: 0

volumes:
  ssl_certs:
    external: true

networks:
  default:
    name: https-portal-nw
    external: true

のように、トップレベルの networks を定義して、デフォルトのネットワークとして先ほど作成した https-portal-nw を指定しておきます。

一方、 Roundcube 用の docker-compose.yml は

services:
  roundcubemail:
    image: myroundcube:0.3
    container_name: myrc
#    restart: unless-stopped
    volumes:
      - rcmail_www:/var/www/html
      - rcmail_sqlite:/var/roundcube/db
#    ports:
#      - 9001:80
    environment:
      - ROUNDCUBEMAIL_DB_TYPE=sqlite
      - ROUNDCUBEMAIL_SKIN=elastic
      - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://サーバー名
      - ROUNDCUBEMAIL_DEFAULT_PORT=993
      - ROUNDCUBEMAIL_SMTP_SERVER=ssl://サーバー名
      - ROUNDCUBEMAIL_SMTP_PORT=465
      - ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE=30M
      - ROUNDCUBEMAIL_PLUGINS=archive,zipdownload,attachment_reminder
    networks:
      - default
      - https-portal-nw
      
volumes:
    rcmail_www:
        external: true
    rcmail_sqlite:
        external: true

networks:
  https-portal-nw:
    external: true

のように、 既存のものと比べると https-portal のサービス定義を削除して、トップレベルの networks を追加して、それを Roundcube のサービス定義でも参照するようにしています。

起動テスト

準備ができたら、一度動かしてみます。まず最初は Roundcube から起動します。

QNAP の Container Station で停止済みの Roundcube のアプリケーションを選択し、歯車アイコンから『再作成』を選択します。

docker-compose.yml が表示されるので、上記の修正したものに書き換えて『更新』を押します。問題がなければ無事に Roundcube が立ち上がります。

次は、 https-portal を起動します。QNAP の Container Station から『アプリケーション』を選択し『作成』を選びます。

アプリケーション名を入力後、 docker-compose.yml の内容を記入します。最後に『作成』ボタンを押します。ログを見て、特にエラー等が出力されていなければ OK です。

実際にブラウザで Roundcube に https で接続できることも確認しておきます。

(参考)ちょっと問題があった例

もし、最初に https-portal から起動すると、サービス名 roundcubemail がないということで、

という感じのエラーを吐き出して、起動に失敗します。今回の場合 restart: always とあるため、なんども再起動を繰り返してしまいます。本来は、同じネットワーク内にサービスがあれば名前が解決するはずなんですが、まだ、 Roundcube を起動していないための現象ですね。

このとき、急いで Roundcube も起動すると、無事に https-portal のエラーもなくなり、起動できるようになりました。一応動きましたが、ちょっと怖いですよね。

また、今回はコマンドラインで作成した https-portal-nw を https-portal の docker-compose.yml に含ませることもできます。

services:
  https-portal:
    image: steveltn/https-portal:1.25.2
(略)
networks:
  default:
    name: https-portal-nw

こんな感じに external の指定をなくした形になります。

この場合は、 Roundcube 側に https-portal-nw が external として与えられているため、必ず https-portal から起動する必要があります。そうなると、間違いなく上記で書いた現象になるので、個人的にはこの書き方はちょっと微妙な感じがしてしまいますね。

追加のコンテナを起動して https-portal へ追加

次に、追加のコンテナを起動します。追加のコンテナは、こちらで試した Pleasanter のものとします。

いま使っている docker-compose.yml に networks 設定を追加します。

services:
  db:
    container_name: postgres
    image: postgres:16
(略)
    volumes:
      - type: volume
        source: pg_data
        target: /var/lib/postgresql/data
  pleasanter:
    container_name: pleasanter
    image: implem/pleasanter:1.4.13.0
(略)
    networks:
      - default
      - https-portal-nw

volumes:
  pg_data:
    name: ${COMPOSE_PROJECT_NAME:-default}_pg_data_volume

networks:
  https-portal-nw:
    external: true

また、 postgres コンテナとの通信で使うため、 Pleasanter コンテナではアプリケーションのデフォルトのネットワークも利用するように明示的に指定しています。

Pleasanter の場合は QNAP の Container Station から再作成するだけではできなくて、こちらの記事にあるように

  1. Container Station から再作成
  2. 必要なファイルをコピー
  3. codedefiner を実行
  4. pleasanter を起動

として再作成します。なお、最後の2つの手順はこんな感じのコマンドラインになります。

[ユーザ名@NAS名 pleasanter-test]$ docker compose -f docker-compose.yml -f docker-compose.codedefiner.yml run --rm codedefiner
(略)
[ユーザ名@NAS名 pleasanter-test]$ docker compose up -d pleasanter

これで、 Pleasanter が無事に起動できているのを確認したら、次は https-portal コンテナを再作成します。このとき、https-portal のドキュメントに従って、マルチドメインに対応できるようにしておきます。

services:
  https-portal:
    image: steveltn/https-portal:1.25.2
    ports:
      - 'xxxx:80'
      - '443:443'
    restart: always
    volumes:
      - ssl_certs:/var/lib/https-portal
    environment:
      DOMAINS: >
        xxxx.yyyy.zzzz -> http://roundcubemail:80,
        aaaa.bbbb.cccc -> http://pleasanter:50001,
      STAGE: 'production'
(略)
networks:
  default:
    name: https-portal-nw
    external: true

マルチドメインの設定のほかにも、https のポート番号を 443 にしました(理由は後述します)。

テスト

さて、これで問題なく接続できるかテストします。ブラウザから https でそれぞれのドメイン名を指定して接続すると問題なく表示されました。

また、 https-portal のログを見てみると、

のように出ており、どちらのドメインも正しく認識できているようです。うまくできているようですね。

(参考) https-portal のホスト側のポート番号を 443 にした理由

さて、上記でマルチドメインに対応する際、 https-portal のホスト側のポート番号を 443 にしました。 実はこれ、今回追加した Pleasanter の場合、ホスト側のポート番号が 443 以外の場合(仮に 4444 としておきます)、うまく接続ないという問題が起きていました。

調べてみると、 Pleasanter の場合、 https://サーバー名 としてアクセスすると、ログイン画面が表示されるのですが、この際 302 リダイレクトでログイン画面の呼び出しを行っています。

開発者ツールで確認するとわかるのですが、このとき Location として

http://サーバー名/users/login?RerunUrl=%2F

のような http の絶対 URL が返ってきます。

すると、ブラウザはこれにアクセスしようとするのですが、 https-portal では日本語訳の説明

にあるように、 http 接続を https 接続にリダイレクトするため、https での接続になります。

この時、https-portal は https 接続は 443 として扱うので、ブラウザに返される Location は

https://サーバー名/users/login?RerunUrl=%2F

になり、ブラウザからは https のデフォルトのポート番号 443 でアクセスすることになります。すると、 https-portal で想定しているホスト側のポート番号 4444 ではアクセスできないため、接続ができなくなってしまいます。

具体例

このやり取りが、ブラウザの開発者ツールだと若干わかりにくかったので、コンソールから wget で実際の応答を確認してみました。

https で接続するポート番号をここでは 4444 としています。

mor@DESKTOP-DE7IL4F:~$ wget --server-response https://pleasanter.aaaa.bbbb.cccc:4444
--2025-06-14 22:50:05--  https://pleasanter.aaaa.bbbb.cccc:4444/
pleasanter.aaaa.bbbb.cccc (pleasanter.aaaa.bbbb.cccc) をDNSに問いあわせています... IPアドレス
pleasanter.aaaa.bbbb.cccc (pleasanter.aaaa.bbbb.cccc)|IPアドレス|:4444 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています...
  HTTP/1.1 302 Found
  Server: nginx
  Date: Sat, 14 Jun 2025 13:50:05 GMT
  Content-Length: 0
  Connection: keep-alive
  Location: http://pleasanter.aaaa.bbbb.cccc/users/login?ReturnUrl=%2F
  X-Frame-Options: SAMEORIGIN
  X-XSS-Protection: 1; mode=block
  X-Content-Type-Options: nosniff
場所: http://pleasanter.aaaa.bbbb.cccc/users/login?ReturnUrl=%2F [続く]

となり、 302 リダイレクトで Location に http:// で始まる絶対 URL が返ってきていることがわかります。続いて

--2025-06-14 22:50:06--  http://pleasanter.aaaa.bbbb.cccc/users/login?ReturnUrl=%2F
pleasanter.aaaa.bbbb.cccc (pleasanter.aaaa.bbbb.cccc)|IPアドレス|:80 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています...
  HTTP/1.1 301 Moved Permanently
  Server: nginx
  Date: Sat, 14 Jun 2025 13:50:06 GMT
  Content-Type: text/html
  Content-Length: 162
  Connection: keep-alive
  Location: https://pleasanter.aaaa.bbbb.cccc/users/login?ReturnUrl=%2F
場所: https://pleasanter.aaaa.bbbb.cccc/users/login?ReturnUrl=%2F [続く]

となっていることがわかります。 http で接続を試みるのですが https にリダイレクトされています。また、その際の Location は

Location: https://pleasanter.aaaa.bbbb.cccc/users/login?ReturnUrl=%2F

のように、 スキーマは https ですがポート番号の記載がないことがわかります。最初に書いた https 接続時のポート番号 4444 は docker のホスト側の値なので https-portal は知らないと考えていいのでしょうね。

すると、

--2025-06-14 22:50:06--  https://pleasanter.aaaa.bbbb.cccc/users/login?ReturnUrl=%2F
pleasanter.aaaa.bbbb.cccc (pleasanter.aaaa.bbbb.cccc)|IPアドレス|:443 に接続しています... 失敗しました: 接続を拒否されました.
pleasanter.aaaa.bbbb.cccc (pleasanter.aaaa.bbbb.cccc) をDNSに問いあわせています... IPアドレス
pleasanter.aaaa.bbbb.cccc (pleasanter.aaaa.bbbb.cccc)|IPアドレス|:443 に接続しています... 失敗しました: 接続を拒否されました.
mor@DESKTOP-DE7IL4F:~$

のように、ポート番号が違うため接続できなくなってしまっています。

最初に利用していた Roundcube でもリダイレクト処理はあったのですが、改めて調べてみると Location の書き方が相対 URL になっていたためか、特に問題が起きていませんでした。

https-portal の機能で、ホスト側のポート番号を返せるようなものがあるなら、公開するポート番号を自由に決められるのですが、とりあえずは 443 を使った形になりそうですね。

まとめ

若干苦労もしましたが、なんとか NAS 内のコンテナで複数の公開サーバーを立てた場合でも、 https 経由で接続できるようになりました。

この方法だと、公開サーバーが増えて、アプリケーションを追加する場合でも、 https-portal の docker-compose.yml を書き換えて再起動(再作成)すれば、既存のアプリケーションには手をつけなくてもよいのでなかなか便利そうです。