From 3cfc74affa37dbeac3e7d869d3e9c9e240d8d0af Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Sat, 13 Sep 2025 18:13:50 +0500 Subject: [PATCH] Client build scripts fixes and server stub with local devzone. --- .gitignore | 3 +- Taskfile.yml | 10 +- client/cmd/client/Taskfile.yml | 10 +- client/cmd/client/main.go | 2 +- client/internal/application/application.go | 17 +- client/internal/application/fyne.go | 9 +- server/Dockerfile | 18 ++ server/Taskfile.yml | 6 + server/cmd/Taskfile.yml | 5 + server/cmd/bunkerd/Taskfile.yml | 26 +++ server/cmd/bunkerd/main.go | 39 ++++ server/entrypoint.sh | 8 + server/internal/application/application.go | 199 ++++++++++++++++++ server/internal/application/logger.go | 20 ++ server/internal/application/server.go | 26 +++ server/internal/application/service.go | 80 +++++++ server/localdevzone/Taskfile.yml | 19 ++ server/localdevzone/bunkerd/Taskfile.yml | 47 +++++ .../localdevzone/bunkerd/docker-compose.yaml | 24 +++ server/localdevzone/common/Taskfile.yml | 15 ++ server/localdevzone/common/network.yaml | 18 ++ 21 files changed, 578 insertions(+), 23 deletions(-) create mode 100644 server/Dockerfile create mode 100644 server/Taskfile.yml create mode 100644 server/cmd/Taskfile.yml create mode 100644 server/cmd/bunkerd/Taskfile.yml create mode 100644 server/cmd/bunkerd/main.go create mode 100755 server/entrypoint.sh create mode 100644 server/internal/application/application.go create mode 100644 server/internal/application/logger.go create mode 100644 server/internal/application/server.go create mode 100644 server/internal/application/service.go create mode 100644 server/localdevzone/Taskfile.yml create mode 100644 server/localdevzone/bunkerd/Taskfile.yml create mode 100644 server/localdevzone/bunkerd/docker-compose.yaml create mode 100644 server/localdevzone/common/Taskfile.yml create mode 100644 server/localdevzone/common/network.yaml diff --git a/.gitignore b/.gitignore index 25b41f1..5500a2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *DS_Store* fyne-cross -.build +_build *.apk *.app dist +.task diff --git a/Taskfile.yml b/Taskfile.yml index 23649f4..5994d24 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -14,19 +14,25 @@ vars: sh: git rev-list --count HEAD BUILD_DATE: sh: TZ=UTC date +'%Y-%m-%d %H:%M:%S %Z' - BASIC_LDFLAGS: "-X 'bunker/commons/constants.Version={{.VERSION}}' -X 'bunker/commons/constants.Branch={{.BRANCH}}' -X 'bunker/commons/constants.Commit={{.COMMIT}}' -X 'bunker/commons/constants.Build={{.BUILD}}' -X 'bunker/commons/constants.BuildDate={{.BUILD_DATE}}'" + BASIC_LDFLAGS: "-X 'bunker/commons.Version={{.VERSION}}' -X 'bunker/commons.Branch={{.BRANCH}}' -X 'bunker/commons.Commit={{.COMMIT}}' -X 'bunker/commons.Build={{.BUILD}}' -X 'bunker/commons.BuildDate={{.BUILD_DATE}}'" env: GOFLAGS: "-trimpath" includes: client: ./client + server: ./server tasks: + cleanup: + desc: "Cleanup _build directory." + cmds: + - task: server:cmd:bunkerd:cleanup + ensure-builddir: internal: true cmds: - - mkdir -p .build + - mkdir -p _build lint: desc: "Lints whole workspace." diff --git a/client/cmd/client/Taskfile.yml b/client/cmd/client/Taskfile.yml index da7cac7..697343e 100644 --- a/client/cmd/client/Taskfile.yml +++ b/client/cmd/client/Taskfile.yml @@ -16,7 +16,7 @@ tasks: cmds: - task: ::ensure-builddir - task: cleanup - - fyne build --release -o ../../../.build/{{ .ClientBinary }} --pprof --pprof-port 6060 {{ .MetadataParams }} + - fyne build --release -o ../../../_build/{{ .ClientBinary }} --pprof --pprof-port 6060 {{ .MetadataParams }} build-debug: desc: "Build client in debug mode." @@ -25,7 +25,7 @@ tasks: cmds: - task: ::ensure-builddir - task: cleanup - - fyne build -o ../../../.build/{{ .ClientBinary }} --tags debug {{ .MetadataParams }} + - fyne build -o ../../../_build/{{ .ClientBinary }} --tags debug {{ .MetadataParams }} # build-production: # desc: "Build production package for current OS." @@ -92,7 +92,7 @@ tasks: cleanup: desc: "Cleanup build environment." cmds: - - rm .build/{{ .ClientBinary }} + - rm _build/{{ .ClientBinary }} ignore_error: true ensure-dist-dir: @@ -105,10 +105,10 @@ tasks: desc: "Launch client." cmds: - task: build - - .build/{{ .ClientBinary }} + - _build/{{ .ClientBinary }} run-debug: desc: "Launch client in debug mode." cmds: - task: build-debug - - .build/{{ .ClientBinary }} + - _build/{{ .ClientBinary }} diff --git a/client/cmd/client/main.go b/client/cmd/client/main.go index 8d210ae..08b8b41 100644 --- a/client/cmd/client/main.go +++ b/client/cmd/client/main.go @@ -23,7 +23,7 @@ func main() { lgr := app.NewLogger("module", "main") lgr.Info( - "Starting Bunker...", + "Starting Bunker client...", "version", app.Fyne().Metadata().Custom["Version"], "build_no", app.Fyne().Metadata().Custom["Build"], "commit", app.Fyne().Metadata().Custom["Commit"], diff --git a/client/internal/application/application.go b/client/internal/application/application.go index 0e5f832..e05ed8b 100644 --- a/client/internal/application/application.go +++ b/client/internal/application/application.go @@ -9,7 +9,10 @@ import ( "strings" "time" + "bunker/commons" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" ) var ( @@ -19,6 +22,8 @@ var ( // Application is a lifecycle controlling structure for application. type Application struct { + ctx context.Context + cancelFunc context.CancelFunc fyneApp fyne.App baseLogger *slog.Logger appLogger *slog.Logger @@ -96,7 +101,7 @@ func (a *Application) connectDependencies() error { // ContextWithTimeout returns context.Context with requested timeout. func (a *Application) ContextWithTimeout(timeout time.Duration) context.Context { - ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) + ctx, cancelFunc := context.WithTimeout(a.ctx, timeout) // As we do not need to call cancelFunc - make linter happy. // This probably will lead to context leak, so it should be investigated. @@ -106,11 +111,13 @@ func (a *Application) ContextWithTimeout(timeout time.Duration) context.Context } func (a *Application) initialize() { + a.ctx, a.cancelFunc = context.WithCancel(context.Background()) + a.initializeLogger() - a.services = make([]Service, 0) + a.fyneApp = app.NewWithID(commons.ClientAppID) - a.initializeFyne() + a.services = make([]Service, 0) } func (a *Application) launchStartupTasks() error { @@ -167,7 +174,6 @@ func (a *Application) launchStartupTasks() error { func (a *Application) Shutdown() error { a.appLogger.Info("Stopping pztrn's Bunker...") - // Сначала тушим фичи. for _, service := range a.services { if !strings.Contains(service.Name(), "features/") { continue @@ -180,7 +186,6 @@ func (a *Application) Shutdown() error { } } - // Потом тушим ядро. for _, service := range a.services { if !strings.Contains(service.Name(), "core/") { continue @@ -198,7 +203,7 @@ func (a *Application) Shutdown() error { return nil } -// Start запускает приложение. +// Start starts application. func (a *Application) Start() error { if err := a.connectDependencies(); err != nil { return fmt.Errorf("%w: %w", errApplication, err) diff --git a/client/internal/application/fyne.go b/client/internal/application/fyne.go index 8be7d4f..dabc7f0 100644 --- a/client/internal/application/fyne.go +++ b/client/internal/application/fyne.go @@ -1,17 +1,10 @@ package application import ( - "bunker/commons" - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/app" ) -// Fyne возвращает экземпляр Fyne для взаимодействия с ним. +// Fyne returns Fyne instance. func (a *Application) Fyne() fyne.App { return a.fyneApp } - -func (a *Application) initializeFyne() { - a.fyneApp = app.NewWithID(commons.ClientAppID) -} diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..1c79681 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,18 @@ +FROM code.pztrn.name/containers/go-toolbox:v8 AS build + +WORKDIR /bunkerd +COPY . /bunkerd +RUN --mount=type=cache,target="/home/container/go" task server:cmd:bunkerd:build --force + +FROM debian:13-slim + +RUN apt-get update && \ + apt-get install -y ca-certificates iputils-ping coreutils && \ + rm -rf /var/lib/apt/* /var/cache/apt/* + +COPY --from=build /bunkerd/_build/bunkerd /bunkerd +COPY --from=build /usr/local/bin/dlv /dlv + +COPY server/entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/server/Taskfile.yml b/server/Taskfile.yml new file mode 100644 index 0000000..b47ddd6 --- /dev/null +++ b/server/Taskfile.yml @@ -0,0 +1,6 @@ +--- +version: "3" + +includes: + localdev: ./localdevzone + cmd: ./cmd diff --git a/server/cmd/Taskfile.yml b/server/cmd/Taskfile.yml new file mode 100644 index 0000000..76eeb15 --- /dev/null +++ b/server/cmd/Taskfile.yml @@ -0,0 +1,5 @@ +--- +version: "3" + +includes: + bunkerd: ./bunkerd diff --git a/server/cmd/bunkerd/Taskfile.yml b/server/cmd/bunkerd/Taskfile.yml new file mode 100644 index 0000000..093a1f7 --- /dev/null +++ b/server/cmd/bunkerd/Taskfile.yml @@ -0,0 +1,26 @@ +--- +version: "3" + +tasks: + build: + desc: "Builds bunkerd binary." + cmds: + - task: :::ensure-builddir + - task: cleanup + - go build -ldflags="{{ .BASIC_LDFLAGS }}" -o _build/bunkerd{{exeExt}} ./server/cmd/bunkerd/main.go + sources: + - ./Taskfile.yml + - ./go.mod + - ./commons/* + - ./server/**/*.go + - ./server/Taskfile.yml + - ./server/**/Taskfile.yml + - ./server/entrypoint.sh + generates: + - ./_build/bunkerd{{exeExt}} + method: timestamp + + cleanup: + desc: "Deletes bunkerd binary from local build cache." + cmds: + - rm -f _build/bunkerd{{exeExt}} diff --git a/server/cmd/bunkerd/main.go b/server/cmd/bunkerd/main.go new file mode 100644 index 0000000..5e074bb --- /dev/null +++ b/server/cmd/bunkerd/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "os" + + "bunker/commons" + "bunker/server/internal/application" +) + +func main() { + app := application.New() + + lgr := app.NewLogger("module", "main") + lgr.Info( + "Starting bunkerd...", + "version", commons.Version, + "build_no", commons.Build, + "buint_on", commons.BuildDate, + "commit", commons.Commit, + "branch", commons.Branch, + ) + + if err := app.Start(); err != nil { + lgr.Error("Failed to start bunkerd!", "error", err.Error()) + + os.Exit(1) + } + + lgr.Info("bunkerd started.") + + <-app.ShutdownChan() + lgr.Info("Shutting down bunkerd...") + + if err := app.Shutdown(); err != nil { + lgr.Error("Failed to shutdown bunkerd!", "error", err.Error()) + + os.Exit(1) + } +} diff --git a/server/entrypoint.sh b/server/entrypoint.sh new file mode 100755 index 0000000..12bbe61 --- /dev/null +++ b/server/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +echo "* Starting bunkerd..." +if [ -n "${BUNKERD_DEBUG}" ]; then + exec /dlv --listen=:4000 --headless=true --log=true --accept-multiclient --api-version=2 exec /bunkerd --continue +else + exec /bunkerd +fi diff --git a/server/internal/application/application.go b/server/internal/application/application.go new file mode 100644 index 0000000..a6523d3 --- /dev/null +++ b/server/internal/application/application.go @@ -0,0 +1,199 @@ +package application + +import ( + "context" + "errors" + "fmt" + "log/slog" + "os" + "strings" + "time" +) + +var ( + errApplication = errors.New("application") + errNoMainWindow = errors.New("no main window service registered") +) + +// Application is a lifecycle controlling structure for application. +type Application struct { + shutdownChan chan struct{} + ctx context.Context + cancelFunc context.CancelFunc + baseLogger *slog.Logger + appLogger *slog.Logger + services []Service +} + +// New creates new instance of lifecycle controlling structure. +func New() *Application { + appl := &Application{} + + appl.initialize() + + return appl +} + +func (a *Application) configure() error { + // First iteration - core services. + for _, service := range a.services { + if !strings.Contains(service.Name(), "core/") { + continue + } + + a.appLogger.Debug("Launching configuration procedure for service", "service", service.Name()) + + if err := service.Configure(); err != nil { + return fmt.Errorf("configure service '%s': %w", service.Name(), err) + } + } + + // Second iteration - rest of the services. + for _, service := range a.services { + if strings.Contains(service.Name(), "core/") { + continue + } + + a.appLogger.Debug("Launching configuration procedure for service", "service", service.Name()) + + if err := service.Configure(); err != nil { + return fmt.Errorf("configure service '%s': %w", service.Name(), err) + } + } + + return nil +} + +func (a *Application) connectDependencies() error { + // First iteration - core services. + for _, service := range a.services { + if !strings.Contains(service.Name(), "core/") { + continue + } + + a.appLogger.Debug("Connecting dependencies for service.", "service", service.Name()) + + if err := service.ConnectDependencies(); err != nil { + return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err) + } + } + + // Second iteration - rest of the services. + for _, service := range a.services { + if strings.Contains(service.Name(), "core/") { + continue + } + + a.appLogger.Debug("Connecting dependencies for service.", "service", service.Name()) + + if err := service.ConnectDependencies(); err != nil { + return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err) + } + } + + return nil +} + +// ContextWithTimeout returns context.Context with requested timeout. +func (a *Application) ContextWithTimeout(timeout time.Duration) context.Context { + ctx, cancelFunc := context.WithTimeout(a.ctx, timeout) + + // As we do not need to call cancelFunc - make linter happy. + // This probably will lead to context leak, so it should be investigated. + go func(_ context.CancelFunc) {}(cancelFunc) + + return ctx +} + +func (a *Application) initialize() { + a.ctx, a.cancelFunc = context.WithCancel(context.Background()) + + a.initializeLogger() + + a.services = make([]Service, 0) +} + +func (a *Application) launchStartupTasks() error { + for _, service := range a.services { + if strings.Contains(service.Name(), "mainwindow") { + continue + } + + if !strings.Contains(service.Name(), "core/") { + continue + } + + a.appLogger.Debug("Launching startup tasks for service.", "service", service.Name()) + + if err := service.LaunchStartupTasks(); err != nil { + return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err) + } + } + + for _, service := range a.services { + if strings.Contains(service.Name(), "core/") { + continue + } + + a.appLogger.Debug("Launching startup tasks for service.", "service", service.Name()) + + if err := service.LaunchStartupTasks(); err != nil { + return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err) + } + } + + return nil +} + +// Shutdown stops application. +func (a *Application) Shutdown() error { + a.appLogger.Info("Stopping pztrn's Bunker...") + + for _, service := range a.services { + if !strings.Contains(service.Name(), "features/") { + continue + } + + a.appLogger.Debug("Shutting down service.", "service", service.Name()) + + if err := service.Shutdown(); err != nil { + return fmt.Errorf("shutting down service '%s': %w", service.Name(), err) + } + } + + for _, service := range a.services { + if !strings.Contains(service.Name(), "core/") { + continue + } + + a.appLogger.Debug("Shutting down service.", "service", service.Name()) + + if err := service.Shutdown(); err != nil { + return fmt.Errorf("shutting down service '%s': %w", service.Name(), err) + } + } + + os.Exit(0) + + return nil +} + +// Start starts application. +// Server application will start a goroutine that monitors SIGTERM and sends empty struct to channel. +func (a *Application) Start() error { + if err := a.connectDependencies(); err != nil { + return fmt.Errorf("%w: %w", errApplication, err) + } + + if err := a.configure(); err != nil { + return fmt.Errorf("%w: %w", errApplication, err) + } + + if err := a.launchStartupTasks(); err != nil { + return fmt.Errorf("%w: %w", errApplication, err) + } + + a.startServer() + + return nil +} diff --git a/server/internal/application/logger.go b/server/internal/application/logger.go new file mode 100644 index 0000000..4c95b32 --- /dev/null +++ b/server/internal/application/logger.go @@ -0,0 +1,20 @@ +package application + +import ( + "log/slog" + "os" +) + +func (a *Application) initializeLogger() { + a.baseLogger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + })) + + a.appLogger = a.baseLogger.With("module", "application") +} + +// NewLogger creates new sub-instance of base logger and adds some additional data to it for persistent output. +func (a *Application) NewLogger(withs ...interface{}) *slog.Logger { + return a.baseLogger.With(withs...) +} diff --git a/server/internal/application/server.go b/server/internal/application/server.go new file mode 100644 index 0000000..7984936 --- /dev/null +++ b/server/internal/application/server.go @@ -0,0 +1,26 @@ +package application + +import ( + "os" + "os/signal" + "syscall" +) + +// ShutdownChan returns shutdown channel for main function. +func (a *Application) ShutdownChan() chan struct{} { + return a.shutdownChan +} + +func (a *Application) startServer() { + a.shutdownChan = make(chan struct{}) + + go func() { + listener := make(chan os.Signal, 1) + + signal.Notify(listener, syscall.SIGTERM, os.Interrupt) + + <-listener + + a.shutdownChan <- struct{}{} + }() +} diff --git a/server/internal/application/service.go b/server/internal/application/service.go new file mode 100644 index 0000000..e196301 --- /dev/null +++ b/server/internal/application/service.go @@ -0,0 +1,80 @@ +package application + +import ( + "errors" + "fmt" +) + +var ( + // ErrServiceAlreadyRegistered returns if trying to register a service with name already taken by other service. + ErrServiceAlreadyRegistered = errors.New("service with such name already registered") + // ErrServiceNotFound returns if trying to gather service with unknown name. + ErrServiceNotFound = errors.New("service with such name wasn't found") +) + +// Service is an interface every service should conform to. Specific services will have own interface for +// cross-service interation. +type Service interface { + // Configure configures service. Called after ConnectDependencies and before LaunchStartupTasks. + Configure() error + // ConnectDependencies gets necessary dependencies. + ConnectDependencies() error + // Initialize initializes service's internal state. Called while registering service with Application + // lifecycle controller. + Initialize() error + // Name returns service name. + Name() string + // LaunchStartupTasks launches tasks on application start. Called after ConnectDependencies and Configure. + LaunchStartupTasks() error + // Shutdown stops service. + Shutdown() error +} + +// RegisterService registering service with lifecycle controller for later use in any other service. +func (a *Application) RegisterService(srv Service) error { + var found bool + + for _, knownService := range a.services { + if srv.Name() == knownService.Name() { + found = true + + break + } + } + + if found { + return fmt.Errorf( + "%w: RegisterService: check for service '%s' registration: %w", + errApplication, + srv.Name(), + ErrServiceAlreadyRegistered, + ) + } + + if err := srv.Initialize(); err != nil { + return fmt.Errorf("%w: RegisterService: initialize service '%s': %w", errApplication, srv.Name(), err) + } + + a.services = append(a.services, srv) + + return nil +} + +// Service returns requested service. +func (a *Application) Service(name string) Service { + var srv Service + + for _, knownService := range a.services { + if knownService.Name() == name { + srv = knownService + + break + } + } + + if srv == nil { + return nil + } + + return srv +} diff --git a/server/localdevzone/Taskfile.yml b/server/localdevzone/Taskfile.yml new file mode 100644 index 0000000..7c17fe9 --- /dev/null +++ b/server/localdevzone/Taskfile.yml @@ -0,0 +1,19 @@ +--- +version: "3" + +includes: + bunkerd: ./bunkerd + common: ./common + +tasks: + down: + desc: "Removes development environment." + cmds: + - task: bunkerd:down + - task: common:network-down + + up: + desc: "Creates development environment." + cmds: + - task: common:network-up + - task: bunkerd:up diff --git a/server/localdevzone/bunkerd/Taskfile.yml b/server/localdevzone/bunkerd/Taskfile.yml new file mode 100644 index 0000000..3481e74 --- /dev/null +++ b/server/localdevzone/bunkerd/Taskfile.yml @@ -0,0 +1,47 @@ +--- +version: "3" + +tasks: + build: + desc: "Builds bunkerd's container." + dir: "./server/localdevzone/bunkerd" + cmds: + - docker compose -p bunkerd -f docker-compose.yaml build + + down: + desc: "Deletes all bunkerd's data (down)." + dir: "./server/localdevzone/bunkerd" + cmds: + - docker compose -p bunkerd -f docker-compose.yaml down --volumes + + logs: + desc: "Show bunkerd logs." + dir: "./server/localdevzone/bunkerd" + cmds: + - docker compose -p bunkerd -f docker-compose.yaml logs -f + + restart: + desc: "Restart bunkerd." + dir: "./server/localdevzone/bunkerd" + cmds: + - docker compose -p bunkerd -f docker-compose.yaml restart + + start: + desc: "Start bunkerd." + dir: "./server/localdevzone/bunkerd" + cmds: + - docker compose -p bunkerd -f docker-compose.yaml start + + stop: + desc: "Stop bunkerd without deleting it's data." + dir: "./server/localdevzone/bunkerd" + cmds: + - docker compose -p bunkerd -f docker-compose.yaml stop + + up: + desc: "Start bunkerd (up -d)." + dir: "./server/localdevzone/bunkerd" + cmds: + - task: :common:network-up + - task: build + - docker compose -p bunkerd -f docker-compose.yaml up -d diff --git a/server/localdevzone/bunkerd/docker-compose.yaml b/server/localdevzone/bunkerd/docker-compose.yaml new file mode 100644 index 0000000..8651d48 --- /dev/null +++ b/server/localdevzone/bunkerd/docker-compose.yaml @@ -0,0 +1,24 @@ +--- +services: + bunkerd: + container_name: "bunkerd" + build: + context: ../../../ + dockerfile: server/Dockerfile + ports: + - "53400:53400" + - "54000:4000" + volumes: + - "./data:/data" + networks: + bunkerd: + ipv4_address: 247.247.0.2 + environment: + BUNKERD_DEBUG: "true" + BUNKERD_DATABASE_DSN: " " + cap_add: + - SYS_PTRACE + +networks: + bunkerd: + external: true diff --git a/server/localdevzone/common/Taskfile.yml b/server/localdevzone/common/Taskfile.yml new file mode 100644 index 0000000..b2bdb54 --- /dev/null +++ b/server/localdevzone/common/Taskfile.yml @@ -0,0 +1,15 @@ +--- +version: "3" + +tasks: + network-down: + desc: "Deletes Docker network for development." + dir: "./server/localdevzone/common" + cmds: + - docker compose -p bunkerd-network -f network.yaml down + + network-up: + desc: "Creates Docker network for development" + dir: "./server/localdevzone/common" + cmds: + - docker compose -p bunkerd-network -f network.yaml up -d diff --git a/server/localdevzone/common/network.yaml b/server/localdevzone/common/network.yaml new file mode 100644 index 0000000..23ba840 --- /dev/null +++ b/server/localdevzone/common/network.yaml @@ -0,0 +1,18 @@ +--- +services: + dummy: + image: busybox + container_name: bunkerd-dummy + hostname: dummy + networks: + bunkerd: + ipv4_address: 247.247.0.254 + +networks: + bunkerd: + driver: bridge + name: "bunkerd" + ipam: + config: + - subnet: 247.247.0.0/24 + gateway: 247.247.0.1