Merge pull request #1 from kjannette/setup-and-config

Cleanup file structure, dockerize app
This commit is contained in:
S Jannette
2026-05-19 03:42:54 -04:00
committed by GitHub
18 changed files with 262 additions and 98 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
.git
.gitignore
.env
bin/
*.md
.idea
Dockerfile
docker-compose.yml
.dockerignore

View File

@@ -0,0 +1,6 @@
LOG_LEVEL=info
API_PORT=3000
API_READ_TIMEOUT=7
API_WRITE_TIMEOUT=5
API_IDLE_TIMEOUT=5
API_TIMEOUT=5

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1
FROM golang:1.25-alpine AS builder
WORKDIR /src
RUN apk add --no-cache ca-certificates git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG APP_VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w -X main.appName=go-http-server -X main.appVersion=${APP_VERSION}" \
-o /out/server \
./cmd/app
FROM alpine:3.20
RUN apk add --no-cache ca-certificates wget
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /out/server /app/server
USER app
EXPOSE 3000
ENTRYPOINT ["/app/server"]

View File

@@ -3,26 +3,41 @@
help: ## This help dialog.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2 | "sort -u"}' $(MAKEFILE_LIST)
run: ## Start the application
check-docker: ## Verify Docker CLI is available
@command -v docker >/dev/null 2>&1 || { \
echo "Error: docker not found."; \
echo "Install Docker Desktop: https://docs.docker.com/desktop/setup/install/mac-install/"; \
echo "Or run without Docker: make setup-local && make run-local"; \
exit 1; \
}
run: check-docker ## Start the application with Docker Compose
docker compose up --build --remove-orphans
down: ## Stop the application
run-local: ## Start the application locally (without Docker)
go run ./cmd/app
down: check-docker ## Stop the application
docker compose down --remove-orphans
setup-local: copy-config install-dependencies run ## Sets up your local environment
setup-local: copy-config install-dependencies ## Install config and Go dependencies
setup-docker: setup-local run ## Install dependencies and start with Docker Compose
install-lint: ## Install Go lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
install-dependencies: ## Install go project dependencies
install-dependencies: ## Install Go project dependencies
go mod download
go mod vendor
copy-config: ## Copy config file
cp .env.dist .env
copy-config: ## Copy config file if missing
@test -f .env || cp .env.dist .env
lint: install-lint ## Run lint
golangci-lint run
tests: down ## Run tests
tests: ## Run tests
go test -v ./...
build: ## Build the server binary
go build -o bin/server ./cmd/app

View File

