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:
2026-09-17 11:38:47 +03:30
parent 56ecf089c9
commit 0557635008
40 changed files with 217 additions and 126 deletions
+12
View File
@@ -0,0 +1,12 @@
.git
.gitignore
.idea
.env
.env.*
graphify-out
developer_doc
deploy
tmp
*.log
Dockerfile*
docker-compose*.yml
+26
View File
@@ -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"]
+10
View File
@@ -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
+13
View File
@@ -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)
}
}
+1
View File
@@ -0,0 +1 @@
package main
+8
View File
@@ -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
-126
View File
@@ -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."
+14
View File
@@ -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"
+15
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
module git.my-rm.com/Robot-Market/shop-backend
go 1.27.1
require gopkg.in/yaml.v3 v3.0.1
+4
View File
@@ -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=
+1
View File
@@ -0,0 +1 @@
package auth_test
+1
View File
@@ -0,0 +1 @@
package catalog_test
+1
View File
@@ -0,0 +1 @@
package testutil
+1
View File
@@ -0,0 +1 @@
package testutil
+1
View File
@@ -0,0 +1 @@
package testutil
+1
View File
@@ -0,0 +1 @@
package app
+84
View File
@@ -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
}
}
+1
View File
@@ -0,0 +1 @@
package auth
+1
View File
@@ -0,0 +1 @@
package auth
+1
View File
@@ -0,0 +1 @@
package auth
+1
View File
@@ -0,0 +1 @@
package catalog
+1
View File
@@ -0,0 +1 @@
package catalog
+1
View File
@@ -0,0 +1 @@
package catalog
+1
View File
@@ -0,0 +1 @@
package catalog
+1
View File
@@ -0,0 +1 @@
package catalog
+1
View File
@@ -0,0 +1 @@
package command
+1
View File
@@ -0,0 +1 @@
package command
+1
View File
@@ -0,0 +1 @@
package command
+1
View File
@@ -0,0 +1 @@
package http
+1
View File
@@ -0,0 +1 @@
package http
+1
View File
@@ -0,0 +1 @@
package postgres
+1
View File
@@ -0,0 +1 @@
package postgres
+1
View File
@@ -0,0 +1 @@
package postgres
+1
View File
@@ -0,0 +1 @@
package postgres
+1
View File
@@ -0,0 +1 @@
package user
+1
View File
@@ -0,0 +1 @@
package user
+1
View File
@@ -0,0 +1 @@
package user
+1
View File
@@ -0,0 +1 @@
package apperr
+1
View File
@@ -0,0 +1 @@
package log