diff --git a/.dockerignore b/.dockerignore index f23d2fc..769972b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,33 @@ .git +.github +.next .next-dev +.next/cache node_modules npm-debug.log* .env .env.* deploy-packages output +data +media +files +videos +public +docs +*.md *.dump *.tgz *.tar.gz -cloudflare/.wrangler +cloudflare .wrangler +.claude +.playwright-cli +.tmp +.turbo +tsconfig.tsbuildinfo +tmp-* +**/.DS_Store +**/._* +__MACOSX +src/seed/raw diff --git a/.gitignore b/.gitignore index eb91c30..3bb6f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,11 @@ videos deploy-packages output +# nginx TLS material (upload fullchain.pem + privkey.pem on the server) +nginx/certs/*.pem +nginx/certs/*.key +data/ + # misc .DS_Store ._* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2574c3c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1 + +FROM uhub.service.ucloud.cn/mysql-images/node:22-bookworm-slim AS deps +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm config set registry https://registry.npmmirror.com \ + && npm ci + +FROM uhub.service.ucloud.cn/mysql-images/node:22-bookworm-slim AS builder +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ARG NEXT_PUBLIC_SITE_URL +ARG PAYLOAD_SECRET +ARG DATABASE_URI + +ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL +ENV PAYLOAD_SECRET=$PAYLOAD_SECRET +ENV DATABASE_URI=$DATABASE_URI +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN npm run build + +FROM uhub.service.ucloud.cn/mysql-images/node:22-bookworm-slim AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 --gid nodejs nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +RUN mkdir -p media files videos \ + && chown -R nextjs:nodejs media files videos + +USER nextjs + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..befc4c2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + # Enable BuildKit layer/cache mounts: DOCKER_BUILDKIT=1 docker compose build app + args: + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL} + PAYLOAD_SECRET: ${PAYLOAD_SECRET} + DATABASE_URI: ${DATABASE_URI} + container_name: eversolo-web + restart: unless-stopped + env_file: .env + expose: + - "3000" + volumes: + - ./data/media:/app/media + - ./data/files:/app/files + - ./data/videos:/app/videos + networks: + - web + + nginx: + image: uhub.service.ucloud.cn/mysql-images/nginx:latest + container_name: eversolo-nginx + restart: unless-stopped + depends_on: + - app + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./nginx/certs:/etc/nginx/certs:ro + - ./data/media:/srv/uploads/media:ro + - ./data/files:/srv/uploads/files:ro + - ./data/videos:/srv/uploads/videos:ro + networks: + - web + + bootstrap: + image: uhub.service.ucloud.cn/mysql-images/node:22-bookworm-slim + working_dir: /app + env_file: .env + volumes: + - .:/app + - bootstrap_node_modules:/app/node_modules + - ./data/media:/app/media + - ./data/files:/app/files + - ./data/videos:/app/videos + command: > + bash -lc " + npm ci --include=dev && + npm run payload:migrate && + npm run payload:migrate:status && + npm run seed:import -- --live && + npx tsx ./src/scripts/create-admin.ts + " + profiles: [bootstrap] + networks: + - web + +networks: + web: + driver: bridge + +volumes: + bootstrap_node_modules: diff --git a/next.config.mjs b/next.config.mjs index 3acaeaa..c4a92d5 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -7,6 +7,7 @@ const PUBLIC_PAGE_CACHE_CONTROL = 'public, max-age=0, s-maxage=300, stale-while- /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'standalone', reactStrictMode: true, allowedDevOrigins: ['127.0.0.1', '192.168.1.252'], distDir: process.env.NEXT_DIST_DIR || '.next', diff --git a/nginx/certbot/.gitkeep b/nginx/certbot/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/nginx/certbot/.gitkeep @@ -0,0 +1 @@ + diff --git a/nginx/certbot/www/.gitkeep b/nginx/certbot/www/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/nginx/certbot/www/.gitkeep @@ -0,0 +1 @@ + diff --git a/nginx/certs/.gitkeep b/nginx/certs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/nginx/certs/.gitkeep @@ -0,0 +1 @@ + diff --git a/nginx/conf.d/www.luxsin.net.conf b/nginx/conf.d/www.luxsin.net.conf new file mode 100644 index 0000000..b07a9d9 --- /dev/null +++ b/nginx/conf.d/www.luxsin.net.conf @@ -0,0 +1,84 @@ +upstream eversolo_app { + server app:3000; + keepalive 32; +} + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + listen [::]:80; + server_name www.luxsin.net luxsin.net; + + location / { + return 301 https://www.luxsin.net$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name www.luxsin.net; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options SAMEORIGIN always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + + # Payload uploads served directly from the persistent data volume (see docker-compose nginx mounts). + location /media/ { + alias /srv/uploads/media/; + access_log off; + expires 30d; + add_header Cache-Control "public, max-age=2592000, immutable"; + } + + location /files/ { + alias /srv/uploads/files/; + access_log off; + expires 7d; + add_header Cache-Control "public, max-age=604800"; + } + + location /videos/ { + alias /srv/uploads/videos/; + access_log off; + expires 30d; + add_header Cache-Control "public, max-age=2592000"; + } + + location / { + proxy_pass http://eversolo_app; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name luxsin.net; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + + return 301 https://www.luxsin.net$request_uri; +} diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..aa3d447 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,28 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + client_max_body_size 100m; + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + + include /etc/nginx/conf.d/*.conf; +}