本文へ移動
ZEKILO
開発/クラウド連載 · Oracle Cloud無料サーバーを作る 第4回

Oracle Cloud無料サーバーにDockerを導入する:Portainer管理、Uptime KumaのデプロイとサブドメインHTTPS接続

· 20分で読めます

Ubuntu 24.04 ARMサーバーにDockerとComposeを導入し、PortainerへのSSHトンネル接続、Uptime Kumaのデプロイ、NginxサブドメインでのLet's Encrypt HTTPS化までを実践したシリーズ最終回の作業記録です。

この記事で扱う内容

  • aptでDockerとDocker Compose v2をインストールする
  • sudoなしでdockerコマンドを実行する設定とdockerグループの権限
  • Dockerが公開したポートがUFWをバイパスする問題と127.0.0.1へのバインド
  • Portainerの起動とSSHトンネル経由での安全な接続
  • ComposeによるUptime Kumaとルートドメイン用Webコンテナのデプロイ
  • statusサブドメインへのNginxリバースプロキシとHTTPS設定
  • Uptime Kumaによる自サイトの死活監視の開始

第3回では独自ドメインの接続とHTTPS化まで完了させましたが、リバースプロキシの転送先である8080番ポートにはまだ何も配置しておらず、アクセスすると「502 Bad Gateway」が返ってくる状態でした。いよいよ実際のWebサービスを立ち上げる段階です。サーバーにプログラムを直接インストールしていくと、依存関係やパッケージのバージョンが競合しやすく、アンインストールしても設定ファイルなどの痕跡が残りやすいのが難点です。そこで、アプリケーションごとに独立したコンテナで実行でき、不要になればいつでもクリーンに削除できるDockerを採用することにしました。

今回はDockerとDocker Composeをインストールし、コンテナ管理ツールのPortainer、稼働状況を監視するUptime Kuma、そしてルートドメイン用のWebコンテナを起動して、第3回で構築したNginxリバースプロキシの背後に接続しました。当初の作業メモにあった -p 9000:9000 のままだと管理画面がインターネット上に無防備に公開されてしまう恐れがあったため、コンテナのポートは127.0.0.1(ローカルループバック)にのみバインドし、PortainerにはSSHトンネル経由でのみアクセスする方式に変更しました。 本記事をもって、全4回のシリーズを締めくくります。

ブラウザからのリクエストがCloudflare DNS、OCIセキュリティ・リスト、UFWを通過し、Nginxでserver_nameごとに127.0.0.1:8080のWebコンテナと127.0.0.1:3001のUptime Kumaへ振り分けられ、Portainer(127.0.0.1:9000)にはローカルPCからのSSHトンネル経由でのみアクセスする構成(概念図)

検証環境


01 aptでDockerとCompose v2をインストール

Ubuntu 24.04の公式リポジトリには、Dockerエンジンである docker.io とComposeプラグインである docker-compose-v2 が収録されており、ARM64サーバー環境でもaptコマンド1行で簡単にインストールできます。docker.io はDocker社が公式配布している docker-ce ではなく、Canonical(Ubuntu)がパッケージングしたものです。個人用途のサーバーであれば、OSの定期的なセキュリティ更新と一緒に apt upgrade で一括管理できるほうがメンテナンス性に優れているため、こちらを選択しました。

sudo apt update
sudo apt install -y docker.io docker-compose-v2

# OS起動時の自動起動を有効化し、サービスを今すぐ開始する
sudo systemctl enable --now docker

当初控えていた作業メモには ca-certificates curl gnupg lsb-release をあらかじめインストールする手順がありましたが、これはDocker公式リポジトリ(docker-ce)をAPTソースに追加する際の前準備です。今回はUbuntu公式の docker.io を利用するため不要と判断し、省略しました。インストール完了後、両コマンドのバージョンを確認します。

docker --version
docker compose version

aptでdocker.ioとdocker-compose-v2をインストールし、docker –versionとdocker compose versionでバージョン情報が出力されたターミナル画面

