diff --git a/.dockerignore.yaml b/.dockerignore.yaml new file mode 100644 index 0000000..1c47491 --- /dev/null +++ b/.dockerignore.yaml @@ -0,0 +1,9 @@ +vendor/ + +# Ignore git directory +.git/ + +.gitea/ + +# Ignore environment files (configure via docker-compose instead) +.env* diff --git a/.env.docker.sample b/.env.docker.sample new file mode 100644 index 0000000..32463a1 --- /dev/null +++ b/.env.docker.sample @@ -0,0 +1,7 @@ +JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW! +DB_NAME=textsender_db +DB_USER=textsender +DB_PASSWORD=password +DB_HOST=main_db +DB_PORT=5432 +DB_SSLMODE=disable diff --git a/.gitignore b/.gitignore index 0fd9e89..80799e6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .env .env.local +.env.docker diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fb66b34 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# Multi-stage Dockerfile for Go application +FROM golang:1.25.3 AS builder + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + openssh-client git + +RUN mkdir -p -m 0700 ~/.ssh && \ + ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts + +# Configure Git to use SSH for GitHub +RUN git config --global url."ssh://git@git.kundeng.us".insteadOf "https://git.kundeng.us" + +# Set up the Go environment for private modules +ENV GOPRIVATE=git.kundeng.us + + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +RUN --mount=type=ssh mkdir src && \ + go mod download + +# Copy source code +COPY ./cmd ./cmd +COPY ./internal ./internal +COPY ./Makefile . +COPY ./.env . +COPY ./migrations ./migrations + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux make build + +# Runtime stage +FROM alpine:latest AS production + +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Copy the pre-built binary file from the previous stage +COPY --from=builder /app/textsender-api . +COPY --from=builder /app/.env ./ +COPY --from=builder /app/migrations ./migrations + +# Expose port +EXPOSE 8080 + +# Command to run the executable +CMD ["./textsender-api"] diff --git a/cmd/api/main.go b/cmd/api/main.go index 68a8372..d70d44c 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -14,7 +14,7 @@ import ( "github.com/go-chi/chi/v5/middleware" "git.kundeng.us/phoenix/textsender-api/internal/config" - "git.kundeng.us/phoenix/textsender-api/internal/db" + database "git.kundeng.us/phoenix/textsender-api/internal/db" "git.kundeng.us/phoenix/textsender-api/internal/handler" "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" mdlware "git.kundeng.us/phoenix/textsender-api/internal/middleware" @@ -32,26 +32,36 @@ func main() { os.Exit(-1) } - database, err := db.NewDatabase(cfg.GetDBConnString()) + db, err := database.NewDatabase(cfg.GetDBConnString()) if err != nil { log.Fatalf("Failed to connect to database: %v", err) } - defer database.Close() + defer db.Close() ctx := context.Background() if cfg.ResetDB { log.Println("Resetting database") - if err := database.ResetDatabase(ctx); err != nil { + if err := db.ResetDatabase(ctx); err != nil { log.Fatalf("Failed to reset database: %v", err) } log.Println("Database reset completed. Exiting.") return + } else { + if exists, err := database.TableExists(ctx, db.Pool, "contacts "); err == nil && !exists { + fmt.Println("Resetting database") + err = db.ResetDatabase(ctx) + if err != nil { + fmt.Printf("Error:%v", err) + } + } else { + fmt.Printf("Error:%v", err) + } } jwtService := services.NewJWTService(cfg.JWTSecret) - contactStore := store.NewContactStore(database.Pool) + contactStore := store.NewContactStore(db.Pool) contactHandler := handler.NewContactHandler(contactStore) router := chi.NewRouter() diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..f53b276 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,97 @@ +version: '3.8' # Use a recent version + +services: + api: + build: # Tells docker-compose to build the Dockerfile in the current directory + context: . + ssh: ["default"] # Uses host's SSH agent + container_name: textsender # Optional: Give the container a specific name + ports: + # Map host port 8000 to container port 3000 (adjust as needed) + - "8080:8080" + env_file: + - .env + depends_on: + main_db: + condition: service_healthy # Wait for the DB to be healthy before starting the app + networks: + - textsender-network + restart: unless-stopped # Optional: Restart policy + + auth_api: + build: # Tells docker-compose to build the Dockerfile in the current directory + context: ../textsender-auth + ssh: ["default"] # Uses host's SSH agent + dockerfile: Dockerfile + container_name: textsender_auth # Optional: Give the container a specific name + ports: + # Map host port 8000 to container port 3000 (adjust as needed) + - "9080:9080" + env_file: + - ../textsender-auth/.env + depends_on: + auth_db: + condition: service_healthy # Wait for the DB to be healthy before starting the app + networks: + - textsender-network + restart: unless-stopped # Optional: Restart policy + + # PostgreSQL Database Service + main_db: + image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller) + container_name: textsender_db # Optional: Give the container a specific name + environment: + # These MUST match the user, password, and database name in the DATABASE_URL above + POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender} + POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password} + POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_db} + volumes: + # Persist database data using a named volume + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + # Checks if Postgres is ready to accept connections + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - textsender-network + restart: always # Optional: Restart policy + + auth_db: + image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller) + container_name: textsender_auth_db # Optional: Give the container a specific name + environment: + # These MUST match the user, password, and database name in the DATABASE_URL above + POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender_auth} + POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password} + POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_auth_db} + volumes: + # Persist database data using a named volume + - postgres_data_auth:/var/lib/postgresql/data + ports: + - "5433:5432" + healthcheck: + # Checks if Postgres is ready to accept connections + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - textsender-network + restart: always # Optional: Restart policy + +# Define the named volume for data persistence +volumes: + postgres_data: + driver: local # Use the default local driver + postgres_data_auth: + driver: local # Use the default local driver + +networks: + textsender-network: + driver: bridge diff --git a/internal/db/connection.go b/internal/db/connection.go index beccf11..feab2a3 100644 --- a/internal/db/connection.go +++ b/internal/db/connection.go @@ -54,6 +54,25 @@ func NewDatabase(connString string) (*Database, error) { return &Database{Pool: pool}, nil } +func TableExists(ctx context.Context, conn *pgxpool.Pool, tableName string) (bool, error) { + var exists bool + + query := ` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = $1 + ); + ` + + err := conn.QueryRow(ctx, query, tableName).Scan(&exists) + if err != nil { + return false, fmt.Errorf("error checking if table exists: %w", err) + } + + return exists, nil +} + func (db *Database) Close() { if db.Pool != nil { db.Pool.Close() @@ -115,4 +134,3 @@ func (db *Database) ResetDatabase(ctx context.Context) error { return nil } -