docs(01): create phase 1 foundation plans

Phase 01: Foundation
- 3 plans in 3 waves (sequential dependency)
- Plan 01: Mill build + ZIO HTTP server
- Plan 02: PostgreSQL + Quill + Flyway migrations
- Plan 03: Repository pattern + Pulumi/Besom deployment

Ready for execution
This commit is contained in:
Jakub Zych
2026-02-04 17:21:15 +01:00
parent a0f198e894
commit 3f1fc59d23
4 changed files with 767 additions and 6 deletions

View File

@@ -32,15 +32,15 @@ Decimal phases appear between their surrounding integers in numeric order.
**Success Criteria** (what must be TRUE): **Success Criteria** (what must be TRUE):
1. Developer can run `mill run` and see HTTP server responding to requests 1. Developer can run `mill run` and see HTTP server responding to requests
2. Database queries execute with compile-time SQL validation via Quill 2. Database queries execute with compile-time SQL validation via Quill
3. Database migrations run automatically on application startup 3. Database migrations run via Migrator service
4. Models use Repository pattern with ZIO effects for data access 4. Models use Repository pattern with ZIO effects for data access
5. Pulumi configuration deploys the application to cloud infrastructure 5. Pulumi configuration deploys the application to cloud infrastructure
**Plans**: TBD **Plans**: 3 plans
Plans: Plans:
- [ ] 01-01: Mill build setup and ZIO HTTP server - [ ] 01-01-PLAN.md - Mill build setup and ZIO HTTP server with health endpoint
- [ ] 01-02: PostgreSQL integration with Quill and migrations - [ ] 01-02-PLAN.md - PostgreSQL integration with Quill and Flyway migrations
- [ ] 01-03: Repository pattern and Pulumi deployment - [ ] 01-03-PLAN.md - Repository pattern and Pulumi/Besom deployment config
### Phase 2: Plugin System ### Phase 2: Plugin System
**Goal**: Establish the plugin architecture that all other features build upon **Goal**: Establish the plugin architecture that all other features build upon
@@ -206,7 +206,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
| Phase | Plans Complete | Status | Completed | | Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Foundation | 0/3 | Not started | - | | 1. Foundation | 0/3 | Planned | - |
| 2. Plugin System | 0/3 | Not started | - | | 2. Plugin System | 0/3 | Not started | - |
| 3. Component System | 0/2 | Not started | - | | 3. Component System | 0/2 | Not started | - |
| 4. Theme Engine | 0/2 | Not started | - | | 4. Theme Engine | 0/2 | Not started | - |

View File

