diff --git a/docs/setup.en.md b/docs/setup.en.md
index 72da57a9aa..83392d0d9a 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -47,11 +47,6 @@ In root :
 4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest)
 5. `npm install` Install misskey dependencies.
 
-*(optional)* reCAPTCHA tokens
-----------------------------------------------------------------
-If you want to enable reCAPTCHA, you need to generate reCAPTCHA tokens:
-Please visit https://www.google.com/recaptcha/intro/ and generate keys.
-
 *(optional)* Generating VAPID keys
 ----------------------------------------------------------------
 If you want to enable ServiceWorker, you need to generate VAPID keys:
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index 606857219a..79be1fb881 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -53,11 +53,6 @@ adduser --disabled-password --disabled-login misskey
 4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
 5. `npm install` Misskeyの依存パッケージをインストール
 
-*(オプション)* reCAPTCHAトークン
-----------------------------------------------------------------
-reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。
-https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。
-
 *(オプション)* VAPIDキーペアの生成
 ----------------------------------------------------------------
 ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります:
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 22e6212607..840dce5735 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1085,6 +1085,11 @@ admin/views/instance.vue:
   local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量"
   remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量"
   mb: "メガバイト単位"
+  recaptcha-config: "reCAPTCHAの設定"
+  recaptcha-info: "reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。"
+  enable-recaptcha: "reCAPTCHAを有効にする"
+  recaptcha-site-key: "reCAPTCHA site key"
+  recaptcha-secret-key: "reCAPTCHA secret key"
   max-note-text-length: "投稿の最大文字数"
   disable-registration: "ユーザー登録の受付を停止する"
   disable-local-timeline: "ローカルタイムラインを無効にする"
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue
index 050fb6617d..47ab9e6159 100644
--- a/src/client/app/admin/views/instance.vue
+++ b/src/client/app/admin/views/instance.vue
@@ -16,6 +16,13 @@
 			<ui-input v-model="localDriveCapacityMb">%i18n:@local-drive-capacity-mb%<span slot="desc">%i18n:@mb%</span><span slot="suffix">MB</span></ui-input>
 			<ui-input v-model="remoteDriveCapacityMb" :disabled="!cacheRemoteFiles">%i18n:@remote-drive-capacity-mb%<span slot="desc">%i18n:@mb%</span><span slot="suffix">MB</span></ui-input>
 		</section>
+		<section class="fit-bottom">
+			<header><fa icon="shield-alt"/> %i18n:@recaptcha-config%</header>
+			<ui-switch v-model="enableRecaptcha">%i18n:@enable-recaptcha%</ui-switch>
+			<ui-info>%i18n:@recaptcha-info%</ui-info>
+			<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><i slot="icon"><fa icon="key"/></i>%i18n:@recaptcha-site-key%</ui-input>
+			<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><i slot="icon"><fa icon="key"/></i>%i18n:@recaptcha-secret-key%</ui-input>
+		</section>
 		<section>
 			<ui-button @click="updateMeta">%i18n:@save%</ui-button>
 		</section>
@@ -54,6 +61,9 @@ export default Vue.extend({
 			localDriveCapacityMb: null,
 			remoteDriveCapacityMb: null,
 			maxNoteTextLength: null,
+			enableRecaptcha: false,
+			recaptchaSiteKey: null,
+			recaptchaSecretKey: null,
 			inviteCode: null,
 		};
 	},
@@ -67,6 +77,9 @@ export default Vue.extend({
 			this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
 			this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
 			this.maxNoteTextLength = meta.maxNoteTextLength;
+			this.enableRecaptcha = meta.enableRecaptcha;
+			this.recaptchaSiteKey = meta.recaptchaSiteKey;
+			this.recaptchaSecretKey = meta.recaptchaSecretKey;
 		});
 	},
 
