From eeae3283255eac171f0c7baccc4bff009aa450e2 Mon Sep 17 00:00:00 2001 From: Dave Gallant Date: Mon, 23 Feb 2026 17:58:33 -0500 Subject: [PATCH] Move to a single container --- .dockerignore | 5 +- .github/workflows/publish-container.yml | 50 ++++++++++++ .github/workflows/publish-containers.yml | 98 ------------------------ .gitignore | 2 +- Caddyfile | 8 -- Dockerfile | 23 +++--- Makefile | 19 ++--- backend/Dockerfile | 15 ---- backend/app.go | 53 ++++++++----- backend/dist/index.html | 6 ++ backend/main.go | 15 ---- docker-compose.prod.yml | 16 ---- docker-compose.yml | 18 ----- 13 files changed, 116 insertions(+), 212 deletions(-) create mode 100644 .github/workflows/publish-container.yml delete mode 100644 .github/workflows/publish-containers.yml delete mode 100644 Caddyfile delete mode 100644 backend/Dockerfile create mode 100644 backend/dist/index.html delete mode 100644 docker-compose.prod.yml delete mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore index e34d8c3..63a0bd8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,4 @@ -backend +node_modules +dist +.git +.github diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml new file mode 100644 index 0000000..20adb5b --- /dev/null +++ b/.github/workflows/publish-container.yml @@ -0,0 +1,50 @@ +name: Publish Container +on: + push: + branches: + - main + pull_request: + schedule: + - cron: "0 0 * * 0" + +jobs: + publish: + name: Publish rfd-fyi + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Prepare + id: prep + run: | + IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/rfd-fyi + VERSION=${GITHUB_REF##*/} + if [[ $GITHUB_REF == refs/heads/main ]]; then + VERSION=latest + fi + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF#refs/tags/v} + fi + TAGS="${IMAGE_NAME}:${VERSION}" + if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + TAGS="$TAGS,${IMAGE_NAME}:latest" + fi + echo "tags=${TAGS}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Packages Docker Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build / push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: ${{ github.ref == 'refs/heads/main' }} + tags: ${{ steps.prep.outputs.tags }} diff --git a/.github/workflows/publish-containers.yml b/.github/workflows/publish-containers.yml deleted file mode 100644 index 83076d1..0000000 --- a/.github/workflows/publish-containers.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Publish Containers -on: - push: - branches: - - main - pull_request: - schedule: - - cron: "0 0 * * 0" - -jobs: - publish-backend: - name: Publish rfd-fyi-backend - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Prepare - id: prep - run: | - IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/rfd-fyi-backend - VERSION=${GITHUB_REF##*/} - if [[ $GITHUB_REF == refs/heads/main ]]; then - VERSION=latest - fi - if [[ $GITHUB_REF == refs/tags/* ]]; then - VERSION=${GITHUB_REF#refs/tags/v} - fi - TAGS="${IMAGE_NAME}:${VERSION}" - if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - TAGS="$TAGS,${IMAGE_NAME}:latest" - fi - echo ::set-output name=tags::${TAGS} - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: dhi.io - username: ${{ secrets.DOCKER_USER }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Packages Docker Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build / push - uses: docker/build-push-action@v6 - with: - context: backend - file: ./backend/Dockerfile - push: ${{ github.ref == 'refs/heads/main' }} - tags: ${{ steps.prep.outputs.tags }} - - publish-frontend: - name: Publish rfd-fyi-frontend - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Prepare - id: prep - run: | - IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/rfd-fyi-frontend - VERSION=${GITHUB_REF##*/} - if [[ $GITHUB_REF == refs/heads/main ]]; then - VERSION=latest - fi - if [[ $GITHUB_REF == refs/tags/* ]]; then - VERSION=${GITHUB_REF#refs/tags/v} - fi - TAGS="${IMAGE_NAME}:${VERSION}" - if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - TAGS="$TAGS,${IMAGE_NAME}:latest" - fi - echo ::set-output name=tags::${TAGS} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Packages Docker Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build / push - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - push: ${{ github.ref == 'refs/heads/main' }} - tags: ${{ steps.prep.outputs.tags }} diff --git a/.gitignore b/.gitignore index 8388ebf..19b287b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules -dist +/dist data/ backend/bin/ .vscode diff --git a/Caddyfile b/Caddyfile deleted file mode 100644 index 9b85fe5..0000000 --- a/Caddyfile +++ /dev/null @@ -1,8 +0,0 @@ -{ - auto_https off -} - -:80 { - file_server - reverse_proxy /api/* rfd-fyi-backend:8080 -} diff --git a/Dockerfile b/Dockerfile index 4a2cc8f..b24e2a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,20 @@ -FROM dhi.io/node:25-debian13-dev as builder - +# Build frontend +FROM dhi.io/node:25-debian13-dev AS frontend-builder WORKDIR /app - COPY package*.json ./ - RUN npm install - COPY . . - RUN npm run build -FROM dhi.io/caddy:2 as runtime +# Build backend (with embedded frontend) +FROM dhi.io/golang:1.26-debian13-dev AS backend-builder +WORKDIR /src +COPY backend /src +COPY --from=frontend-builder /app/dist /src/dist +RUN CGO_ENABLED=0 go build -o rfd-fyi . -WORKDIR /my-site +FROM dhi.io/static:20251003-musl-alpine3.23 AS runtime +COPY --from=backend-builder /src/rfd-fyi /rfd-fyi -COPY --from=builder /app/dist ./ - -COPY Caddyfile /etc/caddy/Caddyfile +EXPOSE 8080 +ENTRYPOINT ["/rfd-fyi"] diff --git a/Makefile b/Makefile index cf411df..3f32fbc 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ help: ## backend: Build and run the backend from source backend: - @cd backend && CGO_ENABLED=0 go run . + @cd backend && CGO_ENABLED=0 HTTP_PORT=8080 go run . .PHONY: backend ## frontend: Build and run the frontend from source @@ -23,20 +23,21 @@ frontend: @npm run serve .PHONY: frontend -## dev: Build and run in docker compose +## dev: Build and run in Docker dev: - docker compose up --build -d + docker build -t rfd-fyi:dev . + docker run -d --name rfd-fyi-dev -p 8080:8080 rfd-fyi:dev .PHONY: dev -## prod: Run the latest images in docker compose +## prod: Run the latest image in Docker prod: @git pull - @docker pull ghcr.io/davegallant/rfd-fyi-backend - @docker pull ghcr.io/davegallant/rfd-fyi-frontend - @docker compose -f docker-compose.prod.yml up -d + @docker pull ghcr.io/davegallant/rfd-fyi + @docker run -d --name rfd-fyi-prod -p 8080:8080 ghcr.io/davegallant/rfd-fyi .PHONY: prod -## teardown: Teardown docker +## teardown: Teardown Docker teardown: - docker compose down + docker stop rfd-fyi-dev rfd-fyi-prod || true + docker rm rfd-fyi-dev rfd-fyi-prod || true .PHONY: teardown diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 794ea51..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM dhi.io/golang:1.26-debian13-dev AS build - -WORKDIR /src - -COPY . /src - -RUN CGO_ENABLED=0 go build -o backend . - -FROM dhi.io/static:20251003-musl-alpine3.23 - -COPY --from=build /src/backend /backend - -EXPOSE 8080 - -CMD [ "/backend" ] diff --git a/backend/app.go b/backend/app.go index 79391e8..79cd9cc 100644 --- a/backend/app.go +++ b/backend/app.go @@ -1,9 +1,11 @@ package main import ( + "embed" "encoding/json" "fmt" "io" + "io/fs" "math/rand/v2" "net/http" "net/url" @@ -17,20 +19,8 @@ import ( "github.com/gorilla/mux" ) -// @title RFD FYI API -// @version 1.0 -// @description An API for issue tracking -// @termsOfService http://swagger.io/terms/ - -// @contact.name API Support -// @contact.url https://linktr.ee/davegallant -// @contact.email davegallant@gmail.com - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @host localhost:8080 -// @BasePath /api/v1 +//go:embed dist/* +var frontendFS embed.FS type App struct { Router *mux.Router @@ -48,22 +38,45 @@ type Redirect struct { func (a *App) Initialize() { a.BasePath = "/api/v1" - a.Router = mux.NewRouter().PathPrefix(a.BasePath).Subrouter() - http.Handle("/", a.Router) - + a.Router = mux.NewRouter() a.initializeRoutes() } func (a *App) Run(httpPort string) { log.Info().Msgf("Serving requests on port " + httpPort) - if err := http.ListenAndServe(fmt.Sprintf(":"+httpPort), nil); err != nil { + if err := http.ListenAndServe(fmt.Sprintf(":%s", httpPort), a.Router); err != nil { panic(err) } } func (a *App) initializeRoutes() { - a.Router.HandleFunc("/topics", a.listTopics).Methods("GET") - a.Router.HandleFunc("/topics/{id}", a.getTopicDetails).Methods("GET") + a.Router.HandleFunc(a.BasePath+"/topics", a.listTopics).Methods("GET") + a.Router.HandleFunc(a.BasePath+"/topics/{id}", a.getTopicDetails).Methods("GET") + + distFS, err := fs.Sub(frontendFS, "dist") + if err != nil { + panic(err) + } + fileServer := http.FileServer(http.FS(distFS)) + a.Router.PathPrefix("/").Handler(spaHandler{staticHandler: fileServer, staticFS: distFS}) +} + +// spaHandler serves static files when they exist, otherwise falls back to +// index.html so that client-side routing works. +type spaHandler struct { + staticHandler http.Handler + staticFS fs.FS +} + +func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/") + if path == "" { + path = "index.html" + } + if _, err := fs.Stat(h.staticFS, path); err != nil { + r.URL.Path = "/" + } + h.staticHandler.ServeHTTP(w, r) } // func respondWithError(w http.ResponseWriter, code int, message string) { diff --git a/backend/dist/index.html b/backend/dist/index.html new file mode 100644 index 0000000..f70b046 --- /dev/null +++ b/backend/dist/index.html @@ -0,0 +1,6 @@ + + + +

Run the Vite dev server for the frontend.

+ + diff --git a/backend/main.go b/backend/main.go index 005388b..64103ed 100644 --- a/backend/main.go +++ b/backend/main.go @@ -11,21 +11,6 @@ import ( utils "github.com/davegallant/rfd-fyi/pkg/utils" ) -// @title RFD FYI API -// @version 1.0 -// @description An API for an issue tracking service -// @termsOfService http://swagger.io/terms/ - -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @host localhost:8080 -// @BasePath /api/v1 - func main() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 269283e..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,16 +0,0 @@ -services: - frontend: - image: ghcr.io/davegallant/rfd-fyi-frontend - ports: - - 80:80 - - 443:443 - links: - - "backend:backend" - volumes: - - ./data:/data - restart: always - backend: - image: ghcr.io/davegallant/rfd-fyi-backend - ports: - - 8080:8080 - restart: always diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ffb929e..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -services: - frontend: - build: - dockerfile: Dockerfile - context: . - ports: - - 80:80 - - 443:443 - links: - - "backend:backend" - volumes: - - ./data:/data - backend: - build: - dockerfile: Dockerfile - context: backend - ports: - - 8080:8080