This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
.git
|
||||
.gitea
|
||||
.webarchive
|
||||
deploy
|
||||
web/.next
|
||||
web/node_modules
|
||||
web/out
|
||||
README.md
|
||||
*.log
|
||||
@@ -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" <<EOF
|
||||
Host swarm-manager
|
||||
HostName ${DEPLOY_HOST}
|
||||
Port ${DEPLOY_PORT}
|
||||
User ${DEPLOY_USER}
|
||||
IdentityFile ${HOME}/.ssh/id_deploy
|
||||
IdentitiesOnly yes
|
||||
EOF
|
||||
|
||||
echo "DOCKER_HOST=ssh://swarm-manager" >> "${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}"
|
||||
+25
@@ -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
|
||||
@@ -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`.
|
||||
|
||||
@@ -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}
|
||||
+1
-1
@@ -27,7 +27,7 @@ export default function HomePage() {
|
||||
|
||||
<div className="article-grid">
|
||||
{articles.map((article) => (
|
||||
<Link key={article.slug} className="article-card" href={`/articles/${encodeURIComponent(article.slug)}`}>
|
||||
<Link key={article.slug} className="article-card" href={`/articles/${encodeURIComponent(article.slug)}/`}>
|
||||
<img src={article.cover} alt="" />
|
||||
<div className="article-card__body">
|
||||
<div className="meta-line">
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user