From e04495ee2d99cfd2caf868f3f74b6b7ff9c7a89c Mon Sep 17 00:00:00 2001 From: Ksan Date: Sat, 1 Aug 2026 16:38:15 +0200 Subject: [PATCH] Add Docker/Traefik deployment setup --- .dockerignore | 7 +++++++ .env.example | 12 ++++++++++++ .gitignore | 3 ++- Dockerfile | 30 ++++++++++++++++++++++++++++++ components/layout/Footer.tsx | 4 ++-- docker-compose.yml | 25 +++++++++++++++++++++++++ next.config.ts | 1 + 7 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..96233e0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +.git +.env* +!.env.example +design +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..23fd265 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Used by docker-compose.yml for the Traefik Host() rule +DOMAIN= + +GMAIL_USER= +GMAIL_APP_PASSWORD= +CONTACT_TO_EMAIL= + +# Private pricing pages — JSON array, one object per page, reached at +# //plans/. `landing`/`webApp`/`care` take a number (rendered as +# €N, formatted per locale) or a string (used verbatim). Add or remove objects +# to add or remove pages. Generate an unguessable code with: openssl rand -hex 8 +PLANS_PAGES=[{"code":"REPLACE_WITH_RANDOM_HEX","landing":500,"webApp":2000,"care":[25,60,120]}] diff --git a/.gitignore b/.gitignore index 1d6e478..4fdfc44 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,9 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) +# env files .env* +!.env.example # vercel .vercel diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5e2c0c4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 + +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:20-alpine AS builder +WORKDIR /app +# The /plans/[code] route uses generateStaticParams + dynamicParams = false, +# so PLANS_PAGES must be known at build time — codes not baked in here will +# 404 at runtime no matter what PLANS_PAGES is set to when the container runs. +ARG PLANS_PAGES +ENV PLANS_PAGES=$PLANS_PAGES +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/components/layout/Footer.tsx b/components/layout/Footer.tsx index cf4a7b9..25f6b80 100644 --- a/components/layout/Footer.tsx +++ b/components/layout/Footer.tsx @@ -23,14 +23,14 @@ export default function Footer({ locale, dict }: FooterProps) { [dict.footer.connect]: [ { label: "Source Code", href: "https://git.ksan.dev/ksan", external: true }, { label: "Email", href: "mailto:contact@ksan.dev", external: true }, - { label: "LinkedIn", href: "https://linkedin.com", external: true }, + { label: "LinkedIn", href: "https://www.linkedin.com/in/%C4%91or%C4%91e-k%C5%A1an-886871266", external: true }, ], }; const socials = [ { icon: SiGitea, href: "https://git.ksan.dev/ksan", label: "Gitea" }, { icon: Mail, href: "mailto:contact@ksan.dev", label: "Email" }, - { icon: FaLinkedin, href: "https://linkedin.com", label: "LinkedIn" }, + { icon: FaLinkedin, href: "https://www.linkedin.com/in/%C4%91or%C4%91e-k%C5%A1an-886871266/", label: "LinkedIn" }, ]; return ( diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f4e5a5d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + ksan-dev: + build: + context: . + args: + PLANS_PAGES: ${PLANS_PAGES} + container_name: ksan-dev + restart: unless-stopped + env_file: + - .env + networks: + - frontend + labels: + - traefik.enable=true + - traefik.http.routers.ksan-dev-http.rule=Host(`${DOMAIN}`) || Host(`www.${DOMAIN}`) + - traefik.http.routers.ksan-dev-http.entrypoints=web + - traefik.http.routers.ksan-dev-https.tls=true + - traefik.http.routers.ksan-dev-https.tls.certresolver=cloudflare + - traefik.http.routers.ksan-dev-https.entrypoints=websecure + - traefik.http.routers.ksan-dev-https.rule=Host(`${DOMAIN}`) || Host(`www.${DOMAIN}`) + - traefik.http.services.ksan-dev.loadbalancer.server.port=3000 + +networks: + frontend: + external: true diff --git a/next.config.ts b/next.config.ts index d8a15ba..ceb8aa2 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { allowedDevOrigins: ["192.168.100.6"], + output: "standalone", }; export default nextConfig;