From fc603f9579f05b6510c3bb66df88bf0b0b91e779 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Sun, 17 May 2026 22:35:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=87=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20Docker-=D0=B4=D0=B5=D0=BF=D0=BB=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 9 +++ .gitea/workflows/deploy.yml | 126 ++++++++++++++++++++++++++++++++++++ Dockerfile | 25 +++++++ README.md | 37 +++++++++++ deploy/stack.example.yml | 19 ++++++ web/app/page.jsx | 2 +- web/next.config.mjs | 2 + 7 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/deploy.yml create mode 100644 Dockerfile create mode 100644 deploy/stack.example.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..eb2c3fd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.gitea +.webarchive +deploy +web/.next +web/node_modules +web/out +README.md +*.log diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..a6f011e --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,126 @@ +name: Build and deploy + +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + + env: + DOMAIN: ${{ vars.DOMAIN }} + IMAGE: ${{ vars.IMAGE }} + IMAGE_REPO: ${{ vars.IMAGE_REPO }} + PUSH_LATEST: ${{ vars.PUSH_LATEST }} + REGISTRY: ${{ vars.REGISTRY }} + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + STACK_NAME: ${{ vars.STACK_NAME }} + TRAEFIK_NETWORK: ${{ vars.TRAEFIK_NETWORK }} + TRAEFIK_ENTRYPOINT: ${{ vars.TRAEFIK_ENTRYPOINT }} + DEPLOY_HOST: ${{ vars.DEPLOY_HOST }} + DEPLOY_PORT: ${{ vars.DEPLOY_PORT }} + DEPLOY_USER: ${{ vars.DEPLOY_USER }} + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare image name + shell: bash + run: | + set -euo pipefail + + short_sha="${GITHUB_SHA:-${GITEA_SHA:-manual}}" + short_sha="${short_sha:0:12}" + + if [ -z "${IMAGE}" ]; then + : "${IMAGE_REPO:?Set IMAGE or IMAGE_REPO in Gitea variables}" + IMAGE="${IMAGE_REPO}:${short_sha}" + fi + + if [ -z "${IMAGE_REPO}" ]; then + IMAGE_REPO="${IMAGE%:*}" + fi + + { + echo "IMAGE=${IMAGE}" + echo "IMAGE_REPO=${IMAGE_REPO}" + echo "IMAGE_TAG=${short_sha}" + echo "STACK_NAME=${STACK_NAME:-progcode}" + echo "TRAEFIK_NETWORK=${TRAEFIK_NETWORK:-traefik-public}" + echo "TRAEFIK_ENTRYPOINT=${TRAEFIK_ENTRYPOINT:-websecure}" + echo "DEPLOY_PORT=${DEPLOY_PORT:-22}" + } >> "${GITHUB_ENV}" + + - name: Docker login + shell: bash + run: | + set -euo pipefail + + if [ -n "${REGISTRY_PASSWORD}" ]; then + : "${REGISTRY:?Set REGISTRY when REGISTRY_PASSWORD is configured}" + : "${REGISTRY_USER:?Set REGISTRY_USER secret when REGISTRY_PASSWORD is configured}" + echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin + else + echo "REGISTRY_PASSWORD is not set; skipping docker login" + fi + + - name: Build image + shell: bash + run: | + set -euo pipefail + docker build --pull -t "${IMAGE}" . + + - name: Push image + shell: bash + run: | + set -euo pipefail + + docker push "${IMAGE}" + + if [ "${PUSH_LATEST:-false}" = "true" ]; then + latest_image="${IMAGE_REPO}:latest" + docker tag "${IMAGE}" "${latest_image}" + docker push "${latest_image}" + fi + + - name: Configure remote Docker host + if: ${{ vars.DEPLOY_HOST != '' }} + shell: bash + run: | + set -euo pipefail + : "${DEPLOY_USER:?Set DEPLOY_USER when DEPLOY_HOST is configured}" + : "${DEPLOY_SSH_KEY:?Set DEPLOY_SSH_KEY secret when DEPLOY_HOST is configured}" + + mkdir -p "${HOME}/.ssh" + chmod 700 "${HOME}/.ssh" + printf '%s\n' "${DEPLOY_SSH_KEY}" > "${HOME}/.ssh/id_deploy" + chmod 600 "${HOME}/.ssh/id_deploy" + ssh-keyscan -p "${DEPLOY_PORT}" "${DEPLOY_HOST}" >> "${HOME}/.ssh/known_hosts" + + cat > "${HOME}/.ssh/config" <> "${GITHUB_ENV}" + + - name: Deploy stack + shell: bash + run: | + set -euo pipefail + : "${DOMAIN:?Set DOMAIN in Gitea variables}" + + docker stack deploy \ + --with-registry-auth \ + -c deploy/stack.example.yml \ + "${STACK_NAME}" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..61f40eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:24-alpine AS builder + +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 + +COPY web/package.json web/package-lock.json ./ +RUN npm ci --no-audit --fund=false + +COPY web/ ./ +RUN npm run build + +FROM joseluisq/static-web-server:2-alpine + +ENV SERVER_ROOT=/public \ + SERVER_PORT=8080 \ + SERVER_LOG_LEVEL=warn \ + SERVER_DIRECTORY_LISTING=false \ + SERVER_COMPRESSION=true + +COPY --from=builder /app/out /public + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1:8080/ >/dev/null || exit 1 diff --git a/README.md b/README.md index 97e7f9c..f54695b 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,40 @@ node scripts/buildDatabase.mjs npm run dev -- --port 3000 npm run build ``` + +Docker static deploy: + +```bash +docker build -t progcode:static . +docker run --rm -p 8080:8080 progcode:static +``` + +Swarm example: + +```bash +DOMAIN=progcode.example.com IMAGE=registry.example.com/progcode:static \ + docker stack deploy -c deploy/stack.example.yml progcode +``` + +Gitea Actions variables: + +- `DOMAIN` — public host routed by Traefik. +- `IMAGE` — full image tag to build and deploy, for example `registry.example.com/progcode:latest`. +- `IMAGE_REPO` — optional alternative to `IMAGE`; the workflow will tag images as `${IMAGE_REPO}:${short_sha}`. +- `REGISTRY` — registry host for `docker login`. +- `STACK_NAME` — optional, defaults to `progcode`. +- `TRAEFIK_NETWORK` — optional, defaults to `traefik-public`. +- `TRAEFIK_ENTRYPOINT` — optional, defaults to `websecure`. +- `PUSH_LATEST` — optional, set to `true` to also push `${IMAGE_REPO}:latest`. + +Gitea Actions secrets: + +- `REGISTRY_USER` +- `REGISTRY_PASSWORD` +- `DEPLOY_SSH_KEY` — only when deploying to a remote Swarm manager over SSH. + +Remote Swarm deploy variables: + +- `DEPLOY_HOST` +- `DEPLOY_USER` +- `DEPLOY_PORT` — optional, defaults to `22`. diff --git a/deploy/stack.example.yml b/deploy/stack.example.yml new file mode 100644 index 0000000..54ead1b --- /dev/null +++ b/deploy/stack.example.yml @@ -0,0 +1,19 @@ +services: + progcode: + image: ${IMAGE:-progcode:static} + networks: + - traefik-public + deploy: + replicas: 1 + labels: + - traefik.enable=true + - traefik.docker.network=${TRAEFIK_NETWORK:-traefik-public} + - traefik.http.routers.progcode.rule=Host(`${DOMAIN}`) + - traefik.http.routers.progcode.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure} + - traefik.http.routers.progcode.tls=true + - traefik.http.services.progcode.loadbalancer.server.port=8080 + +networks: + traefik-public: + external: true + name: ${TRAEFIK_NETWORK:-traefik-public} diff --git a/web/app/page.jsx b/web/app/page.jsx index 12946f1..52f0257 100644 --- a/web/app/page.jsx +++ b/web/app/page.jsx @@ -27,7 +27,7 @@ export default function HomePage() {
{articles.map((article) => ( - +
diff --git a/web/next.config.mjs b/web/next.config.mjs index 5bc715f..91d4039 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -2,6 +2,8 @@ import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const nextConfig = { + output: 'export', + trailingSlash: true, turbopack: { root: dirname(fileURLToPath(import.meta.url)), },