Add Docker setup: one script to set up/start/stop the full stack
A new developer needs only Docker Desktop — no local Go, Node, or PostgreSQL. - docker-compose.yml: PostgreSQL + Go API + Angular (nginx) with healthcheck, migrations auto-applied via docker-entrypoint-initdb.d, data in a volume - backend/Dockerfile: static Go binary on distroless (nonroot) - frontend/Dockerfile: Angular prod build served by nginx, which proxies /api to the api service (same origin, no CORS) - docker.ps1: setup / start / stop / restart / reset / rebuild / status / logs; reads DB creds from backend/.env (single source, nothing duplicated) - .dockerignore keeps .env and local artifacts out of image build contexts - README: "Run with Docker" quick start Not yet run end to end here (Docker is not installed on this machine); the compose and script are validated for syntax and structure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
17a7e8ffd5
commit
a9dc273e09
@@ -20,6 +20,33 @@ Authentication is HTTP **Basic auth**, as requested, scoped per tenant.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Run with Docker (quickest — no local toolchain)
|
||||||
|
|
||||||
|
A new developer needs **only Docker Desktop** — no local Go, Node, or
|
||||||
|
PostgreSQL. One script sets up, starts and stops the whole stack:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# one-time: set the DB password the containers will use
|
||||||
|
cp backend\.env.example backend\.env # then edit DB_PASSWORD
|
||||||
|
|
||||||
|
.\docker.ps1 setup # build images, start everything, seed the database
|
||||||
|
.\docker.ps1 stop # stop (keeps data)
|
||||||
|
.\docker.ps1 start # start again
|
||||||
|
.\docker.ps1 reset # wipe data and re-seed with fresh flight dates
|
||||||
|
.\docker.ps1 logs -Follow
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open **http://localhost:4200** (nginx serves the SPA and proxies `/api` to
|
||||||
|
the Go API on `:8080`). Sign in with any demo account, password `demo1234`.
|
||||||
|
|
||||||
|
The database is created and seeded on first `setup` via the migrations mounted
|
||||||
|
into PostgreSQL. Don't run the native `app.ps1` at the same time — it uses the
|
||||||
|
same ports.
|
||||||
|
|
||||||
|
For local development without containers, use the native setup below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
| Tool | Version used here |
|
| Tool | Version used here |
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Keep secrets and local artifacts out of the build context and image layers.
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
!.env.example
|
||||||
|
bin/
|
||||||
|
coverage
|
||||||
|
coverage.out
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# --- build -----------------------------------------------------------------
|
||||||
|
# Static Go binary so the runtime image can be distroless (no shell, tiny,
|
||||||
|
# minimal attack surface). pgx is pure Go, so CGO can be disabled.
|
||||||
|
FROM golang:1.26 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Cache modules separately from source for faster rebuilds.
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/api
|
||||||
|
|
||||||
|
# --- runtime ---------------------------------------------------------------
|
||||||
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
COPY --from=build /out/api /api
|
||||||
|
EXPOSE 8080
|
||||||
|
# Config comes from the environment (DB_HOST, DB_PASSWORD, PORT, …), set by
|
||||||
|
# docker-compose. No .env file is baked into the image.
|
||||||
|
ENTRYPOINT ["/api"]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# TravelHub full stack on Docker. Drive it with docker.ps1 (which supplies the
|
||||||
|
# DB_* values from backend/.env), or run docker compose directly after exporting
|
||||||
|
# DB_USER / DB_PASSWORD / DB_NAME yourself.
|
||||||
|
#
|
||||||
|
# PostgreSQL internal only (data in the pgdata volume)
|
||||||
|
# api Go API -> http://localhost:8080
|
||||||
|
# web Angular via nginx -> http://localhost:4200 (proxies /api to api)
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD (docker.ps1 reads it from backend/.env)}
|
||||||
|
POSTGRES_DB: ${DB_NAME:-travelhub}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
# Migrations run once, on first creation of an empty data directory, in
|
||||||
|
# order (001_init, 002_seed, 003_demo_bookings). The seed generates
|
||||||
|
# flights relative to now(), so a fresh volume always has future-dated
|
||||||
|
# inventory. `docker.ps1 reset` wipes the volume to re-seed.
|
||||||
|
- ./backend/migrations:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-travelhub}']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: ./backend
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: '5432'
|
||||||
|
DB_USER: ${DB_USER:-postgres}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
DB_NAME: ${DB_NAME:-travelhub}
|
||||||
|
DB_SSLMODE: disable
|
||||||
|
PORT: '8080'
|
||||||
|
CORS_ORIGINS: http://localhost:4200
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- '8080:8080'
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: ./frontend
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
ports:
|
||||||
|
- '4200:80'
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
#requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Set up, start and stop the entire TravelHub stack on Docker. A new developer
|
||||||
|
needs only Docker Desktop -- no local Go, Node, or PostgreSQL.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Wraps docker compose for the three services (PostgreSQL + Go API + Angular
|
||||||
|
via nginx). Database credentials are read from backend/.env, so there is one
|
||||||
|
source of truth and no secret is duplicated.
|
||||||
|
|
||||||
|
URLs once up:
|
||||||
|
App -> http://localhost:4200 (nginx serves the SPA and proxies /api)
|
||||||
|
API -> http://localhost:8080
|
||||||
|
|
||||||
|
Demo sign-in: any seeded account, password demo1234
|
||||||
|
(e.g. agent@skyline.test, agency "skyline").
|
||||||
|
|
||||||
|
.PARAMETER Action
|
||||||
|
setup Build images and bring everything up, waiting until healthy. The
|
||||||
|
database is created and seeded on this first run. One command to
|
||||||
|
onboard.
|
||||||
|
start Bring the stack up (images already built).
|
||||||
|
stop Stop and remove the containers; the database volume is kept.
|
||||||
|
restart stop then start.
|
||||||
|
reset Stop and DELETE the database volume, then start fresh (re-seeds).
|
||||||
|
rebuild Rebuild images (use after pulling code changes) and start.
|
||||||
|
status Show container state.
|
||||||
|
logs Tail logs (-Follow to stream; -Service to pick one).
|
||||||
|
|
||||||
|
.PARAMETER Service
|
||||||
|
logs: which service (postgres | api | web). Default: all.
|
||||||
|
|
||||||
|
.PARAMETER Follow
|
||||||
|
logs: stream instead of printing the tail once.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\docker.ps1 setup
|
||||||
|
.EXAMPLE
|
||||||
|
.\docker.ps1 logs -Service api -Follow
|
||||||
|
.EXAMPLE
|
||||||
|
.\docker.ps1 reset # wipe data and re-seed with fresh flight dates
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Position = 0)]
|
||||||
|
[ValidateSet('setup', 'start', 'stop', 'restart', 'reset', 'rebuild', 'status', 'logs', 'help')]
|
||||||
|
[string]$Action = 'help',
|
||||||
|
|
||||||
|
[ValidateSet('postgres', 'api', 'web')]
|
||||||
|
[string]$Service,
|
||||||
|
|
||||||
|
[switch]$Follow
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$Root = $PSScriptRoot
|
||||||
|
|
||||||
|
function Write-Step($m) { Write-Host "==> $m" -ForegroundColor Cyan }
|
||||||
|
function Write-Ok($m) { Write-Host " + $m" -ForegroundColor Green }
|
||||||
|
function Write-Warn2($m){ Write-Host " ! $m" -ForegroundColor Yellow }
|
||||||
|
|
||||||
|
# --- docker availability ---------------------------------------------------
|
||||||
|
|
||||||
|
function Require-Docker {
|
||||||
|
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Docker not found. Install Docker Desktop and ensure it is running: https://www.docker.com/products/docker-desktop/"
|
||||||
|
}
|
||||||
|
# `docker info` fails fast if the engine/daemon is not running.
|
||||||
|
docker info *> $null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw 'Docker is installed but the engine is not running. Start Docker Desktop and retry.'
|
||||||
|
}
|
||||||
|
# Compose v2 (the `docker compose` subcommand) is required.
|
||||||
|
docker compose version *> $null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "This script needs Docker Compose v2 (the 'docker compose' subcommand). Update Docker Desktop."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- credentials from backend/.env -----------------------------------------
|
||||||
|
|
||||||
|
# Load DB_USER / DB_PASSWORD / DB_NAME from backend/.env into this process so
|
||||||
|
# docker compose can substitute them. Nothing is printed or copied to disk.
|
||||||
|
function Import-DbEnv {
|
||||||
|
$envFile = Join-Path $Root 'backend\.env'
|
||||||
|
if (-not (Test-Path $envFile)) {
|
||||||
|
throw "backend\.env not found. Copy backend\.env.example to backend\.env and set DB_PASSWORD first."
|
||||||
|
}
|
||||||
|
foreach ($line in Get-Content $envFile) {
|
||||||
|
$t = $line.Trim()
|
||||||
|
if ($t -eq '' -or $t.StartsWith('#')) { continue }
|
||||||
|
$i = $t.IndexOf('=')
|
||||||
|
if ($i -lt 1) { continue }
|
||||||
|
$key = $t.Substring(0, $i).Trim()
|
||||||
|
$val = $t.Substring($i + 1).Trim().Trim('"').Trim("'")
|
||||||
|
if ($key -in @('DB_USER', 'DB_PASSWORD', 'DB_NAME')) {
|
||||||
|
Set-Item -Path "Env:$key" -Value $val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $env:DB_PASSWORD) {
|
||||||
|
throw 'DB_PASSWORD is not set in backend\.env.'
|
||||||
|
}
|
||||||
|
if (-not $env:DB_USER) { $env:DB_USER = 'postgres' }
|
||||||
|
if (-not $env:DB_NAME) { $env:DB_NAME = 'travelhub' }
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- compose helpers -------------------------------------------------------
|
||||||
|
|
||||||
|
function Compose {
|
||||||
|
param([Parameter(ValueFromRemainingArguments = $true)] $ComposeArgs)
|
||||||
|
Push-Location $Root
|
||||||
|
try { & docker compose @ComposeArgs } finally { Pop-Location }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wait-ForUrl($url, $timeoutSec) {
|
||||||
|
$deadline = (Get-Date).AddSeconds($timeoutSec)
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
try { Invoke-WebRequest $url -UseBasicParsing -TimeoutSec 3 | Out-Null; return $true }
|
||||||
|
catch { Start-Sleep -Seconds 2 }
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Show-Ready {
|
||||||
|
Write-Host ''
|
||||||
|
if (Wait-ForUrl 'http://localhost:8080/api/health' 90) { Write-Ok 'API healthy -> http://localhost:8080' }
|
||||||
|
else { Write-Warn2 'API not healthy yet -- check: .\docker.ps1 logs -Service api' }
|
||||||
|
if (Wait-ForUrl 'http://localhost:4200/' 60) { Write-Ok 'App ready -> http://localhost:4200' }
|
||||||
|
else { Write-Warn2 'Web not ready yet -- check: .\docker.ps1 logs -Service web' }
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Sign in with any demo account, password demo1234 (e.g. agent@skyline.test / skyline).'
|
||||||
|
Write-Host 'Note: stop any native .\app.ps1 first -- it uses the same ports.' -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- actions ---------------------------------------------------------------
|
||||||
|
|
||||||
|
switch ($Action) {
|
||||||
|
'setup' {
|
||||||
|
Require-Docker; Import-DbEnv
|
||||||
|
Write-Step 'Building images (first run downloads base images)'
|
||||||
|
Compose build
|
||||||
|
Write-Step 'Starting stack and seeding the database'
|
||||||
|
Compose up -d
|
||||||
|
Show-Ready
|
||||||
|
Write-Ok 'Setup complete.'
|
||||||
|
}
|
||||||
|
'start' {
|
||||||
|
Require-Docker; Import-DbEnv
|
||||||
|
Write-Step 'Starting stack'
|
||||||
|
Compose up -d
|
||||||
|
Show-Ready
|
||||||
|
}
|
||||||
|
'stop' {
|
||||||
|
Require-Docker; Import-DbEnv
|
||||||
|
Write-Step 'Stopping stack (database volume kept)'
|
||||||
|
Compose down
|
||||||
|
Write-Ok 'Stopped.'
|
||||||
|
}
|
||||||
|
'restart' {
|
||||||
|
Require-Docker; Import-DbEnv
|
||||||
|
Compose down
|
||||||
|
Compose up -d
|
||||||
|
Show-Ready
|
||||||
|
}
|
||||||
|
'reset' {
|
||||||
|
Require-Docker; Import-DbEnv
|
||||||
|
Write-Warn2 'This DELETES the database volume. All data will be lost.'
|
||||||
|
Write-Step 'Removing containers and the data volume'
|
||||||
|
Compose down -v
|
||||||
|
Write-Step 'Starting fresh (re-seeds with current flight dates)'
|
||||||
|
Compose up -d
|
||||||
|
Show-Ready
|
||||||
|
Write-Ok 'Reset complete.'
|
||||||
|
}
|
||||||
|
'rebuild' {
|
||||||
|
Require-Docker; Import-DbEnv
|
||||||
|
Write-Step 'Rebuilding images'
|
||||||
|
Compose build --no-cache
|
||||||
|
Compose up -d
|
||||||
|
Show-Ready
|
||||||
|
}
|
||||||
|
'status' {
|
||||||
|
Require-Docker; Import-DbEnv
|
||||||
|
Compose ps
|
||||||
|
}
|
||||||
|
'logs' {
|
||||||
|
Require-Docker; Import-DbEnv
|
||||||
|
$logArgs = @('logs')
|
||||||
|
if ($Follow) { $logArgs += '-f' } else { $logArgs += @('--tail', '80') }
|
||||||
|
if ($Service) { $logArgs += $Service }
|
||||||
|
Compose @logArgs
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Get-Help $PSCommandPath -Detailed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.angular/
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
coverage/
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# --- build -----------------------------------------------------------------
|
||||||
|
FROM node:24-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
# Angular's application builder emits the browser bundle to dist/frontend/browser.
|
||||||
|
|
||||||
|
# --- runtime ---------------------------------------------------------------
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
# Serve the SPA and proxy /api to the api service (same origin, so no CORS).
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist/frontend/browser /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# API calls go to the Go service over the compose network. Because the
|
||||||
|
# browser reaches both the app and /api on the same origin (localhost:4200),
|
||||||
|
# there is no CORS to configure.
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# Pass through the auth and tenant headers the SPA sends.
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Angular is a single-page app: unknown paths fall back to index.html so
|
||||||
|
# client-side routing works on reload / deep link.
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user