PrismaでエックスサーバのMariaDBにアクセス+Renderでのデプロイ

DB

はじめに

最近自分のポートフォリオサイトを作り始めている。

そにたん's Laboratory
そにたんのすべてを詰めたサイト

ポートフォリオサイトの一部データをDBで管理してみたいと考えた。
現在自分はエックスサーバをレンタルしているため、そこで使えるMariaDBを使ってアクセスしよう!と思った。
ポートフォリオサイトは、Next.jsで構築してVercelでデプロイしている。
過去にエックスサーバのMySQLを使っていたときに、URLで直接アクセスできたので同じ方法を試みたがうまくいかなかった。
調べてみたところMariaDB は外部から直接アクセスできないらしい。

色々調べていく中でSSHトンネルを構築して、アクセスする方法があることを知った。
VercelでSSHの構築は調べてみると難しいとわかった。
ただどうしても、これ以上追加でお金をかけたくないのだ….
そのため、考えたのがRenderの無料枠を使うことでMariaDBへアクセスする方法だ。

構成図

今回は以下のことができるようにしたい

    開発環境

  • NodeExpressでdevモードとproductモードを用意する
  • devモードは、Dockerで構築したMySQLを使う(そのうちMariaDBに変えたい)
  • productモードは、SSHトンネルを構築してエックスサーバのMariaDBを使う
  • devモードは、PrismaのマイグレーションはDocker上のMySQLに行う
  • productモードは、PrismaのマイグレーションはエックスサーバのMariaDBに行う
    デプロイ環境

  • RenderでNodeExpressをデプロイする
  • Render上にSSHトンネルを構築する
  • エックスサーバのMariaDBを使う

上記を踏まえた構成図は以下

プロフィール 3

エックスサーバ

SSH設定

まず、SSHトンネルを構築するに当たりSSHの設定を有効にする。
エックスサーバのサーバーパネルにてSSHの設定を行う。
スクリーンショット 2025 08 12 22 30 35

SSHを有効にした後、国外アクセス制限をOFFにする
Renderが海外サーバのため、OFFにしているが国内のサーバならONでも良さそう。

スクリーンショット 2025 08 12 22 32 06

あとは、SSHの公開鍵を登録する。
自分はed25519形式を使用した以下を参考に鍵は作ればOK。

sshの鍵作成 ed25519 - Qiita
最近強いと言われているed25519の鍵を作る。 Enter passphreseのところでパスワードを設定する。sshするときにパスワード聞かれるのが嫌な人はそのままEnterでOK 作成すると自分のhomeディレクトリの下にsshの鍵が...

ちなみに、.pub形式のファイルが公開鍵

スクリーンショット 2025 08 12 22 34 08

ここで、一点詰まったのが設定をちゃんと行えたのにSSHに接続できなかった。
色々調べてもうまくいかなかったが、2時間ほど時間をおいて試したら成功した。
設定の反映に時間がかかるかもしれないので注意が必要。

MariaDB 作成

次に実際にデプロイ先のDBを作成する。
エックスサーバのサーバーパネルにてデータベースのMySQL設定を選択する

スクリーンショット 2025 08 12 22 39 43

選択後タブのMySQL追加を選びDBを作成する。
文字コードはUTF-8のままで問題ない。

スクリーンショット 2025 08 12 22 43 04

次にユーザの作成を行う。このユーザは後に使う。
MySQLユーザ追加を選択して、MySQLのバージョンをMariaDBにする。

スクリーンショット 2025 08 12 22 45 01

ここまで、作成したらMySQL一覧に戻り作成したMariaDBにこちらも先程作成したユーザを追加する。
アクセス権未使用ユーザに追加したユーザが居るはずなので、選択後追加を押す。
以下画像はすでにユーザを追加しているので、ユーザはいませんになっているが実際にはユーザが選択できるはずです。

スクリーンショット 2025 08 12 22 47 35

ちなみに

ちなみに、なぜMariaDBが外部からアクセスできないかというと
IPがローカルアドレスになっている….そう、つまりエックスサーバ内でしかアクセスできないようになっているようだ
スクリーンショット 2025 08 12 22 50 15
ちなみに、MySQLだと外部からアクセスできるURLが付与されている…
どうやら、エックスサーバではMySQLからMariaDBへ移行したそうだ。

最新サーバー環境における MariaDB 10.5 の提供のお知らせ
レンタルサーバー【エックスサーバー】からのお知らせ | 2021/03/24 最新サーバー環境における MariaDB 10.5 の提供のお知らせ

色々調べたのだが、現状MySQLが作れなくなっている気がする….

ノードエクスプレス

DBの準備が整ったので、今度はNodeExpress 側の実装や設定を確認する。
ローカルで利用するDocker環境については以前記事を書いているのでそちらを参照してください。