@@ -92,7 +105,10 @@ export default Vue.extend({
 				cacheRemoteFiles: this.cacheRemoteFiles,
 				localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
 				remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
-				maxNoteTextLength: parseInt(this.maxNoteTextLength, 10)
+				maxNoteTextLength: parseInt(this.maxNoteTextLength, 10),
+				enableRecaptcha: this.enableRecaptcha,
+				recaptchaSiteKey: this.recaptchaSiteKey,
+				recaptchaSecretKey: this.recaptchaSecretKey
 			}).then(() => {
 				this.$swal({
 					type: 'success',
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index fa26eabc91..91a09e14fb 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -35,7 +35,7 @@
 				<p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@password-not-matched%</p>
 			</div>
 		</ui-input>
-		<div v-if="meta.recaptchaSitekey != null" class="g-recaptcha" :data-sitekey="meta.recaptchaSitekey" style="margin: 16px 0;"></div>
+		<div v-if="meta.recaptchaSiteKey != null" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div>
 		<ui-button type="submit">%i18n:@create%</ui-button>
 	</template>
 </form>
@@ -130,7 +130,7 @@ export default Vue.extend({
 				username: this.username,
 				password: this.password,
 				invitationCode: this.invitationCode,
-				'g-recaptcha-response': this.meta.recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
+				'g-recaptcha-response': this.meta.recaptchaSiteKey != null ? (window as any).grecaptcha.getResponse() : null
 			}, true).then(() => {
 				(this as any).api('signin', {
 					username: this.username,
@@ -141,7 +141,7 @@ export default Vue.extend({
 			}).catch(() => {
 				alert('%i18n:@some-error%');
 
-				if (this.meta.recaptchaSitekey != null) {
+				if (this.meta.recaptchaSiteKey != null) {
 					(window as any).grecaptcha.reset();
 				}
 			});
diff --git a/src/config/types.ts b/src/config/types.ts
index ff19af27c0..07d2ec318f 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -40,11 +40,6 @@ export type Source = {
 		port: number;
 		pass: string;
 	};
-	recaptcha?: {
-		site_key: string;
-		secret_key: string;
-	};
-
 	drive?: {
 		storage: string;
 		bucket?: string;
diff --git a/src/models/meta.ts b/src/models/meta.ts
index 073be7de82..3eb73681ac 100644
--- a/src/models/meta.ts
+++ b/src/models/meta.ts
@@ -61,6 +61,19 @@ if ((config as any).preventCacheRemoteFiles) {
 		}
 	});
 }
+if ((config as any).recaptcha) {
+	Meta.findOne({}).then(m => {
+		if (m != null && m.enableRecaptcha == null) {
+			Meta.update({}, {
+				$set: {
+					enableRecaptcha: (config as any).recaptcha != null,
+					recaptchaSiteKey: (config as any).recaptcha.site_key,
+					recaptchaSecretKey: (config as any).recaptcha.secret_key,
+				}
+			});
+		}
+	});
+}
 
 export type IMeta = {
 	name?: string;
@@ -79,6 +92,10 @@ export type IMeta = {
 
 	cacheRemoteFiles?: boolean;
 
+	enableRecaptcha?: boolean;
+	recaptchaSiteKey?: string;
+	recaptchaSecretKey?: string;
+
 	/**
 	 * Drive capacity of a local user (MB)
 	 */
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index b4b2b231ab..85266b47cf 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -88,6 +88,27 @@ export const meta = {
 			desc: {
 				'ja-JP': 'リモートのファイルをキャッシュするか否か'
 			}
+		},
+
+		enableRecaptcha: {
+			validator: $.bool.optional,
+			desc: {
+				'ja-JP': 'reCAPTCHAを使用するか否か'
+			}
+		},
+
+		recaptchaSiteKey: {
+			validator: $.str.optional,
+			desc: {
+				'ja-JP': 'reCAPTCHA site key'
+			}
+		},
+
+		recaptchaSecretKey: {
+			validator: $.str.optional,
+			desc: {
+				'ja-JP': 'reCAPTCHA secret key'
+			}
 		}
 	}
 };
@@ -139,6 +160,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
 		set.cacheRemoteFiles = ps.cacheRemoteFiles;
 	}
 
+	if (ps.enableRecaptcha !== undefined) {
+		set.enableRecaptcha = ps.enableRecaptcha;
+	}
+
+	if (ps.recaptchaSiteKey !== undefined) {
+		set.recaptchaSiteKey = ps.recaptchaSiteKey;
+	}
+
+	if (ps.recaptchaSecretKey !== undefined) {
+		set.recaptchaSecretKey = ps.recaptchaSecretKey;
+	}
+
 	await Meta.update({}, {
 		$set: set
 	}, { upsert: true });
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index e3d3ad520f..3ed225cc5f 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -35,7 +35,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 		}
 	});
 
-	res({
+	const response: any = {
 		maintainer: config.maintainer,
 
 		version: pkg.version,
@@ -60,24 +60,32 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 		driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
 		driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
 		cacheRemoteFiles: instance.cacheRemoteFiles,
-		recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null,
+		recaptchaSiteKey: instance.enableRecaptcha ? instance.recaptchaSiteKey : null,
 		swPublickey: config.sw ? config.sw.public_key : null,
-		hidedTags: (me && me.isAdmin) ? instance.hidedTags : undefined,
 		bannerUrl: instance.bannerUrl,
 		maxNoteTextLength: instance.maxNoteTextLength,
 
 		emojis: emojis,
+	};
 
-		features: ps.detail ? {
+	if (ps.detail) {
+		response.features = {
 			registration: !instance.disableRegistration,
 			localTimeLine: !instance.disableLocalTimeline,
 			elasticsearch: config.elasticsearch ? true : false,
-			recaptcha: config.recaptcha ? true : false,
+			recaptcha: instance.enableRecaptcha,
 			objectStorage: config.drive && config.drive.storage === 'minio',
 			twitter: config.twitter ? true : false,
 			github: config.github ? true : false,
 			serviceWorker: config.sw ? true : false,
 			userRecommendation: config.user_recommendation ? config.user_recommendation : {}
-		} : undefined
-	});
+		};
+	}
+
+	if (me && me.isAdmin) {
+		response.hidedTags = instance.hidedTags;
+		response.recaptchaSecretKey = instance.recaptchaSecretKey;
+	}
+
+	res(response);
 }));
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index eefffd8554..3a367ff119 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -1,7 +1,6 @@
 import * as Koa from 'koa';
 import * as bcrypt from 'bcryptjs';
 import { generate as generateKeypair } from '../../../crypto_key';
