1 Commits

Author SHA1 Message Date
octoturge e0b04ab5db web: build image in CI, pull it in Terraform instead of building locally
templates/web's docker_image resource used a `build` block, so every
first-use of a new Dockerfile hash triggered a from-scratch build
(including the ~15-20min Rust toolchain compile) right at `terraform
apply` time - i.e. while someone was waiting to create a workspace.

Adds a build-web-image job to coder-templates.yml that builds and pushes
git.octoturge.com/octo-tech/profiles-web:<dockerfile-sha1> to this
instance's container registry, tagged identically to what
docker_image.web now computes and pulls (no build block). provision now
depends on build-web-image so a template never gets pushed pointing at
an image that isn't there yet. Skips the build entirely if that tag's
already in the registry, so an unrelated templates/* change doesn't
pay any cost.

Runs on a new dedicated "docker-build" runner (profiles-web-build),
scoped to just this repo via a repo-level registration token, with
host Docker socket access - deliberately not added to the existing
shared runner-1, which has no such access and stays untouched. Repo is
public, so the pulled image needs no registry auth; the push does, via
a new GITEA_PACKAGE_TOKEN repo secret (write:package scope).

Since CI and this Coder deployment share the same Docker daemon, the
"pull" is normally a same-host cache hit, not a real network pull.

Verified: `terraform validate` passes against the updated
templates/web/main.tf (run directly inside the coder-server container,
which has terraform embedded).
2026-08-27 01:10:02 +02:00
20 changed files with 152 additions and 331 deletions
+38 -149
View File
@@ -2,52 +2,30 @@ name: Provision Coder Templates
# Keeps Coder templates in sync with templates/*/ in this repo: # Keeps Coder templates in sync with templates/*/ in this repo:
# - every push to main pushes a new version of each templates/<env>/ dir # - every push to main pushes a new version of each templates/<env>/ dir
# whose VERSION file names a version not already pushed (coder templates # (coder templates push creates it if it doesn't exist yet, so adding a
# push creates the template if it doesn't exist yet, so adding a new # new templates/<env>/ directory is enough to provision a new one)
# templates/<env>/ directory - with a VERSION file - is enough to
# provision a new one). This workflow triggers on any change under
# templates/**, not just a specific template's own directory, so VERSION
# is what keeps an edit to one template from generating a no-op new
# version for every other, unchanged template.
# - if a templates/<env>/ directory is removed on main, its template is # - if a templates/<env>/ directory is removed on main, its template is
# deleted from Coder. `coder templates delete` refuses to delete a # deleted from Coder. `coder templates delete` refuses to delete a
# template that still has active workspaces, so this can't silently # template that still has active workspaces, so this can't silently
# orphan running workspaces - it just fails loudly and needs a human. # orphan running workspaces - it just fails loudly and needs a human.
# #
# Requires three repo/org secrets (Settings > Actions > Secrets): # Requires two repo/org secrets (Settings > Actions > Secrets):
# CODER_URL e.g. https://code.octoturge.com # CODER_URL e.g. https://code.octoturge.com
# CODER_SESSION_TOKEN a token from `coder tokens create`, ideally under a # CODER_SESSION_TOKEN a token from `coder tokens create`, ideally under a
# dedicated service account rather than a personal one # dedicated service account rather than a personal one
# PACKAGE_REGISTRY_TOKEN a Gitea access token (user Settings > Applications) # GITEA_PACKAGE_TOKEN a Gitea access token (user Settings > Applications)
# with write:package scope, for pushing each # with write:package scope, for pushing templates/web's
# Dockerfile-having template's image to this # image to this instance's container registry. Only
# instance's container registry. # the octoturge account's own token is used - login()
# hardcodes that username to match.
# #
# Not secrets.GITEA_TOKEN (Gitea Actions' built-in token): as of this # templates/web builds its Docker image here (build-web-image, on the
# writing it cannot authenticate to the container registry in any shipped # dedicated "docker-build" runner - see templates/web/main.tf for why: that
# Gitea version - `permissions: packages: write` is a no-op because the # runner is scoped to this repo only and has host Docker socket access that
# Actions token's package scope isn't wired up server-side yet (open since # the shared runner-1 deliberately doesn't). provision then just pulls the
# Gitea 1.19: https://github.com/go-gitea/gitea/issues/23642; fix in # tag build-web-image produced, instead of building it itself at
# https://github.com/go-gitea/gitea/pull/39070, not yet merged). Every # `terraform apply` time - keeps the slow Rust toolchain compile off of
# attempt fails at docker login with a plain "unauthorized", regardless of # "someone is waiting to create a workspace".
# the permissions: block or which account triggered the workflow. A
# manually-created PAT is the only thing that currently works. Named
# without a GITEA_ prefix because Gitea Actions reserves that prefix for
# its own built-in secrets and rejects creating one with that name.
#
# Any templates/<env>/ that has its own Dockerfile gets its image built and
# pushed here (build-images, on the dedicated "docker-build" runner - see
# templates/web/main.tf for why: that runner is scoped to this repo only and
# has host Docker socket access that the shared runner-1 deliberately
# doesn't). provision then just pulls the tag build-images produced, instead
# of building it itself at `terraform apply` time - keeps a slow toolchain
# compile off of "someone is waiting to create a workspace".
#
# Each template's image is built independently - one Dockerfile failing to
# build doesn't stop the others from building, and provision skips pushing
# only the specific template(s) whose image build failed this run (leaving
# their previous, already-working Coder template version in place) rather
# than skipping every template or pushing one with no matching image.
on: on:
push: push:
@@ -58,83 +36,34 @@ on:
workflow_dispatch: {} workflow_dispatch: {}
jobs: jobs:
build-images: build-web-image:
# The docker-build runner's docker_host: "" setting already auto-injects
# /var/run/docker.sock into job containers - an explicit
# container.volumes mount for the same path here fails at container
# creation with "Duplicate mount point: /var/run/docker.sock".
runs-on: docker-build runs-on: docker-build
# Currently a no-op (see the PACKAGE_REGISTRY_TOKEN note above) but container:
# harmless to declare now - once go-gitea/gitea#39070 ships, GITEA_TOKEN volumes:
# will need this to get package write access, so this is one less thing - /var/run/docker.sock:/var/run/docker.sock
# to remember when PACKAGE_REGISTRY_TOKEN can eventually be retired.
permissions:
packages: write
outputs:
failed_templates: ${{ steps.build.outputs.failed_templates }}
# docker:27-cli (Alpine) has no bash - only the POSIX /bin/sh (busybox
# ash) - but run: steps default to bash, which fails with "exec: bash:
# executable file not found in $PATH". Both run: steps below are plain
# POSIX shell already, so just run them under sh.
defaults:
run:
shell: sh
steps: steps:
# actions/checkout is a JS action and needs Node in the job container -
# this job runs in docker:27-cli (Alpine, just the Docker CLI) so it has
# no Node, and actions/checkout fails immediately with "exec: node:
# executable file not found in $PATH". Alpine does have apk/git though,
# so clone directly instead.
- name: Checkout - name: Checkout
uses: actions/checkout@v4
- name: Build and push templates/web's image (skips if the tag already exists)
run: | run: |
set -e set -e
apk add --no-cache git TAG="$(sha1sum templates/web/Dockerfile | cut -d' ' -f1)"
git clone --depth 1 --branch "${{ github.ref_name }}" "${{ github.server_url }}/${{ github.repository }}.git" . IMAGE="git.octoturge.com/octo-tech/profiles-web:${TAG}"
echo "${{ secrets.GITEA_PACKAGE_TOKEN }}" | docker login git.octoturge.com -u octoturge --password-stdin
- name: Build and push every template's image (skips a tag that's already in the registry) if docker manifest inspect "$IMAGE" >/dev/null 2>&1; then
id: build echo "$IMAGE already in the registry (Dockerfile unchanged), skipping build."
run: | exit 0
set -e fi
echo "${{ secrets.PACKAGE_REGISTRY_TOKEN }}" | docker login git.octoturge.com -u octoturge --password-stdin docker build -t "$IMAGE" templates/web
docker push "$IMAGE"
FAILED=""
for dockerfile in templates/*/Dockerfile; do
[ -e "$dockerfile" ] || continue
dir="$(dirname "$dockerfile")"
name="$(basename "$dir")"
TAG="$(sha1sum "$dockerfile" | cut -d' ' -f1)"
IMAGE="git.octoturge.com/octo-tech/profiles-${name}:${TAG}"
echo "::group::${name}"
if docker manifest inspect "$IMAGE" >/dev/null 2>&1; then
echo "$IMAGE already in the registry (Dockerfile unchanged), skipping build."
elif docker build -t "$IMAGE" "$dir" && docker push "$IMAGE"; then
echo "Built and pushed $IMAGE"
else
echo "::warning::Failed to build/push $IMAGE - templates/$name will be skipped this run."
FAILED="$FAILED $name"
fi
echo "::endgroup::"
done
echo "failed_templates=${FAILED# }" >> "$GITHUB_OUTPUT"
provision: provision:
# needs: build-images orders this after the image builds (so a fresh needs: build-web-image
# template push never points at a tag that isn't in the registry yet)
# without making every template's reprovisioning depend on ALL builds
# succeeding - if:always() overrides the default "skip if a dependency
# failed" behavior, since build-images only fails outright on an
# infra-level problem (e.g. registry login); a single template's build
# failure is reported via failed_templates instead and only skips that
# one template below.
needs: build-images
if: always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
CODER_URL: ${{ secrets.CODER_URL }} CODER_URL: ${{ secrets.CODER_URL }}
CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }} CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }}
FAILED_TEMPLATES: ${{ needs.build-images.outputs.failed_templates }}
steps: steps:
- name: Checkout (full history, needed to detect removed templates) - name: Checkout (full history, needed to detect removed templates)
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -151,50 +80,10 @@ jobs:
run: | run: |
set -e set -e
for dir in templates/*/; do for dir in templates/*/; do
name="$(basename "$dir")" name="profiles-$(basename "$dir")"
full="profiles-$name" echo "::group::Pushing $name from $dir"
case " $FAILED_TEMPLATES " in coder templates push "$name" -d "$dir" --yes \
*" $name "*) -m "auto-provisioned from ${GITHUB_SHA:0:12}"
echo "::warning::Skipping $full - its Docker image failed to build this run (see build-images), leaving the previous template version in place."
continue
;;
esac
# templates/<name>/VERSION lets a template opt out of being
# reprovisioned on every push: bump it and coder templates push
# names the new version "v<N>". This is a manual contract, not a
# content hash - editing a template without bumping its VERSION
# means the change won't go out until someone does. paths: on
# this workflow's trigger is templates/** as a whole, so without
# this every template gets a new (identical) version on any push
# under templates/, even ones whose own directory didn't change.
#
# Rather than pre-checking `coder templates versions list` for
# whether v<N> already exists (fragile - depends on its exact
# JSON shape matching what we expect), just attempt the push and
# treat its specific "version already exists" failure as the
# skip signal instead.
version=""
if [ -f "$dir/VERSION" ]; then
version="$(tr -d '[:space:]' < "$dir/VERSION")"
fi
args=(-d "$dir" --yes -m "auto-provisioned from ${GITHUB_SHA:0:12}")
[ -n "$version" ] && args+=(--name "v$version")
echo "::group::Pushing $full from $dir"
if push_output="$(coder templates push "$full" "${args[@]}" 2>&1)"; then
echo "$push_output"
else
push_status=$?
echo "$push_output"
if [ -n "$version" ] && printf '%s' "$push_output" | grep -qF "A template version with name \"v$version\" already exists"; then
echo "Version v$version (templates/$name/VERSION) is already pushed - nothing to do. Bump the VERSION file to push a new one."
else
echo "::endgroup::"
exit "$push_status"
fi
fi
echo "::endgroup::" echo "::endgroup::"
done done
-1
View File
@@ -1 +0,0 @@
2
+16 -9
View File
@@ -143,11 +143,12 @@ else
fi fi
fi fi
fi fi
# `tea whoami` succeeds regardless of how the login was done (personal TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
# access token or OAuth) - more reliable than parsing tea's own if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
# config.yml, whose indentation and fields (no plaintext `token:` at all TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
# for an OAuth login) vary by auth method. TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1 [ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
fi
# --- SSH + GPG keys for the external git host(s) selected above --- # --- SSH + GPG keys for the external git host(s) selected above ---
# Only asks if the user actually set up GitHub and/or Gitea just now - # Only asks if the user actually set up GitHub and/or Gitea just now -
@@ -185,10 +186,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/dev/null 2>&1; then SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
echo "SSH key added to Gitea." echo "SSH key added to Gitea."
else else
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually with: tea ssh-keys add ${SSH_KEY}.pub" echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
@@ -222,10 +226,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/dev/null 2>&1; then GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
echo "GPG key added to Gitea." echo "GPG key added to Gitea."
else else
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-" echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
fi fi
-7
View File
@@ -204,13 +204,6 @@ resource "docker_container" "workspace" {
host = "code.octoturge.com" host = "code.octoturge.com"
ip = "host-gateway" ip = "host-gateway"
} }
# install-skills.sh curls this at startup to pull the repo's Agent Skills;
# without this the workspace's public DNS answer for git.octoturge.com
# NAT-hairpins back to the LAN and times out (curl: (28)).
host {
host = "git.octoturge.com"
ip = "host-gateway"
}
volumes { volumes {
container_path = "/home/coder" container_path = "/home/coder"
volume_name = docker_volume.home_volume.name volume_name = docker_volume.home_volume.name
-1
View File
@@ -1 +0,0 @@
2
+16 -9
View File
@@ -143,11 +143,12 @@ else
fi fi
fi fi
fi fi
# `tea whoami` succeeds regardless of how the login was done (personal TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
# access token or OAuth) - more reliable than parsing tea's own if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
# config.yml, whose indentation and fields (no plaintext `token:` at all TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
# for an OAuth login) vary by auth method. TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1 [ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
fi
# --- SSH + GPG keys for the external git host(s) selected above --- # --- SSH + GPG keys for the external git host(s) selected above ---
# Only asks if the user actually set up GitHub and/or Gitea just now - # Only asks if the user actually set up GitHub and/or Gitea just now -
@@ -185,10 +186,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/dev/null 2>&1; then SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
echo "SSH key added to Gitea." echo "SSH key added to Gitea."
else else
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually with: tea ssh-keys add ${SSH_KEY}.pub" echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
@@ -222,10 +226,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/dev/null 2>&1; then GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
echo "GPG key added to Gitea." echo "GPG key added to Gitea."
else else
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-" echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
fi fi
-7
View File
@@ -204,13 +204,6 @@ resource "docker_container" "workspace" {
host = "code.octoturge.com" host = "code.octoturge.com"
ip = "host-gateway" ip = "host-gateway"
} }
# install-skills.sh curls this at startup to pull the repo's Agent Skills;
# without this the workspace's public DNS answer for git.octoturge.com
# NAT-hairpins back to the LAN and times out (curl: (28)).
host {
host = "git.octoturge.com"
ip = "host-gateway"
}
volumes { volumes {
container_path = "/home/coder" container_path = "/home/coder"
volume_name = docker_volume.home_volume.name volume_name = docker_volume.home_volume.name
-1
View File
@@ -1 +0,0 @@
2
+16 -9
View File
@@ -143,11 +143,12 @@ else
fi fi
fi fi
fi fi
# `tea whoami` succeeds regardless of how the login was done (personal TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
# access token or OAuth) - more reliable than parsing tea's own if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
# config.yml, whose indentation and fields (no plaintext `token:` at all TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
# for an OAuth login) vary by auth method. TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1 [ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
fi
# --- SSH + GPG keys for the external git host(s) selected above --- # --- SSH + GPG keys for the external git host(s) selected above ---
# Only asks if the user actually set up GitHub and/or Gitea just now - # Only asks if the user actually set up GitHub and/or Gitea just now -
@@ -185,10 +186,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/dev/null 2>&1; then SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
echo "SSH key added to Gitea." echo "SSH key added to Gitea."
else else
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually with: tea ssh-keys add ${SSH_KEY}.pub" echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
@@ -222,10 +226,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/dev/null 2>&1; then GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
echo "GPG key added to Gitea." echo "GPG key added to Gitea."
else else
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-" echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
fi fi
-7
View File
@@ -204,13 +204,6 @@ resource "docker_container" "workspace" {
host = "code.octoturge.com" host = "code.octoturge.com"
ip = "host-gateway" ip = "host-gateway"
} }
# install-skills.sh curls this at startup to pull the repo's Agent Skills;
# without this the workspace's public DNS answer for git.octoturge.com
# NAT-hairpins back to the LAN and times out (curl: (28)).
host {
host = "git.octoturge.com"
ip = "host-gateway"
}
volumes { volumes {
container_path = "/home/coder" container_path = "/home/coder"
volume_name = docker_volume.home_volume.name volume_name = docker_volume.home_volume.name
-1
View File
@@ -1 +0,0 @@
2
+16 -9
View File
@@ -143,11 +143,12 @@ else
fi fi
fi fi
fi fi
# `tea whoami` succeeds regardless of how the login was done (personal TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
# access token or OAuth) - more reliable than parsing tea's own if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
# config.yml, whose indentation and fields (no plaintext `token:` at all TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
# for an OAuth login) vary by auth method. TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1 [ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
fi
# --- SSH + GPG keys for the external git host(s) selected above --- # --- SSH + GPG keys for the external git host(s) selected above ---
# Only asks if the user actually set up GitHub and/or Gitea just now - # Only asks if the user actually set up GitHub and/or Gitea just now -
@@ -185,10 +186,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/dev/null 2>&1; then SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
echo "SSH key added to Gitea." echo "SSH key added to Gitea."
else else
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually with: tea ssh-keys add ${SSH_KEY}.pub" echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
@@ -222,10 +226,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/dev/null 2>&1; then GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
echo "GPG key added to Gitea." echo "GPG key added to Gitea."
else else
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-" echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
fi fi
-7
View File
@@ -204,13 +204,6 @@ resource "docker_container" "workspace" {
host = "code.octoturge.com" host = "code.octoturge.com"
ip = "host-gateway" ip = "host-gateway"
} }
# install-skills.sh curls this at startup to pull the repo's Agent Skills;
# without this the workspace's public DNS answer for git.octoturge.com
# NAT-hairpins back to the LAN and times out (curl: (28)).
host {
host = "git.octoturge.com"
ip = "host-gateway"
}
volumes { volumes {
container_path = "/home/coder" container_path = "/home/coder"
volume_name = docker_volume.home_volume.name volume_name = docker_volume.home_volume.name
-1
View File
@@ -1 +0,0 @@
2
+16 -9
View File
@@ -143,11 +143,12 @@ else
fi fi
fi fi
fi fi
# `tea whoami` succeeds regardless of how the login was done (personal TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
# access token or OAuth) - more reliable than parsing tea's own if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
# config.yml, whose indentation and fields (no plaintext `token:` at all TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
# for an OAuth login) vary by auth method. TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1 [ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
fi
# --- SSH + GPG keys for the external git host(s) selected above --- # --- SSH + GPG keys for the external git host(s) selected above ---
# Only asks if the user actually set up GitHub and/or Gitea just now - # Only asks if the user actually set up GitHub and/or Gitea just now -
@@ -185,10 +186,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/dev/null 2>&1; then SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
echo "SSH key added to Gitea." echo "SSH key added to Gitea."
else else
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually with: tea ssh-keys add ${SSH_KEY}.pub" echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
@@ -222,10 +226,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/dev/null 2>&1; then GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
echo "GPG key added to Gitea." echo "GPG key added to Gitea."
else else
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-" echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
fi fi
-7
View File
@@ -204,13 +204,6 @@ resource "docker_container" "workspace" {
host = "code.octoturge.com" host = "code.octoturge.com"
ip = "host-gateway" ip = "host-gateway"
} }
# install-skills.sh curls this at startup to pull the repo's Agent Skills;
# without this the workspace's public DNS answer for git.octoturge.com
# NAT-hairpins back to the LAN and times out (curl: (28)).
host {
host = "git.octoturge.com"
ip = "host-gateway"
}
volumes { volumes {
container_path = "/home/coder" container_path = "/home/coder"
volume_name = docker_volume.home_volume.name volume_name = docker_volume.home_volume.name
+18 -80
View File
@@ -11,8 +11,7 @@ ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
locales sudo ca-certificates gnupg curl wget \ locales sudo ca-certificates gnupg curl wget \
&& locale-gen en_US.UTF-8 \ && locale-gen en_US.UTF-8 \
&& apt-get clean \ && rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
ENV LANG=en_US.UTF-8 \ ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \ LANGUAGE=en_US:en \
@@ -29,69 +28,30 @@ RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" \
> /etc/apt/sources.list.d/google-chrome.list \ > /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update && apt-get install -y google-chrome-stable \ && apt-get update && apt-get install -y google-chrome-stable \
&& apt-get clean \ && rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
# Core build toolchain, crypto/DB headers, Tauri 2 / WebKit GUI prerequisites, # Core build toolchain, crypto/DB headers, Tauri 2 / WebKit GUI prerequisites,
# X11 dev libs, DB CLI clients, Python + OpenCV, protobuf compiler. # X11 dev libs, DB CLI clients, Python + OpenCV, protobuf compiler.
#
# Split into several RUN steps (rather than one big apt-get install) so no
# single resulting layer is too large to push to the registry - it sits
# behind a reverse proxy with a request body size cap, and a couple of these
# packages (llvm, libopencv-dev, libwebkit2gtk-4.1-dev) are individually
# large enough to blow past it if lumped together with everything else.
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential pkg-config cmake \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
RUN apt-get update && apt-get install -y --no-install-recommends \
clang llvm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential pkg-config cmake clang llvm \
git git-lfs jq unzip tar file htop tree tmux zsh openssh-client \ git git-lfs jq unzip tar file htop tree tmux zsh openssh-client \
&& git lfs install --system \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
RUN apt-get update && apt-get install -y --no-install-recommends \
libssl-dev libpq-dev libsqlite3-dev \ libssl-dev libpq-dev libsqlite3-dev \
postgresql-client redis-tools sqlite3 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
RUN apt-get update && apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev \ libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev \
libgtk-3-dev libsoup-3.0-dev \ libgtk-3-dev libsoup-3.0-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
RUN apt-get update && apt-get install -y --no-install-recommends \
libx11-dev libxext-dev libxrender-dev libxtst-dev libxi-dev \ libx11-dev libxext-dev libxrender-dev libxtst-dev libxi-dev \
&& apt-get clean \ postgresql-client redis-tools sqlite3 \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv python3-dev \ python3 python3-pip python3-venv python3-dev \
protobuf-compiler \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
RUN apt-get update && apt-get install -y --no-install-recommends \
libopencv-dev \ libopencv-dev \
&& apt-get clean \ protobuf-compiler \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* && git lfs install --system \
&& rm -rf /var/lib/apt/lists/*
# Node.js LTS (22.x) plus npm/pnpm/yarn as root so global bins land on the # Node.js LTS (22.x) plus npm/pnpm/yarn as root so global bins land on the
# system PATH for every user. # system PATH for every user.
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \ && apt-get install -y --no-install-recommends nodejs \
&& apt-get clean \ && npm install -g pnpm yarn \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* && rm -rf /var/lib/apt/lists/*
RUN npm install -g pnpm yarn
# Global Python prototyping packages: CV, ONNX runtime, CPU-only torch wheel. # Global Python prototyping packages: CV, ONNX runtime, CPU-only torch wheel.
# Ubuntu 24.04's system Python is PEP 668 externally-managed; this is a # Ubuntu 24.04's system Python is PEP 668 externally-managed; this is a
@@ -101,14 +61,9 @@ RUN npm install -g pnpm yarn
# file (dpkg-installed, not pip-installed), so `pip install --upgrade pip` # file (dpkg-installed, not pip-installed), so `pip install --upgrade pip`
# fails trying to uninstall it in place - and it's unneeded anyway, the # fails trying to uninstall it in place - and it's unneeded anyway, the
# packages below install fine under the stock version. # packages below install fine under the stock version.
#
# Each package gets its own RUN/layer for the same reverse-proxy body-size
# reason as the apt-get split above - torch's CPU wheel and opencv-python's
# wheel are each large enough on their own to be worth isolating.
RUN python3 -m pip install --break-system-packages --no-cache-dir numpy
RUN python3 -m pip install --break-system-packages --no-cache-dir opencv-python-headless
RUN python3 -m pip install --break-system-packages --no-cache-dir onnxruntime
RUN python3 -m pip install --break-system-packages --no-cache-dir \ RUN python3 -m pip install --break-system-packages --no-cache-dir \
numpy opencv-python-headless onnxruntime \
&& python3 -m pip install --break-system-packages --no-cache-dir \
torch --index-url https://download.pytorch.org/whl/cpu torch --index-url https://download.pytorch.org/whl/cpu
# Standard non-root dev user with passwordless sudo. Ubuntu 24.04's base # Standard non-root dev user with passwordless sudo. Ubuntu 24.04's base
@@ -133,32 +88,15 @@ WORKDIR /home/coder
# Rust via rustup: stable toolchain, rust-analyzer/clippy/rustfmt/rust-src, # Rust via rustup: stable toolchain, rust-analyzer/clippy/rustfmt/rust-src,
# native + musl targets for x86_64/aarch64, and cargo helper utilities. # native + musl targets for x86_64/aarch64, and cargo helper utilities.
#
# Split into one RUN per target/tool (rather than one big chained command) so
# no single layer is too large to push to the registry, for the same
# reverse-proxy body-size reason as the apt-get split above - the base
# toolchain and each additional target's std library are each sizeable, and
# `cargo install` leaves a build/registry cache behind that needs clearing
# inside its own RUN, or it would just bloat that same layer instead.
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
--default-toolchain stable --profile default \ --default-toolchain stable --profile default \
&& rm -rf "$RUSTUP_HOME"/tmp "$RUSTUP_HOME"/downloads && rustup component add rustfmt clippy rust-analyzer rust-src \
&& rustup target add \
RUN rustup component add rustfmt clippy rust-analyzer rust-src x86_64-unknown-linux-gnu \
x86_64-unknown-linux-musl \
RUN rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu \
RUN rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl \
RUN rustup target add aarch64-unknown-linux-gnu && cargo install --locked cargo-watch cargo-edit cross bacon
RUN rustup target add aarch64-unknown-linux-musl
RUN cargo install --locked cargo-watch \
&& rm -rf "$CARGO_HOME"/registry "$CARGO_HOME"/git
RUN cargo install --locked cargo-edit \
&& rm -rf "$CARGO_HOME"/registry "$CARGO_HOME"/git
RUN cargo install --locked cross \
&& rm -rf "$CARGO_HOME"/registry "$CARGO_HOME"/git
RUN cargo install --locked bacon \
&& rm -rf "$CARGO_HOME"/registry "$CARGO_HOME"/git
# Bun: global runtime for the ElysiaJS backend and fast scripting. # Bun: global runtime for the ElysiaJS backend and fast scripting.
RUN curl -fsSL https://bun.sh/install | bash RUN curl -fsSL https://bun.sh/install | bash
-1
View File
@@ -1 +0,0 @@
2
+16 -9
View File
@@ -143,11 +143,12 @@ else
fi fi
fi fi
fi fi
# `tea whoami` succeeds regardless of how the login was done (personal TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
# access token or OAuth) - more reliable than parsing tea's own if command -v tea >/dev/null 2>&1 && [ -f "$TEA_CONFIG" ]; then
# config.yml, whose indentation and fields (no plaintext `token:` at all TEA_URL="$(awk '/^logins:/{f=1} f && /^ url:/{print $2; exit}' "$TEA_CONFIG")"
# for an OAuth login) vary by auth method. TEA_TOKEN="$(awk '/^logins:/{f=1} f && /^ token:/{print $2; exit}' "$TEA_CONFIG")"
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1 [ -n "$TEA_URL" ] && [ -n "$TEA_TOKEN" ] && DID_GITEA=1
fi
# --- SSH + GPG keys for the external git host(s) selected above --- # --- SSH + GPG keys for the external git host(s) selected above ---
# Only asks if the user actually set up GitHub and/or Gitea just now - # Only asks if the user actually set up GitHub and/or Gitea just now -
@@ -185,10 +186,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/dev/null 2>&1; then SSH_PUB_JSON="$(sed 's/\\/\\\\/g; s/"/\\"/g' "${SSH_KEY}.pub")"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"title\":\"coder-$(hostname)\",\"key\":\"${SSH_PUB_JSON}\"}" >/dev/null 2>&1; then
echo "SSH key added to Gitea." echo "SSH key added to Gitea."
else else
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually with: tea ssh-keys add ${SSH_KEY}.pub" echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
@@ -222,10 +226,13 @@ if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
fi fi
if [ "$DID_GITEA" -eq 1 ]; then if [ "$DID_GITEA" -eq 1 ]; then
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/dev/null 2>&1; then GPG_ARMORED_JSON="$(gpg --armor --export "$GPG_KEY_ID" | awk '{printf "%s\\n", $0}')"
if curl -fsS -X POST "${TEA_URL%/}/api/v1/user/gpg_keys" \
-H "Authorization: token ${TEA_TOKEN}" -H "Content-Type: application/json" \
-d "{\"armored_public_key\":\"${GPG_ARMORED_JSON}\"}" >/dev/null 2>&1; then
echo "GPG key added to Gitea." echo "GPG key added to Gitea."
else else
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-" echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually at: ${TEA_URL%/}/user/settings/keys"
fi fi
fi fi
fi fi
-7
View File
@@ -226,13 +226,6 @@ resource "docker_container" "workspace" {
host = "code.octoturge.com" host = "code.octoturge.com"
ip = "host-gateway" ip = "host-gateway"
} }
# install-skills.sh curls this at startup to pull the repo's Agent Skills;
# without this the workspace's public DNS answer for git.octoturge.com
# NAT-hairpins back to the LAN and times out (curl: (28)).
host {
host = "git.octoturge.com"
ip = "host-gateway"
}
volumes { volumes {
container_path = "/home/coder" container_path = "/home/coder"
volume_name = docker_volume.home_volume.name volume_name = docker_volume.home_volume.name