Dockerisation
Some checks failed
Build And Publish / BuildAndPublish (push) Failing after 1m46s

This commit is contained in:
r-freeman 2024-08-22 17:05:57 +01:00
parent d91c0eae02
commit 94e954b723
15 changed files with 1959 additions and 7942 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
.git
.next
.env

View File

@ -1,14 +1,11 @@
SPOTIFY_CLIENT_ID= SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET= SPOTIFY_CLIENT_SECRET=
SPOTIFY_REFRESH_TOKEN= SPOTIFY_REFRESH_TOKEN=
NEXT_PUBLIC_SITE_URL=https://example.com NEXT_PUBLIC_SITE_URL=
GITHUB_ACCESS_TOKEN= GITHUB_ACCESS_TOKEN=
GITHUB_USERNAME= GITHUB_USERNAME=
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_SECRET= GITHUB_SECRET=
STATSFM_USERNAME=
GRAFANA_URL=
GRAFANA_TOKEN=
NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY= NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY= SUPABASE_SERVICE_ROLE_KEY=

BIN
.env.gpg Normal file

Binary file not shown.

View File

@ -1,19 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@ -0,0 +1,59 @@
name: Build And Publish
run-name: ${{ gitea.actor }} runs ci pipeline
on: [ push ]
jobs:
BuildAndPublish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: https://github.com/actions/checkout@v4
- name: Use Node.js
uses: https://github.com/actions/setup-node@v3
with:
node-version: '18.17.0'
- name: Decrypt secrets
run: ./decrypt_secrets.sh
env:
SECRET_PASSPHRASE: ${{ secrets.SECRET_PASSPHRASE }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{secrets.DOCKER_HUB_USERNAME}}
password: ${{secrets.DOCKER_HUB_PASSWORD}}
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
with:
config-inline: |
[registry."docker.io"]
mirrors = ["mirror.gcr.io"]
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{secrets.DOCKER_HUB_USERNAME}}/portfolio:v1
secrets: |
"NEXT_PUBLIC_SUPABASE_URL=${{ secret.NEXT_PUBLIC_SUPABASE_URL }}"
"NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secret.NEXT_PUBLIC_SUPABASE_ANON_KEY }}"
"SUPABASE_SERVICE_ROLE_KEY=${{ secret.SUPABASE_SERVICE_ROLE_KEY }}"
- name: Stop the docker container
continue-on-error: true
run: sudo docker stop portfolio
- name: Remove the docker container
continue-on-error: true
run: sudo docker rm portfolio
- name: Pull the Docker image
run: sudo docker pull ${{secrets.DOCKER_HUB_USERNAME}}/portfolio:v1
- name: Run the Docker container
run: sudo docker run -d --restart unless-stopped --env-file ./.env --name portfolio -p ${{vars.TAILSCALE_IP}}:3000:3000 ${{secrets.DOCKER_HUB_USERNAME}}/portfolio:v1

76
Dockerfile Normal file
View File

@ -0,0 +1,76 @@
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci --legacy-peer-deps; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED=1
RUN --mount=type=secret,id=NEXT_PUBLIC_SUPABASE_URL \
--mount=type=secret,id=NEXT_PUBLIC_SUPABASE_ANON_KEY \
--mount=type=secret,id=SUPABASE_SERVICE_ROLE_KEY \
export NEXT_PUBLIC_SUPABASE_URL=$(cat /run/secrets/NEXT_PUBLIC_SUPABASE_URL) && \
export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(cat /run/secrets/NEXT_PUBLIC_SUPABASE_ANON_KEY) && \
export SUPABASE_SERVICE_ROLE_KEY=$(cat /run/secrets/SUPABASE_SERVICE_ROLE_KEY)
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]

View File

@ -1,26 +0,0 @@
import {NextResponse} from 'next/server'
import {getRamUsage, getRootFsUsage, getSysLoad, getTemp, getUptime} from '@/lib/pi'
export const fetchCache = 'force-no-store'
export async function GET(request: Request, {params}: { params: { slug: string } }) {
const slug = params.slug
let response
if (slug === 'ram') {
response = await getRamUsage()
} else if (slug === 'rootfs') {
response = await getRootFsUsage()
} else if (slug === 'sysload') {
response = await getSysLoad()
} else if (slug === 'temp') {
response = await getTemp()
} else if (slug === 'uptime') {
response = await getUptime()
} else {
return new Response('Not Found', {
status: 404
})
}
return NextResponse.json(response)
}

View File

