From c2cf725819e2ff7205f7616ba79a7b7a99c52593 Mon Sep 17 00:00:00 2001 From: Skylar Grant Date: Thu, 15 Oct 2026 19:38:08 -0400 Subject: [PATCH] Add Docker image, example compose file and Gitea build workflow - Dockerfile: node:24-alpine, production deps only, runs as the node user, healthcheck on /healthz. No volume, because the server keeps no state. - docker-compose.yml: example for the VPS. It publishes no ports, since Docker's port publishing bypasses ufw and would skip NPM's IP access list. It joins NPM's network instead. - .gitea/workflows/docker.yml: on v*-prod* tags, builds on the self-hosted runner and pushes v0idf1sh/sdp-dashboard: and :latest using the DHUB_UNAME and DHUB_PASS secrets. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 12 +++++++++++ .gitea/workflows/docker.yml | 43 +++++++++++++++++++++++++++++++++++++ Dockerfile | 19 ++++++++++++++++ docker-compose.yml | 25 +++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..615d60f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitea +.vscode +node_modules +npm-debug.log* +.env +.env.* +docs +images +*.md +Dockerfile +docker-compose*.yml diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml new file mode 100644 index 0000000..f532d95 --- /dev/null +++ b/.gitea/workflows/docker.yml @@ -0,0 +1,43 @@ +# Builds the image and pushes it to Docker Hub when a production tag (e.g. v1.1.0-prod1) is pushed. +# Restarting the container on the VPS is done by hand for now. +name: Build and push image + +on: + push: + tags: + - 'v*-prod*' + +env: + IMAGE: v0idf1sh/sdp-dashboard + +jobs: + build: + runs-on: self-hosted + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Log in to Docker Hub + env: + DHUB_UNAME: ${{ secrets.DHUB_UNAME }} + DHUB_PASS: ${{ secrets.DHUB_PASS }} + run: echo "$DHUB_PASS" | docker login --username "$DHUB_UNAME" --password-stdin + + - name: Build + run: | + docker build \ + --pull \ + --label "org.opencontainers.image.version=${{ gitea.ref_name }}" \ + --label "org.opencontainers.image.revision=${{ gitea.sha }}" \ + -t "$IMAGE:${{ gitea.ref_name }}" \ + -t "$IMAGE:latest" \ + . + + - name: Push + run: | + docker push "$IMAGE:${{ gitea.ref_name }}" + docker push "$IMAGE:latest" + + - name: Log out + if: always() + run: docker logout diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..121bc23 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:24-alpine + +WORKDIR /app +ENV NODE_ENV=production + +# install dependencies first so code-only changes reuse this layer +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev && npm cache clean --force + +COPY src ./src + +# no volume: sessions live in each user's browser, so the container keeps no state +USER node +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1 + +CMD ["node", "src/server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..68185c4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +# Example compose file for the VPS. Copy src/env.example to .env next to this file and fill it in. +# Nginx Proxy Manager forwards the subdomain to sdp-dashboard:3000 and enforces the IP allow list. + +services: + sdp-dashboard: + image: v0idf1sh/sdp-dashboard:latest + container_name: sdp-dashboard + restart: unless-stopped + env_file: .env + environment: + # must match the redirect URI registered in the Zoho API console, minus /auth/callback + PUBLIC_URL: https://dashboard.example.com + # No published ports on purpose. Docker's port publishing bypasses ufw, so "3000:3000" would + # let anyone reach the app directly and skip NPM's access list. NPM reaches it over the + # shared network instead, using the container name as the forward hostname. + expose: + - "3000" + networks: + - proxy + +networks: + proxy: + # the network Nginx Proxy Manager's container is on; check with `docker network ls` + external: true + name: ${NPM_NETWORK:-npm_default}