はじめに
最近自分のポートフォリオサイトを作り始めている。
ポートフォリオサイトの一部データを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を使う
上記を踏まえた構成図は以下
エックスサーバ
SSH設定
まず、SSHトンネルを構築するに当たりSSHの設定を有効にする。
エックスサーバのサーバーパネルにてSSHの設定を行う。
SSHを有効にした後、国外アクセス制限をOFFにする
Renderが海外サーバのため、OFFにしているが国内のサーバならONでも良さそう。
あとは、SSHの公開鍵を登録する。
自分はed25519形式を使用した以下を参考に鍵は作ればOK。

ちなみに、.pub形式のファイルが公開鍵
ここで、一点詰まったのが設定をちゃんと行えたのにSSHに接続できなかった。
色々調べてもうまくいかなかったが、2時間ほど時間をおいて試したら成功した。
設定の反映に時間がかかるかもしれないので注意が必要。
MariaDB 作成
次に実際にデプロイ先のDBを作成する。
エックスサーバのサーバーパネルにてデータベースのMySQL設定を選択する
選択後タブのMySQL追加を選びDBを作成する。
文字コードはUTF-8のままで問題ない。
次にユーザの作成を行う。このユーザは後に使う。
MySQLユーザ追加を選択して、MySQLのバージョンをMariaDBにする。
ここまで、作成したらMySQL一覧に戻り作成したMariaDBにこちらも先程作成したユーザを追加する。
アクセス権未使用ユーザに追加したユーザが居るはずなので、選択後追加を押す。
以下画像はすでにユーザを追加しているので、ユーザはいませんになっているが実際にはユーザが選択できるはずです。
ちなみに
ちなみに、なぜMariaDBが外部からアクセスできないかというと
IPがローカルアドレスになっている….そう、つまりエックスサーバ内でしかアクセスできないようになっているようだ
ちなみに、MySQLだと外部からアクセスできるURLが付与されている…
どうやら、エックスサーバではMySQLからMariaDBへ移行したそうだ。

色々調べたのだが、現状MySQLが作れなくなっている気がする….
ノードエクスプレス
DBの準備が整ったので、今度はNodeExpress 側の実装や設定を確認する。
ローカルで利用するDocker環境については以前記事を書いているのでそちらを参照してください。

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はサーバパネルのヘッダーに書いてある。
サーバーのホスト名はサーバ情報のホスト名に書いてある
サーバ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の導入は以下とかを見れば良い

パッケージは前述でインストールしているので割愛
まずは初期化する
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
こんな感じの表示が出る
Renderでのデプロイ
軽い概要
今回はバックエンドのデプロイ先としてRenderを使用する。
Renderは制限があるが、無料プランがある。
そのため、ポートフォリオ程度なら無料プランで十分なので今回は利用する。

ビルドまで
まずは、アカウント登録をする。
ここは、git hubアカウントで登録すればOK
登録方法
登録が完了したら、ダッシュボートにて Add New -> Web Service を選択する
選択後 GitProvider から、対象のプロジェクトを選択する
もし、選択肢がなかったら Search 横の Credentials から github の Configure in GitHub を選んで追加したい git のプロジェクトを選択する。
ビルド設定
ここまで選択できたら、ビルドの設定を行う
Branch は特段理由が無いなら main で問題ない。
Language は自動で選択されていると思うが Node で問題ない。
Build コマンドは以下のように修正する
pnpm install --frozen-lockfile && pnpm prisma generate && pnpm run build
ビルド設定(Environment Variables)
ローカル環境で定義していた .env.product の内容をEnvironment Variables に設定する。
SSH_KEY_PATH はローカルのファイルとは違うので /etc/secrets/ファイル名 にする(鍵の登録が次項)
* NODE_ENVの設定も忘れずに
ビルド設定(Secret Files)
ローカル環境ではローカルSSHキーのファイルを読んでいたが、Renderではもちろんできないので
Secret Files に秘密鍵を登録する。(ローカルとは別の秘密鍵にすることをおすすめします)
動作確認
ビルドが終わったら、ダッシュボードに書いてあるデプロイ先のURLが記述されているので
local環境のときと同じように /sample にアクセスする。
おわりに
まだ、改善点はあるが最初の段階としては良いのでは無いだろうか。
今後直していきたいのは以下
- api のアクセスのJWT等の認証を使いたい
- ローカルのDBをMariaDBに変える
- Renderの無料プランは一定期間アクセス無いとスリープになるので、その対策
- github Actio を使ったデプロイの自動化
コメント