Attempt to be prod ready
Some checks failed
CI (Gitea) / php-tests (push) Failing after 1m30s
CI (Gitea) / docker-image (push) Has been skipped

This commit is contained in:
2025-11-28 02:11:23 +00:00
parent dae8f3d999
commit da9af888c0
7 changed files with 434 additions and 172 deletions

View File

@@ -1,20 +1,74 @@
name: CI - Build Tonehaus Docker image
name: CI (Gitea)
on:
push:
branches: [ main ]
branches:
- main
- prod
pull_request:
branches:
- main
- prod
workflow_dispatch:
env:
IMAGE_NAME: tonehaus
APP_ENV: test
APP_SECRET: ci-secret
DATABASE_DRIVER: sqlite
DATABASE_SQLITE_PATH: ${{ gitea.workspace }}/var/data/database.test.sqlite
DOCKERFILE: docker/php/Dockerfile
BUILD_TARGET: prod
PLATFORMS: linux/amd64
IMAGE_NAME: tonehaus-app
jobs:
tonehaus-ci-build:
php-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: intl, mbstring, pdo_pgsql, pdo_sqlite, zip, gd
coverage: none
ini-values: memory_limit=512M
tools: composer:v2
- name: Validate Composer manifest
run: composer validate --strict
- name: Cache Composer downloads
uses: actions/cache@v4
with:
path: |
~/.cache/composer/files
~/.cache/composer/vcs
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
composer-${{ runner.os }}-
- name: Install Composer dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Prepare SQLite database
run: |
mkdir -p "$(dirname "$DATABASE_SQLITE_PATH")"
touch "$DATABASE_SQLITE_PATH"
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
- name: Run PHPUnit
run: vendor/bin/phpunit --colors=always
docker-image:
needs: php-tests
runs-on: ubuntu-latest
env:
REGISTRY: ${{ secrets.REGISTRY }}
REGISTRY_IMAGE: ${{ secrets.REGISTRY_IMAGE }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -22,62 +76,40 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Compute tags
id: meta
run: |
SHA="${GITHUB_SHA:-${GITEA_SHA:-unknown}}"
SHORT_SHA="${SHA:0:7}"
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
- name: Build prod image (local)
uses: docker/build-push-action@v6
with:
context: .
file: ${{ env.DOCKERFILE }}
target: ${{ env.BUILD_TARGET }}
tags: ${{ env.IMAGE_NAME }}:ci
load: true
- name: Optional registry login
- name: Verify baked APP_ENV
run: docker run --rm --entrypoint sh ${{ env.IMAGE_NAME }}:ci -c 'test "$APP_ENV" = "prod"'
- name: Verify Symfony artifacts exist
run: |
docker run --rm --entrypoint sh ${{ env.IMAGE_NAME }}:ci -c 'test -f /var/www/html/public/index.php'
docker run --rm --entrypoint sh ${{ env.IMAGE_NAME }}:ci -c 'test -f /var/www/html/bin/console'
- name: Smoke-test entrypoint & migrations
run: docker run --rm --entrypoint /entrypoint.sh ${{ env.IMAGE_NAME }}:ci true
- name: Login to registry
if: ${{ env.REGISTRY != '' && env.REGISTRY_USERNAME != '' && env.REGISTRY_PASSWORD != '' }}
env:
REGISTRY: ${{ secrets.REGISTRY }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin
- name: Docker Build
if: ${{ env.REGISTRY != '' && env.REGISTRY_IMAGE != '' }}
env:
REGISTRY: ${{ secrets.REGISTRY }}
REGISTRY_IMAGE: ${{ secrets.REGISTRY_IMAGE }}
run: |
TAG_SHA=${{ steps.meta.outputs.short_sha }}
docker buildx build \
--platform "$PLATFORMS" \
--file "$DOCKERFILE" \
--target "$BUILD_TARGET" \
--build-arg APP_ENV=prod \
--tag "$REGISTRY/$REGISTRY_IMAGE:$TAG_SHA" \
--tag "$REGISTRY/$REGISTRY_IMAGE:ci" \
--push \
.
# - name: Build single-arch images for artifacts (no registry)
# if: ${{ env.REGISTRY == '' }}
# run: |
# TAG_SHA=${{ steps.meta.outputs.short_sha }}
# for P in $PLATFORMS; do \
# ARCH=${P#linux/}; \
# docker buildx build \
# --platform "$P" \
# --file "$DOCKERFILE" \
# --target "$BUILD_TARGET" \
# --build-arg APP_ENV=prod \
# --output type=docker \
# --tag "$IMAGE_NAME:$TAG_SHA-$ARCH" \
# . ; \
# docker save "$IMAGE_NAME:$TAG_SHA-$ARCH" -o "tonehaus-image-$ARCH.tar" ; \
# done
## Artifacts not configured yet..
# - name: Upload artifacts
# if: ${{ env.REGISTRY == '' }}
# uses: actions/upload-artifact@v4
# with:
# name: tonehaus-images
# path: |
# tonehaus-image-amd64.tar
- name: Push prod image
if: ${{ env.REGISTRY != '' && env.REGISTRY_IMAGE != '' && env.REGISTRY_USERNAME != '' && env.REGISTRY_PASSWORD != '' }}
uses: docker/build-push-action@v6
with:
context: .
file: ${{ env.DOCKERFILE }}
target: ${{ env.BUILD_TARGET }}
push: true
tags: |
${{ env.REGISTRY }}/${{ env.REGISTRY_IMAGE }}:ci
${{ env.REGISTRY }}/${{ env.REGISTRY_IMAGE }}:${{ github.sha }}

102
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: CI
on:
push:
branches:
- main
- prod
pull_request:
branches:
- main
- prod
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
APP_ENV: test
APP_SECRET: ci-secret
DATABASE_DRIVER: sqlite
DATABASE_SQLITE_PATH: ${{ github.workspace }}/var/data/database.test.sqlite
jobs:
php-tests:
name: PHPUnit + migrations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: intl, mbstring, pdo_pgsql, pdo_sqlite, zip, gd
coverage: none
ini-values: memory_limit=512M
tools: composer:v2
- name: Validate Composer manifest
run: composer validate --strict
- name: Cache Composer downloads
uses: actions/cache@v4
with:
path: |
~/.cache/composer/files
~/.cache/composer/vcs
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
composer-${{ runner.os }}-
- name: Install Composer dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Prepare SQLite database
run: |
mkdir -p "$(dirname "$DATABASE_SQLITE_PATH")"
touch "$DATABASE_SQLITE_PATH"
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
- name: Run PHPUnit
run: vendor/bin/phpunit --colors=always
docker-image:
name: Build production image
needs: php-tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build prod image
id: build-prod
uses: docker/build-push-action@v6
with:
context: .
file: docker/php/Dockerfile
target: prod
load: true
push: false
tags: tonehaus-app:ci
- name: Verify baked APP_ENV
run: docker run --rm --entrypoint sh tonehaus-app:ci -c 'test "$APP_ENV" = "prod"'
- name: Verify Symfony artifacts exist
run: |
docker run --rm --entrypoint sh tonehaus-app:ci -c 'test -f /var/www/html/public/index.php'
docker run --rm --entrypoint sh tonehaus-app:ci -c 'test -f /var/www/html/bin/console'
- name: Smoke-test entrypoint & migrations
run: docker run --rm --entrypoint /entrypoint.sh tonehaus-app:ci true

View File

@@ -39,6 +39,47 @@ docker compose exec php php bin/console app:seed-demo-albums --count=40 --attach
docker compose exec php php bin/console app:seed-demo-reviews --cover-percent=70 --max-per-album=8
```
## Production container (immutable)
The repository ships with a single-container production target that bundles PHP-FPM, Nginx, your code, and a self-contained SQLite database. The build bakes in the `APP_ENV=prod` flag so production-only Symfony config is used automatically, and no bind mounts are required at runtime.
1. Build the image (uses `docker/php/Dockerfile`'s `prod` stage):
```bash
docker build \
--target=prod \
-t tonehaus-app:latest \
-f docker/php/Dockerfile \
.
```
2. Run the container (listens on port 8080 inside the container):
```bash
docker run -d \
--name tonehaus \
-p 8080:8080 \
-e APP_ENV=prod \
-e APP_SECRET=change_me \
-e SPOTIFY_CLIENT_ID=your_client_id \
-e SPOTIFY_CLIENT_SECRET=your_client_secret \
tonehaus-app:latest
```
- The runtime defaults to `DATABASE_DRIVER=sqlite` and stores the database file inside the image at `var/data/database.sqlite`. On each boot the entrypoint runs Doctrine migrations (safe to re-run) so the schema stays current while the container filesystem remains immutable from the host's perspective.
- To point at Postgres (or any external database), override `DATABASE_DRIVER` and `DATABASE_URL` at `docker run` time and optionally disable auto-migration with `RUN_MIGRATIONS_ON_START=0`.
- Health endpoint: `GET /healthz` on the published port (example: `curl http://localhost:8080/healthz`).
3. Rebuild/redeploy by re-running the `docker build` command; no manual steps or bind mounts are involved.
## Continuous integration
- `.github/workflows/ci.yml` runs on pushes and pull requests targeting `main` or `prod`.
- Job 1 installs Composer deps, prepares a SQLite database, runs Doctrine migrations, and executes the PHPUnit suite under PHP 8.2 so functional regressions are caught early.
- Job 2 builds the production Docker image (`docker/php/Dockerfile` prod stage), checks that key Symfony artifacts (e.g., `public/index.php`, `bin/console`) are present, ensures `APP_ENV=prod` is baked in, and smoke-tests the `/entrypoint.sh` startup path.
- The resulting artifact mirrors the immutable container described above, so a green CI run guarantees the repo can be deployed anywhere via `docker run`.
- Self-hosted runners can use `.gitea/workflows/ci.yml`, which mirrors the GitHub workflow but also supports optional registry pushes after the image passes the same verification steps.
## Database driver
- Set `DATABASE_DRIVER=postgres` (default) to keep using the Postgres 16 container defined in `docker-compose.yml`.

View File

@@ -1,52 +1,47 @@
# FROM php:8.2-fpm
# # Install dependencies
# RUN apt-get update && apt-get install -y \
# git \
# unzip \
# libzip-dev \
# libpng-dev \
# libjpeg-dev \
# libonig-dev \
# libxml2-dev \
# zip \
# && docker-php-ext-install pdo pdo_mysql zip gd mbstring exif pcntl bcmath intl opcache
# # Install Composer
# COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# # Copy PHP config
# COPY docker/php/php.ini /usr/local/etc/php/
# WORKDIR /var/www/html
# -----------------------------------------------------------------------------
# Base PHP-FPM with Composer + Symfony-friendly extensions
# -----------------------------------------------------------------------------
FROM php:8.2-fpm-alpine AS base
WORKDIR /var/www/html
FROM php:8.2-fpm-alpine AS base
# System dependencies
RUN apk add --no-cache \
bash git unzip icu-dev libpng-dev libjpeg-turbo-dev libwebp-dev \
libzip-dev oniguruma-dev libxml2-dev postgresql-dev zlib-dev
ARG APP_ENV=dev
ENV APP_ENV=${APP_ENV}
# PHP extensions commonly used by Symfony
RUN docker-php-ext-configure gd --with-jpeg --with-webp \
WORKDIR /var/www/html
# System dependencies shared across images
RUN apk add --no-cache \
bash \
git \
unzip \
icu-dev \
libpng-dev \
libjpeg-turbo-dev \
libwebp-dev \
libzip-dev \
oniguruma-dev \
libxml2-dev \
postgresql-dev \
sqlite-dev \
zlib-dev \
su-exec
# PHP extensions commonly used by Symfony (plus both Postgres + SQLite)
RUN docker-php-ext-configure gd --with-jpeg --with-webp \
&& docker-php-ext-install -j"$(nproc)" \
intl \
gd \
pdo_pgsql \
pdo_sqlite \
opcache \
mbstring \
zip \
xml
# Composer available in the running container (dev + prod)
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer
# Composer available in every stage (dev + prod)
COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer
# Recommended PHP settings (tweak as needed)
RUN { \
# Recommended PHP settings (tweak as needed)
RUN { \
echo "memory_limit=512M"; \
echo "upload_max_filesize=50M"; \
echo "post_max_size=50M"; \
@@ -63,56 +58,79 @@
echo "opcache.jit_buffer_size=128M"; \
} > /usr/local/etc/php/conf.d/opcache-recommended.ini
# Small healthcheck file for Nginx
RUN mkdir -p public && printf "OK" > public/healthz
# Small healthcheck file for HTTP probes
RUN mkdir -p public && printf "OK" > public/healthz
# Ensure correct user
RUN addgroup -g 1000 app && adduser -D -G app -u 1000 app
# php-fpm uses www-data; keep both available
RUN chown -R www-data:www-data /var/www
# Ensure unprivileged app user exists
RUN addgroup -g 1000 app && adduser -D -G app -u 1000 app \
&& chown -R www-data:www-data /var/www
# -----------------------------------------------------------------------------
# Development image (mount your code via docker-compose volumes)
# -----------------------------------------------------------------------------
FROM base AS dev
ENV APP_ENV=dev
# Optional: enable Xdebug (uncomment to use)
# RUN apk add --no-cache $PHPIZE_DEPS \
# && pecl install xdebug \
# && docker-php-ext-enable xdebug \
# && { \
# echo "xdebug.mode=debug,develop"; \
# echo "xdebug.client_host=host.docker.internal"; \
# } > /usr/local/etc/php/conf.d/xdebug.ini
# Composer cache directory (faster installs inside container)
ENV COMPOSER_CACHE_DIR=/tmp/composer
CMD ["php-fpm"]
# -----------------------------------------------------------------------------
# Development image (mount your code via docker-compose volumes)
# -----------------------------------------------------------------------------
FROM base AS dev
ARG APP_ENV=dev
ENV APP_ENV=${APP_ENV}
ENV APP_DEBUG=1
# -----------------------------------------------------------------------------
# Production image (copies your app + installs deps + warms cache)
# -----------------------------------------------------------------------------
FROM base AS prod
ENV APP_ENV=prod
# Copy only manifests first (better layer caching); ignore if missing
COPY composer.json composer.lock* symfony.lock* ./
# Install vendors (no scripts here; run later with console if needed)
RUN --mount=type=cache,target=/tmp/composer \
# Optional: enable Xdebug by uncommenting below
# RUN apk add --no-cache $PHPIZE_DEPS \
# && pecl install xdebug \
# && docker-php-ext-enable xdebug \
# && { \
# echo "xdebug.mode=debug,develop"; \
# echo "xdebug.client_host=host.docker.internal"; \
# } > /usr/local/etc/php/conf.d/xdebug.ini
ENV COMPOSER_CACHE_DIR=/tmp/composer
CMD ["php-fpm"]
# -----------------------------------------------------------------------------
# Production image (copies your app + installs deps + warms cache)
# -----------------------------------------------------------------------------
FROM base AS prod
ARG APP_ENV=prod
ENV APP_ENV=${APP_ENV}
ENV APP_DEBUG=0 \
DATABASE_DRIVER=sqlite \
DATABASE_SQLITE_PATH=/var/www/html/var/data/database.sqlite \
RUN_MIGRATIONS_ON_START=1
# Copy only composer manifests for layer caching
COPY composer.json composer.lock* symfony.lock* ./
# Install vendors (cached)
RUN --mount=type=cache,target=/tmp/composer \
if [ -f composer.json ]; then \
composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts; \
fi
# Copy the rest of the app
COPY . /var/www/html
# Copy the rest of the app
COPY . /var/www/html
# If Symfony console exists, finalize install & warm cache
RUN if [ -f bin/console ]; then \
# Finalize install & warm cache
RUN if [ -f bin/console ]; then \
set -ex; \
composer dump-autoload --no-dev --optimize; \
php bin/console cache:clear --no-warmup; \
php bin/console cache:warmup; \
mkdir -p var && chown -R www-data:www-data var; \
mkdir -p var var/data public/uploads; \
chown -R www-data:www-data var public/uploads; \
fi
USER www-data
CMD ["php-fpm"]
# Runtime web stack (nginx + supervisor) for a single immutable container
RUN apk add --no-cache nginx supervisor curl
COPY docker/prod/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/prod/supervisord.conf /etc/supervisor/conf.d/app.conf
COPY docker/prod/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh \
&& mkdir -p /run/nginx /var/log/supervisor \
&& chown -R www-data:www-data /var/www/html
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/app.conf"]

19
docker/prod/entrypoint.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
set -eu
if [ "${RUN_MIGRATIONS_ON_START:-1}" = "1" ] && [ -f bin/console ]; then
if [ "${DATABASE_DRIVER:-sqlite}" = "sqlite" ]; then
SQLITE_PATH="${DATABASE_SQLITE_PATH:-/var/www/html/var/data/database.sqlite}"
SQLITE_DIR=$(dirname "${SQLITE_PATH}")
mkdir -p "${SQLITE_DIR}"
if [ ! -f "${SQLITE_PATH}" ]; then
touch "${SQLITE_PATH}"
fi
chown -R www-data:www-data "${SQLITE_DIR}"
fi
su-exec www-data php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
fi
exec "$@"

29
docker/prod/nginx.conf Normal file
View File

@@ -0,0 +1,29 @@
server {
listen 8080;
server_name _;
root /var/www/html/public;
index index.php;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location = /healthz {
access_log off;
add_header Content-Type text/plain;
return 200 "OK";
}
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
}

View File

@@ -0,0 +1,21 @@
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=/usr/local/sbin/php-fpm --nodaemonize
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0