docker-composeでMySQLの環境構築をした(8.4 LTS版)
はじめにDocker環境で、MySQLをよく構築するが何も考えずにいつもコピペしたいたので、改めて公式ドキュメントを読みつつ理解していく。まあ、構築自体は色々記事はあるけどだいたい公式ドキュメントのソースがないから本当にあっているかの検証。...

NodeExpressの初期構築等については割愛する。
今回はpnpmで説明するので、各自npmやyarnに置き換えてください。

開発環境構築

以下コマンドで、パッケージをインストールする

$ pnpm add @prisma/client express prisma tunnel-ssh
$ pnpm add -D @types/express @types/node dotenv-cli ts-node-dev typescript

環境変数

.env

ROOT_PASS=root
DB_NAME=sample
DB_USER=user
DB_PASS=p@ssw0rd
DB_PORT=3306
TZ=Asia/Tokyo

# This was inserted by `prisma init`:
DATABASE_URL="mysql://root:root@localhost:${DB_PORT}/${DB_NAME}"

上記はDocker環境のDBにアクセスする環境変数

.env.product

SSH_KEY_PATH=秘密鍵ファイルのパス
SSH_USER=サーバID
SSH_HOST=サーバーのホスト名
SSH_PORT=10022
LOCAL_PORT=3307
REMOTE_DB_HOST="127.0.0.1"
REMOTE_DB_PORT=3306

DATABASE_URL="mysql://MariaDBのユーザ:MariaDBのユーザパスワード@localhost:3307/MariaDBの名前"

上記はMariaDBへアクセスする環境変数
サーバIDはサーバパネルのヘッダーに書いてある。
スクリーンショット 2025 08 12 23 10 22

サーバーのホスト名はサーバ情報のホスト名に書いてある

スクリーンショット 2025 08 12 23 10 58

サーバIDとサーバ番号が異なるため、ここで躓きかけたので注意

サーバ構築

サーバを動かすためのコマンドを以下に示す。(現状が動かないので、設定とやり方だけ参考に)

Docker環境へアクセスするサーバ構築&実行

$ pnpm run dev

XserverのMariaDBへアクセスするサーバ構築&実行

$ pnpm run build
$ pnpm run start

以下を実行できるように、package.json を以下のように修正する。

"scripts": {
        "dev": "NODE_ENV=dev ts-node-dev --respawn --transpile-only src/index.ts",
        "build": "tsc",
        "start": "NODE_ENV=product dotenv -e .env.product node dist/index.js",
    },

ちなみに、ts-node-devはファイルを変更したときにホットリードする仕組み
dotenvは使用する環境ファイルを変更する

Prismaの準備

Prismaの導入は以下とかを見れば良い

Prisma をインストール|Prisma 入門

パッケージは前述でインストールしているので割愛

まずは初期化する

npx prisma init

Prisma/schema.prisma に以下のような初期ファイルが生成される

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

初期設定では postgresql だが、今回はMySQLを使うので以下に設定を変える

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql" //ここを変えている
  url      = env("DATABASE_URL")
}

今回は、自分のスキルセットを管理したいので試しに以下のようなDBモデルを構築する。

model SkillList {
  id        Int      @id @default(autoincrement())
  category  String
  skill     String
  detail    String
  years     Float
  proficiency Float
  @@unique([category, skill])
}

上記の変更を反映するための、マイグレーション実行を行う準備をする
ちなみにマイグレーションとは、以下の役割らしい。

マイグレーションとは、Prisma で作成したスキーマをデータベースに反映するプロセスです。
Prismaを使い始める

マイグレーションを実行できるように、package.json を以下のように修正する。

 "scripts": {
        "studio": "dotenv -e .env prisma studio --schema=./prisma/schema.prisma",
        "prisma:dev": "dotenv -e .env prisma migrate dev",
        "prisma:deploy": "./deploy.sh"
    },

studioはPrismaStudioを見れるようにする、現状Docker上のデータだけ。
prisma:devは開発環境のDBへのマイグレーション実行
prisma:deployはMariaDBへのマイグレーション実行になる。
deploy.sh は以下のようなSSHトンネルを構築して、マイグレーション実行するスクリプトを用意する。(変数を上記の環境変数で定義していればそのまま使えます)

#!/bin/bash
set -e

npx dotenv -e .env.product -- bash -c '
  echo "🚀 Starting SSH tunnel to $SSH_HOST..."

  ssh -i "$SSH_KEY_PATH" \
      -N -L ${LOCAL_PORT}:${REMOTE_DB_HOST}:${REMOTE_DB_PORT} \
      ${SSH_USER}@${SSH_HOST} -p ${SSH_PORT} &
  SSH_PID=$!

  sleep 2

  echo "📦 Applying Prisma migrations..."
  npx prisma migrate deploy --schema=./prisma/schema.prisma

  kill $SSH_PID
  echo "✅ Deployment complete!"