-const recaptcha = require('recaptcha-promise');
 import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user';
 import generateUserToken from '../common/generate-native-user-token';
 import config from '../../../config';
@@ -10,18 +9,20 @@ import RegistrationTicket from '../../../models/registration-tickets';
 import usersChart from '../../../chart/users';
 import fetchMeta from '../../../misc/fetch-meta';
 
-if (config.recaptcha) {
-	recaptcha.init({
-		secret_key: config.recaptcha.secret_key
-	});
-}
-
 export default async (ctx: Koa.Context) => {
 	const body = ctx.request.body as any;
 
+	const instance = await fetchMeta();
+
+	const recaptcha = require('recaptcha-promise');
+
 	// Verify recaptcha
 	// ただしテスト時はこの機構は障害となるため無効にする
-	if (process.env.NODE_ENV !== 'test' && config.recaptcha != null) {
+	if (process.env.NODE_ENV !== 'test' && instance.enableRecaptcha) {
+		recaptcha.init({
+			secret_key: instance.recaptchaSecretKey
+		});
+
 		const success = await recaptcha(body['g-recaptcha-response']);
 
 		if (!success) {
@@ -34,8 +35,6 @@ export default async (ctx: Koa.Context) => {
 	const password = body['password'];
 	const invitationCode = body['invitationCode'];
 
-	const instance = await fetchMeta();
-
 	if (instance && instance.disableRegistration) {
 		if (invitationCode == null || typeof invitationCode != 'string') {
 			ctx.status = 400;