diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..2b36a59 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,5 @@ +.dart_tool/ +build/ +.env +*.log +pubspec_overrides.yaml diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..e330483 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,23 @@ +# API container. +# Set API_BIND_ADDRESS to the Docker host interface reachable by the external +# reverse proxy. Use 0.0.0.0 for LAN-wide exposure, or a specific host IP to +# restrict where Docker publishes the port. +API_BIND_ADDRESS=0.0.0.0 +API_PORT=8080 +MIGRATE_ON_STARTUP=true + +# PostgreSQL container bootstrap values. Replace the password before deploying. +POSTGRES_DB=gametime +POSTGRES_USER=gametime +POSTGRES_PASSWORD=change-me + +# API database connection values. With this docker-compose.yaml, the database +# host is the Compose service name `postgres`. +DATABASE_HOST=postgres +DATABASE_PORT=5432 +DATABASE_NAME=gametime +DATABASE_USER=gametime +DATABASE_PASSWORD=change-me + +# External HTTPS is terminated by a reverse proxy running outside this Compose +# stack. Point that proxy to http://:${API_PORT}. diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..77ce8a0 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,33 @@ +FROM dart:stable AS build + +WORKDIR /app + +COPY pubspec.* ./ +RUN dart pub get + +COPY . . +RUN dart compile exe bin/server.dart -o build/server \ + && dart compile exe bin/migrate.dart -o build/migrate + +FROM debian:bookworm-slim AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +ENV PORT=8080 +ENV MIGRATE_ON_STARTUP=true + +COPY --from=build /app/build/server /app/bin/server +COPY --from=build /app/build/migrate /app/bin/migrate +COPY migrations/ /app/migrations/ +COPY scripts/docker-entrypoint.sh /app/scripts/docker-entrypoint.sh + +RUN chmod +x /app/bin/server /app/bin/migrate /app/scripts/docker-entrypoint.sh + +EXPOSE 8080 + +ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"] +CMD ["/app/bin/server"] diff --git a/server/README.md b/server/README.md index 42865e5..ef36ae9 100644 --- a/server/README.md +++ b/server/README.md @@ -125,13 +125,73 @@ without failing the whole request. Accepting a share creates a new `409` for revoked or already answered shares, while missing shares or recipients return `404`. +## Docker Deployment + +Ticket #52 adds Docker packaging for a headless Linux deployment. TLS is not +handled by the API container: terminate HTTPS in an external reverse proxy and +forward traffic to the Docker host on `API_PORT`. + +Create a local environment file: + +```bash +cp .env.example .env +``` + +Edit `.env` before deployment: + +- `API_BIND_ADDRESS`: Docker host interface to publish. Use `0.0.0.0` for LAN + access by the external reverse proxy, or a specific host IP to restrict the + bind address. +- `API_PORT`: host port exposed for the reverse proxy. +- `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`: PostgreSQL bootstrap + values. +- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, + `DATABASE_PASSWORD`: database connection used by the API. With the provided + Compose file, `DATABASE_HOST=postgres`. +- `MIGRATE_ON_STARTUP`: when `true`, the API container applies SQL migrations + before starting the server. + +Start the stack from `server/`: + +```bash +docker compose up -d --build +``` + +The `api` service waits for the PostgreSQL healthcheck, runs `/app/bin/migrate`, +then launches `/app/bin/server`. Migrations are copied into the image under +`/app/migrations`. + +To inspect the fully interpolated Compose configuration: + +```bash +docker compose config +``` + +## Gitea Registry + +Build and push the server image to a Gitea Container Registry with no registry +or credential value stored in the repository: + +```bash +GITEA_REGISTRY=gitea.example.com \ +GITEA_OWNER=my-org \ +GITEA_IMAGE_NAME=gametime-server \ +GITEA_IMAGE_TAG=latest \ +GITEA_USERNAME=my-user \ +GITEA_TOKEN='replace-with-token' \ +./scripts/push-gitea-image.sh +``` + +`GITEA_IMAGE_TAG` defaults to `latest` when omitted. The script uses +`docker login --password-stdin` so the token is not printed by the command line. + ## Scope Ticket #47 only scaffolds the Dart server, the hexagonal directory layout and the `/health` endpoint. Ticket #49 adds the first PostgreSQL schema. Ticket #48 adds authentication. Ticket #50 adds sync. Ticket #51 adds targeted sharing. +Ticket #52 adds Docker Compose packaging and Gitea registry publishing. Upcoming tickets will fill the empty adapters and use cases: -- #52: Docker and registry packaging. - #53: API, contract and integration tests. diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml new file mode 100644 index 0000000..1d8c4b6 --- /dev/null +++ b/server/docker-compose.yaml @@ -0,0 +1,42 @@ +services: + api: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + PORT: 8080 + MIGRATE_ON_STARTUP: ${MIGRATE_ON_STARTUP:?set MIGRATE_ON_STARTUP} + DATABASE_HOST: ${DATABASE_HOST:?set DATABASE_HOST} + DATABASE_PORT: ${DATABASE_PORT:?set DATABASE_PORT} + DATABASE_NAME: ${DATABASE_NAME:?set DATABASE_NAME} + DATABASE_USER: ${DATABASE_USER:?set DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD} + ports: + - "${API_BIND_ADDRESS:?set API_BIND_ADDRESS}:${API_PORT:?set API_PORT}:8080" + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\"" + ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +volumes: + postgres_data: diff --git a/server/scripts/README.md b/server/scripts/README.md index 53046b2..d805251 100644 --- a/server/scripts/README.md +++ b/server/scripts/README.md @@ -2,4 +2,7 @@ Operational scripts for the server package. -Docker image publishing is planned for ticket #52. +- `docker-entrypoint.sh`: container entrypoint; applies migrations when + `MIGRATE_ON_STARTUP=true`, then executes the server command. +- `push-gitea-image.sh`: builds and pushes the Docker image to a Gitea + Container Registry using environment variables only. diff --git a/server/scripts/docker-entrypoint.sh b/server/scripts/docker-entrypoint.sh new file mode 100755 index 0000000..3c060d3 --- /dev/null +++ b/server/scripts/docker-entrypoint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +set -eu + +if [ "${MIGRATE_ON_STARTUP:-true}" = "true" ]; then + /app/bin/migrate +fi + +exec "$@" diff --git a/server/scripts/push-gitea-image.sh b/server/scripts/push-gitea-image.sh new file mode 100755 index 0000000..f70a62f --- /dev/null +++ b/server/scripts/push-gitea-image.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +require_env() { + local name="$1" + if [[ -z "${!name:-}" ]]; then + echo "Missing required environment variable: ${name}" >&2 + exit 1 + fi +} + +require_env GITEA_REGISTRY +require_env GITEA_OWNER +require_env GITEA_IMAGE_NAME +require_env GITEA_USERNAME +require_env GITEA_TOKEN + +GITEA_IMAGE_TAG="${GITEA_IMAGE_TAG:-latest}" +IMAGE_REF="${GITEA_REGISTRY}/${GITEA_OWNER}/${GITEA_IMAGE_NAME}:${GITEA_IMAGE_TAG}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SERVER_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +docker build -t "${IMAGE_REF}" "${SERVER_DIR}" +printf '%s' "${GITEA_TOKEN}" | docker login "${GITEA_REGISTRY}" \ + --username "${GITEA_USERNAME}" \ + --password-stdin +docker push "${IMAGE_REF}" + +echo "Pushed ${IMAGE_REF}"