@@ -0,0 +1,208 @@
---
phase: 01-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- build.mill
- src/Main.scala
- src/config/AppConfig.scala
- src/api/Routes.scala
- src/api/HealthRoutes.scala
- resources/application.conf
autonomous: true
must_haves:
truths:
- "Developer can run `mill run` and server starts on port 8080"
- "GET /health returns 200 with 'ok' body"
- "Server reads configuration from application.conf"
- "Environment variables override HOCON values"
artifacts:
- path: "build.mill"
provides: "Mill build configuration with ZIO dependencies"
contains: "zio-http"
- path: "src/Main.scala"
provides: "Application entry point extending ZIOAppDefault"
exports: ["Main"]
- path: "src/config/AppConfig.scala"
provides: "Configuration case classes"
exports: ["AppConfig", "ServerConfig", "DatabaseConfig"]
- path: "src/api/Routes.scala"
provides: "Route composition"
exports: ["Routes"]
- path: "src/api/HealthRoutes.scala"
provides: "Health check endpoint"
contains: "/health"
- path: "resources/application.conf"
provides: "HOCON configuration with env overrides"
contains: "server.port"
key_links:
- from: "src/Main.scala"
to: "src/api/Routes.scala"
via: "Server.serve(routes)"
pattern: "Server\\.serve"
- from: "src/Main.scala"
to: "resources/application.conf"
via: "ConfigProvider.fromResourcePath"
pattern: "ConfigProvider"
---
<objective>
Set up the Mill build system and create a working ZIO HTTP server with health endpoints and HOCON configuration.
Purpose: Establish the foundational build infrastructure and prove the ZIO HTTP stack works before adding database complexity.
Output: Running HTTP server accessible at localhost:8080 with /health endpoint.
</objective>
<execution_context>
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create Mill build configuration</name>
<files>build.mill</files>
<action>
Create Mill build file with Scala 3 and ZIO dependencies.
Use programmatic Mill config (build.mill not YAML) for assembly support:
- Scala version: 3.3.4 (LTS, better library compatibility than 3.8.x)
- Dependencies:
- dev.zio::zio:2.1.14
- dev.zio::zio-http:3.0.1 (stable release, not 3.8.x which is unreleased)
- dev.zio::zio-config:4.0.2
- dev.zio::zio-config-typesafe:4.0.2
- dev.zio::zio-config-magnolia:4.0.2
- io.getquill::quill-jdbc-zio:4.8.5
- org.postgresql:postgresql:42.7.4
- org.flywaydb:flyway-core:10.21.0
- org.flywaydb:flyway-database-postgresql:10.21.0
Include assembly configuration for fat JAR with:
- Service file merging for META-INF/services
- Exclude signature files (*.SF, *.DSA, *.RSA)
Note: Research suggested newer versions but use stable releases that exist on Maven Central.
</action>
<verify>Run `mill resolve _` to verify Mill parses the build file correctly</verify>
<done>build.mill exists and Mill can parse all dependencies</done>
</task>
<task type="auto">
<name>Task 2: Create project structure and configuration</name>
<files>
src/config/AppConfig.scala
resources/application.conf
</files>
<action>
Create configuration infrastructure:
1. src/config/AppConfig.scala:
- Case class AppConfig with server and database nested configs
- Case class ServerConfig(host: String, port: Int)
- Case class DatabaseConfig(host: String, port: Int, database: String, user: String, password: String)
- Use zio-config-magnolia for automatic derivation
2. resources/application.conf (HOCON):
```hocon
server {
host = "0.0.0.0"
host = ${?SERVER_HOST}
port = 8080
port = ${?SERVER_PORT}
}
database {
host = "localhost"
host = ${?DB_HOST}
port = 5432
port = ${?DB_PORT}
database = "summercms"
database = ${?DB_NAME}
user = "summercms"
user = ${?DB_USER}
password = "summercms"
password = ${?DB_PASSWORD}
}
```
Pattern: Environment variables override defaults using HOCON substitution syntax.
</action>
<verify>Files exist at correct paths</verify>
<done>AppConfig case classes defined, application.conf has all database and server settings with env overrides</done>
</task>
<task type="auto">
<name>Task 3: Create HTTP routes and Main entry point</name>
<files>
src/api/HealthRoutes.scala
src/api/Routes.scala
src/Main.scala
</files>
<action>
Create the HTTP layer:
1. src/api/HealthRoutes.scala:
- Object HealthRoutes with `routes` val returning Routes
- GET /health -> Response.text("ok")
- Keep simple for now; /ready endpoint added when database exists
2. src/api/Routes.scala:
- Object Routes that composes all route modules
- For now, just re-exports HealthRoutes.routes
- This is the composition point for future route modules
3. src/Main.scala:
- Object Main extends ZIOAppDefault
- Override bootstrap to set ConfigProvider.fromResourcePath()
- In run: Server.serve(Routes.routes).provide(Server.defaultWithPort(8080))
- Print ASCII art banner on startup (SummerCMS with sun motif)
ASCII banner example:
```
\\ | //
\\ | //
___\\###//___
/ SUMMER \\
\\ CMS /
\\__________/
Starting on port 8080...
```
</action>
<verify>Run `mill run` and curl localhost:8080/health returns "ok"</verify>
<done>Server starts, /health returns 200 with "ok" body, banner displays</done>
</task>
</tasks>
<verification>
1. `mill resolve _` succeeds (build file valid)
2. `mill compile` succeeds (code compiles)
3. `mill run` starts server (process runs)
4. `curl http://localhost:8080/health` returns "ok" (endpoint works)
5. Server logs show startup banner
</verification>
<success_criteria>
- Mill build configuration exists with all ZIO dependencies
- Server starts on port 8080 via `mill run`
- GET /health returns 200 OK with "ok" body
- Configuration loads from application.conf
- ASCII SummerCMS banner displays on startup
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,228 @@
---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on: ["01-01"]
files_modified:
- src/db/QuillContext.scala
- src/db/Migrator.scala
- resources/db/migration/V1__create_summer_users.sql
- src/api/HealthRoutes.scala
autonomous: true
must_haves:
truths:
- "Quill compiles SQL queries at compile time (type errors for bad queries)"
- "Flyway migrations run via ZIO effect"
- "Migration creates summer_users table in PostgreSQL"
- "GET /ready returns 200 when database is connected, 503 when not"
artifacts:
- path: "src/db/QuillContext.scala"
provides: "Quill PostgreSQL context with ZIO integration"
exports: ["quillLayer", "dataSourceLayer"]
- path: "src/db/Migrator.scala"
provides: "Flyway wrapper as ZIO service"
exports: ["Migrator", "MigrationStatus"]
- path: "resources/db/migration/V1__create_summer_users.sql"
provides: "Initial database schema"
contains: "CREATE TABLE summer_users"
key_links:
- from: "src/db/QuillContext.scala"
to: "resources/application.conf"
via: "Quill.DataSource.fromPrefix"
pattern: "fromPrefix.*database"
- from: "src/db/Migrator.scala"
to: "src/db/QuillContext.scala"
via: "ZLayer dependency on DataSource"
pattern: "DataSource"
- from: "src/api/HealthRoutes.scala"
to: "src/db/QuillContext.scala"
via: "/ready endpoint checks connection"
pattern: "/ready"
---
<objective>
Integrate PostgreSQL via Quill with compile-time SQL validation and Flyway migrations wrapped in ZIO effects.
Purpose: Establish database connectivity with type-safe queries and version-controlled schema changes.
Output: Working database layer with migrations and a /ready endpoint that verifies connectivity.
</objective>
<execution_context>
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create Quill PostgreSQL context</name>
<files>src/db/QuillContext.scala</files>
<action>
Create Quill context for PostgreSQL with ZIO integration.
src/db/QuillContext.scala:
- Import io.getquill.* and io.getquill.jdbczio.Quill
- Create dataSourceLayer: ZLayer[Any, Throwable, javax.sql.DataSource]
- Use Quill.DataSource.fromPrefix("database") to read from HOCON config
- Create quillLayer: ZLayer[javax.sql.DataSource, Nothing, Quill.Postgres[SnakeCase]]
- Use Quill.Postgres.fromNamingStrategy(SnakeCase)
- Export combined layer: dataSourceLayer >>> quillLayer
IMPORTANT: Never use "io" as a variable name in this file (Quill package conflict).
Configuration note: Quill reads HikariCP config from the "database" prefix in application.conf.
The existing database config structure should work, but may need adjustment to match HikariCP's expected format:
```hocon
database {
dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource"
dataSource {
serverName = "localhost"
portNumber = 5432
databaseName = "summercms"
user = "summercms"
password = "summercms"
}
}
```
Update resources/application.conf to use HikariCP-compatible format if needed.
</action>
<verify>`mill compile` succeeds with Quill context</verify>
<done>QuillContext.scala exports dataSourceLayer and quillLayer, compiles without errors</done>
</task>
<task type="auto">
<name>Task 2: Create Flyway migrator service</name>
<files>
src/db/Migrator.scala
resources/db/migration/V1__create_summer_users.sql
</files>
<action>
Create Flyway wrapper as ZIO service and initial migration.
1. src/db/Migrator.scala:
- Case class MigrationStatus(current: Option[String], pending: Int)
- Trait Migrator with methods:
- def migrate: Task[Int] (returns count of migrations run)
- def status: Task[MigrationStatus]
- Object Migrator with:
- val live: ZLayer[javax.sql.DataSource, Nothing, Migrator]
- Implementation configures Flyway with:
- dataSource from layer
- locations: "classpath:db/migration"
- table: "summer_migrations" (not default flyway_schema_history)
- baselineOnMigrate: true (for existing databases)
2. resources/db/migration/V1__create_summer_users.sql:
```sql
-- Summer CMS initial schema
-- Creates the base users table for future auth
CREATE TABLE summer_users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_summer_users_email ON summer_users(email);
-- Trigger for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_summer_users_updated_at
BEFORE UPDATE ON summer_users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
```
Naming follows conventions from CONTEXT.md:
- snake_case columns
- summer_ prefix for core tables
- id, created_at, updated_at by default
</action>
<verify>`mill compile` succeeds, migration file exists at correct path</verify>
<done>Migrator service defined with migrate/status methods, V1 migration creates summer_users</done>
</task>
<task type="auto">
<name>Task 3: Add /ready endpoint with database check</name>
<files>src/api/HealthRoutes.scala</files>
<action>
Update HealthRoutes to include database connectivity check.
Modify src/api/HealthRoutes.scala:
- Keep existing GET /health -> Response.text("ok") (always 200)
- Add GET /ready endpoint:
- Requires DataSource from ZLayer
- Attempts: ZIO.attempt(ds.getConnection.close())
- Success: Response.text("ready") with 200
- Failure: Response.status(Status.ServiceUnavailable) with 503
Pattern:
```scala
Method.GET / "ready" -> handler {
ZIO.serviceWithZIO[javax.sql.DataSource] { ds =>
ZIO.attempt {
val conn = ds.getConnection
conn.close()
}.as(Response.text("ready"))
.catchAll(_ => ZIO.succeed(Response.status(Status.ServiceUnavailable)))
}
}
```
Update Main.scala to:
- Provide dataSourceLayer to the server
- On startup, run Migrator.migrate and log result
- Startup sequence: migrate -> start server
Note: The /ready endpoint will return 503 if no database is running, which is correct behavior.
</action>
<verify>
With PostgreSQL running: `curl localhost:8080/ready` returns "ready"
Without PostgreSQL: `curl localhost:8080/ready` returns 503
</verify>
<done>/ready endpoint exists, returns 200 when DB connected, 503 when not</done>
</task>
</tasks>
<verification>
1. `mill compile` succeeds (Quill context compiles)
2. With PostgreSQL running:
- `mill run` starts and runs migrations
- Logs show "Applied N migrations"
- `curl localhost:8080/ready` returns "ready"
3. Check database: `psql -d summercms -c "\\dt"` shows summer_users table
4. Check migrations table: `psql -d summercms -c "SELECT * FROM summer_migrations"`
</verification>
<success_criteria>
- Quill context exists with PostgreSQL configuration
- Flyway migrator runs as ZIO effect on startup
- V1 migration creates summer_users table with proper schema
- /ready endpoint verifies database connectivity
- Compile-time SQL validation works (intentional bad query causes compile error)
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,325 @@
---
phase: 01-foundation
plan: 03
type: execute
wave: 3
depends_on: ["01-02"]
files_modified:
- src/repository/RepositoryError.scala
- src/repository/UserRepository.scala
- src/model/User.scala
- infra/project.scala
- infra/Main.scala
- Dockerfile
autonomous: true
must_haves:
truths:
- "Repository trait defines CRUD operations with typed ZIO errors"
- "UserRepositoryLive implements queries using Quill context"
- "Besom/Pulumi infrastructure code compiles and can run pulumi preview"
- "Dockerfile builds fat JAR into runnable container"
artifacts:
- path: "src/repository/RepositoryError.scala"
provides: "Typed error ADT for repository operations"
exports: ["RepositoryError", "NotFound", "Conflict", "DatabaseError"]
- path: "src/repository/UserRepository.scala"
provides: "User repository trait and implementation"
exports: ["UserRepository", "UserRepositoryLive"]
- path: "src/model/User.scala"
provides: "User domain model"
exports: ["User"]
- path: "infra/Main.scala"
provides: "Pulumi infrastructure definition"
contains: "Pulumi.run"
- path: "Dockerfile"
provides: "Container build configuration"
contains: "FROM"
key_links:
- from: "src/repository/UserRepository.scala"
to: "src/db/QuillContext.scala"
via: "Quill.Postgres dependency"
pattern: "Quill\\.Postgres"
- from: "src/repository/UserRepository.scala"
to: "src/model/User.scala"
via: "CRUD operations on User"
pattern: "User"
- from: "src/repository/UserRepository.scala"
to: "src/repository/RepositoryError.scala"
via: "IO[RepositoryError, _] return type"
pattern: "RepositoryError"
---
<objective>
Implement the Repository pattern with typed errors and create Pulumi infrastructure configuration using Besom.
Purpose: Establish the data access pattern that all future models will follow, and prepare deployment infrastructure.
Output: Working UserRepository with compile-time validated queries, Pulumi config for cloud deployment, Dockerfile for containerization.
</objective>
<execution_context>
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
@.planning/phases/01-foundation/01-02-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create Repository pattern with typed errors</name>
<files>
src/model/User.scala
src/repository/RepositoryError.scala
src/repository/UserRepository.scala
</files>
<action>
Implement the Repository pattern following ZIO best practices.
1. src/model/User.scala:
```scala
package summercms.model
import java.time.Instant
case class User(
id: Long,
email: String,
passwordHash: String,
createdAt: Instant,
updatedAt: Instant
)
```
2. src/repository/RepositoryError.scala:
```scala
package summercms.repository
sealed trait RepositoryError
object RepositoryError:
case class NotFound(entity: String, id: Long) extends RepositoryError
case class NotFoundByField(entity: String, field: String, value: String) extends RepositoryError
case class Conflict(entity: String, message: String) extends RepositoryError
case class ValidationError(errors: List[String]) extends RepositoryError
case class DatabaseError(message: String, cause: Option[Throwable] = None) extends RepositoryError
```
3. src/repository/UserRepository.scala:
- Trait UserRepository with methods:
- def findById(id: Long): IO[RepositoryError, Option[User]]
- def findByEmail(email: String): IO[RepositoryError, Option[User]]
- def create(email: String, passwordHash: String): IO[RepositoryError, User]
- def update(user: User): IO[RepositoryError, User]
- def delete(id: Long): IO[RepositoryError, Unit]
- Object UserRepository with:
- val live: ZLayer[Quill.Postgres[SnakeCase], Nothing, UserRepository]
- Class UserRepositoryLive(quill: Quill.Postgres[SnakeCase]) extends UserRepository:
- import quill.*
- inline def users = quote(querySchema[User]("summer_users"))
- Implement all methods using Quill queries
- Use refineOrDie to convert SQLException to RepositoryError.DatabaseError
- Handle unique constraint violations as Conflict errors
Pattern for error handling:
```scala
def create(email: String, passwordHash: String): IO[RepositoryError, User] =
val now = Instant.now()
val user = User(0, email, passwordHash, now, now)
run(users.insertValue(lift(user)).returningGenerated(_.id))
.map(id => user.copy(id = id))
.refineOrDie {
case e: java.sql.SQLException if e.getSQLState == "23505" =>
RepositoryError.Conflict("User", s"Email $email already exists")
case e: java.sql.SQLException =>
RepositoryError.DatabaseError(e.getMessage, Some(e))
}
```
IMPORTANT: Do not use "io" as a variable name (Quill package conflict).
</action>
<verify>`mill compile` succeeds, repository pattern established</verify>
<done>User model, RepositoryError ADT, and UserRepository trait+implementation exist and compile</done>
</task>
<task type="auto">
<name>Task 2: Create Pulumi infrastructure with Besom</name>
<files>
infra/project.scala
infra/Main.scala
</files>
<action>
Create Pulumi infrastructure using Besom (Scala Pulumi SDK).
IMPORTANT: Besom does NOT support Mill. Use Scala CLI for the infra/ directory.
1. infra/project.scala (Scala CLI project config):
```scala
//> using scala 3.3.4
//> using dep org.virtuslab::besom-core:0.4.0
//> using dep org.virtuslab::besom-aws:6.56.0
```
Note: Use Besom 0.4.0 (latest stable on Maven Central, not 0.5.0 from research).
2. infra/Main.scala:
```scala
import besom.*
import besom.api.aws
@main def main = Pulumi.run {
// Configuration
val config = besom.Config()
val environment = config.get("environment").getOrElse("dev")
// S3 bucket for assets (optional CDN origin)
val assetsBucket = aws.s3.Bucket(
s"summercms-assets-$environment",
aws.s3.BucketArgs()
)
// RDS PostgreSQL instance
val db = aws.rds.Instance(
s"summercms-db-$environment",
aws.rds.InstanceArgs(
engine = "postgres",
engineVersion = "16.4",
instanceClass = "db.t3.micro",
allocatedStorage = 20,
dbName = "summercms",
username = "summercms",
password = config.requireSecret("dbPassword"),
skipFinalSnapshot = environment == "dev",
publiclyAccessible = environment == "dev"
)
)
// ECS for container deployment (placeholder for now)
// Full ECS setup deferred to when we need actual deployment
Stack.exports(
assetsBucketName = assetsBucket.bucket,
dbEndpoint = db.endpoint,
dbPort = db.port
)
}
```
This provides:
- Multi-environment support via Pulumi config
- S3 bucket for future asset storage
- RDS PostgreSQL for database
- Secrets management for DB password
- Outputs for connecting the application
Note: Full ECS/container deployment is deferred. This establishes the pattern.
</action>
<verify>
Run from infra/ directory:
- `scala-cli compile .` compiles
- `pulumi preview` shows resources (requires Pulumi CLI and AWS credentials)
</verify>
<done>Besom infrastructure code compiles, defines RDS and S3 resources</done>
</task>
<task type="auto">
<name>Task 3: Create Dockerfile for containerization</name>
<files>Dockerfile</files>
<action>
Create multi-stage Dockerfile for building and running SummerCMS.
Dockerfile:
```dockerfile
# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder
# Install Mill
RUN curl -L https://github.com/com-lihaoyi/mill/releases/download/0.12.3/0.12.3 > /usr/local/bin/mill && \
chmod +x /usr/local/bin/mill
WORKDIR /app
COPY . .
# Build fat JAR
RUN mill assembly
# Stage 2: Runtime
FROM eclipse-temurin:21-jre
WORKDIR /app
# Copy the fat JAR from builder
COPY --from=builder /app/out/assembly.dest/out.jar /app/summercms.jar
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Install curl for health check
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
EXPOSE 8080
# Run the application
ENTRYPOINT ["java", "-jar", "/app/summercms.jar"]
```
Features:
- Multi-stage build (builder has JDK+Mill, runtime has only JRE)
- Uses Eclipse Temurin JDK 21 (LTS)
- Health check using /health endpoint
- Minimal runtime image size
- Environment variables configure the app (via application.conf ${?VAR} substitution)
Usage:
```bash
docker build -t summercms .
docker run -p 8080:8080 \
-e DB_HOST=host.docker.internal \
-e DB_PASSWORD=secret \
summercms
```
</action>
<verify>
`docker build -t summercms .` succeeds
`docker run -p 8080:8080 summercms` starts (will fail at DB connect without DB, that's expected)
</verify>
<done>Dockerfile exists with multi-stage build, health check, and env var configuration</done>
</task>
</tasks>
<verification>
1. `mill compile` succeeds (repository code compiles)
2. Repository pattern test (manual in mill console or separate test):
- UserRepository.findById returns IO[RepositoryError, Option[User]]
- Type errors when using wrong types
3. Infrastructure:
- `cd infra && scala-cli compile .` succeeds
- `cd infra && pulumi preview` shows planned resources (requires setup)
4. Docker:
- `docker build -t summercms .` builds successfully
- Image contains /app/summercms.jar
</verification>
<success_criteria>
- User model and RepositoryError ADT defined
- UserRepository trait with CRUD methods, typed errors
- UserRepositoryLive implements queries with Quill
- Besom infrastructure defines RDS and S3 for AWS
- Dockerfile builds fat JAR in multi-stage build
- All code compiles without errors
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`
</output>