{"id":630,"date":"2026-06-15T11:40:09","date_gmt":"2026-06-15T11:40:09","guid":{"rendered":"https:\/\/hyc.eshachem.com\/program\/?page_id=630"},"modified":"2026-06-15T11:40:26","modified_gmt":"2026-06-15T11:40:26","slug":"docker-ci-cd-on-next-js","status":"publish","type":"page","link":"https:\/\/hyc.eshachem.com\/program\/birc\/docker\/docker-ci-cd-on-next-js\/","title":{"rendered":"Docker CI\/CD on Next.js"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">\u672c\u6587\u4ef6\u900f\u904e <strong>Campus Activity Frontend<\/strong>\uff08\u6821\u5712\u6d3b\u52d5\u7ba1\u7406\u7cfb\u7d71\u524d\u7aef\uff09\u5c08\u6848\u7684\u5bb9\u5668\u5316\u67b6\u69cb\u3001\u672c\u5730\u958b\u767c\u74b0\u5883\u3001\u751f\u7522\u90e8\u7f72\u65b9\u5f0f\uff0c\u8aaa\u660e <strong>Next.js CI\/CD \u6d41\u7a0b<\/strong>\u3002<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u76f8\u95dc\u6587\u4ef6\uff1a<a href=\".\/%E5%89%8D%E5%8F%B0SSO%E6%8A%80%E8%A1%93%E6%96%87%E4%BB%B6.md\">\u524d\u53f0 SSO \u6280\u8853\u6587\u4ef6<\/a> \u00b7 <a href=\"https:\/\/hyc.eshachem.com\/program\/birc\/docker-%e7%b0%a1%e4%bb%8b\/docker-ci-cd-%e6%8a%80%e8%a1%93%e6%96%87%e4%bb%b6\/\">\u5f8c\u7aef Docker CI\/CD \u53c3\u8003<\/a><\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e7%b3%bb%e7%b5%b1%e6%a6%82%e8%bf%b0\">\u7cfb\u7d71\u6982\u8ff0<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u9805\u76ee<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td>\u61c9\u7528\u6846\u67b6<\/td><td>Next.js 16.2.4 \/ React 19 \/ TypeScript<\/td><\/tr><tr><td>\u5957\u4ef6\u7ba1\u7406<\/td><td>npm\uff08<code>package-lock.json<\/code>\uff09<\/td><\/tr><tr><td>\u5bb9\u5668\u57fa\u5e95<\/td><td><code>node:22-alpine<\/code><\/td><\/tr><tr><td>\u6620\u50cf\u5009\u5eab<\/td><td>Harbor \u2014 <code>harbor.ntubimdbirc.tw<\/code><\/td><\/tr><tr><td>\u539f\u59cb\u78bc\u5009\u5eab<\/td><td>GitLab \u2014 <code>gitlab.ntubimdbirc.tw\/general-hospital\/activity-system-frontend<\/code><\/td><\/tr><tr><td>\u5f8c\u7aef API<\/td><td>Campus Activity Backend\uff08Spring Boot\uff09<\/td><\/tr><tr><td>\u8a8d\u8b49<\/td><td>NTUB IMD BIRC SSO + JWT<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e7%9b%b8%e9%97%9c%e6%aa%94%e6%a1%88%e4%b8%80%e8%a6%bd\">\u76f8\u95dc\u6a94\u6848\u4e00\u89bd<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u6a94\u6848<\/th><th>\u7528\u9014<\/th><\/tr><\/thead><tbody><tr><td><code>Dockerfile<\/code><\/td><td>\u591a\u968e\u6bb5\u5efa\u7f6e\uff0c\u7522\u51fa\u53ef\u90e8\u7f72\u7684 Next.js \u6620\u50cf<\/td><\/tr><tr><td><code>docker-compose.prod.yml<\/code><\/td><td>\u751f\u7522\u74b0\u5883\u90e8\u7f72\uff08\u5f9e Harbor \u62c9\u53d6\u6620\u50cf\uff09<\/td><\/tr><tr><td><code>.dockerignore<\/code><\/td><td>\u5efa\u7f6e\u6620\u50cf\u6642\u6392\u9664\u7684\u6a94\u6848<\/td><\/tr><tr><td><code>.gitlab-ci.yml<\/code><\/td><td>GitLab CI\/CD Pipeline\uff08\u5efa\u7f6e\u3001\u63a8\u9001\u3001\u90e8\u7f72\uff09<\/td><\/tr><tr><td><code>.env<\/code> \/ <code>.env.local<\/code><\/td><td>\u672c\u5730\u958b\u767c\u74b0\u5883\u8b8a\u6578\uff08\u4e0d\u9032\u5165\u6620\u50cf\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"dockerfile-%e6%98%a0%e5%83%8f%e5%bb%ba%e7%bd%ae\">Dockerfile \u6620\u50cf\u5efa\u7f6e<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u63a1\u7528 <strong>\u591a\u968e\u6bb5\u5efa\u7f6e\uff08Multi-stage Build\uff09<\/strong>\uff0c\u5206\u70ba\u4f9d\u8cf4\u5b89\u88dd\u3001\u7de8\u8b6f\u5efa\u7f6e\u3001\u57f7\u884c\u4e09\u500b\u968e\u6bb5\u3002<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">flowchart LR\n    A[deps<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">br<\/span>\/&gt;<\/span>node:22-alpine] --&gt;|npm ci| B[node_modules]\n    B --&gt; C[builder<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">br<\/span>\/&gt;<\/span>node:22-alpine]\n    C --&gt;|npm run build| D[.next \/ public]\n    D --&gt; E[runner<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">br<\/span>\/&gt;<\/span>node:22-alpine]\n    E --&gt; F[\u6700\u7d42\u6620\u50cf<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">br<\/span>\/&gt;<\/span>nextjs \u975e root \u4f7f\u7528\u8005\u57f7\u884c]<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%bb%ba%e7%bd%ae%e9%9a%8e%e6%ae%b5\">\u5efa\u7f6e\u968e\u6bb5<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u968e\u6bb5<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td><strong>deps<\/strong><\/td><td>\u8907\u88fd <code>package.json<\/code>\u3001<code>package-lock.json<\/code>\uff0c\u57f7\u884c <code>npm ci<\/code> \u5b89\u88dd\u4f9d\u8cf4\uff08\u7368\u7acb Layer\uff0c\u5229\u65bc Docker Cache\uff09<\/td><\/tr><tr><td><strong>builder<\/strong><\/td><td>\u8907\u88fd\u539f\u59cb\u78bc\uff0c\u900f\u904e <code>ARG NEXT_PUBLIC_API_URL<\/code> \u6ce8\u5165\u74b0\u5883\u8b8a\u6578\uff0c\u57f7\u884c <code>npm run build<\/code><\/td><\/tr><tr><td><strong>runner<\/strong><\/td><td>\u8907\u88fd <code>.next<\/code>\u3001<code>public<\/code>\u3001<code>node_modules<\/code>\u3001<code>package.json<\/code>\uff0c\u4ee5 <code>nextjs<\/code> \u4f7f\u7528\u8005\u57f7\u884c <code>npm run start<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"next-js-%e5%bb%ba%e7%bd%ae%e6%99%82%e7%92%b0%e5%a2%83%e8%ae%8a%e6%95%b8%e6%b3%a8%e6%84%8f%e4%ba%8b%e9%a0%85\">Next.js \u5efa\u7f6e\u6642\u74b0\u5883\u8b8a\u6578\u6ce8\u610f\u4e8b\u9805<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>NEXT_PUBLIC_*<\/code> \u524d\u7db4\u7684\u8b8a\u6578\u6703\u5728 <strong><code>npm run build<\/code> \u6642\u88ab\u5167\u5d4c\u81f3\u524d\u7aef\u7a0b\u5f0f\u78bc<\/strong>\uff0c\u56e0\u6b64\u5fc5\u9808\u5728 Docker \u5efa\u7f6e\u968e\u6bb5\u900f\u904e <code>--build-arg<\/code> \u50b3\u5165\uff0c\u800c\u975e\u50c5\u5728\u5bb9\u5668\u57f7\u884c\u6642\u8a2d\u5b9a\u3002<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">ARG NEXT_PUBLIC_API_URL\nENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL\nRUN npm run build<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%9f%b7%e8%a1%8c%e9%9a%8e%e6%ae%b5\">\u57f7\u884c\u968e\u6bb5<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u4f7f\u7528\u7cbe\u7c21\u7684 Alpine \u6620\u50cf\uff08<code>node:22-alpine<\/code>\uff09<\/li>\n\n\n\n<li>\u5efa\u7acb\u975e root \u4f7f\u7528\u8005 <code>nextjs<\/code>\uff08uid 1001\uff09<\/li>\n\n\n\n<li>\u76e3\u807d <strong>3000<\/strong> \u57e0<\/li>\n\n\n\n<li>\u4ee5 <code>npm run start<\/code>\uff08\u5373 <code>next start<\/code>\uff09\u555f\u52d5\u751f\u7522\u4f3a\u670d\u5668<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e6%9c%ac%e5%9c%b0%e6%89%8b%e5%8b%95%e5%bb%ba%e7%bd%ae\">\u672c\u5730\u624b\u52d5\u5efa\u7f6e<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">docker build \\\n  --build-arg NEXT_PUBLIC_API_URL=http:<span class=\"hljs-comment\">\/\/localhost:8080\/api \\<\/span>\n  -t activity-system-frontend:local .<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"dockerignore\"><code>.dockerignore<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ee5\u4e0b\u5167\u5bb9\u4e0d\u6703\u9032\u5165\u6620\u50cf\u5efa\u7f6e\u4e0a\u4e0b\u6587\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>node_modules\/<\/code>\u3001<code>.next\/<\/code>\uff08\u672c\u5730\u5feb\u53d6\uff09<\/li>\n\n\n\n<li><code>.git\/<\/code><\/li>\n\n\n\n<li><code>.env.local<\/code>\u3001<code>.env.development.local<\/code> \u7b49\u672c\u5730\u74b0\u5883\u6a94<\/li>\n\n\n\n<li><code>README.md<\/code>\u3001\u9664\u932f\u65e5\u8a8c<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"docker-compose-%e7%92%b0%e5%a2%83\">Docker Compose \u74b0\u5883<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e6%9c%ac%e5%9c%b0%e9%96%8b%e7%99%bc\">\u672c\u5730\u958b\u767c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u672c\u5c08\u6848<strong>\u672a\u63d0\u4f9b<\/strong> <code>docker-compose.yml<\/code> \u958b\u767c\u7528 Compose \u6a94\u3002\u672c\u5730\u958b\u767c\u5efa\u8b70\u76f4\u63a5\u4f7f\u7528 Node.js \u57f7\u884c\uff1a<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">npm<\/span> <span class=\"hljs-selector-tag\">install<\/span>\n<span class=\"hljs-selector-tag\">cp<\/span> <span class=\"hljs-selector-class\">.env<\/span><span class=\"hljs-selector-class\">.example<\/span> <span class=\"hljs-selector-class\">.env<\/span><span class=\"hljs-selector-class\">.local<\/span>   # \u82e5\u5c1a\u672a\u5efa\u7acb\n<span class=\"hljs-selector-tag\">npm<\/span> <span class=\"hljs-selector-tag\">run<\/span> <span class=\"hljs-selector-tag\">dev<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u6307\u4ee4<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td><code>npm run dev<\/code><\/td><td>\u958b\u767c\u4f3a\u670d\u5668\uff08\u9810\u8a2d <code>http:\/\/localhost:3000<\/code>\uff0c\u652f\u63f4\u71b1\u91cd\u8f09\uff09<\/td><\/tr><tr><td><code>npm run build<\/code><\/td><td>\u672c\u5730\u9a57\u8b49\u751f\u7522\u5efa\u7f6e<\/td><\/tr><tr><td><code>npm run start<\/code><\/td><td>\u672c\u5730\u57f7\u884c\u5efa\u7f6e\u5f8c\u7684\u7522\u7269<\/td><\/tr><tr><td><code>npm run lint<\/code><\/td><td>ESLint \u6aa2\u67e5<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u672c\u5730\u958b\u767c\u74b0\u5883\u8b8a\u6578\u7bc4\u4f8b\uff08<code>.env.local<\/code>\uff09\uff1a<\/strong><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">NEXT_PUBLIC_API_URL=http:<span class=\"hljs-comment\">\/\/localhost:8080\/api<\/span>\nNEXT_PUBLIC_SSO_BASE_URL=https:<span class=\"hljs-comment\">\/\/sso.ntubimdbirc.tw<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9a57\u8b49\uff1a<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u524d\u7aef\u9996\u9801\uff1ahttp:\/\/localhost:3000<\/li>\n\n\n\n<li>\u9700\u642d\u914d\u5f8c\u7aef API \u8207 SSO \u767b\u5165\u4e2d\u5fc3\uff08\u898b <a href=\".\/%E5%89%8D%E5%8F%B0SSO%E6%8A%80%E8%A1%93%E6%96%87%E4%BB%B6.md\">\u524d\u53f0 SSO \u6280\u8853\u6587\u4ef6<\/a>\uff09<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e7%94%9f%e7%94%a2%e7%92%b0%e5%a2%83%ef%bc%88docker-compose-prod-yml%ef%bc%89\">\u751f\u7522\u74b0\u5883\uff08<code>docker-compose.prod.yml<\/code>\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u9069\u7528\u65bc\u4f3a\u670d\u5668\u90e8\u7f72\uff0c\u5f9e Harbor \u62c9\u53d6\u5df2\u5efa\u7f6e\u597d\u7684\u6620\u50cf\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u670d\u52d9<\/th><th>\u6620\u50cf<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td>frontend<\/td><td><code>harbor.ntubimdbirc.tw\/teaching-platform\/activity-system-frontend:${APP_TAG:-latest}<\/code><\/td><td>\u6b63\u5f0f\u524d\u7aef\u61c9\u7528\u7a0b\u5f0f<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u751f\u7522\u74b0\u5883\u7279\u9ede\uff1a<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u4f7f\u7528\u9810\u5148\u5efa\u7f6e\u4e26\u63a8\u9001\u81f3 Harbor \u7684\u6620\u50cf\uff0c\u4e0d\u672c\u5730\u7de8\u8b6f<\/li>\n\n\n\n<li><code>NODE_ENV=production<\/code><\/li>\n\n\n\n<li>\u5bb9\u5668\u65e5\u8a8c\u63a1 <code>json-file<\/code>\uff0c\u55ae\u6a94\u4e0a\u9650 10MB\u3001\u4fdd\u7559 3 \u500b\u6a94\u6848<\/li>\n\n\n\n<li>\u900f\u904e <code>.env<\/code> \u6ce8\u5165 <code>APP_TAG<\/code>\u3001<code>FRONTEND_PORT<\/code>\u3001<code>NEXT_PUBLIC_API_URL<\/code> \u7b49\u8b8a\u6578<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u90e8\u7f72\u6b65\u9a5f\uff1a<\/strong><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\"># \u5728\u4f3a\u670d\u5668\u90e8\u7f72\u76ee\u9304\u5efa\u7acb .env<\/span>\ncat &gt; .env &lt;&lt;<span class=\"hljs-string\">'EOF'<\/span>\nAPP_TAG=latest\nFRONTEND_PORT=<span class=\"hljs-number\">3000<\/span>\nNEXT_PUBLIC_API_URL=https:<span class=\"hljs-comment\">\/\/api.example.com\/api<\/span>\nTIMEZONE=Asia\/Taipei\nEOF\n\ndocker compose -f docker-compose.prod.yml pull\ndocker compose -f docker-compose.prod.yml up -d<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e7%92%b0%e5%a2%83%e8%ae%8a%e6%95%b8\">\u74b0\u5883\u8b8a\u6578<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e6%9c%ac%e5%9c%b0%e9%96%8b%e7%99%bc-2\">\u672c\u5730\u958b\u767c<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u8b8a\u6578<\/th><th>\u8aaa\u660e<\/th><th>\u7bc4\u4f8b<\/th><\/tr><\/thead><tbody><tr><td><code>NEXT_PUBLIC_API_URL<\/code><\/td><td>\u5f8c\u7aef API Base URL\uff08\u542b <code>\/api<\/code> \u524d\u7db4\uff09<\/td><td><code>http:\/\/localhost:8080\/api<\/code><\/td><\/tr><tr><td><code>NEXT_PUBLIC_SSO_BASE_URL<\/code><\/td><td>SSO \u767b\u5165\u4e2d\u5fc3 Base URL\uff08\u9078\u586b\uff09<\/td><td><code>https:\/\/sso.ntubimdbirc.tw<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e7%94%9f%e7%94%a2%e7%92%b0%e5%a2%83%ef%bc%88%e4%bc%ba%e6%9c%8d%e5%99%a8-env%ef%bc%89\">\u751f\u7522\u74b0\u5883\uff08\u4f3a\u670d\u5668 <code>.env<\/code>\uff09<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u8b8a\u6578<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td><code>APP_TAG<\/code><\/td><td>Harbor \u6620\u50cf\u6a19\u7c64\uff08\u5982 <code>latest<\/code>\u3001<code>{commit-sha}<\/code>\u3001<code>beta-{commit-sha}<\/code>\uff09<\/td><\/tr><tr><td><code>FRONTEND_PORT<\/code><\/td><td>\u5c0d\u5916\u6620\u5c04\u7684\u524d\u7aef Port<\/td><\/tr><tr><td><code>NEXT_PUBLIC_API_URL<\/code><\/td><td>\u5f8c\u7aef API Base URL\uff08Compose \u57f7\u884c\u6642\u6ce8\u5165\uff1b<strong>\u5efa\u7f6e\u6642\u4ea6\u9700\u900f\u904e CI build-arg \u50b3\u5165<\/strong>\uff09<\/td><\/tr><tr><td><code>TIMEZONE<\/code><\/td><td>\u6642\u5340\uff08\u9810\u8a2d <code>Asia\/Taipei<\/code>\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%bb%ba%e7%bd%ae%e6%99%82-vs-%e5%9f%b7%e8%a1%8c%e6%99%82%e6%b3%a8%e5%85%a5\">\u5efa\u7f6e\u6642 vs \u57f7\u884c\u6642\u6ce8\u5165<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u8b8a\u6578<\/th><th>\u5efa\u7f6e\u6642\uff08<code>--build-arg<\/code>\uff09<\/th><th>\u57f7\u884c\u6642\uff08Compose <code>environment<\/code>\uff09<\/th><\/tr><\/thead><tbody><tr><td><code>NEXT_PUBLIC_API_URL<\/code><\/td><td>\u2705 \u5fc5\u9808\uff08\u5167\u5d4c\u81f3\u524d\u7aef bundle\uff09<\/td><td>\u26a0\ufe0f \u57f7\u884c\u6642\u8a2d\u5b9a<strong>\u7121\u6cd5<\/strong>\u8986\u84cb\u5df2\u5efa\u7f6e\u7684 bundle<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>\u91cd\u8981\uff1a<\/strong> \u82e5\u9700\u8b8a\u66f4 <code>NEXT_PUBLIC_API_URL<\/code>\uff0c\u5fc5\u9808\u91cd\u65b0\u5efa\u7f6e\u4e26\u63a8\u9001 Docker \u6620\u50cf\uff0c\u50c5\u91cd\u555f\u5bb9\u5668\u7121\u6548\u3002<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e9%96%8b%e7%99%bc%e8%88%87%e7%94%9f%e7%94%a2%e5%b7%ae%e7%95%b0%e5%b0%8d%e7%85%a7\">\u958b\u767c\u8207\u751f\u7522\u5dee\u7570\u5c0d\u7167<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u9805\u76ee<\/th><th>\u672c\u5730\u958b\u767c<\/th><th>\u751f\u7522\uff08Docker\uff09<\/th><\/tr><\/thead><tbody><tr><td>\u555f\u52d5\u65b9\u5f0f<\/td><td><code>npm run dev<\/code><\/td><td><code>npm run start<\/code>\uff08\u5bb9\u5668\u5167\uff09<\/td><\/tr><tr><td>\u6620\u50cf\u4f86\u6e90<\/td><td>\u7121\uff08\u76f4\u63a5\u8dd1 Node\uff09<\/td><td>Harbor \u9060\u7aef\u6620\u50cf<\/td><\/tr><tr><td>\u539f\u59cb\u78bc\u639b\u8f09<\/td><td>\u662f\uff08\u71b1\u91cd\u8f09\uff09<\/td><td>\u5426<\/td><\/tr><tr><td>\u74b0\u5883\u8b8a\u6578\u4f86\u6e90<\/td><td><code>.env.local<\/code><\/td><td>CI build-arg + \u4f3a\u670d\u5668 <code>.env<\/code><\/td><\/tr><tr><td>\u57f7\u884c\u4f7f\u7528\u8005<\/td><td>\u672c\u6a5f\u4f7f\u7528\u8005<\/td><td><code>nextjs<\/code>\uff08\u975e root\uff09<\/td><\/tr><tr><td>\u76e3\u807d\u57e0<\/td><td>3000<\/td><td>3000\uff08\u5c0d\u5916\u6620\u5c04 <code>FRONTEND_PORT<\/code>\uff09<\/td><\/tr><tr><td>\u5f8c\u7aef\u4f9d\u8cf4<\/td><td>\u9700\u672c\u5730\u6216\u9060\u7aef\u5f8c\u7aef<\/td><td>\u9700\u53ef\u9023\u7dda\u7684\u751f\u7522\u5f8c\u7aef API<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ci-cd-%e6%b5%81%e7%a8%8b\">CI\/CD \u6d41\u7a0b<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u672c\u5c08\u6848\u5df2\u7d0d\u5165 <code>.gitlab-ci.yml<\/code>\uff0c\u63a1\u7528 <strong>GitLab CI + Harbor + SSH \u9060\u7aef\u90e8\u7f72<\/strong> \u6d41\u7a0b\u3002<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">flowchart LR\n    A[Push] --&gt; B{\u5206\u652f\u5224\u65b7}\n    B --&gt;|development| C[build-beta]\n    B --&gt;|main| D[build-online]\n    C --&gt; E[deploy-beta]\n    D --&gt; F[deploy-online<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">br<\/span>\/&gt;<\/span>\u624b\u52d5\u89f8\u767c]\n    C --&gt; G[Push to Harbor]\n    D --&gt; G\n    G --&gt; H[SSH \u66f4\u65b0\u9060\u7aef Compose]<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"pipeline-%e9%9a%8e%e6%ae%b5\">Pipeline \u968e\u6bb5<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u968e\u6bb5<\/th><th>Job<\/th><th>\u89f8\u767c\u5206\u652f<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td><strong>build<\/strong><\/td><td><code>build-beta<\/code><\/td><td><code>development<\/code><\/td><td>\u5efa\u7f6e Beta \u6620\u50cf\u4e26\u63a8\u9001 Harbor<\/td><\/tr><tr><td><strong>build<\/strong><\/td><td><code>build-online<\/code><\/td><td><code>main<\/code><\/td><td>\u5efa\u7f6e\u6b63\u5f0f\u6620\u50cf\u4e26\u63a8\u9001 Harbor<\/td><\/tr><tr><td><strong>deploy<\/strong><\/td><td><code>deploy-beta<\/code><\/td><td><code>development<\/code><\/td><td>SSH \u90e8\u7f72\u81f3 Beta \u4f3a\u670d\u5668<\/td><\/tr><tr><td><strong>deploy<\/strong><\/td><td><code>deploy-online<\/code><\/td><td><code>main<\/code><\/td><td>SSH \u90e8\u7f72\u81f3\u6b63\u5f0f\u4f3a\u670d\u5668\uff08<strong>\u624b\u52d5\u89f8\u767c<\/strong>\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e6%98%a0%e5%83%8f%e6%a8%99%e7%b1%a4%e7%ad%96%e7%95%a5\">\u6620\u50cf\u6a19\u7c64\u7b56\u7565<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u6a19\u7c64<\/th><th>\u6642\u6a5f<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td><code>beta-{commit-sha}<\/code><\/td><td>development \u5206\u652f\u6bcf\u6b21 Pipeline<\/td><td>Beta \u74b0\u5883\u53ef\u8ffd\u6eaf\u7248\u672c<\/td><\/tr><tr><td><code>beta-latest<\/code><\/td><td>development \u5206\u652f\u6700\u65b0\u7248<\/td><td>\u4f5c\u70ba <code>--cache-from<\/code> \u52a0\u901f\u5efa\u7f6e<\/td><\/tr><tr><td><code>{commit-sha}<\/code><\/td><td>main \u5206\u652f\u6bcf\u6b21 Pipeline<\/td><td>\u6b63\u5f0f\u74b0\u5883\u53ef\u8ffd\u6eaf\u7248\u672c<\/td><\/tr><tr><td><code>latest<\/code><\/td><td>main \u5206\u652f\u6700\u65b0\u7248<\/td><td>\u6b63\u5f0f\u74b0\u5883\u9810\u8a2d\u6a19\u7c64<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"build-job-%e6%b5%81%e7%a8%8b\">Build Job \u6d41\u7a0b<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u767b\u5165 Harbor\uff08<code>HARBOR_USER<\/code> \/ <code>HARBOR_PASSWORD<\/code>\uff09<\/li>\n\n\n\n<li>\u62c9\u53d6\u4e0a\u4e00\u7248\u6620\u50cf\u4f5c\u70ba\u5efa\u7f6e\u5feb\u53d6\uff08<code>--cache-from<\/code>\uff09<\/li>\n\n\n\n<li>\u4ee5 <code>--build-arg NEXT_PUBLIC_API_URL<\/code> \u5efa\u7f6e\u6620\u50cf<\/li>\n\n\n\n<li>\u63a8\u9001 <code>{commit-sha}<\/code> \u8207 <code>latest<\/code> \/ <code>beta-latest<\/code> \u6a19\u7c64<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"deploy-job-%e6%b5%81%e7%a8%8b\">Deploy Job \u6d41\u7a0b<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u900f\u904e SSH \u9023\u7dda\u81f3\u76ee\u6a19\u4f3a\u670d\u5668<\/li>\n\n\n\n<li>\u4e0a\u50b3 <code>docker-compose.prod.yml<\/code> \u81f3\u90e8\u7f72\u76ee\u9304\uff08<code>\/opt\/activity-system-frontend<\/code> \u6216 <code>$HOME\/activity-system-frontend<\/code>\uff09<\/li>\n\n\n\n<li>\u66f4\u65b0\u4f3a\u670d\u5668 <code>.env<\/code> \u4e2d\u7684 <code>APP_TAG<\/code><\/li>\n\n\n\n<li>\u767b\u5165 Harbor\u3001<code>docker compose pull frontend<\/code><\/li>\n\n\n\n<li><code>docker compose up -d --no-deps frontend<\/code>\uff08\u50c5\u66f4\u65b0\u524d\u7aef\uff0c\u4e0d\u5f71\u97ff\u5176\u4ed6\u670d\u52d9\uff09<\/li>\n\n\n\n<li>\u5065\u5eb7\u6aa2\u67e5\uff1a<code>curl http:\/\/localhost:${FRONTEND_PORT}\/<\/code><\/li>\n\n\n\n<li>\u6e05\u7406\u672a\u4f7f\u7528\u7684\u6620\u50cf\uff1a<code>docker image prune -f<\/code><\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"gitlab-ci-yml-%e6%91%98%e8%a6%81\"><code>.gitlab-ci.yml<\/code> \u6458\u8981<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">stages:\n  - build\n  - deploy\n\nvariables:\n  IMAGE_NAME: $HARBOR_URL\/activity-system\/frontend\n\n<span class=\"hljs-comment\"># development \u2192 build-beta \u2192 deploy-beta\uff08\u81ea\u52d5\uff09<\/span>\n<span class=\"hljs-comment\"># main        \u2192 build-online \u2192 deploy-online\uff08\u624b\u52d5\uff09<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"gitlab-ci-cd-%e8%ae%8a%e6%95%b8\">GitLab CI\/CD \u8b8a\u6578<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u9700\u5728 GitLab <strong>Settings \u2192 CI\/CD \u2192 Variables<\/strong> \u8a2d\u5b9a\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u8b8a\u6578<\/th><th>\u8aaa\u660e<\/th><th>\u4fdd\u8b77 \/ \u906e\u7f69<\/th><\/tr><\/thead><tbody><tr><td><code>HARBOR_URL<\/code><\/td><td>Harbor \u5009\u5eab\u4f4d\u5740<\/td><td>\u662f<\/td><\/tr><tr><td><code>HARBOR_USER<\/code><\/td><td>Harbor \u767b\u5165\u5e33\u865f<\/td><td>\u662f<\/td><\/tr><tr><td><code>HARBOR_PASSWORD<\/code><\/td><td>Harbor \u767b\u5165\u5bc6\u78bc<\/td><td>\u662f<\/td><\/tr><tr><td><code>NEXT_PUBLIC_API_URL_BETA<\/code><\/td><td>Beta \u74b0\u5883\u5f8c\u7aef API URL\uff08\u5efa\u7f6e\u7528\uff09<\/td><td>\u662f<\/td><\/tr><tr><td><code>NEXT_PUBLIC_API_URL_PROD<\/code><\/td><td>\u6b63\u5f0f\u74b0\u5883\u5f8c\u7aef API URL\uff08\u5efa\u7f6e\u7528\uff09<\/td><td>\u662f<\/td><\/tr><tr><td><code>BETA_HOST<\/code> \/ <code>BETA_USER<\/code> \/ <code>BETA_SSH_KEY<\/code><\/td><td>Beta \u4f3a\u670d\u5668 SSH \u9023\u7dda\u8cc7\u8a0a<\/td><td>\u662f<\/td><\/tr><tr><td><code>ONLINE_HOST<\/code> \/ <code>ONLINE_USER<\/code> \/ <code>ONLINE_SSH_KEY<\/code><\/td><td>\u6b63\u5f0f\u4f3a\u670d\u5668 SSH \u9023\u7dda\u8cc7\u8a0a<\/td><td>\u662f<\/td><\/tr><tr><td><code>FRONTEND_PORT<\/code><\/td><td>\u524d\u7aef\u5065\u5eb7\u6aa2\u67e5\u8207\u5c0d\u5916 Port<\/td><td>\u5426<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e7%b6%b2%e8%b7%af%e6%9e%b6%e6%a7%8b\">\u7db2\u8def\u67b6\u69cb<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e6%9c%ac%e5%9c%b0%e9%96%8b%e7%99%bc-3\">\u672c\u5730\u958b\u767c<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\">\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  \u958b\u767c\u8005\u672c\u6a5f                              \u2502\n\u2502                                         \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502  \u2502 <span class=\"hljs-selector-tag\">Next<\/span><span class=\"hljs-selector-class\">.js<\/span>  \u2502\u2500\u2500\u2500\u2500\u2500\u25b6\u2502 <span class=\"hljs-selector-tag\">Backend<\/span> <span class=\"hljs-selector-tag\">API<\/span>     \u2502 \u2502\n\u2502  \u2502 <span class=\"hljs-selector-pseudo\">:3000<\/span>    \u2502      \u2502 <span class=\"hljs-selector-pseudo\">:8080<\/span>           \u2502 \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502       \u2502                                 \u2502\n\u2502       \u25bc                                 \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                           \u2502\n\u2502  \u2502 <span class=\"hljs-selector-tag\">SSO<\/span> \u4e2d\u5fc3 \u2502                           \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"%e7%94%9f%e7%94%a2%e7%92%b0%e5%a2%83\">\u751f\u7522\u74b0\u5883<\/h3>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Docker Host                 \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510            \u2502\n\u2502  \u2502 frontend     \u2502            \u2502\n\u2502  \u2502 :3000        \u2502            \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518            \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n          \u2502 FRONTEND_PORT \u5c0d\u5916\u6620\u5c04\n          \u25bc\n   \u53cd\u5411\u4ee3\u7406 \/ \u8ca0\u8f09\u5e73\u8861\u5668\n          \u2502\n          \u25bc\n   Backend API \/ SSO<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\">\u524d\u7aef\u70ba\u7d14\u975c\u614b + SSR \u61c9\u7528\uff0c\u5bb9\u5668\u5167\u50c5\u904b\u884c Next.js \u670d\u52d9\uff0c\u8cc7\u6599\u5eab\u8207\u5feb\u53d6\u7531\u5f8c\u7aef\u8ca0\u8cac\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e7%b6%ad%e9%81%8b%e6%93%8d%e4%bd%9c\">\u7dad\u904b\u64cd\u4f5c<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%b8%b8%e7%94%a8%e6%8c%87%e4%bb%a4\">\u5e38\u7528\u6307\u4ee4<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"># \u67e5\u770b\u670d\u52d9\u72c0\u614b\n<span class=\"hljs-selector-tag\">docker<\/span> <span class=\"hljs-selector-tag\">compose<\/span> <span class=\"hljs-selector-tag\">-f<\/span> <span class=\"hljs-selector-tag\">docker-compose<\/span><span class=\"hljs-selector-class\">.prod<\/span><span class=\"hljs-selector-class\">.yml<\/span> <span class=\"hljs-selector-tag\">ps<\/span>\n\n# \u67e5\u770b\u524d\u7aef\u65e5\u8a8c\n<span class=\"hljs-selector-tag\">docker<\/span> <span class=\"hljs-selector-tag\">compose<\/span> <span class=\"hljs-selector-tag\">-f<\/span> <span class=\"hljs-selector-tag\">docker-compose<\/span><span class=\"hljs-selector-class\">.prod<\/span><span class=\"hljs-selector-class\">.yml<\/span> <span class=\"hljs-selector-tag\">logs<\/span> <span class=\"hljs-selector-tag\">-f<\/span> <span class=\"hljs-selector-tag\">frontend<\/span>\n\n# \u91cd\u555f\u524d\u7aef\uff08\u4e0d\u5f71\u97ff\u5176\u4ed6\u670d\u52d9\uff09\n<span class=\"hljs-selector-tag\">docker<\/span> <span class=\"hljs-selector-tag\">compose<\/span> <span class=\"hljs-selector-tag\">-f<\/span> <span class=\"hljs-selector-tag\">docker-compose<\/span><span class=\"hljs-selector-class\">.prod<\/span><span class=\"hljs-selector-class\">.yml<\/span> <span class=\"hljs-selector-tag\">restart<\/span> <span class=\"hljs-selector-tag\">frontend<\/span>\n\n# \u505c\u6b62\u4e26\u79fb\u9664\u5bb9\u5668\n<span class=\"hljs-selector-tag\">docker<\/span> <span class=\"hljs-selector-tag\">compose<\/span> <span class=\"hljs-selector-tag\">-f<\/span> <span class=\"hljs-selector-tag\">docker-compose<\/span><span class=\"hljs-selector-class\">.prod<\/span><span class=\"hljs-selector-class\">.yml<\/span> <span class=\"hljs-selector-tag\">down<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%81%a5%e5%ba%b7%e6%aa%a2%e6%9f%a5\">\u5065\u5eb7\u6aa2\u67e5<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u6aa2\u67e5\u9805\u76ee<\/th><th>\u65b9\u5f0f<\/th><\/tr><\/thead><tbody><tr><td>\u524d\u7aef\u670d\u52d9<\/td><td><code>curl -sf http:\/\/localhost:${FRONTEND_PORT}\/<\/code><\/td><\/tr><tr><td>\u5bb9\u5668\u72c0\u614b<\/td><td><code>docker compose -f docker-compose.prod.yml ps<\/code> \u78ba\u8a8d <code>running<\/code><\/td><\/tr><tr><td>API \u9023\u7dda<\/td><td>\u700f\u89bd\u5668\u958b\u555f\u9996\u9801\uff0c\u78ba\u8a8d\u6d3b\u52d5\u5217\u8868\u53ef\u8f09\u5165<\/td><\/tr><tr><td>SSO \u767b\u5165<\/td><td>\u9ede\u64ca\u767b\u5165\uff0c\u5b8c\u6210 Google \u9a57\u8b49\u5f8c\u53ef\u9032\u5165\u500b\u4eba\u9801\u9762<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e6%bb%be%e5%8b%95%e6%9b%b4%e6%96%b0\">\u6efe\u52d5\u66f4\u65b0<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\"># \u6307\u5b9a\u6620\u50cf\u7248\u672c<\/span>\nexport APP_TAG=abc1234\ndocker compose -f docker-compose.prod.yml pull frontend\ndocker compose -f docker-compose.prod.yml up -d --no-deps frontend<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">CI\/CD Pipeline \u90e8\u7f72\u6642\u6703\u81ea\u52d5\u66f4\u65b0\u4f3a\u670d\u5668 <code>.env<\/code> \u4e2d\u7684 <code>APP_TAG<\/code> \u4e26\u57f7\u884c\u4e0a\u8ff0\u6d41\u7a0b\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%b8%b8%e8%a6%8b%e5%95%8f%e9%a1%8c\">\u5e38\u898b\u554f\u984c<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u554f\u984c<\/th><th>\u53ef\u80fd\u539f\u56e0<\/th><th>\u8655\u7406\u65b9\u5f0f<\/th><\/tr><\/thead><tbody><tr><td>API \u8acb\u6c42 404 \/ CORS \u932f\u8aa4<\/td><td><code>NEXT_PUBLIC_API_URL<\/code> \u5efa\u7f6e\u6642\u8a2d\u5b9a\u932f\u8aa4<\/td><td>\u4fee\u6b63 GitLab \u8b8a\u6578\u5f8c\u91cd\u65b0\u89f8\u767c Pipeline \u5efa\u7f6e<\/td><\/tr><tr><td>\u767b\u5165\u5f8c\u7121\u6cd5\u53d6\u5f97\u8cc7\u6599<\/td><td>\u5f8c\u7aef\u672a\u555f\u52d5\u6216 JWT \u4ea4\u63db\u5931\u6557<\/td><td>\u6aa2\u67e5\u5f8c\u7aef\u65e5\u8a8c\u8207 SSO \u8a2d\u5b9a<\/td><\/tr><tr><td>\u5bb9\u5668\u555f\u52d5\u5f8c\u7acb\u5373\u9000\u51fa<\/td><td>Port \u885d\u7a81\u6216\u6620\u50cf\u640d\u58de<\/td><td><code>docker compose logs frontend<\/code> \u67e5\u770b\u932f\u8aa4<\/td><\/tr><tr><td>Harbor pull \u5931\u6557<\/td><td>\u767b\u5165\u6191\u8b49\u904e\u671f\u6216\u6620\u50cf\u4e0d\u5b58\u5728<\/td><td>\u78ba\u8a8d <code>APP_TAG<\/code> \u8207 Harbor \u4e0a\u7684\u6a19\u7c64\u4e00\u81f4<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e5%8e%9f%e5%a7%8b%e7%a2%bc%e7%b4%a2%e5%bc%95\">\u539f\u59cb\u78bc\u7d22\u5f15<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u6a94\u6848<\/th><th>\u8077\u8cac<\/th><\/tr><\/thead><tbody><tr><td><code>Dockerfile<\/code><\/td><td>\u591a\u968e\u6bb5\u6620\u50cf\u5efa\u7f6e\uff08deps \u2192 builder \u2192 runner\uff09<\/td><\/tr><tr><td><code>docker-compose.prod.yml<\/code><\/td><td>\u751f\u7522\u90e8\u7f72 Compose \u5b9a\u7fa9<\/td><\/tr><tr><td><code>.dockerignore<\/code><\/td><td>Docker \u5efa\u7f6e\u6392\u9664\u6e05\u55ae<\/td><\/tr><tr><td><code>.gitlab-ci.yml<\/code><\/td><td>GitLab CI\/CD Pipeline\uff08build + deploy\uff09<\/td><\/tr><tr><td><code>next.config.ts<\/code><\/td><td>Next.js \u8a2d\u5b9a<\/td><\/tr><tr><td><code>package.json<\/code><\/td><td>\u4f9d\u8cf4\u8207 npm scripts<\/td><\/tr><tr><td><code>lib\/sso.ts<\/code><\/td><td>SSO \u5c0e\u5411\u3001JWT \u8655\u7406\u3001<code>getApiBaseUrl()<\/code><\/td><\/tr><tr><td><code>lib\/api.ts<\/code><\/td><td>\u9644\u5e36 Bearer Token \u7684 API \u8acb\u6c42\u5c01\u88dd<\/td><\/tr><tr><td><code>lib\/hooks\/AuthContext.tsx<\/code><\/td><td>\u5168\u7ad9\u8a8d\u8b49\u72c0\u614b\u7ba1\u7406<\/td><\/tr><tr><td><code>\u524d\u53f0SSO\u6280\u8853\u6587\u4ef6.md<\/code><\/td><td>SSO \u6574\u5408\u8a73\u7d30\u8aaa\u660e<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e8%88%87%e5%be%8c%e7%ab%af-ci-cd-%e7%9a%84%e5%8d%94%e4%bd%9c%e9%97%9c%e4%bf%82\">\u8207\u5f8c\u7aef CI\/CD \u7684\u5354\u4f5c\u95dc\u4fc2<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u9762\u5411<\/th><th>\u524d\u7aef\uff08\u672c\u5c08\u6848\uff09<\/th><th>\u5f8c\u7aef\uff08Campus Activity Backend\uff09<\/th><\/tr><\/thead><tbody><tr><td>\u6846\u67b6<\/td><td>Next.js 16<\/td><td>Spring Boot 4<\/td><\/tr><tr><td>\u5bb9\u5668\u57e0<\/td><td>3000<\/td><td>8080<\/td><\/tr><tr><td>\u6620\u50cf\u5009\u5eab<\/td><td><code>harbor...\/activity-system\/frontend<\/code><\/td><td><code>harbor...\/campus-activity\/campus-activity_app<\/code><\/td><\/tr><tr><td>\u90e8\u7f72\u65b9\u5f0f<\/td><td>SSH + <code>docker compose up --no-deps frontend<\/code><\/td><td>SSH + <code>docker compose up -d<\/code><\/td><\/tr><tr><td>\u74b0\u5883\u8b8a\u6578\u91cd\u9ede<\/td><td><code>NEXT_PUBLIC_API_URL<\/code>\uff08\u5efa\u7f6e\u6642\u6ce8\u5165\uff09<\/td><td><code>SPRING_DATASOURCE_*<\/code>\u3001SSO Secret \u7b49<\/td><\/tr><tr><td>CI \u89f8\u767c<\/td><td><code>development<\/code> \/ <code>main<\/code> \u5206\u652f<\/td><td><code>develop<\/code> \/ <code>main<\/code> \u5206\u652f<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u524d\u5f8c\u7aef\u53ef\u7368\u7acb\u90e8\u7f72\uff1b\u524d\u7aef\u66f4\u65b0\u901a\u5e38\u4e0d\u9700\u91cd\u555f\u5f8c\u7aef\uff0c\u4f46\u8b8a\u66f4 API \u5951\u7d04\u6642\u9700\u5354\u8abf\u5169\u908a\u7248\u672c\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u672c\u6587\u4ef6\u900f\u904e Campus Activity Frontend\uff08\u6821\u5712\u6d3b\u52d5\u7ba1\u7406\u7cfb\u7d71\u524d\u7aef\uff09\u5c08\u6848\u7684\u5bb9\u5668\u5316\u67b6\u69cb\u3001\u672c\u5730\u958b\u767c\u74b0\u5883\u3001\u751f\u7522\u90e8\u7f72\u65b9\u5f0f\uff0c\u8aaa\u660e Next.js CI\/CD \u6d41\u7a0b\u3002 \u76f8\u95dc\u6587\u4ef6\uff1a\u524d\u53f0 SSO \u6280\u8853\u6587\u4ef6 \u00b7 \u5f8c\u7aef Docker CI\/CD \u53c3\u8003 \u7cfb\u7d71\u6982\u8ff0 \u9805\u76ee \u8aaa\u660e \u61c9\u7528\u6846\u67b6 Next.js 16.2.4 \/ React 19 \/ TypeScript \u5957\u4ef6\u7ba1\u7406 npm\uff08package-lock.json\uff09 \u5bb9\u5668\u57fa\u5e95 node:22-alpine \u6620\u50cf\u5009\u5eab Harbor \u2014 harbor.ntubimdbirc.tw \u539f\u59cb\u78bc\u5009\u5eab GitLab \u2014 gitlab.ntubimdbirc.tw\/general-hospital\/activity-system-frontend \u5f8c\u7aef API Campus Activity Backend\uff08Spring Boot\uff09 \u8a8d\u8b49 NTUB IMD BIRC SSO + JWT \u76f8\u95dc\u6a94\u6848\u4e00\u89bd \u6a94\u6848 \u7528\u9014 Dockerfile [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":611,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"class_list":["post-630","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages\/630","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/comments?post=630"}],"version-history":[{"count":1,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages\/630\/revisions"}],"predecessor-version":[{"id":631,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages\/630\/revisions\/631"}],"up":[{"embeddable":true,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages\/611"}],"wp:attachment":[{"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/media?parent=630"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}