Fix: harden local dev startup flow

This commit is contained in:
MangoPig
2026-07-06 13:06:32 +01:00
parent 58bc285748
commit b232d6ef36
10 changed files with 149 additions and 26 deletions
+3
View File
@@ -6,6 +6,9 @@ WORKDIR /app
RUN apk add --no-cache ca-certificates curl git tzdata && update-ca-certificates
RUN mkdir -p /tmp/home /tmp/go/pkg/mod /tmp/go-build \
&& chmod 0777 /tmp/home /tmp/go /tmp/go/pkg /tmp/go/pkg/mod /tmp/go-build
COPY go.mod go.sum ./
RUN go mod download
+12 -3
View File
@@ -1,30 +1,39 @@
project_root := justfile_directory()
backend_dir := project_root + "/Backend"
common_sh := project_root + "/Commands/Local/scripts/common.sh"
posix_root := project_root + "/POSIX"
# Apply embedded database migrations.
migrate-up:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
cd '{{backend_dir}}' && go run ./cmd/migrate up
# Roll back the most recent embedded database migration.
# Roll back the most recent embedded database migration (confirmation required).
migrate-down:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"; confirm_destructive_action "This will roll back the most recent embedded database migration. Continue?"'
cd '{{backend_dir}}' && go run ./cmd/migrate down
# Reset all embedded database migrations.
# Reset all embedded database migrations (confirmation required).
migrate-reset:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"; confirm_destructive_action "This will reset all embedded database migrations. Continue?"'
cd '{{backend_dir}}' && go run ./cmd/migrate reset
# Reset embedded database migrations and apply them again from scratch.
# Reset embedded database migrations and apply them again from scratch (confirmation required).
migrate-rebuild:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"; confirm_destructive_action "This will reset all embedded database migrations and reapply them from scratch. Continue?"'
cd '{{backend_dir}}' && go run ./cmd/migrate reset && go run ./cmd/migrate up
# Show the embedded database migration status.
migrate-status:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
cd '{{backend_dir}}' && go run ./cmd/migrate status
# Rebuild the POSIX-to-DB projection from the current POSIX root.
posix-rebuild:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
cd '{{backend_dir}}' && go run ./cmd/posix rebuild
# Format backend Go source files.
fmt:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
cd '{{backend_dir}}' && gofmt -w ./cmd ./db ./internal
+5 -1
View File
@@ -2,13 +2,17 @@ project_root := justfile_directory()
local_compose := project_root + "/Docker/docker-compose.local.dev.yaml"
frontend_dir := project_root + "/Frontend"
node_modules_volume := "moku_work_frontend_node_modules"
common_sh := project_root + "/Commands/Local/scripts/common.sh"
posix_root := project_root + "/POSIX"
# Recreate the frontend node_modules Docker volume.
# Recreate the frontend node_modules Docker volume (confirmation required).
node_modules:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"; confirm_destructive_action "This will recreate the frontend node_modules Docker volume. Continue?"'
docker compose -f '{{local_compose}}' rm -sf frontend >/dev/null 2>&1 || true
docker volume rm -f '{{node_modules_volume}}' >/dev/null 2>&1 || true
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate frontend
# Run the frontend TypeScript check.
tsc:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
cd '{{frontend_dir}}' && pnpm typecheck
+2 -2
View File
@@ -18,7 +18,7 @@ start:
# Alias for the main full local development flow.
dev: up
# Stop and remove the local development stack.
# Stop and remove the local development stack (confirmation required).
down:
bash '{{stack_runner}}' down
@@ -34,6 +34,6 @@ logs:
restart:
bash '{{stack_runner}}' restart
# Stop the local development stack and remove local images, volumes, and backend dev state.
# Stop the local development stack and remove local images, volumes, backend dev state, and the local POSIX folder (confirmation required).
clean:
bash '{{stack_runner}}' clean
+19 -5
View File
@@ -7,6 +7,7 @@ action=${1:-up}
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
project_root=$(cd -- "$script_dir/../../../.." && pwd)
posix_root="$project_root/POSIX"
backend_dir="$project_root/Backend"
backend_bake="$backend_dir/docker-bake.hcl"
env_dir="$project_root/Env"
@@ -15,11 +16,14 @@ runtime_dir="$backend_dir/tmp/dev"
backend_image="moku/work-backend:dev"
backend_go_pkg_volume="moku_work_backend_go_pkg"
backend_go_build_volume="moku_work_backend_go_build"
local_uid=$(id -u)
local_gid=$(id -g)
services=(web api worker)
source "$script_dir/docker.sh"
source "$script_dir/env.sh"
source "$project_root/Commands/Local/scripts/common.sh"
build_backend() {
cd "$backend_dir"
@@ -27,20 +31,20 @@ build_backend() {
}
up_backend() {
docker compose -f "$compose_file" up -d --remove-orphans --force-recreate "${services[@]}"
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" up -d --remove-orphans --force-recreate "${services[@]}"
}
down_backend() {
docker compose -f "$compose_file" stop "${services[@]}" >/dev/null 2>&1 || true
docker compose -f "$compose_file" rm -f "${services[@]}" >/dev/null 2>&1 || true
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" stop "${services[@]}" >/dev/null 2>&1 || true
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" rm -f "${services[@]}" >/dev/null 2>&1 || true
}
restart_backend() {
docker compose -f "$compose_file" restart "${services[@]}"
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" restart "${services[@]}"
}
follow_logs() {
docker compose -f "$compose_file" logs -f "${services[@]}"
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" logs -f "${services[@]}"
}
clean_runtime() {
@@ -50,34 +54,44 @@ clean_runtime() {
case "$action" in
check)
ensure_docker 'docker is required for the local backend dev runtime. Install Docker first.'
ensure_posix_root "$project_root" "$posix_root"
;;
build)
ensure_docker 'docker is required for the local backend dev runtime. Install Docker first.'
ensure_posix_root "$project_root" "$posix_root"
build_backend
;;
up)
ensure_docker 'docker is required for the local backend dev runtime. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
up_backend
run_compose_api_migrations "$compose_file" "api"
;;
down)
ensure_docker 'docker is required for the local backend dev runtime. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
confirm_destructive_action 'This will stop and remove the local backend dev containers. Continue?'
down_backend
;;
restart)
ensure_docker 'docker is required for the local backend dev runtime. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
restart_backend
;;
logs)
ensure_docker 'docker is required for the local backend dev runtime. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
follow_logs
;;
clean)
ensure_docker 'docker is required for the local backend dev runtime. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
confirm_destructive_action 'This will remove the local backend dev containers, images, volumes, and runtime state. Continue?'
down_backend
remove_docker_image_if_present "$backend_image"
remove_docker_volume_if_present "$backend_go_pkg_volume"
+19 -5
View File
@@ -7,12 +7,15 @@ action=${1:-up}
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
project_root=$(cd -- "$script_dir/../../../.." && pwd)
posix_root="$project_root/POSIX"
frontend_dir="$project_root/Frontend"
frontend_bake="$frontend_dir/docker-bake.hcl"
backend_dir="$project_root/Backend"
backend_bake="$backend_dir/docker-bake.hcl"
env_dir="$project_root/Env"
compose_file="$project_root/Docker/docker-compose.local.dev.yaml"
local_uid=$(id -u)
local_gid=$(id -g)
frontend_image="moku/work-frontend:dev"
backend_image="moku/work-backend:dev"
frontend_volume="moku_work_frontend_node_modules"
@@ -22,6 +25,7 @@ backend_runtime_dir="$backend_dir/tmp/dev"
source "$script_dir/docker.sh"
source "$script_dir/env.sh"
source "$project_root/Commands/Local/scripts/common.sh"
build_frontend() {
cd "$frontend_dir"
@@ -39,19 +43,19 @@ build_images() {
}
up_stack() {
docker compose -f "$compose_file" up -d --remove-orphans --force-recreate
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" up -d --remove-orphans --force-recreate
}
down_stack() {
docker compose -f "$compose_file" down --remove-orphans --volumes
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" down --remove-orphans --volumes
}
follow_logs() {
docker compose -f "$compose_file" logs -f
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" logs -f
}
clean_stack() {
docker compose -f "$compose_file" down --remove-orphans --volumes >/dev/null 2>&1 || true
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" down --remove-orphans --volumes >/dev/null 2>&1 || true
remove_docker_image_if_present "$frontend_image"
remove_docker_image_if_present "$backend_image"
remove_docker_volume_if_present "$frontend_volume"
@@ -63,37 +67,47 @@ clean_stack() {
start_stack() {
build_images
up_stack
run_compose_api_migrations "$compose_file" "api"
}
case "$action" in
build)
ensure_docker 'docker is required for the local development stack. Install Docker first.'
ensure_posix_root "$project_root" "$posix_root"
build_images
;;
up|start|rebuild)
ensure_docker 'docker is required for the local development stack. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
start_stack
;;
down)
ensure_docker 'docker is required for the local development stack. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
confirm_destructive_action 'This will stop the local development stack and remove its containers and volumes. Continue?'
down_stack
;;
restart)
ensure_docker 'docker is required for the local development stack. Install Docker first.'
ensure_local_env_file "$env_dir"
docker compose -f "$compose_file" restart
ensure_posix_root "$project_root" "$posix_root"
LOCAL_UID="$local_uid" LOCAL_GID="$local_gid" docker compose -f "$compose_file" restart
;;
logs)
ensure_docker 'docker is required for the local development stack. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
follow_logs
;;
clean)
ensure_docker 'docker is required for the local development stack. Install Docker first.'
ensure_local_env_file "$env_dir"
ensure_posix_root "$project_root" "$posix_root"
confirm_destructive_action 'This will remove the local development stack, local images, volumes, backend dev state, and the local POSIX folder. Continue?'
clean_stack
rm -rf "$posix_root"
;;
*)
printf 'Unsupported dev stack action: %s\n' "$action" >&2
+17 -8
View File
@@ -5,40 +5,49 @@ local_compose := project_root + "/Docker/docker-compose.local.prod.yaml"
proxy_image := "moku/work-proxy:local-prod"
backend_api_image := "moku/work-backend:local-prod-api"
backend_worker_image := "moku/work-backend:local-prod-worker"
common_sh := project_root + "/Commands/Local/scripts/common.sh"
posix_root := project_root + "/POSIX"
# Build the local production proxy image locally.
build:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
cd '{{project_root}}' && docker buildx bake -f '{{proxy_bake}}' prod
cd '{{project_root}}' && docker buildx bake -f '{{backend_bake}}' prod-api prod-worker
# Start the local production stack in the background using the current image.
up:
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
LOCAL_UID="$$(id -u)" LOCAL_GID="$$(id -g)" docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
# Build first, then start the local production stack in the background.
start: build up
# Rebuild the local production proxy image locally.
rebuild:
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
cd '{{project_root}}' && docker buildx bake -f '{{proxy_bake}}' --set '*.no-cache=true' prod
cd '{{project_root}}' && docker buildx bake -f '{{backend_bake}}' --set '*.no-cache=true' prod-api prod-worker
docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
LOCAL_UID="$$(id -u)" LOCAL_GID="$$(id -g)" docker compose -f '{{local_compose}}' up -d --remove-orphans --force-recreate
# Stop and remove the local production stack.
# Stop and remove the local production stack (confirmation required).
down:
docker compose -f '{{local_compose}}' down --remove-orphans --volumes
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"; confirm_destructive_action "This will stop the local production stack and remove its containers and volumes. Continue?"'
LOCAL_UID="$$(id -u)" LOCAL_GID="$$(id -g)" docker compose -f '{{local_compose}}' down --remove-orphans --volumes
# Follow logs for the local production stack.
logs:
docker compose -f '{{local_compose}}' logs -f
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
LOCAL_UID="$$(id -u)" LOCAL_GID="$$(id -g)" docker compose -f '{{local_compose}}' logs -f
# Restart the local production stack.
restart:
docker compose -f '{{local_compose}}' restart
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"'
LOCAL_UID="$$(id -u)" LOCAL_GID="$$(id -g)" docker compose -f '{{local_compose}}' restart
# Stop the local production stack and remove local images.
# Stop the local production stack and remove local images (confirmation required).
clean:
docker compose -f '{{local_compose}}' down --remove-orphans --volumes
bash -c 'source "{{common_sh}}"; ensure_posix_root "{{project_root}}" "{{posix_root}}"; confirm_destructive_action "This will remove the local production stack, its volumes, and the local production images. Continue?"'
LOCAL_UID="$$(id -u)" LOCAL_GID="$$(id -g)" docker compose -f '{{local_compose}}' down --remove-orphans --volumes
docker image rm -f '{{proxy_image}}' >/dev/null 2>&1 || true
docker image rm -f '{{backend_api_image}}' >/dev/null 2>&1 || true
docker image rm -f '{{backend_worker_image}}' >/dev/null 2>&1 || true
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
ensure_posix_root() {
local project_root=${1:?project root is required}
local posix_root=${2:-"$project_root/POSIX"}
mkdir -p "$posix_root"
}
confirm_destructive_action() {
local prompt=${1:-Are you sure you want to continue?}
local response_input=""
if [[ "${JUST_YES:-0}" == "1" ]]; then
return 0
fi
if [[ -t 0 ]]; then
printf '%s [y/N] ' "$prompt"
IFS= read -r response_input
elif [[ -r /dev/tty ]]; then
printf '%s [y/N] ' "$prompt" > /dev/tty
IFS= read -r response_input < /dev/tty
else
printf 'Refusing destructive action without interactive confirmation. Re-run with JUST_YES=1 to continue.\n' >&2
exit 1
fi
case "$response_input" in
y|Y|yes|YES|Yes)
return 0
;;
*)
printf 'Aborted.\n' >&2
exit 1
;;
esac
}
run_compose_api_migrations() {
local compose_file=${1:?compose file is required}
local service_name=${2:-api}
local max_attempts=${3:-20}
local sleep_seconds=${4:-2}
local attempt=1
local output=""
while (( attempt <= max_attempts )); do
if output=$(docker compose -f "$compose_file" exec -T "$service_name" sh -lc 'go run ./cmd/migrate up' 2>&1); then
printf '%s\n' "$output"
return 0
fi
if (( attempt == max_attempts )); then
printf 'Failed to apply database migrations automatically after %d attempts.\n' "$max_attempts" >&2
printf '%s\n' "$output" >&2
return 1
fi
sleep "$sleep_seconds"
attempt=$((attempt + 1))
done
}
+6 -2
View File
@@ -1,12 +1,16 @@
x-backend-service: &backend-service
image: moku/work-backend:dev
restart: unless-stopped
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
env_file:
- ../Env/.env.local
environment:
DATABASE_URL: postgres://moku:moku_dev_password@postgres:5432/moku?sslmode=disable
VALKEY_URL: redis://valkey:6379/0
POSIX_ROOT: /posix
HOME: /tmp/home
GOMODCACHE: /tmp/go/pkg/mod
GOCACHE: /tmp/go-build
depends_on:
postgres:
condition: service_healthy
@@ -15,8 +19,8 @@ x-backend-service: &backend-service
volumes:
- ../Backend:/app
- ../POSIX:/posix
- moku_work_backend_go_pkg:/go/pkg/mod
- moku_work_backend_go_build:/root/.cache/go-build
- moku_work_backend_go_pkg:/tmp/go/pkg/mod
- moku_work_backend_go_build:/tmp/go-build
services:
postgres:
+1
View File
@@ -1,5 +1,6 @@
x-backend-service: &backend-service
restart: unless-stopped
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
env_file:
- ../Env/.env.local
environment: