feat(deploy): split compose into prod base + dev override

Restructure the container setup to support registry-based deployment:
the default docker-compose.yml now pulls a prebuilt app image from the
Gitea container registry instead of building locally, while a new
docker-compose.dev.yml override preserves the local build + integration
test workflow for development.

Why:
- Production and self-hosting should consume a published image, not
  rebuild from source on the host. The default `docker compose up` now
  pulls the app image (git.linuxhg.com/bookhoard/bookhoard) alongside the
  public postgres image, with no build step required.
- Development still needs to build from source and run integration
  tests, so those concerns move to an override file the Makefile applies.
  Shared config (env, volumes, ports, healthchecks) lives in one place to
  avoid drift between environments.

Changes:
- docker-compose.yml (prod base): the app service now references
  `image: git.linuxhg.com/bookhoard/bookhoard:${IMAGE_TAG:-latest}` instead
  of a build context. The tests service is removed (moved to the
  override). IMAGE_TAG lets deployers pin or roll back a specific version.
- docker-compose.dev.yml (new override): adds the local `build:` context
  for the app and defines the integration `tests` service (profile-gated).
  Everything else is inherited from the base file via compose merging.
- Makefile: introduce a COMPOSE variable that merges the base and
  override (`-f docker-compose.yml -f docker-compose.dev.yml`); all dev
  targets now use it. Plain `docker compose` against the base file only
  remains the production path.
- README: quickstart updated to pull and start prebuilt images; clone URL
  points at the Gitea instance.

