Getting started
Servicepack is the starting point for a Go application with multiple long-running things to run: an API, workers, feeds, schedulers, migration commands, whatever. Clone it, make the clone yours, and put your services in internal/pkg/services/.
It is not intended to be imported wholesale into an existing project with go get.
1. Make a fresh clone yours
git clone https://github.com/psyb0t/servicepack.git my-service
cd my-service
make own MODNAME=github.com/yourname/my-service
Run that once, only in a disposable fresh clone. make own:
- removes shipped
example-*services but leaveshello-world; - removes servicepack’s own agent instructions (
AGENTS.md,CLAUDE.md,.claude,.agents), its.github/FUNDING.yml, its mirror-and-archive and issue-pull workflows, and the GitHub Pages files of its documentation site, keeping thedocs/*.mdpages; - removes the clone’s
.git, module manifests, andvendor/directory; - recreates the module using the supplied
MODNAMEwhile retaining the framework’s pinned dependency and tool declarations; - rewrites framework imports to your module path;
- replaces this README with a project stub;
- records the framework revision in
servicepack.versionif it does not already exist; - runs dependency and service-registration targets; and
- initializes a new Git repository on
mainand creates an initial commit.
It needs Git identity configured well enough for that initial commit. Its normal dependency and generation targets run in the Docker development image; it does not reject an older host Go toolchain.
Your binary name is the final segment of MODNAME: for github.com/yourname/my-service, it is my-service.
2. Add a service
make service NAME=price-worker
That creates internal/pkg/services/price-worker/priceworker.go and then regenerates internal/pkg/services/services.gen.go. The generated file finds the service factories at runtime; do not hand-edit it.
The scaffold is intentionally boring:
func (s *PriceWorker) Run(ctx context.Context) error {
ctx = ctxscope.Set(ctx, ctxscope.Attr("service", ServiceName))
logger := ctxscope.GetLogger(ctx)
logger.Info("starting service")
<-ctx.Done()
logger.Info("service context cancelled")
return nil
}
Replace the wait with the real work, but keep cancellation part of the contract. Run returning a non-nil error normally starts application shutdown; see services and lifecycle for the exceptions and retry rules.
When you add, remove, rename, or materially change a service implementation, regenerate registration:
make service-registration
3. Configure and run it
Generated services load their own typed config with gonfiguration:
type Config struct {
Endpoint string `env:"PRICEWORKER_ENDPOINT"`
Interval time.Duration `env:"PRICEWORKER_INTERVAL" default:"15s"`
}
Parse configuration in New, fail with context, and do not reach straight for os.Getenv:
func New() (*PriceWorker, error) {
cfg := Config{}
if err := gonfiguration.Parse(&cfg); err != nil {
return nil, ctxerrors.Wrap(err, "parse price-worker config")
}
return &PriceWorker{config: cfg}, nil
}
Build and run:
make build
./build/my-service run
For the everyday development loop, make run-dev builds the development image and runs the application with the race detector. It is useful for the shipped examples; a made-own project usually needs its own config and services first.
Framework configuration
These names belong to the framework itself. Your services own their own configuration names.
| Variable | Meaning | Default |
|---|---|---|
LOG_LEVEL | debug, info, warn, or error logging threshold. | info |
LOG_FORMAT | json or text output. | text |
LOG_ADD_SOURCE | Include source location in log records. | false |
ENV | Environment selected by goenv. | prod |
RUNNER_SHUTDOWNTIMEOUT | Whole-application graceful shutdown deadline. | 10s |
SERVICES_ENABLED | Comma-separated in-process service allowlist. Empty/unset means all registered services. | all |
Example:
SERVICES_ENABLED=price-worker,api LOG_LEVEL=debug ./build/my-service run
Where your changes go
| You are changing | Put it here |
|---|---|
| A business service | internal/pkg/services/<name>/ |
| Service-specific config | That service’s Config struct and project env documentation |
| Startup/shutdown extensions | cmd/init.go hooks |
| Standalone application commands | cmd/commands.go |
| Project build behavior | Makefile or scripts/make/ overrides |
| Project Docker behavior | Dockerfile, Dockerfile.dev |
The framework-owned directories are deliberately replaceable by make servicepack-update: internal/app/, internal/pkg/service-manager/, pkg/runner/, cmd/main.go, Makefile.servicepack, scripts/make/servicepack/, and Dockerfile.servicepack*. Do not put application-specific changes there.
Your docs/ and tests/ trees are the opposite. The framework ships them as a scaffold, they become yours, and an update never overwrites them.
Next: learn the service lifecycle, then the Docker-first development workflow.