どちらのコマンドでもバージョンが正常に出力されることを確認しました。Compose v2からは従来のスタンドアロンコマンド docker-compose(ハイフン区切り)ではなく、Dockerサブコマンドの docker compose(スペース区切り)で実行します。なお、起動するコンテナはすべて既存のインスタンス(2 OCPU / 12GB、ブートボリューム200GB)内部のリソースで動作するため、Always Free枠を超えるOCIの追加費用は一切発生しません。


02 sudoなしでdockerコマンドを使う

インストール直後の状態では、一般ユーザー(ubuntu)で docker ps を実行すると権限エラー(Permission denied)が発生します。Dockerデーモンと通信するUNIXドメインソケット(/var/run/docker.sock)へのアクセス権限が、rootユーザーおよび docker グループに限定されているためです。

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

ubuntuユーザーをdockerグループに追加しました。

sudo usermod -aG docker $USER

# 現在のシェルにグループ変更を即座に反映(SSHを切断・再接続しても反映される)
newgrp docker

docker ps

docker psがpermission deniedで失敗した後、usermod -aG dockerとnewgrp dockerを実行してsudoなしでdocker psの一覧が表示された画面

再度 docker ps を実行し、エラーなく空のコンテナ一覧が返ってくることを確認しました。なお、docker グループに所属するユーザーは、コンテナ起動時にホストOSの任意のディレクトリをマウントできるため、実質的にrootと同等の特権を持ちます。セキュリティ上の観点から、このグループにはサーバー管理用の ubuntu アカウントのみを追加し、不要なアカウントは所属させないようにします。


03 Dockerが公開したポートはUFWを通らない

コンテナを起動する前に、必ず押さえておくべき重要な注意点があります。docker run -p や Docker Compose の ports でポートをマッピングすると、DockerはLinuxのiptablesに直接PREROUTING転送ルールを追加します。そしてこのルールは、第2回で設定したUFWのパケットフィルタリングルールよりも優先して評価されます。つまり、UFWで22・80・443番ポート以外をすべてブロックしていても、-p 9000:9000 と指定した時点で、UFWをすり抜けて9000番ポートがインターネット全体に開放されてしまうのです。

ポートの指定方法 公開される範囲
-p 9000:9000 ホストのすべてのインターフェース(0.0.0.0)。UFWのルールに関係なく外部に公開される
-p 127.0.0.1:9000:9000 ローカルループバック(127.0.0.1)のみ。ホスト内部(NginxやSSHトンネル経由)からのみアクセス可能

本サーバーではOCIのセキュリティ・リストが外側のファイアウォールとして機能していますが、第1回で8080番を開放していたため、コンテナを 0.0.0.0 で公開してしまうとそのまま外部インターネットに露出してしまいます。Nginxは同一サーバー内部から 127.0.0.1 経由でコンテナにリバースプロキシするため、本記事で起動するコンテナはすべて 127.0.0.1:ホスト側ポート:コンテナ側ポート の形式でバインドしました。なお、8080番ポートは今後Nginxからしかアクセスしないため、第1回でOCIセキュリティ・リストに追加した8080番のイングレス・ルールは削除してしまって差し支えありません。

注意:ポート指定時には必ず先頭に 127.0.0.1 を付与する

Dockerで公開したポートはUFWのフィルタリングを経由しないため、Nginxの背後に配置するWebサービスや各種管理画面は、必ず 127.0.0.1 にのみバインドする必要があります。 ホスト側ポートの前のIP指定を省略すると 0.0.0.0 とみなされ、UFWで遮断したつもりでも外部からアクセス可能な状態になってしまいます。


04 Portainerを起動する

Portainerは、コンテナの一覧や稼働ステータス、ログ、リソース使用量(CPU/メモリ)をWeb UIからGUIで監視・操作(起動・停止・再起動など)できるコンテナ管理ツールです。無料のCommunity Edition(portainer-ce)公式イメージはARM64アーキテクチャにも標準対応しているため、そのまま利用できます。各種データや設定情報は専用のDockerボリューム portainer_data に永続化し、コンテナを破棄・再作成しても設定が失われないようにします。