The development workflow (`make rebuild-app`, `make test-integration`,
etc.) is functionally unchanged.
This commit is contained in:
2026-07-29 15:47:40 -04:00
parent 75b33fdae6
commit 76c6826920
4 changed files with 89 additions and 69 deletions
+24 -20
View File
@@ -11,6 +11,10 @@ endif
# Override with: CONTAINER_RUNTIME=podman make rebuild-app
CONTAINER_RUNTIME ?= $(shell command -v docker 2>/dev/null || command -v podman 2>/dev/null)
# Dev compose stack: base prod file merged with the dev override (local build + tests).
# Prod deploy does NOT use this — it runs plain `docker compose` against the base file only.
COMPOSE := $(CONTAINER_RUNTIME) compose -f docker-compose.yml -f docker-compose.dev.yml
# Default target
help:
@echo "Available targets:"
@@ -48,9 +52,9 @@ test:
# Run integration tests in containers (matches production environment)
test-integration:
@echo "Building test containers..."
$(CONTAINER_RUNTIME) compose --profile tests build
$(COMPOSE) --profile tests build
@echo "Starting application containers..."
$(CONTAINER_RUNTIME) compose up -d db app
$(COMPOSE) up -d db app
@echo "Waiting for services to be healthy..."
@until $(CONTAINER_RUNTIME) exec bookhoard_db pg_isready -U postgres > /dev/null 2>&1; do \
echo " Database not ready yet..."; \
@@ -64,7 +68,7 @@ test-integration:
echo " ✓ Application is ready"
@echo ""
@echo "Running integration tests in container..."
$(CONTAINER_RUNTIME) compose --profile tests run --rm tests
$(COMPOSE) --profile tests run --rm tests
@echo ""
@echo "✅ Integration tests completed!"
@echo "📝 Containers are still running. Use 'make logs' to view logs or 'make clean' to stop."
@@ -75,67 +79,67 @@ test-all: test test-integration
# Rebuild app container only (preserve DB, with cache)
rebuild-app:
@echo "Rebuilding app container (database stays running)..."
$(CONTAINER_RUNTIME) compose up --build --force-recreate -d app
$(COMPOSE) up --build --force-recreate -d app
@echo "✓ App container rebuilt and restarted"
# Rebuild app container only (preserve DB, no cache)
rebuild-app-force:
@echo "Force rebuilding app container (database stays running, no cache)..."
$(CONTAINER_RUNTIME) compose build --no-cache app
$(CONTAINER_RUNTIME) compose up --force-recreate -d app
$(COMPOSE) build --no-cache app
$(COMPOSE) up --force-recreate -d app
@echo "✓ App container rebuilt and restarted"
# Rebuild all containers (preserve DB, with cache)
rebuild:
@echo "Rebuilding all containers (database preserved)..."
$(CONTAINER_RUNTIME) compose up --build --force-recreate -d
$(COMPOSE) up --build --force-recreate -d
@echo "✓ All containers rebuilt and restarted"
# Rebuild all containers (preserve DB, no cache)
rebuild-force:
@echo "Force rebuilding all containers (database preserved, no cache)..."
$(CONTAINER_RUNTIME) compose build --no-cache
$(CONTAINER_RUNTIME) compose up --force-recreate -d
$(COMPOSE) build --no-cache
$(COMPOSE) up --force-recreate -d
@echo "✓ All containers rebuilt and restarted"
# Rebuild all containers (remove DB, no cache)
rebuild-force-db:
@echo "Force rebuilding all containers (database will be DELETED, no cache)..."
$(CONTAINER_RUNTIME) compose down -v
$(CONTAINER_RUNTIME) compose build --no-cache
$(CONTAINER_RUNTIME) compose up --force-recreate -d
$(COMPOSE) down -v
$(COMPOSE) build --no-cache
$(COMPOSE) up --force-recreate -d
@echo "✓ All containers rebuilt and restarted"
# Stop and remove containers
clean:
$(CONTAINER_RUNTIME) compose down -v
$(COMPOSE) down -v
# Quick start (if already built)
up:
$(CONTAINER_RUNTIME) compose up -d
$(COMPOSE) up -d
# Stop all containers (alias for clean)
down:
$(CONTAINER_RUNTIME) compose down
$(COMPOSE) down
# Restart app container (preserves database)
restart:
@echo "Restarting app container (database stays running)..."
$(CONTAINER_RUNTIME) compose restart app
$(COMPOSE) restart app
@echo "✓ App container restarted"
# Show container status
ps:
$(CONTAINER_RUNTIME) compose ps
$(COMPOSE) ps
# Show container logs
logs:
$(CONTAINER_RUNTIME) compose logs -f
$(COMPOSE) logs -f
# Start containers with test mode enabled for manual testing
test-env-up:
@echo "Starting containers with test mode enabled..."
TEST_MODE=true RATE_LIMIT_ENABLED=false REQUESTS_PER_MINUTE=1000 $(CONTAINER_RUNTIME) compose up --build --force-recreate -d
TEST_MODE=true RATE_LIMIT_ENABLED=false REQUESTS_PER_MINUTE=1000 $(COMPOSE) up --build --force-recreate -d
@echo "Waiting for services to be ready..."
@until $(CONTAINER_RUNTIME) exec bookhoard_db pg_isready -U postgres > /dev/null 2>&1; do sleep 1; done
@until $(CONTAINER_RUNTIME) exec bookhoard curl -sf http://localhost:8765/health > /dev/null 2>&1; do sleep 1; done
@@ -144,7 +148,7 @@ test-env-up:
# Stop test environment
test-env-down:
$(CONTAINER_RUNTIME) compose down -v
$(COMPOSE) down -v
# Verify project guidelines compliance
verify-guidelines:
+5 -3
View File
@@ -25,7 +25,7 @@ A modern self-hosted media library system built with Go, PostgreSQL, HTMX, and T
```bash
# 1. Clone the repository
git clone https://github.com/yourusername/bookhoard.git
git clone https://git.linuxhg.com/Bookhoard/bookhoard.git
cd bookhoard
# 2. Set up environment
@@ -35,8 +35,10 @@ cp .env.example .env
# DBPASS: openssl rand -hex 16
# Edit .env with your generated values
# 3. Start the server
podman-compose up --build -d # or: docker-compose up --build -d
# 3. Pull images and start the server
docker compose pull
docker compose up -d
# Optionally pin a specific version: set IMAGE_TAG in .env (defaults to "latest")
# 4. Open your browser
open http://localhost:8765
+57
View File
@@ -0,0 +1,57 @@
# Development override — merged on top of docker-compose.yml (the base/prod file).
# Activated by all `make` targets via:
# COMPOSE = <runtime> compose -f docker-compose.yml -f docker-compose.dev.yml
#
# What this adds over prod:
# - Local image BUILDING (prod pulls a prebuilt image from the registry)
# - The integration-tests service (dev only, gated behind the "tests" profile)
# Everything else (env vars, volumes, ports, healthchecks) is inherited from the base file.
services:
# Build the app image locally instead of pulling from the registry
app:
build:
context: .
dockerfile: ./Dockerfile
# Integration Tests - runs against containerized app and db (dev only)
tests:
build:
context: .
dockerfile: ./Dockerfile
target: test-runner
container_name: bookhoard_tests
environment:
# Database Configuration
DATABASE_HOST: db
DATABASE_PORT: "5432"
DATABASE_USER: postgres
DATABASE_PASSWORD: ${DBPASS}
DATABASE_NAME: bookhoard
COOKIE_SECURE: false
# Application Configuration
JWT_SECRET: ${JWT_SECRET}
SERVER_PORT: "8765"
# Test Configuration
TEST_MODE: "true"
RATE_LIMIT_ENABLED: "false"
REQUESTS_PER_MINUTE: 1000
# Conversion Service Configuration
BOOKHOARD_CONVERSION_CACHE_DIR: /app/cache/kepub
BOOKHOARD_CONVERSION_TOOL: /usr/bin/kepubify
BOOKHOARD_CONVERSION_CACHE_TTL: 24h
# Test upload path (inside container)
TEST_UPLOAD_PATH: /app/uploads
depends_on:
db:
condition: service_healthy
app:
condition: service_healthy
volumes:
- ./uploads:/app/uploads
- bookhoard_conversion_cache:/app/cache/kepub
profiles:
- tests
+3 -46
View File
@@ -27,10 +27,10 @@ services:
- .env
# Bookhoard Application
# In production this image is pulled from the Gitea container registry.
# Override IMAGE_TAG in .env to pin or rollback a specific version (defaults to "latest").
app:
build:
context: .
dockerfile: ./Dockerfile
image: git.linuxhg.com/bookhoard/bookhoard:${IMAGE_TAG:-latest}
container_name: bookhoard
environment:
# Database Configuration
@@ -76,49 +76,6 @@ services:
retries: 3
start_period: 10s
# Integration Tests - runs against containerized app and db
tests:
build:
context: .
dockerfile: ./Dockerfile
target: test-runner
container_name: bookhoard_tests
environment:
# Database Configuration
DATABASE_HOST: db
DATABASE_PORT: "5432"
DATABASE_USER: postgres
DATABASE_PASSWORD: ${DBPASS}
DATABASE_NAME: bookhoard
COOKIE_SECURE: false
# Application Configuration
JWT_SECRET: ${JWT_SECRET}
SERVER_PORT: "8765"
# Test Configuration
TEST_MODE: "true"
RATE_LIMIT_ENABLED: "false"
REQUESTS_PER_MINUTE: 1000
# Conversion Service Configuration
BOOKHOARD_CONVERSION_CACHE_DIR: /app/cache/kepub
BOOKHOARD_CONVERSION_TOOL: /usr/bin/kepubify
BOOKHOARD_CONVERSION_CACHE_TTL: 24h
# Test upload path (inside container)
TEST_UPLOAD_PATH: /app/uploads
depends_on:
db:
condition: service_healthy
app:
condition: service_healthy
volumes:
- ./uploads:/app/uploads
- bookhoard_conversion_cache:/app/cache/kepub
profiles:
- tests
# Named Volumes
volumes:
postgres_data: