自作アプリをDockerで公開する手順

みなさんは、自作アプリをどのように公開したり、他の人に提供していますか?
Windowsアプリなら、Installerに固めて配付したり、実行に必要なファイルのみを提供し、必要なランタイムは個別にインストールをお願いしたりされているのでしょうか。
Linux系のアプリケーションの場合も、同じような方法で提供されているのでしょうか。

あるいは、ソースコードを提供し、受け取った人がビルドして使用する方法もありますね。

私は、最近Rapberry PiのUbuntu上で動くアプリケーションを開発する機会がありました。
Native環境でビルドして動作するアプリを作り、ソースコードと、出来上がった実行モジュールを提供する形で、依頼主に提供するスタイルで作業をしていました。

Raspberry Piでの動作は問題なかったのですが、そのアプリケーションをフィールドで使用するとなると、Raspberry Pi自体の安定性など、いろいろクリアーしなければいけないハードルがでてきます。
そのような場合に、最近は、ARMチップベースのエッジ・ゲートウェイ製品が、各社からリリースされており、それを使うと、ハードウェアのハードルがクリアでき、フィールド利用へも一歩前進します。
ARMチップベースということもあり、OSをUbuntuをサポートしていたりすると、アプリケーションレイヤから見ると、Raspberry Pi固有のI/Oデバイスでもない限り、かなり融通がききます。

そんなこともあり、ARM64アーキテクチャの、エッジ・ゲートウェイ製品に、アプリケーションを移植することになりました。

しかし、この新しい環境では、私が作成するアプリに必要な機能と同じものを使う別アプリが動作しています。
困ったことに、その別アプリと私のアプリが使用するバージョンが異なり、共存が難しいということが分かりました。
この実行環境での競合問題をクリアしなければならないハードル乗り越えるため、私のアプリに必要なパッケージの一部を、システム側ではなく、プライベート側で稼働する方法などを模索しましたが、うまく分離することは難しいと結論しました。

最終的には、仮想化技術で対応することを検討し、Dockerコンテナによるアプリケーション実行を選択しました。

Dockerは以前に少し使ったことはありましたが、アプリのビルドから、配布までをきちんと学んだことはなかったので、これを機会にDockerで、自作アプリをDocker上に移植し、配布する手順を忘備録として整理してみたいと思います。

Native Build手順

Dockerコンテナで自分のアプリを実行させるためには、Dockerイメージを作る必要があります。
このDockerイメージ作成のために、最初に行うことは、Native環境構築手順をまとめることです。

ポイント:

  • BaseとなるOS:例えば、Ubuntu20.04 LTS→FROMコマンドに指定することになります。
  • 必要なビルド済パッケージ:アプリのビルドに必要なパッケージのインストール手順
  • コードビルドが必要なパッケージ:コードからビルドが必要なパッケージがある場合コードを取得しインストールする手順
  • アプリコードのビルド手順:アプリのソースコードから実行モジュール生成
  • アプリ実行のための設定:アプリを実行するために必要な各種設定などの情報を整理

Docker上実行を整理

アプリをDockerで実行ときに、Nativeとは違う注意点をまとめます。

注意点:

  • ビルドとプロダクションを分けて考える:「マルチステージビルド」という考え方でソースコードを含めないで軽量化する
  • Dockerイメージ作成時のユーザ応用:Docker imageビルド時にはユーザ入力できません。インストール時の応答指定も忘れずに指定することが必要
  • Native環境構築のコマンドをDockerfileのコマンド化:cd→WORKDIR、コマンド実行はRUN、HOST環境からのファイル/ディレクトリのコピーはCOPY
  • Buildステージで生成/インストールしたファイルとディレクトリをプロダクションステージにコピー:COPY –fromを使う
  • ステージ内でのコピー:RUN cpで指定
  • ステージ内での各種操作:RUNコマンドでOSのコマンドを実行

これらの注意点を想定して、Native Build手順を、Dockerfileへ置き換えていくことになります。

Dockerfileを作成

Dockerイメージをビルドするために、Dockerfileを作成します。
マルチステージビルドを想定するため、FROMコマンドを2つ持つ以下のような形式のファイルを作成します。