# Portainerのデータを永続化するボリュームを作成
docker volume create portainer_data

# Portainer CEを起動(127.0.0.1にのみバインド)
docker run -d \
  -p 127.0.0.1:9000:9000 \
  --name=portainer \
  --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:latest

# コンテナ名・ステータス・公開ポートのみを整形して表示
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

--restart=always は、ホストサーバーの再起動時やコンテナの異常停止時に、Dockerデーモンが自動的にコンテナを再起動させるオプションです。また、ホストの /var/run/docker.sock をマウントすることで、Portainerコンテナ内部からホストのDockerデーモンを直接制御できるようにしています。

Portainerのボリュームを作成し、-p 127.0.0.1:9000:9000で起動後、docker psのPORTSに127.0.0.1:9000->9000/tcpと表示された画面

PORTS列に 127.0.0.1:9000->9000/tcp と出力され、ホストのローカルループバックのみにバインドされていることを確認しました。なお、並んで表示されている 8000/tcp や 9443/tcp はDockerイメージ内で EXPOSE 定義されているポート番号であり、ホスト側にマッピング(公開)されているわけではないので気にする必要はありません。


05 SSHトンネルでPortainerに接続

当初のメモではブラウザから http://サーバーIP:9000 に直接アクセスする予定でしたが、現在の構成ではアクセスできません。OCIセキュリティ・リストで9000番ポートを開放していない上、管理画面を暗号化されていない平文のHTTPでインターネット上に露出させるのはセキュリティ上避けるべきだからです。そこで、普段使用しているSSH接続を利用して「SSHポートフォワーディング(トンネル)」を確立することにしました。

作業端末(ローカルPC)のコマンドプロンプトで次のコマンドを実行します。

ssh -i <秘密鍵のパス> -L 9000:127.0.0.1:9000 ubuntu@<サーバーのIP>

-L 9000:127.0.0.1:9000 は、ローカルPCのポート9000番宛ての接続をSSHの暗号化セッションを通じてサーバーの 127.0.0.1:9000 に転送するオプションです。このSSH接続ウィンドウを開いたまま、ローカルPCのブラウザで http://localhost:9000 にアクセスすると、Portainerの管理画面が表示されます。すべての通信データはSSHによって暗号化されるため、HTTP接続であっても第三者に傍受や盗聴をされる心配はありません。

ローカルのコマンドプロンプトからssh -L 9000:127.0.0.1:9000でサーバーに接続し、Ubuntuのウェルカムメッセージが表示された画面(パブリックIPはマスク処理済み)

初期画面が表示されたら、管理者アカウントを作成します。「Username」(デフォルトは admin)と「Password」を入力し、[Create user] をクリックします。以前の古いノウハウ記事などでは「パスワードは8文字以上」と書かれていることがありますが、現在のPortainer CEは最小パスワード長が12文字以上に変更されています。アカウント作成後、[Get Started] をクリックすると「Home」画面にローカル環境(local)が登録され、コンテナ・ボリューム・イメージの総数や、CPU・メモリのリソース使用状況がグラフィカルに確認できるようになります。

なお、Portainerはセキュリティ保護のため、コンテナの初回起動から約5分以内に管理者アカウントを作成しないと、初期セットアップ画面が自動的に無効化(タイムアウト)される仕様になっています。

Tips:SSHトンネルを確立してからコンテナを起動するとスムーズ

管理者アカウントを作成できる猶予時間(5分間)が短いため、あらかじめローカルPCからSSHトンネルを接続しておいた状態でPortainerコンテナを起動すれば、慌てずに初期設定を完了できます。もしタイムアウトによって画面が無効化されてしまった場合は、サーバー側で sudo docker restart portainer を実行してコンテナを再起動すれば、再び初期設定を受け付ける状態に戻ります。


06 ComposeでUptime Kumaをデプロイ

Uptime Kumaは、Webサイトや各種サーバーの稼働状態(死活状況)を定期的にチェックし、応答時間や稼働率を美しいダッシュボード画面で可視化してくれるオープンソースのセルフホスト型監視ツールです。今回は単発の docker run コマンドではなく、構成をコード(YAMLファイル)として管理・再現できる Docker Compose を使用します。管理のしやすさを考慮し、サービスごとに独立したディレクトリを作成して構成ファイルを配置します。