'

マイグレーション実行

Docker環境のDBにマイグレーション実行するのは以下コマンド

$ pnpm run prisma:dev

MariaDB環境にマイグレーション実行するのは以下コマンド

$ pnpm prisma:deploy

実装

ここからは NodeExpress の実装を示す。
実装のディレクトリ構成は以下のようにした。

.
|_src
	|_index.ts
	|_routes
		|_sample,ts

まずは、mainのindex.ts

import express from "express";
import sample from "./routes/sample";
import fs from "fs";
import {
    createTunnel,
    TunnelOptions,
    ServerOptions,
    ForwardOptions,
    SshOptions,
} from "tunnel-ssh";

const app = express();

async function startSSH(): Promise {
    if (
        !process.env.SSH_USER ||
        !process.env.SSH_HOST ||
        !process.env.SSH_PORT ||
        !process.env.REMOTE_DB_HOST ||
        !process.env.REMOTE_DB_PORT ||
        !process.env.SSH_KEY_PATH
    ) {
        throw new Error("Missing environment variables for SSH tunnel");
    }

    // トンネルオプション
    const TUNNEL_OPTION: TunnelOptions = {
        autoClose: false,
        reconnectOnError: false,
    };
    // SSHオプション
    const SSH_OPTION: SshOptions = {
        username: process.env.SSH_USER,
        host: process.env.SSH_HOST,
        port: Number(process.env.SSH_PORT),
        privateKey: fs.readFileSync(process.env.SSH_KEY_PATH, "utf-8"),
    };
    // Severオプション
    const SERVER_OPTION: ServerOptions = {
        port: Number(process.env.LOCAL_PORT),
    };
    // FORWARDオプション
    const FORWARD_OPTION: ForwardOptions = {
        srcAddr: "localhost",
        srcPort: Number(process.env.REMOTE_DB_PORT),
        dstAddr: "localhost",
        dstPort: Number(process.env.REMOTE_DB_PORT),
    };
    // createTunnelはasync関数なのでawaitで呼び出す
    const server = await createTunnel(
        TUNNEL_OPTION,
        SERVER_OPTION,
        SSH_OPTION,
        FORWARD_OPTION
    );
    return server;
}

async function main(): Promise {
    if (process.env.NODE_ENV === "product") {
        console.log("Mode: product");
        await startSSH();
        console.log("production Start", process.env.DATABASE_URL);
    }

    app.get("/", (req, res) => {
        res.send("Hello World!");
    });

    // get_user.ts のルートを適用
    app.use("/", sammple);

    app.listen(3000, () => {
        console.log("Server running on port 3000");
    });
}

main().catch((e) => {
    console.error(e);
    process.exit(1);
});

今回はtunnel-sshを使用してSSHトンネルを構築している。

import {
    createTunnel,
    TunnelOptions,
    ServerOptions,
    ForwardOptions,
    SshOptions,
} from "tunnel-ssh";

async function startSSH(): Promise {
    if (
        !process.env.SSH_USER ||
        !process.env.SSH_HOST ||
        !process.env.SSH_PORT ||
        !process.env.REMOTE_DB_HOST ||
        !process.env.REMOTE_DB_PORT ||
        !process.env.SSH_KEY_PATH
    ) {
        throw new Error("Missing environment variables for SSH tunnel");
    }

    // トンネルオプション
    const TUNNEL_OPTION: TunnelOptions = {
        autoClose: false,
        reconnectOnError: false,
    };
    // SSHオプション
    const SSH_OPTION: SshOptions = {
        username: process.env.SSH_USER,
        host: process.env.SSH_HOST,
        port: Number(process.env.SSH_PORT),
        privateKey: fs.readFileSync(process.env.SSH_KEY_PATH, "utf-8"),
    };
    // Severオプション
    const SERVER_OPTION: ServerOptions = {
        port: Number(process.env.LOCAL_PORT),
    };
    // FORWARDオプション
    const FORWARD_OPTION: ForwardOptions = {
        srcAddr: "localhost",
        srcPort: Number(process.env.REMOTE_DB_PORT),
        dstAddr: "localhost",
        dstPort: Number(process.env.REMOTE_DB_PORT),
    };
    // createTunnelはasync関数なのでawaitで呼び出す
    const server = await createTunnel(
        TUNNEL_OPTION,
        SERVER_OPTION,
        SSH_OPTION,
        FORWARD_OPTION
    );
    return server;
}

createTunnel でSSHトンネル接続をしている。
基本的な値はすべて環境変数から取得している。
ちなみに、本番環境のRenderでも秘密鍵のファイルや環境変数は登録できるため
同じ環境で問題ない。

Main関数内で、NODE_ENVによってSSHトンネルで接続してMariaDBにアクセスするか
Docker環境のローカルホスト環境にアクセスするか分ける

