feat: add Docker deployment and application configuration
Add package declarations across the Go scaffold, YAML configuration loading and validation, and a minimal API entry point. Add development and production Compose files, a multi-stage Docker image, configuration template, and Make targets. Remove the directory scaffolding script.
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.idea
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
graphify-out
|
||||||
|
developer_doc
|
||||||
|
deploy
|
||||||
|
tmp
|
||||||
|
*.log
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*.yml
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
FROM golang:1.27-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||||
|
-trimpath \
|
||||||
|
-ldflags="-s -w" \
|
||||||
|
-o /out/robot-market-api \
|
||||||
|
./cmd/api
|
||||||
|
|
||||||
|
FROM alpine:3.22 AS production
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates \
|
||||||
|
&& addgroup -S app \
|
||||||
|
&& adduser -S -G app app
|
||||||
|
|
||||||
|
COPY --from=build /out/robot-market-api /usr/local/bin/robot-market-api
|
||||||
|
|
||||||
|
USER app
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/robot-market-api"]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.PHONY: dev dev-down docker-build
|
||||||
|
|
||||||
|
dev:
|
||||||
|
docker compose -f deploy/docker-compose.dev.yml up --build
|
||||||
|
|
||||||
|
dev-down:
|
||||||
|
docker compose -f deploy/docker-compose.dev.yml down
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
docker compose -f deploy/docker-compose.prod.yml build
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.my-rm.com/Robot-Market/shop-backend/internal/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if _, err := app.LoadConfig(""); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package main
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
environment: development
|
||||||
|
|
||||||
|
http:
|
||||||
|
address: 0.0.0.0:8080
|
||||||
|
|
||||||
|
# The database is managed outside Docker Compose.
|
||||||
|
database:
|
||||||
|
url: postgres://robot_market:change-me@host.docker.internal:5432/robot_market?sslmode=disable
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
echo "Creating Robot Market Phase 1 backend structure..."
|
|
||||||
|
|
||||||
# Directories
|
|
||||||
directories=(
|
|
||||||
# Applications
|
|
||||||
"cmd/api"
|
|
||||||
"cmd/cli"
|
|
||||||
|
|
||||||
# Application bootstrap
|
|
||||||
"internal/app"
|
|
||||||
|
|
||||||
# CLI commands
|
|
||||||
"internal/command"
|
|
||||||
|
|
||||||
# Domain features
|
|
||||||
"internal/user"
|
|
||||||
"internal/auth"
|
|
||||||
"internal/catalog"
|
|
||||||
|
|
||||||
# HTTP layer
|
|
||||||
"internal/http/handler"
|
|
||||||
"internal/http/route"
|
|
||||||
"internal/http/middleware"
|
|
||||||
|
|
||||||
# Database layer
|
|
||||||
"internal/postgres/query"
|
|
||||||
"internal/postgres/sqlc"
|
|
||||||
|
|
||||||
# Shared packages
|
|
||||||
"pkg/apperr"
|
|
||||||
"pkg/log"
|
|
||||||
|
|
||||||
# Integration tests
|
|
||||||
"integration/auth"
|
|
||||||
"integration/catalog"
|
|
||||||
"integration/testutil"
|
|
||||||
|
|
||||||
# Database migrations
|
|
||||||
"migrations"
|
|
||||||
|
|
||||||
# Scripts
|
|
||||||
"scripts"
|
|
||||||
)
|
|
||||||
|
|
||||||
for dir in "${directories[@]}"; do
|
|
||||||
mkdir -p "$dir"
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
# Placeholder files
|
|
||||||
files=(
|
|
||||||
# API
|
|
||||||
"cmd/api/main.go"
|
|
||||||
|
|
||||||
# CLI
|
|
||||||
"cmd/cli/main.go"
|
|
||||||
|
|
||||||
# App
|
|
||||||
"internal/app/app.go"
|
|
||||||
"internal/app/config.go"
|
|
||||||
|
|
||||||
# Commands
|
|
||||||
"internal/command/root.go"
|
|
||||||
"internal/command/admin.go"
|
|
||||||
"internal/command/seed.go"
|
|
||||||
|
|
||||||
# User
|
|
||||||
"internal/user/domain.go"
|
|
||||||
"internal/user/repository.go"
|
|
||||||
"internal/user/errors.go"
|
|
||||||
|
|
||||||
# Auth
|
|
||||||
"internal/auth/dto.go"
|
|
||||||
"internal/auth/service.go"
|
|
||||||
"internal/auth/errors.go"
|
|
||||||
|
|
||||||
# Catalog
|
|
||||||
"internal/catalog/domain.go"
|
|
||||||
"internal/catalog/dto.go"
|
|
||||||
"internal/catalog/service.go"
|
|
||||||
"internal/catalog/repository.go"
|
|
||||||
"internal/catalog/errors.go"
|
|
||||||
|
|
||||||
# HTTP
|
|
||||||
"internal/http/server.go"
|
|
||||||
"internal/http/config.go"
|
|
||||||
|
|
||||||
# PostgreSQL
|
|
||||||
"internal/postgres/db.go"
|
|
||||||
"internal/postgres/user.go"
|
|
||||||
"internal/postgres/auth.go"
|
|
||||||
"internal/postgres/catalog.go"
|
|
||||||
|
|
||||||
# sqlc query placeholders
|
|
||||||
"internal/postgres/query/user.sql"
|
|
||||||
"internal/postgres/query/auth.sql"
|
|
||||||
"internal/postgres/query/catalog.sql"
|
|
||||||
|
|
||||||
# Shared packages
|
|
||||||
"pkg/apperr/error.go"
|
|
||||||
"pkg/log/log.go"
|
|
||||||
|
|
||||||
# Integration tests
|
|
||||||
"integration/auth/service_test.go"
|
|
||||||
"integration/catalog/service_test.go"
|
|
||||||
|
|
||||||
"integration/testutil/postgres.go"
|
|
||||||
"integration/testutil/migrations.go"
|
|
||||||
"integration/testutil/fixtures.go"
|
|
||||||
|
|
||||||
# Root files
|
|
||||||
"sqlc.yaml"
|
|
||||||
"Makefile"
|
|
||||||
"go.mod"
|
|
||||||
)
|
|
||||||
|
|
||||||
for file in "${files[@]}"; do
|
|
||||||
touch "$file"
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
echo "Phase 1 structure created successfully."
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: robot-market-backend:dev
|
||||||
|
environment:
|
||||||
|
CONFIG_FILE: /etc/robot-market/config.yaml
|
||||||
|
volumes:
|
||||||
|
- ../configs/config.example.yaml:/etc/robot-market/config.yaml:ro
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-8080}:8080"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: ${ROBOT_MARKET_IMAGE:-robot-market-backend:latest}
|
||||||
|
environment:
|
||||||
|
CONFIG_FILE: /etc/robot-market/config.yaml
|
||||||
|
volumes:
|
||||||
|
- ${CONFIG_FILE:-../configs/config.example.yaml}:/etc/robot-market/config.yaml:ro
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-8080}:8080"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
restart: unless-stopped
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
module git.my-rm.com/Robot-Market/shop-backend
|
module git.my-rm.com/Robot-Market/shop-backend
|
||||||
|
|
||||||
go 1.27.1
|
go 1.27.1
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package auth_test
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package catalog_test
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package testutil
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package testutil
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package testutil
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package app
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultConfigPath = "configs/config.example.yaml"
|
||||||
|
|
||||||
|
// Config contains the settings required to start the application.
|
||||||
|
type Config struct {
|
||||||
|
Environment string `yaml:"environment"`
|
||||||
|
HTTP HTTPConfig `yaml:"http"`
|
||||||
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPConfig struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig reads configuration from YAML, applies environment overrides,
|
||||||
|
// and validates the result. An empty path uses CONFIG_FILE when set.
|
||||||
|
func LoadConfig(path string) (Config, error) {
|
||||||
|
if path == "" {
|
||||||
|
path = os.Getenv("CONFIG_FILE")
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
path = defaultConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("read config %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := yaml.Unmarshal(contents, &config); err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse config %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyEnvironment(&config)
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
return Config{}, fmt.Errorf("validate config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config Config) Validate() error {
|
||||||
|
var missing []string
|
||||||
|
if strings.TrimSpace(config.Environment) == "" {
|
||||||
|
missing = append(missing, "environment")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(config.HTTP.Address) == "" {
|
||||||
|
missing = append(missing, "http.address")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(config.Database.URL) == "" {
|
||||||
|
missing = append(missing, "database.url")
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return errors.New("missing required values: " + strings.Join(missing, ", "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyEnvironment(config *Config) {
|
||||||
|
if value := os.Getenv("APP_ENVIRONMENT"); value != "" {
|
||||||
|
config.Environment = value
|
||||||
|
}
|
||||||
|
if value := os.Getenv("APP_HTTP_ADDRESS"); value != "" {
|
||||||
|
config.HTTP.Address = value
|
||||||
|
}
|
||||||
|
if value := os.Getenv("APP_DATABASE_URL"); value != "" {
|
||||||
|
config.Database.URL = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package auth
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package auth
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package auth
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package catalog
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package catalog
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package catalog
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package catalog
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package catalog
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package command
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package command
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package command
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package http
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package http
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package postgres
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package postgres
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package postgres
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package postgres
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package user
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package user
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package user
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package apperr
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package log
|
||||||
|
|||||||
Reference in New Issue
Block a user