diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5896a31..7c4a5a2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,15 +2,8 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile { "name": "Base Gui", - "build": { - // Sets the run context to one level up instead of the .devcontainer folder. - "context": "..", - // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. - "dockerfile": "../Dockerfile", - "args": { - "OS": "debian:bookworm" - } - }, + // Build using `docker buildx build dev` if image can't be found. + "image": "local/base-gui:dev", // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers/features/git:1": {}, @@ -19,11 +12,29 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ 4000 - ] + ], // Uncomment the next line to run commands after the container is created. // "postCreateCommand": "cat /etc/os-release", // Configure tool-specific properties. - // "customizations": {}, + "mounts": [ + { + "source": "${localEnv:HOME}${localEnv:USERPROFILE}/.claude.json", + "target": "/home/vscode/.claude.json", + "type": "bind" + }, + { + "source": "${localEnv:HOME}${localEnv:USERPROFILE}/.claude", + "target": "/home/vscode/.claude", + "type": "bind" + } + ], + "customizations": { + "vscode": { + "extensions": [ + "Anthropic.claude-code" + ] + } + } // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "devcontainer" } diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index ac0e62e..6431bf6 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -3,85 +3,133 @@ name: Docker on: push: branches: - - master # Will be `dev` + - master tags: - - v* # Publish `v1.2.3` tags as releases. - - pull_request: # Run tests for any PRs. - + - v* jobs: - test: # See also https://docs.docker.com/docker-hub/builds/automated-testing/ + prepare: runs-on: ubuntu-latest - + outputs: + matrix: ${{ steps.filter.outputs.matrix }} + targets: ${{ steps.targets.outputs.targets }} steps: - - uses: actions/checkout@v2 - - - name: Run tests + - + name: Checkout + uses: actions/checkout@v4 + - + name: Generate matrix + id: generate + uses: docker/bake-action/subaction/matrix@v6 + with: + fields: target,platforms + - + name: Filter matrix + id: filter + run: | + matrix=$(echo '${{ steps.generate.outputs.matrix }}' | \ + jq -c '[.[] | select(.target | IN("novnc", "websockify", "dev") | not)]') + echo "matrix=$matrix" >> $GITHUB_OUTPUT + - + name: Extract unique targets + id: targets run: | - if [ -f docker-compose.test.yml ]; then - docker-compose --file docker-compose.test.yml build - docker-compose --file docker-compose.test.yml run sut - else - docker build --build-arg OS="debian:trixie-slim" --file Dockerfile . - fi + targets=$(echo '${{ steps.filter.outputs.matrix }}' | \ + jq -c '[.[].target] | unique') + echo "targets=$targets" >> $GITHUB_OUTPUT build: - runs-on: ubuntu-latest - - needs: test # Ensure test job passes before pushing image. - if: github.event_name == 'push' - permissions: - contents: read - packages: write - + runs-on: ${{ startsWith(matrix.platforms, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + needs: + - prepare strategy: + fail-fast: false matrix: - os: [bookworm-slim, bookworm, trixie, trixie-slim] - include: - - os: bookworm-slim - image: debian:bookworm-slim - - os: bookworm - image: debian:bookworm - - os: trixie-slim - image: debian:trixie-slim - - os: trixie - image: debian:trixie - + include: ${{ fromJson(needs.prepare.outputs.matrix) }} steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Dockerhub Login - uses: docker/login-action@v1.10.0 + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Dockerhub Login + uses: docker/login-action@v3 with: username: max06net password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push by digest + id: build + uses: docker/bake-action@v6 + with: + targets: ${{ matrix.target }} + set: | + ${{ matrix.target }}.platform=${{ matrix.platforms }} + ${{ matrix.target }}.tags=docker.io/max06net/base-gui + ${{ matrix.target }}.output=type=image,push-by-digest=true,name-canonical=true,push=true + provenance: false + - + name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest=$(echo '${{ steps.build.outputs.metadata }}' | jq -r '."${{ matrix.target }}"."containerimage.digest"') + touch "${{ runner.temp }}/digests/${digest#sha256:}" + - + name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.target }}-${{ startsWith(matrix.platforms, 'linux/arm') && 'arm64' || 'amd64' }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 - - name: Docker meta + merge: + runs-on: ubuntu-latest + needs: + - prepare + - build + strategy: + fail-fast: false + matrix: + target: ${{ fromJson(needs.prepare.outputs.targets) }} + steps: + - + name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-${{ matrix.target }}-* + merge-multiple: true + - + name: Dockerhub Login + uses: docker/login-action@v3 + with: + username: max06net + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Docker meta id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@v5 with: - images: max06net/base-gui + images: docker.io/max06net/base-gui tags: | - type=semver,pattern={{version}}-${{ matrix.os }} - type=semver,pattern={{major}}.{{minor}}-${{ matrix.os }} - type=semver,pattern={{major}}-${{ matrix.os }} - type=edge,branch=master,suffix=-${{ matrix.os }} - - - name: Build and push Docker images - uses: docker/build-push-action@v4 - with: - build-args: | - OS=${{ !matrix.image && matrix.os || matrix.image }} - # platforms: # optional - pull: true - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - # env: - # OS: ${{ matrix.os }} + type=semver,pattern={{version}},suffix=-${{ matrix.target }} + type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.target }} + type=semver,pattern={{major}},suffix=-${{ matrix.target }} + type=edge,branch=master,suffix=-${{ matrix.target }} + - + name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'docker.io/max06net/base-gui@sha256:%s ' *) + - + name: Inspect image + run: | + docker buildx imagetools inspect docker.io/max06net/base-gui:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..d365cb8 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,144 @@ +name: PR + +on: + pull_request: + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.filter.outputs.matrix }} + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Generate matrix + id: generate + uses: docker/bake-action/subaction/matrix@v6 + with: + fields: target,platforms + - + name: Filter matrix + id: filter + run: | + matrix=$(echo '${{ steps.generate.outputs.matrix }}' | \ + jq -c '[.[] | select(.target | IN("novnc", "websockify", "dev") | not)]') + echo "matrix=$matrix" >> $GITHUB_OUTPUT + + validate: + runs-on: ${{ startsWith(matrix.platforms, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + needs: + - prepare + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.prepare.outputs.matrix) }} + steps: + - + name: Checkout + uses: actions/checkout@v4 + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - + name: Dockerhub Login + uses: docker/login-action@v3 + with: + username: max06net + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - + name: Build and push by digest + id: build + if: endsWith(matrix.target, 'trixie-slim') + uses: docker/bake-action@v6 + with: + targets: ${{ matrix.target }} + set: | + ${{ matrix.target }}.platform=${{ matrix.platforms }} + ${{ matrix.target }}.tags=docker.io/max06net/base-gui + ${{ matrix.target }}.output=type=image,push-by-digest=true,name-canonical=true,push=true + provenance: false + + - + name: Build (cache only) + if: "!endsWith(matrix.target, 'trixie-slim')" + uses: docker/bake-action@v6 + with: + targets: ${{ matrix.target }} + set: | + ${{ matrix.target }}.platform=${{ matrix.platforms }} + ${{ matrix.target }}.output=type=cacheonly + + - + name: Export digest + if: endsWith(matrix.target, 'trixie-slim') + run: | + mkdir -p ${{ runner.temp }}/digests + digest=$(echo '${{ steps.build.outputs.metadata }}' | jq -r '."${{ matrix.target }}"."containerimage.digest"') + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - + name: Upload digest + if: endsWith(matrix.target, 'trixie-slim') + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.target }}-${{ startsWith(matrix.platforms, 'linux/arm') && 'arm64' || 'amd64' }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - prepare + - validate + strategy: + fail-fast: false + matrix: + target: + - trixie-slim + steps: + - + name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-${{ matrix.target }}-* + merge-multiple: true + + - + name: Dockerhub Login + uses: docker/login-action@v3 + with: + username: max06net + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - + name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: docker.io/max06net/base-gui + tags: | + type=ref,prefix=pr-,suffix=-${{ matrix.target }},event=pr + + - + name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'docker.io/max06net/base-gui@sha256:%s ' *) + + - + name: Inspect image + run: | + docker buildx imagetools inspect docker.io/max06net/base-gui:${{ steps.meta.outputs.version }} diff --git a/Dockerfile b/Dockerfile index 4e0cddd..1beb5e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,78 +1,60 @@ -ARG OS=debian:bookworm-slim +# syntax=docker/dockerfile:1.19 +# check=skip=SecretsUsedInArgOrEnv -# Multistage build +# The base-image to be used +ARG OS=debian:trixie-slim -# Stage 1: Get novnc -FROM bitnami/git:latest as novnc - -ARG NOVNC_VERSION=v1.5.0 - -WORKDIR /app -RUN git clone --depth 1 --branch ${NOVNC_VERSION} https://github.com/novnc/novnc - -# Stage 2: Final image +# The build starts here. FROM $OS -LABEL Name=base-gui Author=max06/base-gui Flavor=$OS - -ARG PKG="apt-get install --no-install-recommends -y --ignore-missing" - -ENV LANG en_US.UTF-8 -ENV LC_ALL ${LANG} +ARG OS -ENV USER_ID=1000 GROUP_ID=1000 -ENV APP=unknown -ENV PASSWORD="" -ENV UMASK=0022 -ENV ALLOW_DIRECT_VNC=false - -# Prepare installation -RUN apt-get -q update -RUN LC_ALL=C DEBIAN_FRONTEND=noninteractive ${PKG} lsb-release - -# Install all the stuff -RUN LC_ALL=C DEBIAN_FRONTEND=noninteractive ${PKG} \ - gosu \ - locales \ - openbox \ - supervisor \ - tigervnc-common \ - tigervnc-tools \ - tigervnc-standalone-server \ - tint2 \ - python3-pip \ - python3-venv \ - nginx-light - -# Cleanup -RUN apt-get clean && \ - apt-get autoremove && \ - rm -rf /var/lib/apt/lists/* && \ - rm -rf /var/cache/fontconfig/* - -# novnc installation and patching -COPY --from=novnc /app/novnc/ /opt/novnc/ -RUN sed -i "s/UI.initSetting('resize', 'off')/UI.initSetting('resize', 'remote')/g" /opt/novnc/app/ui.js +LABEL Name=base-gui Author=max06/base-gui Flavor=$OS +# Container Language +ENV LANG=en_US.UTF-8 +ENV LC_ALL=${LANG} + +# App options +ENV USER_ID=1000 GROUP_ID=1000 APP=unknown UMASK=0022 PASSWORD="" ALLOW_DIRECT_VNC=false + +# Configure apt and install required packages +RUN \ + rm -f /etc/apt/apt.conf.d/docker-clean; \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +RUN \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt -q update && \ + LC_ALL=C DEBIAN_FRONTEND=noninteractive apt install --no-install-recommends -y --ignore-missing \ + gosu \ + locales \ + openbox \ + supervisor \ + tigervnc-common \ + tigervnc-tools \ + tigervnc-standalone-server \ + tint2 \ + ca-certificates \ + python3-minimal \ + python3-setuptools \ + nginx-light && \ + rm /var/cache/fontconfig/* + +# novnc installation +COPY --from=novnc /tmp/ /opt/novnc/ # install websockify -RUN python3 -m venv /opt/websockify -RUN /opt/websockify/bin/pip3 install websockify - -# prepare nginx -COPY localfs/nginx/vnc.conf /etc/nginx/sites-enabled/vnc.conf +RUN --mount=type=bind,from=websockify,source=/tmp,target=/tmp/websockify,rw \ + cd /tmp/websockify && \ + python3 setup.py install -# Place our own configuration files -COPY localfs/supervisord.conf /etc/ -COPY localfs/startup.sh /usr/local/bin/ -COPY localfs/tint2rc /etc/xdg/tint2/ -COPY localfs/openbox-autostart /etc/xdg/openbox/autostart +# Copy configuration files and scripts +COPY localfs/ / RUN chmod +x /usr/local/bin/startup.sh # Add user to avoid root and create directories RUN mkdir -p /app /data - -# Set working dir WORKDIR /app # Web viewer diff --git a/README.md b/README.md new file mode 100644 index 0000000..93830eb --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +Available Options: + +At buildtime: +ARG OS=debian:trixie-slim +ARG NOVNC_VERSION=v1.6.0 +ENV LANG=en_US.UTF-8 + +At runtime: +ENV LANG=en_US.UTF-8 (inherited) +ENV USER_ID=1000 GROUP_ID=1000 +ENV APP=unknown +ENV UMASK=0022 +ENV PASSWORD="" +ENV ALLOW_DIRECT_VNC=false diff --git a/dependencies/novnc.Dockerfile b/dependencies/novnc.Dockerfile new file mode 100644 index 0000000..7b67c5a --- /dev/null +++ b/dependencies/novnc.Dockerfile @@ -0,0 +1,6 @@ +FROM alpine + +ARG VERSION + +ADD https://github.com/novnc/novnc.git#${VERSION} /tmp +RUN sed -i "s/UI.initSetting('resize', 'off')/UI.initSetting('resize', 'remote')/g" /tmp/app/ui.js diff --git a/dependencies/websockify.Dockerfile b/dependencies/websockify.Dockerfile new file mode 100644 index 0000000..42655e8 --- /dev/null +++ b/dependencies/websockify.Dockerfile @@ -0,0 +1,5 @@ +FROM alpine + +ARG VERSION + +ADD https://github.com/novnc/websockify.git#${VERSION} /tmp diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..3ac1c50 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,57 @@ + +group "default" { + targets = ["base-gui"] +} + +variable "TAG" { + default = "dev" +} + +target "novnc" { + args = { + VERSION = "v1.6.0" + } + + dockerfile = "./dependencies/novnc.Dockerfile" +} + +target "websockify" { + args = { + VERSION = "v0.13.0" + } + + dockerfile = "./dependencies/websockify.Dockerfile" +} + + +target "base-gui" { + name = "${base}" + + args = { + OS = "debian:${base}" + } + + matrix = { + base = ["bookworm", "bookworm-slim", "trixie", "trixie-slim"] + } + + platforms = ["linux/amd64", "linux/arm64"] + + contexts = { + novnc = "target:novnc" + websockify = "target:websockify" + } + + tags = [ + "max06net/base-gui:${TAG}-${base}" + ] + + output = ["type=cacheonly"] +} + +target "dev" { + inherits = ["trixie-slim"] + platforms = ["linux/amd64"] + tags = ["local/base-gui:dev"] + output = ["type=docker"] +} diff --git a/localfs/nginx/vnc.conf b/localfs/etc/nginx/sites-enabled/vnc.conf similarity index 100% rename from localfs/nginx/vnc.conf rename to localfs/etc/nginx/sites-enabled/vnc.conf diff --git a/localfs/supervisord.conf b/localfs/etc/supervisord.conf similarity index 97% rename from localfs/supervisord.conf rename to localfs/etc/supervisord.conf index b0cdc0e..21e6df7 100644 --- a/localfs/supervisord.conf +++ b/localfs/etc/supervisord.conf @@ -1,9 +1,9 @@ [supervisord] nodaemon=true pidfile=/tmp/supervisord.pid +logfile=/dev/null logfile_maxbytes=0 - [program:x11] priority=0 command=/usr/bin/Xtigervnc -desktop "%(ENV_APP)s" %(ENV_VNC_ARGS)s :0 diff --git a/localfs/openbox-autostart b/localfs/etc/xdg/openbox/autostart similarity index 100% rename from localfs/openbox-autostart rename to localfs/etc/xdg/openbox/autostart diff --git a/localfs/tint2rc b/localfs/etc/xdg/tint2/tint2rc similarity index 100% rename from localfs/tint2rc rename to localfs/etc/xdg/tint2/tint2rc diff --git a/localfs/startup.sh b/localfs/usr/local/bin/startup.sh similarity index 100% rename from localfs/startup.sh rename to localfs/usr/local/bin/startup.sh