├── src ├── base-alpine │ ├── .devcontainer │ │ ├── Dockerfile │ │ └── devcontainer.json │ └── README.md ├── base │ ├── README.md │ ├── test-project │ │ ├── test.sh │ │ └── test-utils.sh │ └── .devcontainer │ │ ├── Dockerfile │ │ └── devcontainer.json ├── base-debian │ ├── README.md │ └── .devcontainer │ │ ├── Dockerfile │ │ └── devcontainer.json └── me │ ├── .devcontainer │ ├── Dockerfile │ ├── scripts │ │ └── pnpm-shell-completion.sh │ └── devcontainer.json │ └── README.md ├── .vscode └── settings.json ├── .devcontainer └── devcontainer.json ├── .github ├── actions │ └── smoke-test │ │ ├── build.sh │ │ ├── test.sh │ │ ├── action.yaml │ │ ├── check-image-size.sh │ │ └── validate-tags.sh └── workflows │ ├── build-app.yml │ └── build.yml └── README.md /src/base-alpine/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT=3.22 2 | FROM alpine:${VARIANT} 3 | 4 | -------------------------------------------------------------------------------- /src/base/README.md: -------------------------------------------------------------------------------- 1 | # base 2 | 3 | 基于 的 devcontainer 镜像,添加了一些常用工具和配置 4 | -------------------------------------------------------------------------------- /src/base-alpine/README.md: -------------------------------------------------------------------------------- 1 | # base-alpine 2 | 3 | 基于 的 devcontainer 镜像,添加了一些常用工具和配置 4 | -------------------------------------------------------------------------------- /src/base-debian/README.md: -------------------------------------------------------------------------------- 1 | # base-debian 2 | 3 | 基于 的 devcontainer 镜像,添加了一些常用工具和配置 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "aliyuncs", 4 | "davidanson", 5 | "dearmor", 6 | "pkief", 7 | "shfmt", 8 | "trixie", 9 | "yazi", 10 | "zhuangtongfa" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ghcr.io/aliuq/devcontainer:me", 3 | "customizations": {}, 4 | "features": { 5 | "ghcr.io/aliuq/devcontainer-features/common:0": { 6 | // "MISE_GITHUB_TOKEN": "" // Set this variable if you hit rate limiting issues 7 | } 8 | }, 9 | "updateContentCommand": "npm install -g @devcontainers/cli" 10 | } 11 | -------------------------------------------------------------------------------- /src/me/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/aliuq/devcontainer:base 2 | 3 | COPY ./scripts /usr/local/share/ 4 | 5 | ENV PATH="/root/.bun/bin:${PATH}" 6 | 7 | RUN set -eux; export DEBIAN_FRONTEND=noninteractive; \ 8 | sudo apt update && sudo apt upgrade -y && \ 9 | sudo apt install -y --no-install-recommends \ 10 | vim && \ 11 | rm -rf /var/lib/apt/lists/* 12 | -------------------------------------------------------------------------------- /src/base-debian/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian version (use bullseye on local arm64/Apple Silicon): trixie, bookworm, bullseye, buster 2 | ARG VARIANT="trixie" 3 | FROM buildpack-deps:${VARIANT}-curl 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | -------------------------------------------------------------------------------- /src/base/test-project/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source test-utils.sh 6 | 7 | # git 8 | check "git installed" git --version 9 | # fzf 10 | check "fzf installed" fzf --version 11 | # eza 12 | check "eza installed" eza --version 13 | # zoxide 14 | check "zoxide installed" zoxide --version 15 | # mise 16 | check "mise installed" mise --version 17 | # starship 18 | check "starship not installed" bash -c 'if ! command -v starship &> /dev/null; then exit 0; else exit 1; fi' 19 | 20 | reportResults 21 | -------------------------------------------------------------------------------- /src/base/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT="noble" 2 | FROM buildpack-deps:${VARIANT}-curl 3 | 4 | ARG VARIANT 5 | RUN if [ "$VARIANT" = "noble" ]; then \ 6 | if id "ubuntu" &>/dev/null; then \ 7 | echo "Deleting user 'ubuntu' for $VARIANT" && userdel -f -r ubuntu || echo "Failed to delete ubuntu user for $VARIANT"; \ 8 | else \ 9 | echo "User 'ubuntu' does not exist for $VARIANT"; \ 10 | fi; \ 11 | fi 12 | 13 | # [Optional] Uncomment this section to install additional OS packages. 14 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 15 | # && apt-get -y install --no-install-recommends 16 | -------------------------------------------------------------------------------- /src/me/README.md: -------------------------------------------------------------------------------- 1 | # me 2 | 3 | 基于 的 devcontainer 镜像,添加了一些常用工具和配置 4 | 5 | ## QA 6 | 7 | ### 如果 `remoteUser` 不是 `root`,`postCreateCommand` 等生命周期中出现命令找不到问题 8 | 9 | 重新添加 `ghcr.io/aliuq/devcontainer-features/common:0` 特性,以修正权限问题 10 | 11 | ```json 12 | { 13 | "features": { 14 | "ghcr.io/aliuq/devcontainer-features/common:0": {} 15 | } 16 | } 17 | ``` 18 | 19 | ### pnpm shell 补全无法生效问题 20 | 21 | 默认是在 root 用户下执行的,需要执行补全版本,或者手动执行 `/usr/local/share/pnpm-shell-completion.sh` 脚本 22 | 23 | ```json 24 | { 25 | "postCreateCommand": { 26 | "setup pnpm-shell-completion": "/usr/local/share/pnpm-shell-completion.sh" 27 | } 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /.github/actions/smoke-test/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | IMAGE="$1" 3 | VALIDATE_TAGS="${INPUT_VALIDATE_TAGS:-false}" 4 | 5 | set -e 6 | 7 | export DOCKER_BUILDKIT=1 8 | # echo "(*) Installing @devcontainer/cli" 9 | # npm install -g @devcontainers/cli 10 | 11 | # Validate base image tags before building (if enabled) 12 | if [[ "$VALIDATE_TAGS" == "true" ]]; then 13 | echo "(*) Validating base image tags for ${IMAGE}..." 14 | "$(dirname "$0")/validate-tags.sh" "$IMAGE" 15 | else 16 | echo "(*) Skipping tag validation (validate-tags=false)" 17 | fi 18 | 19 | id_label="test-container=${IMAGE}" 20 | id_image="${IMAGE}-test-image" 21 | echo "(*) Building image - ${IMAGE}" # --no-cache 22 | BUILDKIT_PROGRESS=plain devcontainer build --image-name ${id_image} --workspace-folder "src/${IMAGE}/" 23 | echo "(*) Starting container - ${IMAGE}" 24 | devcontainer up --id-label ${id_label} --workspace-folder "src/${IMAGE}/" 25 | 26 | -------------------------------------------------------------------------------- /.github/actions/smoke-test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | IMAGE="$1" 3 | THRESHOLD_IN_GB="$2" 4 | 5 | source $(pwd)/.github/actions/smoke-test/check-image-size.sh 6 | 7 | export DOCKER_BUILDKIT=1 8 | set -e 9 | 10 | # Run actual test 11 | echo "(*) Running test..." 12 | id_label="test-container=${IMAGE}" 13 | id_image="${IMAGE}-test-image" 14 | devcontainer exec --workspace-folder $(pwd)/src/$IMAGE --id-label ${id_label} /bin/sh -c 'set -e && if [ -f "test-project/test.sh" ]; then cd test-project && if [ "$(id -u)" = "0" ]; then chmod +x test.sh; else sudo chmod +x test.sh; fi && ./test.sh; else ls -a; fi' 15 | 16 | echo "(*) Docker image details..." 17 | docker images --filter=reference="${id_image}" 18 | # Checking size of universal image 19 | 20 | if [ $IMAGE == "universal" ]; then 21 | check_image_size $IMAGE $THRESHOLD_IN_GB $id_image 22 | fi 23 | 24 | # Clean up 25 | docker rm -f $(docker container ls -f "label=${id_label}" -q) 26 | -------------------------------------------------------------------------------- /src/base-alpine/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "./Dockerfile", 4 | "context": "." 5 | }, 6 | "features": { 7 | "ghcr.io/devcontainers/features/common-utils:2": { 8 | "installZsh": "true", 9 | "configureZshAsDefaultShell": "true", 10 | "installOhMyZsh": "true", 11 | "installOhMyZshConfig": "true", 12 | "upgradePackages": "true" 13 | }, 14 | "ghcr.io/aliuq/devcontainer-features/common:0": { 15 | "defaultShell": "zsh", 16 | "installEza": "true", 17 | "installFzf": "true", 18 | "installZoxide": "true", 19 | "installMise": "true" 20 | } 21 | } 22 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 23 | // "forwardPorts": [], 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | // "postCreateCommand": "uname -a", 26 | // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 27 | // "remoteUser": "vscode" 28 | } 29 | -------------------------------------------------------------------------------- /src/base-debian/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "./Dockerfile", 4 | "context": "." 5 | }, 6 | "features": { 7 | "ghcr.io/devcontainers/features/common-utils:2": { 8 | "installZsh": "true", 9 | "configureZshAsDefaultShell": "true", 10 | "installOhMyZsh": "true", 11 | "installOhMyZshConfig": "true", 12 | "upgradePackages": "true" 13 | }, 14 | "ghcr.io/aliuq/devcontainer-features/common:0": { 15 | "defaultShell": "zsh", 16 | "installEza": "true", 17 | "installFzf": "true", 18 | "installZoxide": "true", 19 | "installMise": "true" 20 | } 21 | } 22 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 23 | // "forwardPorts": [], 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | // "postCreateCommand": "uname -a", 26 | // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 27 | // "remoteUser": "vscode" 28 | } 29 | -------------------------------------------------------------------------------- /.github/actions/smoke-test/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Smoke test' 2 | inputs: 3 | image: 4 | description: 'Image to test' 5 | required: true 6 | default: 'base-debian' 7 | threshold: 8 | description: 'Threshold (in GB) to validate that the image size does not excced this limit' 9 | required: false 10 | default: 14 11 | validate-tags: 12 | description: 'Validate that base image tags exist upstream before building' 13 | required: false 14 | default: 'true' 15 | 16 | runs: 17 | using: composite 18 | steps: 19 | - name: Checkout main 20 | id: checkout_release 21 | uses: actions/checkout@v3 22 | with: 23 | repository: 'devcontainers/images' 24 | path: '__build' 25 | ref: 'main' 26 | 27 | - name: Build image 28 | id: build_image 29 | shell: bash 30 | env: 31 | INPUT_VALIDATE_TAGS: ${{ inputs.validate-tags }} 32 | run: ${{ github.action_path }}/build.sh ${{ inputs.image }} 33 | 34 | - name: Test image 35 | id: test_image 36 | shell: bash 37 | run: ${{ github.action_path }}/test.sh ${{ inputs.image }} ${{ inputs.threshold }} 38 | -------------------------------------------------------------------------------- /src/base/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "./Dockerfile", 4 | "context": "." 5 | }, 6 | "features": { 7 | "ghcr.io/devcontainers/features/common-utils:2": { 8 | "installZsh": "true", 9 | "configureZshAsDefaultShell": "true", 10 | "installOhMyZsh": "true", 11 | "installOhMyZshConfig": "true", 12 | "upgradePackages": "true" 13 | }, 14 | "ghcr.io/devcontainers/features/git:1": { 15 | "version": "latest", 16 | "ppa": "true" 17 | }, 18 | "ghcr.io/aliuq/devcontainer-features/common:0": { 19 | "defaultShell": "zsh", 20 | "installEza": "true", 21 | "installFzf": "true", 22 | "installZoxide": "true", 23 | "installMise": "true" 24 | } 25 | } 26 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 27 | // "forwardPorts": [], 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | // "postCreateCommand": "uname -a", 30 | // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 31 | // "remoteUser": "vscode" 32 | } 33 | -------------------------------------------------------------------------------- /src/me/.devcontainer/scripts/pnpm-shell-completion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" 6 | 7 | if [ ! -d "$ZSH_CUSTOM" ]; then 8 | echo "Oh My Zsh custom directory not found at $ZSH_CUSTOM. Please install Oh My Zsh first." 9 | exit 0 10 | fi 11 | 12 | echo "Installing pnpm shell completion..." 13 | 14 | pnpm_name="pnpm-shell-completion_x86_64-unknown-linux-gnu" 15 | pnpm_url="https://github.com/g-plane/pnpm-shell-completion/releases/latest/download/${pnpm_name}.tar.gz" 16 | 17 | mkdir -p /tmp/${pnpm_name} 18 | curl -sSL ${pnpm_url} | tar -xz -C /tmp/${pnpm_name} 19 | mkdir -p ${ZSH_CUSTOM}/plugins/pnpm-shell-completion 20 | 21 | cp /tmp/${pnpm_name}/pnpm-shell-completion.plugin.zsh ${ZSH_CUSTOM}/plugins/pnpm-shell-completion/ 22 | cp /tmp/${pnpm_name}/pnpm-shell-completion ${ZSH_CUSTOM}/plugins/pnpm-shell-completion/ 23 | rm -rf /tmp/${pnpm_name} 24 | 25 | if ! grep -q "plugins=.*pnpm-shell-completion" ~/.zshrc; then 26 | sed -i "s/^plugins=(\(.*\))/plugins=(\1 pnpm-shell-completion)/" ~/.zshrc; 27 | fi 28 | 29 | echo "pnpm shell completion installed successfully in Oh My Zsh." 30 | echo "Please restart your terminal or run 'source ~/.zshrc' to apply the changes." 31 | -------------------------------------------------------------------------------- /src/me/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "./Dockerfile", 4 | "context": "." 5 | }, 6 | "features": { 7 | // "ghcr.io/devcontainers/features/common-utils": {}, 8 | "ghcr.io/aliuq/devcontainer-features/common:0": { 9 | "installStarship": "true", 10 | "installHttpie": "true", 11 | "installYazi": "true", 12 | "pnpmCompletion": "true", 13 | "misePackages": "shfmt@latest jq@latest node@lts bun@latest yarn@1 pnpm@latest uv@latest neovim@latest", 14 | "zshPlugins": "docker docker-compose bun uv gh" 15 | }, 16 | "ghcr.io/devcontainers/features/python:1": {}, 17 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 18 | "ghcr.io/devcontainers/features/github-cli:1": {}, 19 | "ghcr.io/devcontainers/features/sshd:1": {} 20 | }, 21 | "customizations": { 22 | "vscode": { 23 | "extensions": [ 24 | "github.copilot-chat", 25 | "streetsidesoftware.code-spell-checker", 26 | "davidanson.vscode-markdownlint", 27 | "mads-hartmann.bash-ide-vscode", 28 | "editorconfig.editorconfig", 29 | "github.vscode-pull-request-github", 30 | "github.vscode-github-actions", 31 | "pkief.material-icon-theme", 32 | "zhuangtongfa.material-theme" 33 | ], 34 | "settings": { 35 | "workbench.iconTheme": "material-icon-theme", 36 | "workbench.colorTheme": "One Dark Pro Darker", 37 | "workbench.preferredDarkColorTheme": "One Dark Pro Darker" 38 | } 39 | } 40 | } 41 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 42 | // "forwardPorts": [], 43 | // Use 'postCreateCommand' to run commands after the container is created. 44 | // "postCreateCommand": "" 45 | // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 46 | // "remoteUser": "vscode" 47 | } 48 | -------------------------------------------------------------------------------- /.github/actions/smoke-test/check-image-size.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Function to handle errors 6 | handle_error() { 7 | local exit_code=$? 8 | local line_number=$1 9 | local command=$2 10 | echo "Error occurred at line $line_number with exit code $exit_code in command $command" 11 | exit $exit_code 12 | } 13 | trap 'handle_error $LINENO ${BASH_COMMAND%% *}' ERR 14 | 15 | convert_gb_to_bytes() { 16 | local gb="$1" 17 | local bytes 18 | bytes=$(echo "scale=0; $gb * 1024^3" | bc) 19 | printf "%.0f\n" "$bytes" 20 | } 21 | 22 | # Check if bc is installed 23 | install_bc() { 24 | if ! command -v bc &> /dev/null; then 25 | echo "bc is not installed. Installing..." 26 | # Install bc using apt-get (for Debian-based systems) 27 | sudo apt-get update 28 | sudo apt-get install -y bc 29 | fi 30 | } 31 | 32 | check_image_size() { 33 | IMAGE="$1" 34 | THRESHOLD_IN_GB="$2" 35 | id_image="$3" 36 | # call install_bc 37 | install_bc 38 | 39 | #Read the image id of the original image, not the modified image with uid and gid 40 | IMAGE_ID=$(docker images -q --filter=reference="$id_image") 41 | # Find the size of the image 42 | IMAGE_SIZE=$(docker image inspect --format='{{.Size}}' "$IMAGE_ID") 43 | # Output the size 44 | echo "Size of the image $IMAGE_ID: $IMAGE_SIZE bytes" 45 | threshold=$(convert_gb_to_bytes "$THRESHOLD_IN_GB") 46 | # Retrieve the Docker image size 47 | echo -e "\nThreshold is $threshold bytes ie $THRESHOLD_IN_GB GB" 48 | # Remove the 'MB' from the size string and convert to an integer 49 | image_size=${IMAGE_SIZE%bytes} 50 | image_size=${image_size//.} 51 | # Check if the image size is above the threshold 52 | echo -e "\n🧪 Checking image size of $IMAGE :" 53 | if [ -n $image_size ] && [ $image_size -gt $threshold ]; then 54 | echo -e "\nImage size exceeds the threshold of $THRESHOLD_IN_GB gb" 55 | echo -e "\n❌ Image size check failed." 56 | exit 1; 57 | else 58 | echo -e "\n✅ Passed!" 59 | fi 60 | } 61 | -------------------------------------------------------------------------------- /.github/actions/smoke-test/validate-tags.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to validate that all base image tags in manifest.json exist upstream 4 | set -e 5 | 6 | IMAGE="$1" 7 | MANIFEST_FILE="src/${IMAGE}/manifest.json" 8 | 9 | if [[ ! -f "$MANIFEST_FILE" ]]; then 10 | echo "ERROR: Manifest file not found: $MANIFEST_FILE" 11 | exit 1 12 | fi 13 | 14 | echo "(*) Validating base image tags for ${IMAGE}..." 15 | 16 | # Extract base image pattern from manifest.json 17 | BASE_IMAGE=$(jq -r '.dependencies.image // empty' "$MANIFEST_FILE") 18 | 19 | if [[ -z "$BASE_IMAGE" ]]; then 20 | echo "WARNING: No base image found in dependencies.image, skipping validation" 21 | exit 0 22 | fi 23 | 24 | echo "Base image pattern: $BASE_IMAGE" 25 | 26 | # Extract variants from manifest.json (may be empty) 27 | VARIANTS=$(jq -r '.variants[]?' "$MANIFEST_FILE" 2>/dev/null || true) 28 | 29 | # Track validation results 30 | INVALID_TAGS=() 31 | VALID_TAGS=() 32 | 33 | # Function to check if a Docker image tag exists 34 | check_image_exists() { 35 | local image_tag="$1" 36 | echo " Checking: $image_tag" 37 | 38 | if docker manifest inspect "$image_tag" > /dev/null 2>&1; then 39 | echo " ✓ Valid" 40 | return 0 41 | else 42 | echo " ✗ Invalid - tag does not exist" 43 | return 1 44 | fi 45 | } 46 | 47 | # Check if this image has variants 48 | if [[ -n "$VARIANTS" ]]; then 49 | echo "Found variants, validating each one..." 50 | # Check each variant 51 | for variant in $VARIANTS; do 52 | image_tag="$BASE_IMAGE" 53 | 54 | # Replace ${VARIANT} placeholder with actual variant 55 | image_tag=$(echo "$image_tag" | sed "s/\${VARIANT}/$variant/g") 56 | 57 | # Check if there are variantBuildArgs for this variant 58 | VARIANT_BUILD_ARGS=$(jq -r ".build.variantBuildArgs.\"$variant\" // empty" "$MANIFEST_FILE" 2>/dev/null) 59 | 60 | if [[ -n "$VARIANT_BUILD_ARGS" && "$VARIANT_BUILD_ARGS" != "empty" && "$VARIANT_BUILD_ARGS" != "null" ]]; then 61 | echo " Found build args for variant: $variant" 62 | # Extract build args and replace placeholders 63 | while IFS= read -r build_arg; do 64 | if [[ -n "$build_arg" ]]; then 65 | arg_name=$(echo "$build_arg" | cut -d':' -f1 | tr -d '"') 66 | arg_value=$(echo "$build_arg" | cut -d':' -f2- | tr -d '"' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') 67 | image_tag=$(echo "$image_tag" | sed "s/\${$arg_name}/$arg_value/g") 68 | fi 69 | done < <(echo "$VARIANT_BUILD_ARGS" | jq -r 'to_entries[] | "\(.key):\(.value)"' 2>/dev/null) 70 | elif [[ "$image_tag" == *'${BASE_IMAGE_VERSION_CODENAME}'* ]]; then 71 | echo " Using variant name '$variant' as BASE_IMAGE_VERSION_CODENAME" 72 | image_tag=$(echo "$image_tag" | sed "s/\${BASE_IMAGE_VERSION_CODENAME}/$variant/g") 73 | fi 74 | 75 | if check_image_exists "$image_tag"; then 76 | VALID_TAGS+=("$variant") 77 | else 78 | INVALID_TAGS+=("$variant") 79 | fi 80 | done 81 | else 82 | echo "No variants found, validating single base image..." 83 | if check_image_exists "$BASE_IMAGE"; then 84 | VALID_TAGS+=("base") 85 | else 86 | INVALID_TAGS+=("base") 87 | fi 88 | fi 89 | 90 | # Report results 91 | echo "" 92 | echo "=== Validation Results ===" 93 | echo "Valid $(if [[ -n "$VARIANTS" ]]; then echo "variants"; else echo "base images"; fi) (${#VALID_TAGS[@]}):" 94 | 95 | for tag in "${VALID_TAGS[@]}"; do 96 | if [[ "$tag" == "base" ]]; then 97 | echo " ✓ $BASE_IMAGE" 98 | else 99 | echo " ✓ $tag" 100 | fi 101 | done 102 | 103 | if [[ ${#INVALID_TAGS[@]} -gt 0 ]]; then 104 | echo "" 105 | echo "Invalid $(if [[ -n "$VARIANTS" ]]; then echo "variants"; else echo "base images"; fi) (${#INVALID_TAGS[@]}):" 106 | 107 | for tag in "${INVALID_TAGS[@]}"; do 108 | if [[ "$tag" == "base" ]]; then 109 | echo " ✗ $BASE_IMAGE" 110 | else 111 | echo " ✗ $tag" 112 | fi 113 | done 114 | echo "" 115 | echo "ERROR: Found ${#INVALID_TAGS[@]} invalid base image tags!" 116 | echo "Please verify these tags exist upstream before proceeding." 117 | exit 1 118 | fi 119 | 120 | echo "" 121 | echo "✓ All base image tags are valid!" -------------------------------------------------------------------------------- /.github/workflows/build-app.yml: -------------------------------------------------------------------------------- 1 | name: Build and push image (single app) 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | app: 7 | description: "Which app to build (base, base-debian, base-alpine, all)" 8 | required: true 9 | type: choice 10 | default: me 11 | options: 12 | - me 13 | 14 | jobs: 15 | build-and-push: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v3 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | with: 28 | use: true 29 | - name: Login to GitHub Container Registry 30 | uses: docker/login-action@v3 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.repository_owner }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | - name: Login to Docker Hub 36 | uses: docker/login-action@v3 37 | with: 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | - name: Login to Aliyun ACR 41 | uses: docker/login-action@v3 42 | if: ${{ env.ACR_REGISTRY && env.ACR_USERNAME && env.ACR_PASSWORD }} 43 | env: 44 | ACR_REGISTRY: ${{ secrets.ALI_ACR_REGISTRY }} 45 | ACR_USERNAME: ${{ secrets.ALI_ACR_USERNAME }} 46 | ACR_PASSWORD: ${{ secrets.ALI_ACR_PASSWORD }} 47 | with: 48 | registry: ${{ secrets.ALI_ACR_REGISTRY }} 49 | username: ${{ secrets.ALI_ACR_USERNAME }} 50 | password: ${{ secrets.ALI_ACR_PASSWORD }} 51 | 52 | - name: Prepare variables 53 | id: prepare 54 | uses: actions/github-script@v7 55 | with: 56 | script: | 57 | const app = '${{ inputs.app }}' 58 | 59 | const tagMap = { 60 | 'me': 'me,aliuq', 61 | } 62 | const tags = tagMap[app] || app 63 | 64 | const cacheMap = { 65 | 'me': 'me', 66 | } 67 | const cacheTag = cacheMap[app] || app 68 | 69 | core.setOutput('tags', tags) 70 | core.setOutput('cacheFrom', `ghcr.io/aliuq/devcontainer:${cacheTag}`) 71 | 72 | - name: Build & Push 73 | uses: devcontainers/ci@v0.3 74 | with: 75 | imageName: ghcr.io/aliuq/devcontainer 76 | imageTag: ${{ steps.prepare.outputs.tags }} 77 | # platform: linux/amd64 78 | platform: linux/amd64,linux/arm64 79 | subFolder: src/${{ inputs.app }} 80 | cacheFrom: ${{ steps.prepare.outputs.cacheFrom }} 81 | # push: never 82 | push: always 83 | env: 84 | MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | 86 | - name: Sync to another registry (docker.io) 87 | uses: actions/github-script@v7 88 | env: 89 | ACR_REGISTRY: ${{ secrets.ALI_ACR_REGISTRY }} 90 | ACR_USERNAME: ${{ secrets.ALI_ACR_USERNAME }} 91 | ACR_PASSWORD: ${{ secrets.ALI_ACR_PASSWORD }} 92 | with: 93 | script: | 94 | const inputTags = '${{ steps.prepare.outputs.tags }}'; 95 | 96 | const tags = inputTags?.split(','); 97 | 98 | for await (const tag of tags) { 99 | console.log(`\n(${tag}) Pushing to Docker Hub (docker.io/aliuq/devcontainer:${tag}) ...`); 100 | 101 | await exec.exec('/usr/bin/skopeo', [ 102 | 'copy', 103 | '--all', 104 | `oci-archive:/tmp/output.tar:${tag}`, 105 | `docker://docker.io/aliuq/devcontainer:${tag}` 106 | ]); 107 | } 108 | 109 | if (process.env.ACR_REGISTRY && process.env.ACR_USERNAME && process.env.ACR_PASSWORD) { 110 | for await (const tag of tags) { 111 | console.log(`\n(${tag}) Pushing to Aliyun ACR (registry.cn-hangzhou.aliyuncs.com/aliuq/devcontainer:${tag}) ...`); 112 | 113 | await exec.exec('/usr/bin/skopeo', [ 114 | 'copy', 115 | '--all', 116 | `oci-archive:/tmp/output.tar:${tag}`, 117 | `docker://registry.cn-hangzhou.aliyuncs.com/aliuq/devcontainer:${tag}` 118 | ]); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and push images 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-and-push: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | packages: write 12 | strategy: 13 | matrix: 14 | app: 15 | - base 16 | - base-debian 17 | - base-alpine 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | with: 26 | use: true 27 | - name: Login to GitHub Container Registry 28 | uses: docker/login-action@v3 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.repository_owner }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | - name: Login to Docker Hub 34 | uses: docker/login-action@v3 35 | with: 36 | username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | password: ${{ secrets.DOCKERHUB_TOKEN }} 38 | - name: Login to Aliyun ACR 39 | uses: docker/login-action@v3 40 | if: ${{ env.ACR_REGISTRY && env.ACR_USERNAME && env.ACR_PASSWORD }} 41 | env: 42 | ACR_REGISTRY: ${{ secrets.ALI_ACR_REGISTRY }} 43 | ACR_USERNAME: ${{ secrets.ALI_ACR_USERNAME }} 44 | ACR_PASSWORD: ${{ secrets.ALI_ACR_PASSWORD }} 45 | with: 46 | registry: ${{ secrets.ALI_ACR_REGISTRY }} 47 | username: ${{ secrets.ALI_ACR_USERNAME }} 48 | password: ${{ secrets.ALI_ACR_PASSWORD }} 49 | 50 | - name: Prepare variables 51 | id: prepare 52 | uses: actions/github-script@v7 53 | with: 54 | script: | 55 | const app = '${{ matrix.app }}' 56 | 57 | const tagMap = { 58 | 'base': 'base,ubuntu', 59 | 'base-debian': 'debian', 60 | 'base-alpine': 'alpine' 61 | } 62 | const tags = tagMap[app] || app 63 | 64 | const cacheMap = { 65 | 'base-debian': 'debian', 66 | 'base-alpine': 'alpine' 67 | } 68 | const cacheTag = cacheMap[app] || app 69 | 70 | core.setOutput('tags', tags) 71 | core.setOutput('cacheFrom', `ghcr.io/aliuq/devcontainer:${cacheTag}`) 72 | 73 | - name: Build & Push 74 | uses: devcontainers/ci@v0.3 75 | with: 76 | imageName: ghcr.io/aliuq/devcontainer 77 | imageTag: ${{ steps.prepare.outputs.tags }} 78 | # platform: linux/amd64 79 | platform: linux/amd64,linux/arm64 80 | subFolder: src/${{ matrix.app }} 81 | cacheFrom: ${{ steps.prepare.outputs.cacheFrom }} 82 | # push: never 83 | push: always 84 | env: 85 | MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | 87 | - name: Sync to another registry (docker.io, aliyun ACR) 88 | uses: actions/github-script@v7 89 | env: 90 | ACR_REGISTRY: ${{ secrets.ALI_ACR_REGISTRY }} 91 | ACR_USERNAME: ${{ secrets.ALI_ACR_USERNAME }} 92 | ACR_PASSWORD: ${{ secrets.ALI_ACR_PASSWORD }} 93 | with: 94 | script: | 95 | const inputTags = '${{ steps.prepare.outputs.tags }}'; 96 | 97 | const tags = inputTags?.split(','); 98 | 99 | for await (const tag of tags) { 100 | console.log(`\n(${tag}) Pushing to Docker Hub (docker.io/aliuq/devcontainer:${tag}) ...`); 101 | 102 | await exec.exec('/usr/bin/skopeo', [ 103 | 'copy', 104 | '--all', 105 | `oci-archive:/tmp/output.tar:${tag}`, 106 | `docker://docker.io/aliuq/devcontainer:${tag}` 107 | ]); 108 | } 109 | 110 | if (process.env.ACR_REGISTRY && process.env.ACR_USERNAME && process.env.ACR_PASSWORD) { 111 | for await (const tag of tags) { 112 | console.log(`\n(${tag}) Pushing to Aliyun ACR (registry.cn-hangzhou.aliyuncs.com/aliuq/devcontainer:${tag}) ...`); 113 | 114 | await exec.exec('/usr/bin/skopeo', [ 115 | 'copy', 116 | '--all', 117 | `oci-archive:/tmp/output.tar:${tag}`, 118 | `docker://registry.cn-hangzhou.aliyuncs.com/aliuq/devcontainer:${tag}` 119 | ]); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/base/test-project/test-utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_FOLDER="$(cd "$(dirname $0)" && pwd)" 3 | USERNAME=${1:-vscode} 4 | 5 | if [ -z $HOME ]; then 6 | HOME="/root" 7 | fi 8 | 9 | FAILED=() 10 | 11 | echoStderr() 12 | { 13 | echo "$@" 1>&2 14 | } 15 | 16 | check() { 17 | LABEL=$1 18 | shift 19 | echo -e "\n🧪 Testing $LABEL" 20 | if "$@"; then 21 | echo "✅ Passed!" 22 | return 0 23 | else 24 | echoStderr "❌ $LABEL check failed." 25 | FAILED+=("$LABEL") 26 | return 1 27 | fi 28 | } 29 | 30 | check-version-ge() { 31 | LABEL=$1 32 | CURRENT_VERSION=$2 33 | REQUIRED_VERSION=$3 34 | shift 35 | echo -e "\n🧪 Testing $LABEL: '$CURRENT_VERSION' is >= '$REQUIRED_VERSION'" 36 | local GREATER_VERSION=$((echo ${CURRENT_VERSION}; echo ${REQUIRED_VERSION}) | sort -V | tail -1) 37 | if [ "${CURRENT_VERSION}" == "${GREATER_VERSION}" ]; then 38 | echo "✅ Passed!" 39 | return 0 40 | else 41 | echoStderr "❌ $LABEL check failed." 42 | FAILED+=("$LABEL") 43 | return 1 44 | fi 45 | } 46 | 47 | checkMultiple() { 48 | PASSED=0 49 | LABEL="$1" 50 | echo -e "\n🧪 Testing $LABEL." 51 | shift; MINIMUMPASSED=$1 52 | shift; EXPRESSION="$1" 53 | while [ "$EXPRESSION" != "" ]; do 54 | if $EXPRESSION; then ((PASSED++)); fi 55 | shift; EXPRESSION=$1 56 | done 57 | if [ $PASSED -ge $MINIMUMPASSED ]; then 58 | echo "✅ Passed!" 59 | return 0 60 | else 61 | echoStderr "❌ $LABEL check failed." 62 | FAILED+=("$LABEL") 63 | return 1 64 | fi 65 | } 66 | 67 | checkOSPackages() { 68 | LABEL=$1 69 | shift 70 | echo -e "\n🧪 Testing $LABEL" 71 | if dpkg-query --show -f='${Package}: ${Version}\n' "$@"; then 72 | echo "✅ Passed!" 73 | return 0 74 | else 75 | echoStderr "❌ $LABEL check failed." 76 | FAILED+=("$LABEL") 77 | return 1 78 | fi 79 | } 80 | 81 | checkExtension() { 82 | # Happens asynchronusly, so keep retrying 10 times with an increasing delay 83 | EXTN_ID="$1" 84 | TIMEOUT_SECONDS="${2:-10}" 85 | RETRY_COUNT=0 86 | echo -e -n "\n🧪 Looking for extension $1 for maximum of ${TIMEOUT_SECONDS}s" 87 | until [ "${RETRY_COUNT}" -eq "${TIMEOUT_SECONDS}" ] || \ 88 | [ ! -e $HOME/.vscode-server/extensions/${EXTN_ID}* ] || \ 89 | [ ! -e $HOME/.vscode-server-insiders/extensions/${EXTN_ID}* ] || \ 90 | [ ! -e $HOME/.vscode-test-server/extensions/${EXTN_ID}* ] || \ 91 | [ ! -e $HOME/.vscode-remote/extensions/${EXTN_ID}* ] 92 | do 93 | sleep 1s 94 | (( RETRY_COUNT++ )) 95 | echo -n "." 96 | done 97 | 98 | if [ ${RETRY_COUNT} -lt ${TIMEOUT_SECONDS} ]; then 99 | echo -e "\n✅ Passed!" 100 | return 0 101 | else 102 | echoStderr -e "\n❌ Extension $EXTN_ID not found." 103 | FAILED+=("$LABEL") 104 | return 1 105 | fi 106 | } 107 | 108 | checkCommon() 109 | { 110 | PACKAGE_LIST="apt-utils \ 111 | openssh-client \ 112 | less \ 113 | iproute2 \ 114 | procps \ 115 | curl \ 116 | wget \ 117 | unzip \ 118 | nano \ 119 | jq \ 120 | lsb-release \ 121 | ca-certificates \ 122 | apt-transport-https \ 123 | dialog \ 124 | gnupg2 \ 125 | libc6 \ 126 | libgcc1 \ 127 | libgssapi-krb5-2 \ 128 | liblttng-ust1 \ 129 | libstdc++6 \ 130 | zlib1g \ 131 | locales \ 132 | sudo" 133 | 134 | # Actual tests 135 | checkOSPackages "common-os-packages" ${PACKAGE_LIST} 136 | check "non-root-user" id ${USERNAME} 137 | check "locale" [ $(locale -a | grep en_US.utf8) ] 138 | check "sudo" sudo echo "sudo works." 139 | check "zsh" zsh --version 140 | check "oh-my-zsh" [ -d "$HOME/.oh-my-zsh" ] 141 | check "login-shell-path" [ -f "/etc/profile.d/00-restore-env.sh" ] 142 | check "code" which code 143 | } 144 | 145 | reportResults() { 146 | if [ ${#FAILED[@]} -ne 0 ]; then 147 | echoStderr -e "\n💥 Failed tests: ${FAILED[@]}" 148 | exit 1 149 | else 150 | echo -e "\n💯 All passed!" 151 | exit 0 152 | fi 153 | } 154 | 155 | fixTestProjectFolderPrivs() { 156 | if [ "${USERNAME}" != "root" ]; then 157 | TEST_PROJECT_FOLDER="${1:-$SCRIPT_FOLDER}" 158 | FOLDER_USER="$(stat -c '%U' "${TEST_PROJECT_FOLDER}")" 159 | if [ "${FOLDER_USER}" != "${USERNAME}" ]; then 160 | echoStderr "WARNING: Test project folder is owned by ${FOLDER_USER}. Updating to ${USERNAME}." 161 | sudo chown -R ${USERNAME} "${TEST_PROJECT_FOLDER}" 162 | fi 163 | fi 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DevContainer Images 2 | 3 | [![DevContainer](https://img.shields.io/github/actions/workflow/status/aliuq/devcontainer-images/build.yml?label=Build)](./.github/workflows/build.yml) 4 | [![DevContainer](https://img.shields.io/github/actions/workflow/status/aliuq/devcontainer-images/build-app.yml?label=Build%20App)](./.github/workflows/build-app.yml) 5 | [![Pulls](https://img.shields.io/docker/pulls/aliuq/devcontainer?label=Docker%20Pulls)](https://hub.docker.com/r/aliuq/devcontainer) 6 | 7 | 一套预配置的 DevContainer 镜像集合,基于官方 [devcontainers/images](https://github.com/devcontainers/images) 构建,集成了常用的开发工具和配置 8 | 9 | ## 使用方式 10 | 11 | 1. `ghcr.io/aliuq/devcontainer:`: GitHub Container Registry 12 | 2. `aliuq/devcontainer:`: Docker Hub 13 | 3. `registry.cn-hangzhou.aliyuncs.com/aliuq/devcontainer:`: 阿里云容器镜像服务 14 | 15 | 快速运行 16 | 17 | ```bash 18 | # GitHub Container Registry 19 | docker run -it --rm ghcr.io/aliuq/devcontainer:base /bin/zsh 20 | 21 | # Docker Hub 22 | docker run -it --rm aliuq/devcontainer:base /bin/zsh 23 | 24 | # 阿里云 (推荐国内用户) 25 | docker run -it --rm registry.cn-hangzhou.aliyuncs.com/aliuq/devcontainer:base /bin/zsh 26 | ``` 27 | 28 | ## 镜像列表 29 | 30 | | 标签 | 描述 | 基础镜像 | 大小 | 31 | |------|------|---------|------| 32 | | [`base`](./src/base) | Ubuntu 基础镜像,功能完整 | `buildpack-deps:noble` | ![Size](https://img.shields.io/docker/image-size/aliuq/devcontainer/base?label=) | 33 | | [`alpine`](./src/base-alpine) | Alpine Linux 轻量级镜像,体积小 | `alpine:3.22` | ![Size](https://img.shields.io/docker/image-size/aliuq/devcontainer/alpine?label=) | 34 | | [`debian`](./src/base-debian) | Debian 稳定版镜像,兼容性好 | `buildpack-deps:trixie` | ![Size](https://img.shields.io/docker/image-size/aliuq/devcontainer/debian?label=) | 35 | | [`me`](./src/me) | 个人定制镜像,包含常用配置 | `ghcr.io/aliuq/devcontainer:base` | ![Size](https://img.shields.io/docker/image-size/aliuq/devcontainer/me?label=) | 36 | 37 | ## 工具列表 38 | 39 | 在 [aliuq/devcontainer-features](https://github.com/aliuq/devcontainer-features) 中以可选 feature 形式提供, 支持按需安装: 40 | 41 | - [`Zsh`](https://github.com/devcontainers/features/tree/main/src/common-utils): Shell 环境, 集成了 [Oh-My-Zsh](https://github.com/ohmyzsh/ohmyzsh) 42 | - [`Git`](https://github.com/devcontainers/features/tree/main/src/git): 版本控制工具 43 | - [`Eza`](https://github.com/eza-community/eza): 现代化的 `ls` 命令替代品,具有更好的输出格式和颜色 44 | - [`Fzf`](https://github.com/junegunn/fzf): 强大的命令行模糊查找工具,支持快速搜索和导航 45 | - [`Zoxide`](https://github.com/ajeetdsouza/zoxide): 智能 `cd` 命令,记住常用目录并快速跳转 46 | - [`Mise`](https://github.com/jdx/mise): 多语言工具版本管理器,统一管理 Node.js/Python/Ruby 等运行环境 47 | - [`Starship`](https://github.com/starship/starship): 快速、可定制的跨平台 Shell 提示符 48 | - [`Httpie`](https://github.com/httpie/cli): 用户友好的 HTTP 客户端,简化 API 调试 49 | - [`Yazi`](https://github.com/sxyazi/yazi): 快速的终端文件管理器,支持预览和批量操作 50 | - [`Pnpm Completion`](https://pnpm.io/zh/completion): Pnpm 命令自动补全 51 | 52 | ## 快速开始 53 | 54 | ### 使用预构建镜像 55 | 56 | 在项目根目录创建 `.devcontainer/devcontainer.json`: 57 | 58 | ```json 59 | { 60 | "name": "My Project", 61 | "image": "ghcr.io/aliuq/devcontainer:base", 62 | "customizations": { 63 | "vscode": { 64 | "extensions": [ 65 | "github.copilot-chat", 66 | "streetsidesoftware.code-spell-checker", 67 | "davidanson.vscode-markdownlint", 68 | "mads-hartmann.bash-ide-vscode", 69 | "editorconfig.editorconfig", 70 | "github.vscode-pull-request-github", 71 | "github.vscode-github-actions", 72 | "pkief.material-icon-theme", 73 | "zhuangtongfa.material-theme" 74 | ], 75 | "settings": { 76 | "workbench.iconTheme": "material-icon-theme", 77 | "workbench.colorTheme": "One Dark Pro Darker", 78 | "workbench.preferredDarkColorTheme": "One Dark Pro Darker" 79 | } 80 | } 81 | }, 82 | "features": { 83 | "ghcr.io/aliuq/devcontainer-features/common:0": { 84 | "installStarship": true, 85 | "installHttpie": "true", 86 | "installYazi": "true", 87 | "misePackages": "shfmt@latest,node@lts,yarn@1,pnpm@latest,bun@latest", 88 | "zshPlugins": "bun", 89 | "pnpmCompletion": "true", 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | > [!NOTE] 96 | > 如果在国内访问 GitHub 速度较慢,可以使用阿里云镜像 97 | > 98 | > `registry.cn-hangzhou.aliyuncs.com/aliuq/devcontainer:base` 99 | 100 | ### 本地构建 101 | 102 | 适用于需要自定义镜像的场景: 103 | 104 | ```bash 105 | # 1. 克隆仓库 106 | git clone https://github.com/aliuq/devcontainer-images.git 107 | cd devcontainer-images 108 | 109 | # 2. 构建基础镜像 110 | devcontainer build --image-name base:local --workspace-folder src/base 111 | 112 | # 3. 运行测试 113 | docker run -it --rm base:local /bin/zsh 114 | 115 | # 其他构建选项: 116 | # 不使用缓存重新构建 117 | devcontainer build --image-name base:local --workspace-folder src/base --no-cache 118 | 119 | # 查看详细构建日志 120 | BUILDKIT_PROGRESS=plain devcontainer build --image-name base:local --workspace-folder src/base 121 | 122 | # 使用 vscode 用户测试 (模拟实际使用环境) 123 | docker run -it --rm -u vscode base:local /bin/zsh 124 | ``` 125 | 126 | ## 自定义配置 127 | 128 | ### 添加开发语言环境 129 | 130 | 使用官方 Features 添加所需的开发环境: 131 | 132 | ```json 133 | { 134 | "image": "ghcr.io/aliuq/devcontainer:base", 135 | "features": { 136 | "ghcr.io/devcontainers/features/node:1": { 137 | "version": "lts" 138 | }, 139 | "ghcr.io/devcontainers/features/python:1": { 140 | "version": "3.11" 141 | }, 142 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 143 | } 144 | } 145 | ``` 146 | 147 | ### 容器生命周期钩子 148 | 149 | 在容器不同阶段执行自定义脚本: 150 | 151 | ```json 152 | { 153 | "image": "ghcr.io/aliuq/devcontainer:base", 154 | "onCreateCommand": "echo 'Container created!'", 155 | "postCreateCommand": "npm install", 156 | "postStartCommand": "git config --global core.editor 'code --wait'", 157 | "postAttachCommand": "echo 'Welcome to DevContainer!'" 158 | } 159 | ``` 160 | 161 | ### 环境变量和端口转发 162 | 163 | ```json 164 | { 165 | "image": "ghcr.io/aliuq/devcontainer:base", 166 | "containerEnv": { 167 | "NODE_ENV": "development", 168 | "API_URL": "http://localhost:3000" 169 | }, 170 | "forwardPorts": [3000, 5173], 171 | "portsAttributes": { 172 | "3000": { 173 | "label": "Backend", 174 | "onAutoForward": "notify" 175 | } 176 | } 177 | } 178 | ``` 179 | 180 | ## 开发说明 181 | 182 | ### 项目结构 183 | 184 | ```text 185 | devcontainer-images/ 186 | ├── src/ 187 | │ ├── base/ # 基础镜像 (Ubuntu) 188 | │ ├── base-alpine/ # Alpine 镜像 189 | │ ├── base-debian/ # Debian 镜像 190 | │ └── me/ # 个人定制镜像 191 | ├── .github/ 192 | │ └── workflows/ # CI/CD 工作流 193 | └── README.md 194 | ``` 195 | 196 | ## 相关链接 197 | 198 | - [DevContainers 官方文档](https://containers.dev/) 199 | - [DevContainer Images](https://github.com/devcontainers/images) 200 | - [DevContainer Features](https://github.com/devcontainers/features) 201 | - [我的 DevContainer Features](https://github.com/aliuq/devcontainer-features) 202 | - [VS Code Remote - Containers](https://code.visualstudio.com/docs/remote/containers) 203 | --------------------------------------------------------------------------------