6 Commits
Author SHA1 Message Date
haylanandClaude-Bot 3e3a6752af chore(dev): remove broken auto-run PHPUnit hook
The PostToolUse hook called php bin/phpunit directly, which does not
work inside the Docker-based dev setup. Remove it; tests should be
run via docker compose exec graph vendor/bin/phpunit as documented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:39:39 +02:00
haylanandClaude-Bot 5b07eae672 docs: update installation guide to use pre-built image
Replace the clone-and-build workflow with a docker-compose snippet
that pulls the image directly from the Gitea container registry.
Add semver tag reference and rename steps to match the new flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:39:33 +02:00
haylanandClaude-Bot 235e63bfc0 fix(config): add default empty values for optional env vars
Declare empty-string defaults for all optional env vars as Symfony
parameters so the container compiles without errors when GITHUB_USER,
GITLAB_TOKEN, GITEA_URL, etc. are not set in the environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:39:29 +02:00
haylanandClaude-Bot 92380e534a feat(docker): migrate base image from php-cli to FrankenPHP Alpine
Replace php:8.4-cli-alpine3.21 with dunglas/frankenphp:1-php8.4-alpine.
No Go toolchain required — the official image ships as a pre-compiled
Alpine binary.

- Drop docker-php-ext-install opcache (FrankenPHP enables it by default)
- Both dev and final stages run frankenphp run --config /etc/caddy/Caddyfile
- Dev stage bakes in Caddyfile.dev (no worker mode, Xdebug compatible)
- Final stage bakes in production Caddyfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:38:38 +02:00
haylanandClaude-Bot 4654a287a8 feat(app): add FrankenPHP worker script for Symfony
Boot the Symfony kernel once at startup, then loop with
frankenphp_handle_request() to handle every incoming request without
re-bootstrapping the framework. Supports MAX_REQUESTS env var for
graceful worker restarts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:38:27 +02:00
haylanandClaude-Bot 2427c07f4c feat(docker): add FrankenPHP Caddy configuration
Add production and dev Caddyfiles for FrankenPHP:
- Caddyfile: HTTP-only on :8080, auto_https off, worker mode
- Caddyfile.dev: same but without worker directive so Xdebug
  step-debugging remains functional; Caddy debug logging enabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:38:23 +02:00
7 changed files with 105 additions and 25 deletions
+1 -10
View File
@@ -1,10 +1 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "php bin/phpunit 2>&1 | tail -20"
}
]
}
}
{}
+6 -5
View File
@@ -1,10 +1,9 @@
FROM php:8.4-cli-alpine3.21 AS base
FROM dunglas/frankenphp:1-php8.4-alpine AS base
RUN apk add --no-cache \
curl \
icu-libs \
libzip \
&& docker-php-ext-install opcache
libzip
WORKDIR /app
@@ -32,11 +31,12 @@ RUN apk add --no-cache ${PHPIZE_DEPS} linux-headers \
&& docker-php-ext-enable xdebug \
&& apk del ${PHPIZE_DEPS}
COPY docker/php/xdebug.ini /usr/local/etc/php/conf.d/docker-xdebug.ini
COPY docker/frankenphp/Caddyfile.dev /etc/caddy/Caddyfile
COPY composer.json composer.lock* ./
RUN composer install --no-scripts --no-interaction --prefer-dist
EXPOSE 8080
ENV APP_ENV=dev APP_DEBUG=1
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public", "public/index.php"]
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
# ── final (prod) stage — no composer binary ────────────────────────────────────
FROM base AS final
@@ -45,6 +45,7 @@ RUN addgroup -S app && adduser -S -G app app
COPY --from=build /app/vendor /app/vendor
COPY . .
COPY docker/frankenphp/Caddyfile /etc/caddy/Caddyfile
RUN mkdir -p var/cache var/log \
&& chown -R app:app /app
@@ -54,4 +55,4 @@ USER app
EXPOSE 8080
ENV APP_ENV=prod APP_DEBUG=0
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public", "public/index.php"]
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
+32 -10
View File
@@ -27,15 +27,36 @@ https://your-host/graph.svg?theme=dark
- Docker + Docker Compose
### 1. Clone and configure
### 1. Create a `docker-compose.yml`
```bash
git clone https://git.arthurerlich.de/haylan/git-contribution-graph.git
cd git-contribution-graph
cp .env .env.local
Use the pre-built image — no need to clone the repo:
```yaml
services:
graph:
image: git.arthurerlich.de/haylan/git-contribution-graph:latest
ports:
- "8080:8080"
env_file:
- .env.local
volumes:
- cache:/var/www/html/var/cache
volumes:
cache:
```
Edit `.env.local` with your credentials:
The image is published to the Gitea container registry. Pull it manually with:
```bash
docker pull git.arthurerlich.de/haylan/git-contribution-graph:latest
```
Semver tags are published (`0`, `0.0`, `0.0.1`) alongside `latest` — see the [container registry](https://git.arthurerlich.de/haylan/-/packages/container/git-contribution-graph/latest) for all available tags.
### 2. Configure
Create a `.env.local` file next to your `docker-compose.yml`:
```dotenv
APP_SECRET=<generate with: openssl rand -hex 16>
@@ -60,7 +81,7 @@ ALLOWED_HOSTS=
Only configure the platforms you use — unused ones are silently skipped.
### 2. Start
### 3. Start
```bash
docker compose up -d
@@ -68,10 +89,11 @@ docker compose up -d
The service listens on **port 8080** by default. Put Traefik or nginx in front of it for HTTPS.
### 3. Health check
### 4. Verify
```
GET /health → {"status":"ok"}
```bash
curl http://localhost:8080/health
# {"status":"ok"}
```
---
+12
View File
@@ -1,3 +1,15 @@
parameters:
env(APP_SECRET): ''
env(ALLOWED_HOSTS): ''
env(GITHUB_USER): ''
env(GITHUB_TOKEN): ''
env(GITLAB_USER): ''
env(GITLAB_TOKEN): ''
env(GITLAB_URL): ''
env(GITEA_USER): ''
env(GITEA_TOKEN): ''
env(GITEA_URL): ''
services:
_defaults:
autowire: true
+13
View File
@@ -0,0 +1,13 @@
{
admin off
auto_https off
frankenphp {
worker /app/public/worker.php
}
}
:8080 {
root * /app/public
php_server
}
+12
View File
@@ -0,0 +1,12 @@
{
admin off
auto_https off
debug
frankenphp
}
:8080 {
root * /app/public
php_server
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use App\Kernel;
use Symfony\Component\HttpFoundation\Request;
require_once dirname(__DIR__).'/vendor/autoload.php';
$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'prod', (bool) ($_SERVER['APP_DEBUG'] ?? false));
$kernel->boot();
$handler = static function () use ($kernel): void {
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
};
$maxRequests = (int) ($_SERVER['MAX_REQUESTS'] ?? 0);
$requestCount = 0;
while (\frankenphp_handle_request($handler)) {
if ($maxRequests > 0 && ++$requestCount >= $maxRequests) {
break;
}
}
$kernel->shutdown();