async function main(): Promise {
    if (process.env.NODE_ENV === "product") {
        console.log("Mode: product");
        await startSSH();
        console.log("production Start", process.env.DATABASE_URL);
    }

    app.get("/", (req, res) => {
        res.send("Hello World!");
    });

    // get_user.ts のルートを適用
    app.use("/", sammple);

    app.listen(3000, () => {
        console.log("Server running on port 3000");
    });
}

NODE_ENVはスクリプトに仕込んである。

"scripts": {
        "dev": "NODE_ENV=dev ts-node-dev --respawn --transpile-only src/index.ts",
        "start": "NODE_ENV=product dotenv -e .env.product node dist/index.js",
    },

次にDBにアクセスするAPIを用意する
今回は以下のようにDBをすべて取得してコンソール表示した後に、適当な値を返すようにしている。

import { Router } from "express";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();
const router = Router();

// GET /user
router.get("/sample", async (req, res) => {
    const skills = await prisma.skillList.findMany();
    console.log("skills=", skills);

    res.json({
        id: 1,
        name: "John Doe",
        email: "john@example.com",
    });
});

export default router;

サーバ実行

前述しているが以下のコマンドでサーバを実行できる。

Docker環境へアクセスするサーバ構築&実行

$ pnpm run dev

XserverのMariaDBへアクセスするサーバ構築&実行

$ pnpm run build
$ pnpm run start

サーバ起動後に、Chorome等で以下URLを入力してレスポンスが返ってくることを確認する。

http://localhost:3000/sample

こんな感じの表示が出る

スクリーンショット 2025 08 13 10 31 56

Renderでのデプロイ

軽い概要

今回はバックエンドのデプロイ先としてRenderを使用する。
Renderは制限があるが、無料プランがある。
そのため、ポートフォリオ程度なら無料プランで十分なので今回は利用する。

Cloud Application Platform | Render
On Render, you can build, deploy, and scale your apps with unparalleled ease – from your first user to your billionth.

ビルドまで

まずは、アカウント登録をする。
ここは、git hubアカウントで登録すればOK

登録方法

登録が完了したら、ダッシュボートにて Add New -> Web Service を選択する
スクリーンショット 2025 08 13 10 39 41

選択後 GitProvider から、対象のプロジェクトを選択する

スクリーンショット 2025 08 13 10 40 41

もし、選択肢がなかったら Search 横の Credentials から github の Configure in GitHub を選んで追加したい git のプロジェクトを選択する。

スクリーンショット 2025 08 13 10 41 45

ビルド設定

ここまで選択できたら、ビルドの設定を行う
Branch は特段理由が無いなら main で問題ない。
Language は自動で選択されていると思うが Node で問題ない。

スクリーンショット 2025 08 12 20 45 48

Build コマンドは以下のように修正する

pnpm install --frozen-lockfile && pnpm prisma generate && pnpm run build

スクリーンショット 2025 08 12 23 32 29

ビルド設定(Environment Variables)

ローカル環境で定義していた .env.product の内容をEnvironment Variables に設定する。
SSH_KEY_PATH はローカルのファイルとは違うので /etc/secrets/ファイル名 にする(鍵の登録が次項)

スクリーンショット 2025 08 12 20 46 09

* NODE_ENVの設定も忘れずに

ビルド設定(Secret Files)

ローカル環境ではローカルSSHキーのファイルを読んでいたが、Renderではもちろんできないので
Secret Files に秘密鍵を登録する。(ローカルとは別の秘密鍵にすることをおすすめします)

スクリーンショット 2025 08 12 20 46 16

動作確認

ビルドが終わったら、ダッシュボードに書いてあるデプロイ先のURLが記述されているので
local環境のときと同じように /sample にアクセスする。

おわりに

まだ、改善点はあるが最初の段階としては良いのでは無いだろうか。
今後直していきたいのは以下

  1. api のアクセスのJWT等の認証を使いたい
  2. ローカルのDBをMariaDBに変える
  3. Renderの無料プランは一定期間アクセス無いとスリープになるので、その対策
  4. github Actio を使ったデプロイの自動化
この記事を書いた人
あかちゃん

現職のフロントエンジニア&組み込みエンジニア(現在2社目)
仕事では、C,C++,C#やTypescript,JavaScript,Html,CSS等の技術をメインに仕事しています。
組み込みだけをメインに7年ほどやり、ここ数年でweb関係の仕事をするようになってきました。
現在Web技術は副業をするため絶賛修行中

記事にはなるべくQitaとか個人ブログじゃなくて、公式などの一次ソースをなるべく載せるようにしたい。

あかちゃんをフォローする
DBMySQLNext.jsバックエンドフロントエンド
スポンサーリンク
あかちゃんをフォローする

コメント

タイトルとURLをコピーしました