# My Application Dockerfile
FROM ubuntu20.04 AS build-stage

# ここに、Buildステージの手順を記載


FROM ubuntu20.04 

# ここに、プロダクションステージの手順を記載
#  dockerイメージは、このステージで生成されます。
#  実行時に必要なものだけを含めます。
#  build-stageでの生成物は、以下のようにコピーします
# COPY --from

Docker build

さて、いよいよDocker imageをつくます。
docker build コマンドを使い、imageを作成します。

docker build -t {ターゲットイメージ名}:{タグ名} -f {Dockerfileへのパス} {buildに含めるデータのパス}

{ターゲット名}:{タグ名}で生成イメージを決めます。{タグ名}は、バージョンを記載すると管理が容易になります。
{Dockerfileへのパス}を使い、Dockerfileを明示的に指定します。
{buildに含めるデータのパス}は、Dockerbuildに、Netから取得しないLocalなファイルがあれば、そのパスを指定します。Buildに先立ち、このデータは、docker daemonに渡され、docker daemonのビルドで使用されます。
このデータ大きくなると、ビルドに必要なリソースや時間が増大します。

ビルドが完了すると、Docker imageが、{ターゲット名}:{タグ名}で一意になるイメージが作成できます。

ビルド中に、外部からのデータの取得を行う場合、timeoutする場合もあります。
エラーメッセージに合わせて対応が必要です。

私の場合は、Node.jsのアプリケーションに必要な、パッケージの取得に時間がかかり、数回ビルドコマンドの再実行が必要でした。通常は、Dockerビルド時には、各ステップ単位の生成データがキャッシュに保持されます。
再実行時に、キャッシュ済の箇所はSkipできます。
リソースの都合で、キャッシュを行わない指定で、ビルドを行うと、再実行時にも、最初から行うことになるので注意してください。通常はキャッシュが有効としてビルドを行います。

異なるバージョンを確実にDockerfileに従い生成するためには、次のコマンドで、キャッシュをクリアする必要があります。
あるいは、ビルドがリソース不足で完了できない場合も、不要な過去のキャッシュを削除することで行える場合もあります。

docker builder prune

Dockerで作成イメージを実行

作成した、Docker imageを実行するには、docker container run コマンドで行います。
アプリケーションが、ユーザとの対話で行う場合は、-itを指定します。
対話の必要がない場合は、-itdを指定します。

アプリケーションにもよりますか、特定のポートでデータを待受けている場合、-pオプションで、Host OSのポート:Dockerコンテナ内のポートへのマップを指定してdocker container runを実行します。
通常のTCPポートの場合は、ポート番号だけですが、UDPポートの場合は、-pオプションのポート記述の最後に”/udp”文字列を付加します。
必要なポートの数だけ、コマンドラインで指定します。

コマンドライン記述が多い場合は、docker-composeを使う方法がいいかもしれません。

Dockerイメージの他のDocker環境で使うために

作成したイメージが、期待通りの動作を確認したら、このイメージを他の環境でも使えるように、DockerHubなどに登録するのが一般的ですが、クローズド環境での利用とか、外部に置きたくない場合は、Dockerコンテナをファイルにエクスポートする方法があります。
私の場合は、このファイルへのエクスポートを行い、利用する環境でインポートする方法を使いました。

# Dockerイメージのエクスポート

docker save {イメージ名}:{タグ名} | gzip > {docker image export file name}

# Dockerイメージのインポート

gunzip -c {docker image export file name} | docker load

エクスポートするファイルサイズが大きい場合、tar.gzに圧縮するほうがサイズが小さくなり可搬性が上がる場合があります。ここでは、圧縮方式をお勧めします。

最後に…

Dockerを使うことで、既存の環境の影響をすくなくして、アプリを遮蔽して配布のハードルを下げられます。
実行環境依存のアプリケーションでは、そうもいかないかもしれませんが、実行環境の特にハードウェアに依存しないアプリであれば、Dockerベースの実行環境へ踏み込んでみてはいかがでしょうか。