@ -9,6 +9,7 @@ export async function GET(request: Request, {params}: { params: { slug: string }
const supabase = createServerComponentClient<Database>({cookies}) const supabase = createServerComponentClient<Database>({cookies})
const slug = params.slug.toString() const slug = params.slug.toString()
const response = await supabase const response = await supabase
// @ts-ignore
.from('analytics') .from('analytics')
.select('views') .select('views')
.eq('slug', slug) .eq('slug', slug)

5
decrypt_secrets.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
# --batch to prevent interactive command
# --yes to assume "yes" for questions
gpg --quiet --batch --yes --decrypt --passphrase="$SECRET_PASSPHRASE" --output ./.env ./.env.gpg

View File

@ -1,145 +0,0 @@
import {cookies} from 'next/headers'
import {createServerComponentClient} from '@supabase/auth-helpers-nextjs'
import {getTopRepo, getTotalFollowers, getTotalForks, getTotalRepos, getTotalStars} from '@/lib/github'
import {getAllArticles} from '@/lib/getAllArticles'
import {getTopArtist, getTopGenre} from '@/lib/spotify'
import {getRamUsage, getRootFsUsage, getSysLoad, getTemp, getUptime} from '@/lib/pi'
import {getStats} from '@/lib/statsfm'
import {Metric} from '@/types'
export async function getDashboardData() {
const supabase = createServerComponentClient({cookies})
const {data: views} = await supabase.rpc('total_views')
const [totalRepos, totalFollowers] = await Promise.all([
getTotalRepos(),
getTotalFollowers()
])
const topRepo = await getTopRepo()
const totalStars = await getTotalStars(totalRepos)
const totalForks = await getTotalForks(totalRepos)
const totalArticles = (await getAllArticles()).length
const topArtist = await getTopArtist()
const {genre} = await getTopGenre()
const {hoursListened, minutesListened, streams} = await getStats()
const {temp} = await getTemp()
const {sysLoad} = await getSysLoad()
const {ramUsage} = await getRamUsage()
const {rootFsUsage} = await getRootFsUsage()
const {days} = await getUptime()
const metrics: Metric[] = [
{
title: "Streams",
value: +streams,
group: "Spotify",
href: "https://open.spotify.com/?"
},
{
title: "Hours listened",
value: +hoursListened,
group: "Spotify",
href: "https://open.spotify.com/?"
},
{
title: "Minutes listened",
value: +minutesListened,
group: "Spotify",
href: "https://open.spotify.com/?"
},
{
title: "Top genre",
value: genre,
group: "Spotify",
href: "https://open.spotify.com/?"
},
{
title: "Top artist",
value: topArtist.artist,
group: "Spotify",
href: topArtist.uri
},
{
title: "Repos",
value: +totalRepos,
group: "GitHub",
href: "https://github.com/r-freeman?tab=repositories"
},
{
title: "Top repo",
value: topRepo.name,
group: "GitHub",
href: topRepo.url
},
{
title: "Followers",
value: +totalFollowers,
group: "GitHub",
href: "https://github.com/r-freeman?tab=followers"
},
{
title: "Stars",
value: +totalStars,
group: "GitHub",
href: "https://github.com/r-freeman/"
},
{
title: "Forks",
value: +totalForks,
group: "GitHub",
href: "https://github.com/r-freeman/"
},
{
title: "Total articles",
value: +totalArticles,
group: "Blog",
href: "/writing"
},
{
title: "Total article views",
value: +views,
group: "Blog",
href: "/writing"
},
{
title: "Temp",
value: `${temp}`,
group: "Raspberry Pi",
href: ""
},
{
title: "Sys load (5m avg)",
value: `${sysLoad}%`,
group: "Raspberry Pi",
href: ""
},
{
title: "RAM usage",
value: `${ramUsage}%`,
group: "Raspberry Pi",
href: ""
},
{
title: "Root FS usage",
value: `${rootFsUsage}%`,
group: "Raspberry Pi",
href: ""
},
{
title: "Uptime days",
value: `${Math.round(days)}`,
group: "Raspberry Pi",
href: ""
}
]
// sort metrics into named groups
const groups = metrics.reduce((acc: { [key: string]: Metric[] }, item) => {
(acc[item.group] = acc[item.group] || []).push(item);
return acc
}, {} as { [key: string]: Metric[] })
return Object.entries(groups).map(([groupName, groupItems]) => {
return {groupName, groupItems}
})
}

170
lib/pi.ts
View File

@ -1,170 +0,0 @@
import fetcher from '@/lib/fetcher'
const GRAFANA_URL: string = process.env.GRAFANA_URL ?? ""
const GRAFANA_TOKEN = process.env.GRAFANA_TOKEN
const day = 24 * 60 * 60 * 1000;
const yesterday = Date.now() - day;
export const getTemp = async () => {
const response = await fetcher(GRAFANA_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${GRAFANA_TOKEN}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"queries": [
{
"datasource": {
"uid": "4f-R6jgRz",
"type": "prometheus"
},
"expr": "node_hwmon_temp_celsius",
"maxDataPoints": 100
}
],
"from": yesterday.toString(),
"to": "now"
})
})
const temp = parseInt(response.results.A.frames[0].data.values[1].slice(-1))
return {
temp
}
}
export const getRootFsUsage = async () => {
const response = await fetcher(GRAFANA_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${GRAFANA_TOKEN}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"queries": [
{
"datasource": {
"uid": "4f-R6jgRz",
"type": "prometheus"
},
"expr": "100 - ((node_filesystem_avail_bytes{mountpoint='/',fstype!='rootfs'} * 100) / node_filesystem_size_bytes{mountpoint='/',fstype!='rootfs'})",
"maxDataPoints": 100
}
],
"from": yesterday.toString(),
"to": "now"
})
})
const rootFsUsage = parseInt(response.results.A.frames[0].data.values[1].slice(-1))
return {
rootFsUsage
}
}
export const getUptime = async () => {
const response = await fetcher(GRAFANA_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${GRAFANA_TOKEN}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"queries": [
{
"datasource": {
"uid": "4f-R6jgRz",
"type": "prometheus"
},
"expr": "node_time_seconds - node_boot_time_seconds",
"maxDataPoints": 100
}
],
"from": yesterday.toString(),
"to": "now"
})
})
const seconds = parseInt(response.results.A.frames[0].data.values[1].slice(-1))
const minutes = (seconds / 60)
const hours = (seconds / 3_600)
const days = (seconds / 86400)
const weeks = (seconds / 604_800)
return {
seconds: (seconds),
minutes,
hours,
days,
weeks
}
}
export const getRamUsage = async () => {
const response = await fetcher(GRAFANA_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${GRAFANA_TOKEN}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"queries": [
{
"datasource": {
"uid": "4f-R6jgRz",
"type": "prometheus"
},
"expr": "100 - ((node_memory_MemAvailable_bytes * 100) / node_memory_MemTotal_bytes)",
"maxDataPoints": 100
}
],
"from": yesterday.toString(),
"to": "now"
})
})
const ramUsage = parseInt(response.results.A.frames[0].data.values[1].slice(-1))
return {
ramUsage
}
}
export const getSysLoad = async () => {
const response = await fetcher(GRAFANA_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${GRAFANA_TOKEN}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"queries": [
{
"datasource": {
"uid": "4f-R6jgRz",
"type": "prometheus"
},
"expr": "avg(node_load5) / count(count(node_cpu_seconds_total) by (cpu)) * 100",
"maxDataPoints": 100
}
],
"from": yesterday.toString(),
"to": "now"
})
})
const sysLoad = parseInt(response.results.A.frames[0].data.values[1].slice(-1))
return {
sysLoad
}
}

View File

@ -1,31 +0,0 @@
import fetcher from './fetcher'
const STATSFM_USERNAME = process.env.STATSFM_USERNAME
const STATSFM_LIFETIME_STATS = `https://beta-api.stats.fm/api/v1/users/${STATSFM_USERNAME}/streams/stats?range=lifetime`
type StatsFmResponse = {
items: {
durationMs: number
count: number
}
}
export const getStats = async () => {
const response = await fetcher(STATSFM_LIFETIME_STATS, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
}) as StatsFmResponse
const {durationMs} = response.items
const hoursListened = (durationMs / 3_600_000).toFixed(0)
const minutesListened = (durationMs / 60_000).toFixed(0)
const streams = response.items.count
return {
hoursListened,
minutesListened,
streams
}
}

View File

@ -11,7 +11,8 @@ const nextConfig = {
hostname: 'i.scdn.co', hostname: 'i.scdn.co',
port: '' port: ''
}] }]
} },
output: 'standalone'
} }
const withMDX = nextMDX({ const withMDX = nextMDX({

9355
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,7 @@
"feed": "^4.2.2", "feed": "^4.2.2",
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"motion": "^10.15.5", "motion": "^10.15.5",
"next": "^14.1.0", "next": "^14.1.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"postcss-focus-visible": "^7.1.0", "postcss-focus-visible": "^7.1.0",