@@ -1,28 +1,80 @@
# Go skeleton http server
# Go HTTP server
### A simple Go http server boilerplate project equipped with the essentials.
A simple Go HTTP server boilerplate with Echo, structured logging, graceful shutdown, and Docker support.
## How to run the application
1 - Copy the .env.dist to .env (you can edit the values if you want)
## Prerequisites
`$ cp .env.dist .env`
- Go 1.25+
- Docker and Docker Compose (for containerized runs)
2 - Run make command, it will show the list of commands we have to make easier to start the application.
## Configuration
`$ make`
Copy the example environment file and adjust values if needed:
3 - Start the application
```bash
cp .env.dist .env
```
`$ make setup-local`
The application reads configuration from process environment variables (via [envconfig](https://github.com/kelseyhightower/envconfig)). Docker Compose loads `.env` automatically. For a local `go run`, export variables or source the file:
4 - Run the tests
```bash
set -a && source .env && set +a
```
`$ make tests`
| Variable | Default | Description |
|----------|---------|-------------|
| `LOG_LEVEL` | `info` | Zap log level (`debug`, `info`, `warn`, `error`) |
| `API_PORT` | `3000` | HTTP listen port |
| `API_READ_TIMEOUT` | `7` | Server read timeout (seconds) |
| `API_WRITE_TIMEOUT` | `5` | Server write timeout (seconds) |
| `API_IDLE_TIMEOUT` | `5` | Server idle timeout (seconds) |
| `API_TIMEOUT` | `5` | Reserved for future use |
5 - Run the lint
Inside Docker, the process always listens on port `3000`; `API_PORT` in `.env` controls the host port mapping.
`$ make lint`
## Run with Docker (recommended)
6 - Run all the validations (lint + tests)
```bash
make setup-docker # copy .env, download modules, build and start containers
```
`$ make validation`
Or step by step:
```bash
make setup-local # copy .env (if missing) and go mod download
make run # docker compose up --build
```
Verify:
```bash
curl http://localhost:3000/health
```
Stop:
```bash
make down
```
## Run locally (without Docker)
```bash
make setup-local
make run-local
```
## Build binary
```bash
make build
./bin/server
```
## Make targets
```bash
make help
```
Common targets: `run`, `run-local`, `down`, `setup-local`, `setup-docker`, `build`, `tests`, `lint`.

BIN
bin/server Executable file

Binary file not shown.

View File

@@ -6,7 +6,7 @@ import (
"net/http"
"time"
"github.com/kjannette/go-http-server/internal/config"
"github.com/kjannette/go_http_server/internal/config"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"go.uber.org/zap"

View File

@@ -3,16 +3,17 @@ package main
import (
"context"
"github.com/felipecoppola/go-boilerplate/internal/config"
"github.com/felipecoppola/go-boilerplate/internal/infrastructure/os"
"github.com/kjannette/go_http_server/internal/config"
infrasignal "github.com/kjannette/go_http_server/internal/infrastructure/os"
_ "github.com/kjannette/go_http_server/pkg/logger"
"go.uber.org/zap"
)
// Globals that are set at build time.
var (
appName = ""
appVersion = ""
appName = "go-http-server"
appVersion = "dev"
)
func main() {
@@ -28,15 +29,15 @@ func main() {
cfg, err := config.NewConfig()
if err != nil {
logger.Error("error creating config: %w", zap.Error(err))
logger.Error("error creating config", zap.Error(err))
return
}
logger.Info("starting app")
os.SignalListener(logger, cancel)
infrasignal.SignalListener(logger, cancel)
if err = run(ctx, cfg); err != nil {
logger.Error("command failed: %w", zap.Error(err))
logger.Error("command failed", zap.Error(err))
return
}

View File

@@ -3,9 +3,8 @@ package main
import (
"context"
"github.com/kjannette/go-http-server/cmd/app/handler/httpjson"
"github.com/kjannnette/go-http-server/internal/config"
"github.com/kjannette/go-http-server/pkg/logger"
"github.com/kjannette/go_http_server/cmd/app/handler/httpjson"
"github.com/kjannette/go_http_server/internal/config"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
@@ -16,14 +15,13 @@ func run(ctx context.Context, cfg *config.Config) error {
eg, ctx := errgroup.WithContext(ctx)
app, h := httpjson.New(lg)
serverLog := lg.Named("run_server")
eg.Go(func(l logger.Logger) func() error {
return func() error {
l.Info("starting server")
defer l.Info("terminated server")
eg.Go(func() error {
serverLog.Info("starting server")
defer serverLog.Info("terminated server")
return h.RunServer(ctx, cfg, app)
}
}(lg.Named("run_server")))
})
return eg.Wait()
}

View File

@@ -1,17 +0,0 @@
package main
import (
"context"
"github.com/kjannette/go-server/internal/config"
"github.com/kjannette/go-server/internal/infrastructure/os"
"go.uber.org/zap"
)
var (
appName = ""
appVarsion = ""
)
func main() {
defer func() {_ = zap.L().Sync() }()
}

View File

18
docker-compose.yml Normal file
View File

@@ -0,0 +1,18 @@
services:
api:
build:
context: .
args:
APP_VERSION: ${APP_VERSION:-dev}
ports:
- "${API_PORT:-3000}:3000"
env_file:
- .env
environment:
API_PORT: "3000"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:3000/health"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s

24
go.mod Normal file
View File

@@ -0,0 +1,24 @@
module github.com/kjannette/go_http_server
go 1.25.0
require (
github.com/kelseyhightower/envconfig v1.4.0
github.com/labstack/echo/v4 v4.15.2
github.com/pkg/errors v0.9.1
go.uber.org/zap v1.28.0
golang.org/x/sync v0.20.0
)
require (
github.com/labstack/gommon v0.5.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
)

42
go.sum Normal file
View File

@@ -0,0 +1,42 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/labstack/echo/v4 v4.15.2 h1:nnh2sCzGCVYnU+wCisMPiYapEg/QVo/gcI9ePKg5/T4=
github.com/labstack/echo/v4 v4.15.2/go.mod h1:Xzp1Ns1RA2c9fY7nSgUJkpkUZGNbEIVHZbtbOMPktBI=
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,9 +1,8 @@
package config
import (
"github.com/pkg/errors"
"github.com/kelseyhightower/envconfig"
"github.com/pkg/errors"
)
// Config represents the application configuration.

View File

@@ -1,29 +1,22 @@
package config
package os
import (
"github.com/pkg/errors"
"context"
stdlib "os"
"os/signal"
"syscall"
"github.com/kelseyhightower/envconfig"
"go.uber.org/zap"
)
// Config represents the application configuration.
type Config struct {
API struct {
Port int `envconfig:"API_PORT" default:"3000"`
ReadTimeout int `envconfig:"API_READ_TIMEOUT" default:"7"`
WriteTimeout int `envconfig:"API_WRITE_TIMEOUT" default:"5"`
IdleTimeout int `envconfig:"API_IDLE_TIMEOUT" default:"5"`
Timeout int `envconfig:"API_TIMEOUT" default:"5"`
}
}
// SignalListener listens for SIGINT and SIGTERM and cancels the root context.
func SignalListener(logger *zap.Logger, cancel context.CancelFunc) {
sigCh := make(chan stdlib.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// NewConfig creates a new initialised application Config.
func NewConfig() (*Config, error) {
var config Config
if err := envconfig.Process("", &config); err != nil {
return nil, errors.Wrap(err, "error parsing config")
}
return &config, nil
go func() {
sig := <-sigCh
logger.Info("received shutdown signal", zap.String("signal", sig.String()))
cancel()
}()
}

View File

@@ -3,21 +3,11 @@ package logger
import (
"log"
"os"
"go.uber.org/zap"
"go.uber.go/zap/zapcore"
"go.uber.org/zap/zapcore"
)
type Logger interface {
Debug(msg string, fields ...zap.Field)
Info(msg string, fields ...zap.Field)
Warn(msg string, fields ...zap.Field)
Error(msg string, fields ...zap.Field)
With(fields ...zap.Field) *zap.Logger
}
// Option is a function to provide the Logger creation with extra initialisation options.
type Option func(*zap.Config)
// init initializes zap's global loggers (i.e. zap.L() and zap.S()).
// Log level is retrieved from environment variable LOG_LEVEL, defaulting to "info" otherwise.
// To activate this facility, add a blank import to this package.