Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
VITE_API_URL=http://localhost:8080

CLIENT_PORT=8000
SERVER_PORT=8080

POSTGRES_USER=capuchin_user
POSTGRES_PASSWORD=capuchin
POSTGRES_DB=capuchin_dev
POSTGRES_HOST=capuchin-db
POSTGRES_PORT=5432
# Only needed when running the backend outside Docker (e.g. `go run` or `air` directly).
# In compose, the host is hardcoded to the postgres container name (capuchin-db).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not hardcoded, compose is using the network name which is capuchin-db else you won't be able to connect

# POSTGRES_HOST=localhost

JWT_SECRET=your_jwt_secret_here
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dist-ssr/
# Test binary, built with `go test -c`
*.test
server
test.sh

# Go workspace file
go.work
Expand Down Expand Up @@ -97,3 +98,8 @@ crash.*.log
# personal
docs/ideas.md
backup/
.kiro


# removing for now
.github/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.github contains workflow folder, which are our github action files. Why are we ignoring them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's intentional, I don't want ci pipeline to be triggered yet, once migration implementation is merged then this can be removed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't create the PR then, or use a feature called 'Draft PR'

48 changes: 35 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,56 @@ ifneq (, $(shell command -v docker 2> /dev/null))
CONTAINER_RUNTIME := docker
endif

# Docker Dev Mode (Hot Reload)
dev:
.DEFAULT_GOAL := help

help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'

# ── Dev (hot reload via Docker) ───────────────────────────────────────────────

dev: ## Start all services in dev mode (hot reload)
$(CONTAINER_RUNTIME) compose --env-file .env.example -f compose-dev.yml up --build -d

dev-logs:
$(CONTAINER_RUNTIME) compose -f compose-dev.yml logs
dev-logs: ## Tail dev logs
$(CONTAINER_RUNTIME) compose -f compose-dev.yml logs -f

dev-down:
dev-down: ## Stop dev services
$(CONTAINER_RUNTIME) compose -f compose-dev.yml down
clean:

clean: ## Stop dev services and remove volumes, images, orphans
$(CONTAINER_RUNTIME) compose -f compose-dev.yml down --volumes --remove-orphans --rmi all

# ── Prod ──────────────────────────────────────────────────────────────────────

prod:
$(CONTAINER_RUNTIME) compose --env-file .env -f compose.yml up
prod: ## Start all services in prod mode (detached)
$(CONTAINER_RUNTIME) compose --env-file .env -f compose.yml up -d

logs:
logs: ## Tail prod logs
$(CONTAINER_RUNTIME) compose -f compose.yml logs -f

down:
down: ## Stop prod services
$(CONTAINER_RUNTIME) compose -f compose.yml down

# ── Local dev (outside Docker) ────────────────────────────────────────────────

frontend:
frontend: ## Start frontend dev server
cd frontend && npm run dev

backend:
backend: ## Start backend with hot reload (requires air: go install github.com/air-verse/air@v1.61.7)
cd backend && air

.PHONY: dev dev-logs dev-down prod logs down
# ── Database ──────────────────────────────────────────────────────────────────

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this


migrate: ## Run migrations against localhost DB (reads .env for credentials)
@set -a && . ./.env.example && set +a && export POSTGRES_HOST=localhost && cd backend/migration && go run ./cmd/migrate up

migrate-down: ## Roll back the last migration against localhost DB
@set -a && . ./.env.example && set +a && export POSTGRES_HOST=localhost && cd backend/migration && go run ./cmd/migrate down

seed: ## Seed dev database with sample data (reads .env.example for credentials)
@set -a && . ./.env.example && set +a && export POSTGRES_HOST=localhost && cd backend && go run ./cmd/seed

migrate-build: ## Build migration Docker image
docker build -f backend/migration/Dockerfile -t capuchin-migration ./backend

.PHONY: help dev dev-logs dev-down clean prod logs down frontend backend migrate migrate-down seed migrate-build
1 change: 1 addition & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
vendor
bin
server
tmp
*.exe
*.exe~
*.dll
Expand Down
22 changes: 9 additions & 13 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,41 +1,37 @@
FROM golang:1.25.5-alpine AS deps
FROM golang:1.26-alpine AS deps

