feat: add Docker deployment and Gitea Actions CI/CD

Add multi-stage Dockerfile, docker-compose with persistent volumes,
and Gitea Actions workflows for CI (lint/test/build) and releases
(tag, Docker image push, changelog). Support DATABASE_PATH env var
for configurable SQLite location to enable proper volume mounting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 19:36:23 +01:00
parent 37c4272c08
commit 48985b5eb2
8 changed files with 199 additions and 7 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
node_modules
dist
gearbox.db*
uploads/*
!uploads/.gitkeep
.git
.idea
.claude
.gitea
.planning

28
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,28 @@
name: CI
on:
push:
branches: [develop]
pull_request:
branches: [develop]
jobs:
ci:
runs-on: docker
container:
image: oven/bun:1
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint
run: bun run lint
- name: Test
run: bun test
- name: Build
run: bun run build

View File

@@ -0,0 +1,108 @@
name: Release
on:
workflow_dispatch:
inputs:
bump:
description: "Version bump type"
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
jobs:
ci:
runs-on: docker
container:
image: oven/bun:1
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint
run: bun run lint
- name: Test
run: bun test
- name: Build
run: bun run build
release:
needs: ci
runs-on: dind
steps:
- name: Clone repository
run: |
apk add --no-cache git curl jq
git clone https://${{ secrets.GITEA_TOKEN }}@gitea.jeanlucmakiola.de/${{ gitea.repository }}.git repo
cd repo
git checkout ${{ gitea.ref_name }}
- name: Compute version
working-directory: repo
run: |
LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -n1)
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="v0.0.0"
fi
MAJOR=$(echo "$LATEST_TAG" | sed 's/v//' | cut -d. -f1)
MINOR=$(echo "$LATEST_TAG" | sed 's/v//' | cut -d. -f2)
PATCH=$(echo "$LATEST_TAG" | sed 's/v//' | cut -d. -f3)
case "${{ gitea.event.inputs.bump }}" in
major) MAJOR=$((MAJOR+1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR+1)); PATCH=0 ;;
patch) PATCH=$((PATCH+1)) ;;
esac
NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
echo "VERSION=$NEW_VERSION" >> "$GITHUB_ENV"
echo "PREV_TAG=$LATEST_TAG" >> "$GITHUB_ENV"
echo "New version: $NEW_VERSION"
- name: Generate changelog
working-directory: repo
run: |
if [ "$PREV_TAG" = "v0.0.0" ]; then
CHANGELOG=$(git log --pretty=format:"- %s" HEAD)
else
CHANGELOG=$(git log --pretty=format:"- %s" "${PREV_TAG}..HEAD")
fi
echo "CHANGELOG<<CHANGELOG_EOF" >> "$GITHUB_ENV"
echo "$CHANGELOG" >> "$GITHUB_ENV"
echo "CHANGELOG_EOF" >> "$GITHUB_ENV"
- name: Create and push tag
working-directory: repo
run: |
git config user.name "Gitea Actions"
git config user.email "actions@gitea.jeanlucmakiola.de"
git tag -a "$VERSION" -m "Release $VERSION"
git push origin "$VERSION"
- name: Build and push Docker image
working-directory: repo
run: |
REGISTRY="gitea.jeanlucmakiola.de"
IMAGE="${REGISTRY}/${{ gitea.repository_owner }}/gearbox"
docker build -t "${IMAGE}:${VERSION}" -t "${IMAGE}:latest" .
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ gitea.repository_owner }}" --password-stdin
docker push "${IMAGE}:${VERSION}"
docker push "${IMAGE}:latest"
- name: Create Gitea release
run: |
API_URL="${GITHUB_SERVER_URL}/api/v1/repos/${{ gitea.repository }}/releases"
curl -s -X POST "$API_URL" \
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg tag "$VERSION" \
--arg name "$VERSION" \
--arg body "$CHANGELOG" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')"

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM oven/bun:1 AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
FROM deps AS build
COPY . .
RUN bun run build
FROM oven/bun:1-slim AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist/client ./dist/client
COPY src/server ./src/server
COPY src/db ./src/db
COPY src/shared ./src/shared
COPY drizzle.config.ts package.json ./
COPY drizzle ./drizzle
COPY entrypoint.sh ./
RUN chmod +x entrypoint.sh && mkdir -p data uploads
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD bun -e "fetch('http://localhost:3000/api/health').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))"
ENTRYPOINT ["./entrypoint.sh"]

17
docker-compose.yml Normal file
View File

@@ -0,0 +1,17 @@
services:
gearbox:
build: .
container_name: gearbox
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_PATH=./data/gearbox.db
volumes:
- gearbox-data:/app/data
- gearbox-uploads:/app/uploads
restart: unless-stopped
volumes:
gearbox-data:
gearbox-uploads:

View File

@@ -5,6 +5,6 @@ export default defineConfig({
schema: "./src/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: "gearbox.db",
url: process.env.DATABASE_PATH || "gearbox.db",
},
});

4
entrypoint.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
set -e
bun run db:push
exec bun run src/server/index.ts

View File

@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";
import * as schema from "./schema.ts";
const sqlite = new Database("gearbox.db");
const sqlite = new Database(process.env.DATABASE_PATH || "gearbox.db");
sqlite.run("PRAGMA journal_mode = WAL");
sqlite.run("PRAGMA foreign_keys = ON");