# GoVueKit labs — the whole kit running on your machine, from the published # image: nothing to build, no source tree needed. This file is self-contained. # # curl -fsSL https://govuekit.dev/labs/docker-compose.yml -o docker-compose.yml # docker compose up -d # # http://labs.localhost the app # http://mail.labs.localhost Mailpit — every mail the app sends # http://auth.labs.localhost Dex — sign in with dev@labs.local / labs # http://s3.labs.localhost MinIO console — labs-minio / labs-minio-secret # http://proxy.labs.localhost/dashboard/ Traefik, when a route misbehaves # # No domain, no certificate, no DNS to configure: *.localhost already # resolves to this machine, and the containers reach the same URLs through # network aliases on the proxy — which matters because an OIDC issuer has to # be one URL for both the browser and the server. # # Everything is HTTP and every credential below is a constant in this file, # on purpose: this stack is disposable and never leaves the machine. It runs # APP_ENV=development for the same reason — that is the mode the kit is # developed in, and the mode where a plain-http issuer is accepted. # # What you are running is the compiled kit; what is for sale is its source — # the same tree this stack is built from, including this labs setup: # https://govuekit.dev # # docker compose pull && docker compose up -d # update to the latest kit # docker compose down -v # wipe it all # # Ports taken on the host: 80 only. Everything else talks over the compose # network. services: proxy: image: traefik:v3.3 restart: unless-stopped command: - --providers.docker=true - --providers.docker.exposedbydefault=false - --entrypoints.web.address=:80 - --api.dashboard=true ports: - "80:80" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: labs: # The containers resolve these names to the proxy, so the app and # the browser use the exact same URLs. aliases: - labs.localhost - auth.labs.localhost - mail.labs.localhost - s3.labs.localhost - proxy.labs.localhost labels: # Routed rather than published: one port on the host, and no clash # with whatever else is already listening on this machine. - traefik.enable=true - traefik.http.routers.labs-proxy.rule=Host(`proxy.labs.localhost`) - traefik.http.routers.labs-proxy.entrypoints=web - traefik.http.routers.labs-proxy.service=api@internal app: image: ghcr.io/kolapsis/go-vue-kit:latest restart: unless-stopped environment: APP_ENV: development HTTP_ADDR: :8080 BASE_URL: http://labs.localhost DB_DRIVER: postgres DB_DSN: postgres://govuekit:govuekit@db:5432/govuekit?sslmode=disable AUTO_MIGRATE: "true" CSRF_KEY: labs-csrf-key-not-a-secret-0000000000 TRUSTED_PROXIES: 172.16.238.0/24 # safehttp blocks private ranges by design; every service here lives on # one, so this stack's network is the documented exception. It has to # match the subnet pinned at the bottom of this file — left to Docker, # the range depends on which pools the machine already uses. EGRESS_ALLOW_PREFIXES: 172.16.238.0/24 # The Dex user below, so signing in gives you /admin too. Promotion is # an UPDATE run at startup: the account has to exist first, so the very # first sign-in needs a `docker compose restart app` to take effect. SUPER_ADMIN_EMAIL: ${LABS_ADMIN_EMAIL:-dev@labs.local} # Mailpit catches everything — nothing can reach a real address. MAILER: smtp SMTP_HOST: mailpit SMTP_PORT: "1025" # Dex. Plain http works because APP_ENV=development; anywhere else the # kit requires an https issuer. OIDC_ISSUER: http://auth.labs.localhost OIDC_CLIENT_ID: govuekit OIDC_CLIENT_SECRET: labs-oidc-secret OIDC_NAME: Dex (labs) # MinIO, so uploads take the S3 path rather than the disk one. STORAGE: s3 S3_ENDPOINT: http://minio:9000 S3_REGION: us-east-1 S3_BUCKET: govuekit-labs S3_ACCESS_KEY_ID: labs-minio S3_SECRET_ACCESS_KEY: labs-minio-secret # stripe-mock answers the API with fixtures, so checkout can be walked # through without a Stripe account. It emits no webhook: to exercise # those, put real test keys in a .env next to this file and set # STRIPE_API_BASE= (empty) to reach the real test API. Blank # STRIPE_SECRET_KEY instead to see the app as a deployment that never # configured billing. STRIPE_API_BASE: ${STRIPE_API_BASE-http://stripe-mock:12111} STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-sk_test_labs} STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-} # Optional: anything you want to try (ORG_MODE, BILLING_PLANS, APP_NAME, # OAuth keys…) goes in a .env next to this file. The values above win. env_file: - path: .env required: false networks: - labs depends_on: db: condition: service_healthy dex: condition: service_healthy minio-init: condition: service_completed_successfully mailpit: condition: service_started labels: - traefik.enable=true - traefik.http.routers.labs-app.rule=Host(`labs.localhost`) - traefik.http.routers.labs-app.entrypoints=web - traefik.http.services.labs-app.loadbalancer.server.port=8080 db: image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_USER: govuekit POSTGRES_PASSWORD: govuekit POSTGRES_DB: govuekit volumes: - pgdata:/var/lib/postgresql/data networks: - labs healthcheck: test: ["CMD-SHELL", "pg_isready -U govuekit -d govuekit"] interval: 5s timeout: 3s retries: 12 mailpit: image: axllent/mailpit:latest restart: unless-stopped environment: MP_MAX_MESSAGES: "500" # Without this Mailpit keeps everything in memory and the volume below # would hold nothing. MP_DATABASE: /data/mailpit.db volumes: - mailpit:/data networks: - labs labels: - traefik.enable=true - traefik.http.routers.labs-mail.rule=Host(`mail.labs.localhost`) - traefik.http.routers.labs-mail.entrypoints=web - traefik.http.services.labs-mail.loadbalancer.server.port=8025 # OIDC provider. Pinned: the config file is a gomplate template rendered by # this image's entrypoint, so image and template move together. The template # is inlined below (top-level `configs:`) so this file stands alone. dex: image: dexidp/dex:v2.44.0 restart: unless-stopped command: ["dex", "serve", "/etc/dex/config.yaml"] environment: DEX_ISSUER: http://auth.labs.localhost DEX_CLIENT_ID: govuekit DEX_CLIENT_SECRET: labs-oidc-secret DEX_REDIRECT_URI: http://labs.localhost/api/auth/oauth/oidc/callback DEX_USER_EMAIL: dev@labs.local DEX_USER_NAME: Labs user # bcrypt("labs"). Dollars doubled: compose eats undoubled ones. DEX_USER_HASH: $$2b$$10$$5eLozufICgyxUFD1S.O2aOjOPnSZdcBwemSccTN0DBFBaNkmOtCqK configs: - source: dex-config target: /etc/dex/config.yaml volumes: - dex:/var/dex networks: - labs healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5558/healthz/ready"] interval: 5s timeout: 3s retries: 12 labels: - traefik.enable=true - traefik.http.routers.labs-oidc.rule=Host(`auth.labs.localhost`) - traefik.http.routers.labs-oidc.entrypoints=web - traefik.http.services.labs-oidc.loadbalancer.server.port=5556 minio: image: minio/minio:latest restart: unless-stopped command: ["server", "/data", "--console-address", ":9001"] environment: MINIO_ROOT_USER: labs-minio MINIO_ROOT_PASSWORD: labs-minio-secret MINIO_BROWSER_REDIRECT_URL: http://s3.labs.localhost volumes: - minio:/data networks: - labs healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 5s timeout: 3s retries: 12 labels: - traefik.enable=true - traefik.http.routers.labs-s3.rule=Host(`s3.labs.localhost`) - traefik.http.routers.labs-s3.entrypoints=web - traefik.http.services.labs-s3.loadbalancer.server.port=9001 # Creates the bucket, then exits. The app waits for it, so a first upload # never races an empty MinIO. minio-init: image: minio/minio:latest restart: "no" entrypoint: - sh - -c - | mc alias set labs http://minio:9000 labs-minio labs-minio-secret && mc mb --ignore-existing labs/govuekit-labs networks: - labs depends_on: minio: condition: service_healthy # Fixture-backed Stripe API: exercises the outbound calls with no account. # It validates nothing and emits no webhook. stripe-mock: image: stripe/stripe-mock:latest restart: unless-stopped networks: - labs # The Dex configuration, verbatim from the kit's deploy/labs/dex/config.yaml. # Inlined so a downloaded copy of this file needs nothing next to it. configs: dex-config: content: | {{- /* Dex configuration for the labs stack. The official image renders this file with gomplate before `dex serve` reads it, so every {{ getenv }} below is resolved at container start and no secret is committed. Dex does NOT expand $$VAR — a `secret: $$MY_VAR` would be taken literally, and a bcrypt hash written inline here would be read as-is too. This templating is the mechanism; use it for both. */ -}} issuer: {{ getenv "DEX_ISSUER" | required "DEX_ISSUER" }} storage: type: sqlite3 config: file: /var/dex/dex.db web: http: 0.0.0.0:5556 # Serves /healthz/ready, which the compose healthcheck polls. telemetry: http: 0.0.0.0:5558 logger: level: {{ getenv "DEX_LOG_LEVEL" "info" }} format: json oauth2: # A labs IdP exists to be walked through quickly: no consent screen # between the sign-in form and the callback. skipApprovalScreen: true staticClients: - id: {{ getenv "DEX_CLIENT_ID" "govuekit" }} name: "{{ getenv "DEX_CLIENT_NAME" "GoVueKit labs" }}" secret: {{ getenv "DEX_CLIENT_SECRET" | required "DEX_CLIENT_SECRET" }} redirectURIs: - {{ getenv "DEX_REDIRECT_URI" | required "DEX_REDIRECT_URI" }} # Local accounts rather than an upstream connector: labs must sign in with # nothing but this stack running. Add a connector here (GitHub, Google, an # LDAP…) when you need to rehearse a federated flow. enablePasswordDB: true staticPasswords: - email: "{{ getenv "DEX_USER_EMAIL" "dev@labs.local" }}" hash: "{{ getenv "DEX_USER_HASH" | required "DEX_USER_HASH" }}" username: "{{ getenv "DEX_USER_NAME" "Labs user" }}" # The kit keys OAuth identities on `sub`, which derives from this ID. # Keep it stable or a redeploy looks like a different person. userID: "5f9c8d2e-6b31-4a7f-9e14-2c8d7b3a1f60" volumes: pgdata: mailpit: # Dex's signing keys: wiping this invalidates every issued token. dex: minio: networks: labs: # Pinned so EGRESS_ALLOW_PREFIXES above can name it exactly. Docker hands # out its own range otherwise, and on a machine whose 172.16/12 pools are # taken it lands on 192.168.x — where safehttp blocks the app's own # dependencies. Change both if this /24 clashes with something local. ipam: config: - subnet: 172.16.238.0/24