WORKDIR /app

# Download dependencies
COPY go.mod go.sum ./
RUN go mod download

# Build Stage
FROM deps AS builder

# Copy source code
COPY . .

# Build the application
# CGO_ENABLED=0 ensures a statically linked binary
RUN CGO_ENABLED=0 GOOS=linux go build -o server cmd/server/main.go

# Development Stage
# Development Stage - pinned air version for reproducible dev builds
FROM deps AS dev

RUN go install github.com/air-verse/air@latest
RUN go install github.com/air-verse/air@v1.61.7

CMD ["air", "-c", "air.toml"]


# Final Stage
# Final Stage - minimal image, non-root user for security
FROM scratch

# Set working directory to the app root
WORKDIR /app

# Copy the binary from the builder stage
# Copy passwd so the non-root user exists in scratch
COPY --from=builder /etc/passwd /etc/passwd

COPY --from=builder /app/server ./

# Expose the application port
EXPOSE 8080

# Run the application
CMD ["./server"]
USER nobody

CMD ["./server"]
4 changes: 2 additions & 2 deletions backend/air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ tmp_dir = "tmp"

[build]
cmd = "go build -o ./tmp/main ./cmd/server/main.go"
bin = "./tmp/main"
full_bin = ""
bin = ""
entrypoint = "./tmp/main"
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["assets", "tmp", "vendor"]
include_dir = []
Expand Down
55 changes: 26 additions & 29 deletions compose-dev.yml
Original file line number Diff line number Diff line change
@@ -1,45 +1,40 @@
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
target: dev
container_name: capuchin-server
depends_on:
capuchin-db:
condition: service_healthy
environment:
MODE: dev
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_HOST: ${POSTGRES_HOST}
ports:
- "${SERVER_PORT:-8080}:8080"
restart: unless-stopped
volumes:
- ./backend:/app

capuchin-db:
container_name: capuchin-db
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_HOST: ${POSTGRES_HOST}

ports:
- "5432:5432"

healthcheck:
interval: 5s
retries: 5
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
timeout: 5s
image: postgres:17-alpine
volumes:
- ./backup:/var/lib/postgresql
- ./backend/db/init.sql:/docker-entrypoint-initdb.d/init.sql
# Separate dev volume - keeps dev data isolated from prod backup/data
- capuchin-dev-data:/var/lib/postgresql/data
- ./backend/db/init.sql:/docker-entrypoint-initdb.d/init.sql

backend:
build:
context: ./backend
dockerfile: Dockerfile
target: dev
container_name: capuchin-server
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_HOST: capuchin-db
JWT_SECRET: ${JWT_SECRET}
ports:
- "${SERVER_PORT:-8080}:8080"
restart: unless-stopped
volumes:
- ./backend:/app

frontend:
build:
Expand All @@ -53,6 +48,8 @@ services:
- "${CLIENT_PORT:-5173}:5173"
restart: unless-stopped
volumes:
- ./frontend:/app
- /app/node_modules
- ./frontend:/app
- /app/node_modules

volumes:
capuchin-dev-data:
21 changes: 5 additions & 16 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,39 @@ services:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_HOST: ${POSTGRES_HOST}
# Healthcheck is informational - backend manages its own DB connection retry.
healthcheck:
interval: 5s
retries: 5
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
timeout: 5s
image: postgres:17-alpine
volumes:
- ./backup:/var/lib/postgresql
- ./backend/db/init.sql:/docker-entrypoint-initdb.d/init.sql

- ./backup/data:/var/lib/postgresql/data

backend:
container_name: capuchin-server

build:
context: ./backend
dockerfile: Dockerfile
ports:
- "${SERVER_PORT:-8080}:8080"
volumes:
- ./backend/db:/app/db
restart: unless-stopped

environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_HOST: ${POSTGRES_HOST}

depends_on:
capuchin-db:
condition: service_healthy
POSTGRES_HOST: capuchin-db
JWT_SECRET: ${JWT_SECRET}

frontend:
container_name: capuchin-client

build:
context: ./frontend
dockerfile: Dockerfile
target: prod
args:
VITE_API_URL: ${VITE_API_URL}

ports:
- "${CLIENT_PORT:-8000}:80"
restart: unless-stopped
Loading