Add a runnable example compose stack for WarpEngine

examples/compose boots everything the engine's workflow assumes: mysql, a
minimal Rails host consuming the engine as a path gem, an SSH drop area
sharing the softwares volume with the app, and — behind the ci profile —
gitea plus woodpecker (agent attached to the stack network so pipeline
steps reach droparea/app by service name).

host_app doubles as a reference for a brand-new host: Gemfile, the two
initializers, apipie + engine mounts in routes.rb; on first boot the
entrypoint runs the install generator and db:prepare.

The README gains a detailed bring-up walkthrough: quickstart, publishing
a release by hand over scp + /update (verified end to end from a clean
slate), and the full gitea/woodpecker OAuth wiring.
This commit is contained in:
2026-08-05 06:56:12 +02:00
parent d569c3366a
commit 60e196a03e
18 changed files with 394 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Copy to .env and adjust. Every value except the OAuth pair has a working
# default for a throwaway local demo, so the quickstart runs without edits.
MYSQL_ROOT_PASSWORD=warpengine
# Shared secret for the /update endpoint (X-Update-Secret header).
UPDATE_SECRET=example-update-secret
# Password of the "drop" user on the artifact drop area (SSH, port 2222).
DROP_PASSWORD=drop
# Published ports.
APP_PORT=8080
DROPAREA_SSH_PORT=2222
GITEA_SSH_PORT=2223
# --- profile "ci" only -------------------------------------------------------
WOODPECKER_AGENT_SECRET=example-agent-secret
# OAuth2 application created in gitea (Settings -> Applications), redirect URI
# http://woodpecker:8000/authorize — required before the ci profile starts.
WOODPECKER_GITEA_CLIENT=
WOODPECKER_GITEA_SECRET=
+131
View File
@@ -0,0 +1,131 @@
# Example stack: everything WarpEngine needs to come alive, end to end.
#
# mysql the catalog database
# app a minimal Rails host with the engine mounted from this
# repo checkout (headless: API + updater, no ActiveAdmin)
# droparea SSH server where build pipelines drop artifacts; shares
# the "softwares" volume with the app
# gitea (profile "ci") the git forge
# woodpecker (profile "ci") CI server + agent, wired to gitea
#
# Quickstart (catalog + drop area only):
# cp .env.example .env
# docker compose up --build
#
# Full loop with forge + CI:
# docker compose --profile ci up --build
#
# See ../../README.md ("Example stack") or the WarpEngine wiki page for the
# full walkthrough, including the one-time gitea/woodpecker OAuth wiring.
services:
mysql:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-warpengine}
MYSQL_DATABASE: warp_engine_example
volumes:
- mysql-data:/var/lib/mysql
app:
build: ./host_app
ports:
- "${APP_PORT:-8080}:3000"
environment:
RAILS_ENV: development
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-warpengine}
UPDATE_SECRET: ${UPDATE_SECRET:-example-update-secret}
FILE_CONTAINER_PATH: /softwares
IMAGE_CONTAINER_PATH: /images
depends_on:
mysql:
condition: service_healthy
volumes:
- ./host_app:/app
- ../..:/warp_engine # the engine itself, consumed as a path gem
- bundle:/usr/local/bundle
- softwares:/softwares
- images:/images
# SSH landing zone for build artifacts. Pipelines (or you, with scp) upload
# into ~/drop here; the app sees the same files under /softwares.
droparea:
image: linuxserver/openssh-server
environment:
PUID: 1
PGID: 1
SUDO_ACCESS: "false"
PASSWORD_ACCESS: "true"
USER_NAME: drop
USER_PASSWORD: ${DROP_PASSWORD:-drop}
ports:
- "${DROPAREA_SSH_PORT:-2222}:2222"
volumes:
# A subdir of the drop user's home (/config), so sshd's own state files
# never end up in the catalog directory.
- softwares:/config/drop
# --- profile "ci": the forge + CI producing releases for the catalog ------
#
# gitea and woodpecker refer to each other by their service names, so your
# browser needs to resolve those names too:
# echo "127.0.0.1 gitea woodpecker" | sudo tee -a /etc/hosts
# gitea: http://gitea:3000 woodpecker: http://woodpecker:8000
gitea:
image: gitea/gitea:1.27.0
profiles: ["ci"]
environment:
DISABLE_REGISTRATION: "true"
ROOT_URL: "http://gitea:3000"
ports:
- "3000:3000"
- "${GITEA_SSH_PORT:-2223}:22"
volumes:
- gitea-data:/data
woodpecker:
image: woodpeckerci/woodpecker-server:v3.16.0
profiles: ["ci"]
environment:
WOODPECKER_HOST: "http://woodpecker:8000"
WOODPECKER_OPEN: "true"
WOODPECKER_GITEA: "true"
WOODPECKER_GITEA_URL: "http://gitea:3000"
# Create an OAuth2 app in gitea first — see the README walkthrough.
WOODPECKER_GITEA_CLIENT: ${WOODPECKER_GITEA_CLIENT:-}
WOODPECKER_GITEA_SECRET: ${WOODPECKER_GITEA_SECRET:-}
WOODPECKER_SERVER_ADDR: ":8000"
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:-example-agent-secret}
ports:
- "8000:8000"
volumes:
- woodpecker-data:/var/lib/woodpecker
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:v3.16.0
profiles: ["ci"]
environment:
WOODPECKER_SERVER: "woodpecker:9000"
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:-example-agent-secret}
# Attach pipeline containers to the stack network so steps can reach
# gitea, droparea and the app by service name.
WOODPECKER_BACKEND_DOCKER_NETWORK: warp-example
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
default:
name: warp-example
volumes:
mysql-data:
bundle:
softwares:
images:
gitea-data:
woodpecker-data:
+6
View File
@@ -0,0 +1,6 @@
# Generated on first boot by the entrypoint (install generator + db:prepare).
db/migrate/
db/schema.rb
log/
tmp/
Gemfile.lock
+20
View File
@@ -0,0 +1,20 @@
FROM ruby:3.3-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
git \
tzdata \
libyaml-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# The app source, the engine checkout (/warp_engine) and the bundle are all
# mounted at runtime by the compose file; the entrypoint bundles on first boot.
ENTRYPOINT ["./bin/docker-entrypoint"]
EXPOSE 3000
CMD ["bin/rails", "server", "-b", "0.0.0.0", "-p", "3000"]
+10
View File
@@ -0,0 +1,10 @@
source "https://rubygems.org"
gem "rails", "~> 8.0.0"
gem "mysql2", "~> 0.5"
gem "puma"
# In this example stack the engine comes straight from the repo checkout the
# compose file mounts at /warp_engine. A real host would use the git source or
# the Forgejo rubygems registry instead — see the engine README.
gem "warp_engine", path: ENV.fetch("WARP_ENGINE_PATH", "/warp_engine")
+3
View File
@@ -0,0 +1,3 @@
require_relative "config/application"
Rails.application.load_tasks
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
set -e
bundle check || bundle install
# First boot: run the install generator the way a real host would. It creates
# the create_warp_engine_tables migration; --skip leaves our initializer alone.
if ! ls db/migrate/*create_warp_engine_tables* >/dev/null 2>&1; then
bin/rails generate warp_engine:install --skip
fi
bin/rails db:prepare
rm -f tmp/pids/server.pid
exec "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"
+4
View File
@@ -0,0 +1,4 @@
require_relative "config/environment"
run Rails.application
Rails.application.load_server
@@ -0,0 +1,23 @@
require_relative "boot"
require "rails"
require "active_model/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_view/railtie"
require "action_dispatch/railtie"
Bundler.require(*Rails.groups)
require "warp_engine"
module HostApp
class Application < Rails::Application
config.load_defaults 8.0
config.time_zone = "UTC"
config.active_record.default_timezone = :utc
# Demo stack: reachable as localhost, gitea-network hostnames, etc.
config.hosts.clear
end
end
+3
View File
@@ -0,0 +1,3 @@
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup"
@@ -0,0 +1,8 @@
development:
adapter: mysql2
encoding: utf8mb4
username: root
password: <%= ENV.fetch("MYSQL_ROOT_PASSWORD", "warpengine") %>
host: mysql
port: 3306
database: warp_engine_example
@@ -0,0 +1,3 @@
require_relative "application"
Rails.application.initialize!
@@ -0,0 +1,10 @@
Rails.application.configure do
config.enable_reloading = true
config.eager_load = false
config.consider_all_requests_local = true
config.active_record.migration_error = :page_load
config.active_record.verbose_query_logs = true
config.logger = ActiveSupport::Logger.new($stdout)
end
@@ -0,0 +1,11 @@
Apipie.configure do |config|
config.app_name = "WarpEngine Example Host"
config.api_base_url = ""
config.doc_base_url = "/api/docs"
config.api_controllers_matcher = [
"#{WarpEngine::Engine.root}/app/controllers/**/*.rb"
]
config.validate = false
config.translate = false
config.default_version = "1.0"
end
@@ -0,0 +1,9 @@
Rails.application.config.to_prepare do
WarpEngine.configure do |c|
c.file_container_path = ENV.fetch("FILE_CONTAINER_PATH", "/softwares")
c.image_container_path = ENV.fetch("IMAGE_CONTAINER_PATH", "/images")
# Beállítatlan secret esetén a /update endpoint minden kérést elutasít.
c.update_secret = ENV["UPDATE_SECRET"]
end
end
@@ -0,0 +1,6 @@
Rails.application.routes.draw do
apipie
# Keep the engine mount the last entry so the host's own routes win.
mount WarpEngine::Engine => "/"
end