diff --git a/package.json b/package.json
index 6136ef39df..c670e232e4 100644
--- a/package.json
+++ b/package.json
@@ -48,10 +48,13 @@
 		"lodash": "4.17.21"
 	},
 	"dependencies": {
-		"execa": "8.0.1",
 		"cssnano": "6.0.3",
+		"execa": "8.0.1",
+		"fast-glob": "3.3.2",
+		"ignore-walk": "6.0.4",
 		"js-yaml": "4.1.0",
 		"postcss": "8.4.33",
+		"tar": "6.2.0",
 		"terser": "5.27.0",
 		"typescript": "5.3.3"
 	},
@@ -61,8 +64,8 @@
 		"cross-env": "7.0.3",
 		"cypress": "13.6.3",
 		"eslint": "8.56.0",
-		"start-server-and-test": "2.0.3",
-		"ncp": "2.0.0"
+		"ncp": "2.0.0",
+		"start-server-and-test": "2.0.3"
 	},
 	"optionalDependencies": {
 		"@tensorflow/tfjs-core": "4.4.0"
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 8e1a89d55f..a5094e6a86 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -51,6 +51,7 @@ const clientAssets = `${_dirname}/../../../../frontend/assets/`;
 const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
 const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
 const viteOut = `${_dirname}/../../../../../built/_vite_/`;
+const tarball = `${_dirname}/../../../../../built/tarball/`;
 
 @Injectable()
 export class ClientServerService {
@@ -291,6 +292,13 @@ export class ClientServerService {
 			decorateReply: false,
 		});
 
+		fastify.register(fastifyStatic, {
+			root: tarball,
+			prefix: '/tarball/',
+			immutable: true,
+			decorateReply: false,
+		});
+
 		fastify.get('/favicon.ico', async (request, reply) => {
 			return reply.sendFile('/favicon.ico', staticAssets);
 		});
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index 69cb6ef647..8e5fcda283 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -86,6 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
 						<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
 						<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
+						<FormLink :to="`/tarball/misskey-${version}.tar.gz`" external>source code</FormLink>
 					</div>
 				</FormSection>
 			</div>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 640713067f..6435298264 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -18,12 +18,21 @@ importers:
       execa:
         specifier: 8.0.1
         version: 8.0.1
+      fast-glob:
+        specifier: 3.3.2
+        version: 3.3.2
+      ignore-walk:
+        specifier: 6.0.4
+        version: 6.0.4
       js-yaml:
         specifier: 4.1.0
         version: 4.1.0
       postcss:
         specifier: 8.4.33
         version: 8.4.33
+      tar:
+        specifier: 6.2.0
+        version: 6.2.0
       terser:
         specifier: 5.27.0
         version: 5.27.0
@@ -13488,6 +13497,13 @@ packages:
     resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
     dev: true
 
+  /ignore-walk@6.0.4:
+    resolution: {integrity: sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    dependencies:
+      minimatch: 9.0.3
+    dev: false
+
   /ignore@5.2.4:
     resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
     engines: {node: '>= 4'}
@@ -19095,6 +19111,18 @@ packages:
       mkdirp: 1.0.4
       yallist: 4.0.0
 
+  /tar@6.2.0:
+    resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==}
+    engines: {node: '>=10'}
+    dependencies:
+      chownr: 2.0.0
+      fs-minipass: 2.1.0
+      minipass: 5.0.0
+      minizlib: 2.1.2
+      mkdirp: 1.0.4
+      yallist: 4.0.0
+    dev: false
+
   /taskkill@5.0.0:
     resolution: {integrity: sha512-+HRtZ40Vc+6YfCDWCeAsixwxJgMbPY4HHuTgzPYH3JXvqHWUlsCfy+ylXlAKhFNcuLp4xVeWeFBUhDk+7KYUvQ==}
     engines: {node: '>=14.16'}
diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs
index d2dabe8532..8a8a50d9ba 100644
--- a/scripts/build-assets.mjs
+++ b/scripts/build-assets.mjs
@@ -12,6 +12,7 @@ import * as terser from 'terser';
 import { build as buildLocales } from '../locales/index.js';
 import generateDTS from '../locales/generateDTS.js';
 import meta from '../package.json' assert { type: "json" };
+import buildTarball from './tarball.mjs';
 
 let locales = buildLocales();
 
@@ -77,12 +78,13 @@ async function build() {
     copyBackendViews(),
     buildBackendScript(),
     buildBackendStyle(),
+		buildTarball(),
   ]);
 }
 
 await build();
 
-if (process.argv.includes("--watch")) {
+if (process.argv.includes('--watch')) {
 	const watcher = fs.watch('./locales');
 	for await (const event of watcher) {
 		const filename = event.filename?.replaceAll('\\', '/');
diff --git a/scripts/tarball.mjs b/scripts/tarball.mjs
new file mode 100644
index 0000000000..936a43d270
--- /dev/null
+++ b/scripts/tarball.mjs
@@ -0,0 +1,32 @@
+import { createWriteStream } from 'node:fs';
+import { mkdir } from 'node:fs/promises';
+import { resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import glob from 'fast-glob';
+import walk from 'ignore-walk';
+import Pack from 'tar/lib/pack.js';
+import meta from '../package.json' assert { type: "json" };
+
+const cwd = fileURLToPath(new URL('..', import.meta.url));
+const ignore = [
+	'**/.git/**/*',
+	'**/*ignore',
+	'**/.gitmodules',
+	// Exclude files you don't want to include in the tarball here
+];
+
+export default async function build() {
+	const mkdirPromise = mkdir(resolve(cwd, 'built', 'tarball'), { recursive: true });
+	const pack = new Pack({ cwd, gzip: true });
+	const patterns = await walk({ path: cwd, ignoreFiles: ['.gitignore'] });
+
+	for await (const entry of glob.stream(patterns, { cwd, ignore, dot: true })) {
+		pack.add(entry);
+	}
+
+	pack.end();
+
+	await mkdirPromise;
+
+	pack.pipe(createWriteStream(resolve(cwd, 'built', 'tarball', `misskey-${meta.version}.tar.gz`)));
+}