mkdir -p ~/services/uptime-kuma
cd ~/services/uptime-kuma
nano docker-compose.yml
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    volumes:
      - ./data:/app/data
    ports:
      - "127.0.0.1:3001:3001"
    restart: always

過去のノウハウメモなどには最上部に version: '3.3' といった記述が残っていることが多いですが、現在の Docker Compose v2 仕様ではこの version 行は非推奨(Obsolete)となっており、残したままだと警告が出るため削除しています。監視対象の設定や死活ログデータはホスト側の ./data ディレクトリにマウントして永続化されるため、コンテナの停止や再作成を行っても監視履歴が消えることはありません。

nanoエディタで編集中のdocker-compose.yml。services直下のuptime-kuma設定とportsの“127.0.0.1:3001:3001”が確認できる画面

Ctrl + O → Enter でファイルを保存し、Ctrl + X でエディタを終了後、バックグラウンド(デタッチモード -d)でコンテナを起動します。

docker compose up -d
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"

docker compose up -dでイメージ取得から起動まで完了し、docker compose psでuptime-kumaがUp (healthy)と表示された画面

docker compose up -d の実行によってコンテナが立ち上がり、docker compose ps でSTATUSが Up (healthy)、ポートが 127.0.0.1:3001->3001/tcp と正常にバインドされていることを確認しました。


07 ルートドメインの8080にWebコンテナをつなぐ

第3回でルートドメインのリバースプロキシ転送先を 127.0.0.1:8080 に設定したため、この8080番ポートで待ち受けるサービスを立ち上げない限り「502 Bad Gateway」が返り続けます。そこで、今後本格的なサービスを導入するまでのプレースホルダー(仮設ページ)も兼ねて、軽量な nginx:alpine イメージを用いたWebコンテナを同じCompose方式で立ち上げることにしました。

mkdir -p ~/services/web/html
cd ~/services/web
echo '<h1>Hello from Oracle Cloud</h1>' > html/index.html
nano docker-compose.yml
services:
  web:
    image: nginx:alpine
    container_name: web
    volumes:
      - ./html:/usr/share/nginx/html:ro
    ports:
      - "127.0.0.1:8080:80"
    restart: always
docker compose up -d
curl -I http://127.0.0.1:8080

コンテナ内部のNginxプロセスはコンテナ内ネットワークの80番ポートをリスンしますが、ホスト側の 127.0.0.1:8080 にマッピングしているため、ホストOS上で稼働しているNginx(80番・443番ポート)と競合することはありません。ホストOS側からは単なる 127.0.0.1:8080 のローカルサービスとして認識されます。

webディレクトリのdocker-compose.ymlを確認し、docker compose up -d後にcurl -I http://127.0.0.1:8080でHTTP/1.1 200 OKが返ってきた画面

curl -I を実行して HTTP/1.1 200 OK のレスポンスヘッダーが返ってくることを確認しました。これで、ブラウザから https://<ドメイン> にアクセスした際、前回の502エラーではなく「Hello from Oracle Cloud」のテストページが表示されるようになります。コンテンツを変更したい場合は html/index.html を編集するだけで即座に反映され、将来別のWebアプリケーションをルートドメインで動かしたい場合も、そのコンテナのポートを 127.0.0.1:8080 にバインドするだけで簡単に差し替えが可能です。


08 statusサブドメインのAレコードを追加

Uptime Kumaのダッシュボードは status.<ドメイン> というサブドメインでアクセスできるように構成します。当初の作業メモではDNS設定の手順が抜け落ちていましたが、サブドメインのFQDNがサーバーのパブリックIPを正しく指していなければ、この後のCertbotによるLet’s Encryptのドメイン所有権確認(ACMEチャレンジ)が失敗してしまいます。第3回と同様に、Cloudflareの管理ダッシュボードからDNSレコードを追加します。

[DNS] > [レコード (Records)] > [レコードを追加 (Add record)]

項目 設定値
タイプ (Type) A
名前 (Name) status
IPv4アドレス Oracleの予約済パブリックIP(第3回と同じIP)
プロキシ状態 (Proxy status) DNSのみ(DNS only / グレーの雲アイコン)
TTL 自動 (Auto)

設定を保存した後、ローカルPCのコマンドプロンプトで nslookup status.<ドメイン> を実行し、正しく予約済IPが返ってくることを確認します。

注意:DNSの名前解決を確認してからCertbotを実行する

nslookupで予約済IPへの名前解決が確認できる前にCertbotを実行してしまうと、ドメイン確認(ACME認証)に失敗します。短期間に連続で失敗するとLet’s Encryptのレート制限(Rate Limits)に引っかかり、証明書の発行が一時的にブロックされる原因になります。 また第3回で触れたとおり、Cloudflareのプロキシ状態も初回証明書発行時は必ず「DNSのみ」に設定しておきます。


09 Nginxにstatusサブドメインを設定

第3回と同様の手順で、Nginxにサブドメイン専用のバーチャルホスト設定ファイルを作成します。

sudo nano /etc/nginx/sites-available/status.<ドメイン>.conf
server {
    listen 80;
    server_name status.<ドメイン>;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket対応(リアルタイム監視用)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

第3回のルートドメイン設定と異なる極めて重要なポイントが、末尾の3行(WebSocket対応設定)です。Uptime Kumaのダッシュボード画面は、サーバーとの間でWebSocket接続を常時確立してリアルタイムに死活監視データを受信します。しかしNginxはデフォルトのHTTP/1.0プロキシ動作では、クライアントからのプロトコル切り替え(Upgrade要求)ヘッダーをバックエンドに転送しません。そのため、proxy_http_version 1.1 を明示し、Upgrade および Connection ヘッダーを適切に中継する設定を追加して初めて、WebSocket通信が正常に維持されダッシュボードが途切れることなくリアルタイム更新されるようになります。

nanoで開いた/etc/nginx/sites-available/status.<ドメイン>.conf。proxy_pass http://127.0.0.1:3001の下にWebSocket転送用のproxy_http_version 1.1、Upgrade、Connectionの3行が強調されている画面


10 設定の有効化とHTTPSの取得

シンボリックリンクを作成して設定を有効化し、構文チェック(nginx -t)が成功したらNginxをリロードします。続いてCertbotを実行し、サブドメイン専用のSSL/TLS証明書を取得・適用します。

sudo ln -s /etc/nginx/sites-available/status.<ドメイン>.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d status.<ドメイン>

第3回でLet’s Encryptのアカウント登録を済ませているため、今回はメールアドレスの入力や利用規約への同意プロンプトは表示されず、即座に証明書が発行・Nginxに自動組み込みされます。

ln -s、nginx -tの成功、reload後、certbot –nginx -d status.<ドメイン>が証明書を正常に取得・適用したターミナル画面

新しく取得した証明書も、第3回で確認した certbot.timer(systemdタイマー)がまとめて自動更新してくれるため、サブドメインを増やすたびに更新スケジュールを個別設定する必要はありません。 sudo certbot renew --dry-run を実行して確認すると、ルートドメインとstatusサブドメインの2つの証明書がどちらも正常に更新シミュレーションを通過することが確認できます。


11 Uptime Kumaへの接続と最初のモニター

ブラウザで https://status.<ドメイン> にアクセスします。アドレスバー左側のサイト設定アイコンをクリックすると「この接続は保護されています」と表示され、SSL/TLS証明書が正しく適用されていることが確認できます(第3回で触れたとおり、現在のChromeでは緑色の鍵アイコンではなくチューンアイコンが表示されます)。初回アクセス時は、管理者アカウントの作成画面が表示されます。

注意:初回アクセス時、直ちに管理者アカウントを作成する

Uptime Kumaは管理者アカウントが作成されるまでの間、URLを知っている人なら誰でも初期アカウントを作成できてしまう仕様です。 サブドメインが開通したら、他の作業より先に最優先で管理者アカウントを作成してください。

アカウント作成後、ダッシュボードの [Add New Monitor] をクリックして最初の死活監視モニターを追加します。

項目 設定値
Monitor Type HTTP(s)
Friendly Name ルートドメイン
URL https://<ドメイン>
Heartbeat Interval 60秒

第07節でルートドメインにWebコンテナを事前に接続しておいたため、設定を保存してしばらく待つとモニターのステータスが緑色の「Up」に変わり、応答時間のグラフがリアルタイムに描画され始めることを確認しました。もし8080番ポートを空けたままこのモニターを登録していたら、502 Bad Gatewayを検知して「Down」と表示されていたはずです。


まとめ

  • Ubuntu 24.04 ARM64環境では、aptパッケージの docker.io と docker-compose-v2 でDocker環境を構築し、docker グループに所属させてsudoなしでコマンドを実行できるようにしました(※dockerグループは実質rootと同等の権限を持ちます)。
  • Dockerのポートマッピング(-p)はUFWのルールをバイパスするため、コンテナのポートはすべて 127.0.0.1(ローカルループバック)にのみバインドしました。
  • Portainerの9000番ポートはインターネットに公開せず、SSHトンネル経由でのみ安全に接続しました。管理者アカウントは初回起動から5分以内に作成する必要があります。
  • Composeファイルの version 行は非推奨のため削除し、サブドメインは「Aレコード追加 → Nginxリバースプロキシ設定(WebSocketヘッダー含む) → Certbot証明書発行」の手順でHTTPS化しました。
  • ルートドメインの8080番ポートにWebコンテナを接続して第3回の502エラーを解消し、サブドメイン上のUptime Kumaから自サイトの死活監視を開始しました。

おわりに

本記事をもって、Oracle Cloud Always Freeを活用した無料サーバー構築シリーズは完結となります。全4回を通じて構築した環境は次のとおりです。

回 構築・設定内容
第1回 アカウント登録、PAYG移行、Ampere A1(2 OCPU / 12GBメモリ / 200GBブートボリューム)インスタンス作成
第2回 エフェメラルIPの仕様確認、UFWファイアウォール(22・80・443番のみ許可)、Swap 4GB、タイムゾーン設定
第3回 予約済IPへの切り替え、Cloudflare DNS、Nginxリバースプロキシ、Let’s EncryptのHTTPS化と自動更新
第4回 Docker・Compose導入、Portainer(SSHトンネル)、Uptime Kumaデプロイ、ルートWebコンテナ、サブドメインHTTPS化

OCI側のインフラ費用はすべてAlways Free(無料枠)の範囲内に収まっており0円で、別途発生したのは独自ドメインの登録費用のみです。

一方で、今後の運用に向けて検証すべき課題も残されています。Let’s Encryptによる90日後の実稼働での自動更新挙動や、コンテナイメージの定期的な更新サイクル(docker compose pull からの docker compose up -d)の運用ルールはまだ決めていません。また、Uptime Kumaの ./data やPortainerの portainer_data ボリュームを外部にバックアップする方法も今後検討が必要です。この安定したサーバー環境にサービスを1つずつ展開しながら得られた知見やトラブルシューティングについては、また別の機会に記録していく予定です。

参考リンク

  • #オラクルクラウド
  • #OCI
  • #無料サーバー
  • #Docker
  • #Portainer

連載の記事一覧 · Oracle Cloud無料サーバーを作る

  1. 第1回Oracle Cloud無料サーバー、登録からPAYG移行とAmpereインスタンス作成まで
  2. 第2回Oracle Cloud無料サーバーの安定化、エフェメラルIPの正体とUFW・Swap・タイムゾーン設定
  3. 第3回Oracle Cloud無料サーバーに独自ドメインを接続する:DNS Aレコード、Nginxリバースプロキシ、Let's Encryptで無料HTTPS
  4. 第4回Oracle Cloud無料サーバーにDockerを導入する:Portainer管理、Uptime KumaのデプロイとサブドメインHTTPS接続 (この記事)
ブログの記事をもっと見る