diff --git a/examples/docker-image-builds/README.md b/examples/docker-image-builds/README.md new file mode 100644 index 0000000000000..3513f631cc677 --- /dev/null +++ b/examples/docker-image-builds/README.md @@ -0,0 +1,159 @@ +--- +name: Develop in Docker with custom image builds +description: Build images and run workspaces on the Docker host with no image registry required +tags: [local, docker] +--- + +# docker-image-builds + +This example bundles Dockerfiles with the Coder template, allowing the Docker host to build images itself instead of relying on an external registry. + +For large use cases, we recommend building images using CI/CD pipelines and registries instead of at workspace runtime. However, this example is practical for tinkering and iterating on Dockerfiles. + +## Getting started + +Run `coder templates init` and select this template. Follow the instructions that appear. + +## Adding images + +Create a Dockerfile (e.g `images/golang.Dockerfile`): + +```sh +vim images/golang.Dockerfile +``` + +```Dockerfile +# Start from base image (built on Docker host) +FROM coder-base:latest + +# Install everything as root +USER root + +# Install go +RUN curl -L "https://dl.google.com/go/go1.18.1.linux-amd64.tar.gz" | tar -C /usr/local -xzvf - + +# Setup go env vars +ENV GOROOT /usr/local/go +ENV PATH $PATH:$GOROOT/bin + +ENV GOPATH /home/coder/go +ENV GOBIN $GOPATH/bin +ENV PATH $PATH:$GOBIN + +# Set back to coder user +USER coder +``` + +Edit the Terraform template (`main.tf`): + +```sh +vim main.tf +``` + +Edit the validation to include the new image: + +```diff +variable "docker_image" { + description = "What Docker imagewould you like to use for your workspace?" + default = "base" + + # List of images available for the user to choose from. + # Delete this condition to give users free text input. + validation { +- condition = contains(["base", "java", "node"], var.docker_image) ++ condition = contains(["base", "java", "node", "golang], var.docker_image) + error_message = "Invalid Docker image!" + } +} +``` + +Bump the image tag to a new version: + +```diff +resource "docker_image" "coder_image" { + name = "coder-base-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + build { + path = "./images/" + dockerfile = "${var.docker_image}.Dockerfile" +- tag = ["coder-${var.docker_image}:v0.1"] ++ tag = ["coder-${var.docker_image}:v0.2"] + } + + # Keep alive for other workspaces to use upon deletion + keep_locally = true +} +``` + +Update the template: + +```sh +coder template update docker-image-builds +``` + +You can also remove images from the validation list. Workspaces using older template versions will continue using +the removed image until you update the workspace to the latest version. + +## Updating images + +Edit the Dockerfile (or related assets): + +```sh +vim images/node.Dockerfile +``` + +```diff +# Install Node +- RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - ++ RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - +RUN DEBIAN_FRONTEND="noninteractive" apt-get update -y && \ + apt-get install -y nodejs +``` + +1. Edit the Terraform template (`main.tf`) + +```sh +vim main.tf +``` + +Bump the image tag to a new version: + +```diff +resource "docker_image" "coder_image" { + name = "coder-base-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + build { + path = "./images/" + dockerfile = "${var.docker_image}.Dockerfile" +- tag = ["coder-${var.docker_image}:v0.1"] ++ tag = ["coder-${var.docker_image}:v0.2"] + } + + # Keep alive for other workspaces to use upon deletion + keep_locally = true +} +``` + +Update the template: + +```sh +coder template update docker-image-builds +``` + +Optional: Update workspaces to the latest template version + +```sh +coder ls +coder update [workspace name] +``` + +## Extending this template + +See the [kreuzwerker/docker](https://registry.terraform.io/providers/kreuzwerker/docker) Terraform provider documentation to +add the following features to your Coder template: + +- SSH/TCP docker host +- Build args +- Volume mounts +- Custom container spec +- More + +We also welcome all contributions! diff --git a/examples/docker-image-builds/images/base.Dockerfile b/examples/docker-image-builds/images/base.Dockerfile new file mode 100644 index 0000000000000..620b0fa0cd088 --- /dev/null +++ b/examples/docker-image-builds/images/base.Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:20.04 + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" apt-get install --yes \ + bash \ + build-essential \ + ca-certificates \ + curl \ + htop \ + locales \ + man \ + python3 \ + python3-pip \ + software-properties-common \ + sudo \ + systemd \ + systemd-sysv \ + unzip \ + vim \ + wget && \ + # Install latest Git using their official PPA + add-apt-repository ppa:git-core/ppa && \ + DEBIAN_FRONTEND="noninteractive" apt-get install --yes git + +# Add a user `coder` so that you're not developing as the `root` user +RUN useradd coder \ + --create-home \ + --shell=/bin/bash \ + --uid=1000 \ + --user-group && \ + echo "coder ALL=(ALL) NOPASSWD:ALL" >>/etc/sudoers.d/nopasswd + +USER coder diff --git a/examples/docker-image-builds/images/java.Dockerfile b/examples/docker-image-builds/images/java.Dockerfile new file mode 100644 index 0000000000000..a35fc20230786 --- /dev/null +++ b/examples/docker-image-builds/images/java.Dockerfile @@ -0,0 +1,57 @@ +# From the base image (built on Docker host) +FROM coder-base:v0.1 + +# Install everything as root +USER root + +# Install JDK (OpenJDK 8) +RUN DEBIAN_FRONTEND="noninteractive" apt-get update -y && \ + apt-get install -y openjdk-11-jdk +ENV JAVA_HOME /usr/lib/jvm/java-11-openjdk-amd64 +ENV PATH $PATH:$JAVA_HOME/bin + +# Install Maven +ARG MAVEN_VERSION=3.6.3 +ARG MAVEN_SHA512=c35a1803a6e70a126e80b2b3ae33eed961f83ed74d18fcd16909b2d44d7dada3203f1ffe726c17ef8dcca2dcaa9fca676987befeadc9b9f759967a8cb77181c0 + +ENV MAVEN_HOME /usr/share/maven +ENV MAVEN_CONFIG "/home/coder/.m2" + +RUN mkdir -p $MAVEN_HOME $MAVEN_HOME/ref \ + && echo "Downloading Maven" \ + && curl -fsSL -o /tmp/apache-maven.tar.gz https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz \ + \ + && echo "Checking downloaded file hash" \ + && echo "${MAVEN_SHA512} /tmp/apache-maven.tar.gz" | sha512sum -c - \ + \ + && echo "Unzipping Maven" \ + && tar -xzf /tmp/apache-maven.tar.gz -C $MAVEN_HOME --strip-components=1 \ + \ + && echo "Cleaning and setting links" \ + && rm -f /tmp/apache-maven.tar.gz \ + && ln -s $MAVEN_HOME/bin/mvn /usr/bin/mvn + +# Install Gradle +ENV GRADLE_VERSION=6.7 +ARG GRADLE_SHA512=d495bc65379d2a854d2cca843bd2eeb94f381e5a7dcae89e6ceb6ef4c5835524932313e7f30d7a875d5330add37a5fe23447dc3b55b4d95dffffa870c0b24493 + +ENV GRADLE_HOME /usr/bin/gradle + +RUN mkdir -p /usr/share/gradle /usr/share/gradle/ref \ + && echo "Downloading Gradle" \ + && curl -fsSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip \ + \ + && echo "Checking downloaded file hash" \ + && echo "${GRADLE_SHA512} /tmp/gradle.zip" | sha512sum -c - \ + \ + && echo "Unziping gradle" \ + && unzip -d /usr/share/gradle /tmp/gradle.zip \ + \ + && echo "Cleaning and setting links" \ + && rm -f /tmp/gradle.zip \ + && ln -s /usr/share/gradle/gradle-${GRADLE_VERSION} /usr/bin/gradle + +ENV PATH $PATH:$GRADLE_HOME/bin + +# Set back to coder user +USER coder diff --git a/examples/docker-image-builds/images/node.Dockerfile b/examples/docker-image-builds/images/node.Dockerfile new file mode 100644 index 0000000000000..1c371f8bb40a1 --- /dev/null +++ b/examples/docker-image-builds/images/node.Dockerfile @@ -0,0 +1,18 @@ +# Start from base image (built on Docker host) +FROM coder-base:v0.1 + +# Install everything as root +USER root + +# Install Node +RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - +RUN DEBIAN_FRONTEND="noninteractive" apt-get update -y && \ + apt-get install -y nodejs + +# Install Yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - +RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list +RUN DEBIAN_FRONTEND="noninteractive" apt-get update && apt-get install -y yarn + +# Set back to coder user +USER coder diff --git a/examples/docker-image-builds/main.tf b/examples/docker-image-builds/main.tf new file mode 100644 index 0000000000000..122bd20c3abcc --- /dev/null +++ b/examples/docker-image-builds/main.tf @@ -0,0 +1,111 @@ + +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.4.1" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.16.0" + } + } +} + +# Admin parameters +variable "step1_docker_host_warning" { + description = <<-EOF + This template will use the Docker socket present on + the Coder host, which is not necessarily your local machine. + + You can specify a different host in the template file and + surpress this warning. + EOF + validation { + condition = contains(["Continue using /var/run/docker.sock on the Coder host"], var.step1_docker_host_warning) + error_message = "Cancelling template create." + } + + sensitive = true +} +variable "step2_arch" { + description = "arch: What archicture is your Docker host on?" + validation { + condition = contains(["amd64", "arm64", "armv7"], var.step2_arch) + error_message = "Value must be amd64, arm64, or armv7." + } + sensitive = true +} + +provider "docker" { + host = "unix:///var/run/docker.sock" +} + +provider "coder" { + # The below assumes your Coder deployment is running in docker-compose. + # If this is not the case, either comment or edit the below. + url = "http://host.docker.internal:7080" +} + +data "coder_workspace" "me" { +} + +resource "coder_agent" "dev" { + arch = var.step2_arch + os = "linux" +} + +variable "docker_image" { + description = "What Docker imagewould you like to use for your workspace?" + default = "base" + + # List of images available for the user to choose from. + # Delete this condition to give users free text input. + validation { + condition = contains(["base", "java", "node"], var.docker_image) + error_message = "Invalid Docker image!" + } + + # Prevents admin errors when the image is not found + validation { + condition = fileexists("images/${var.docker_image}.Dockerfile") + error_message = "Invalid Docker image. The file does not exist in the images directory." + } +} + +resource "docker_volume" "home_volume" { + name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}-root" +} + +resource "docker_image" "coder_image" { + name = "coder-base-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + build { + path = "./images/" + dockerfile = "${var.docker_image}.Dockerfile" + tag = ["coder-${var.docker_image}:v0.1"] + } + + # Keep alive for other workspaces to use upon deletion + keep_locally = true +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.coder_image.latest + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = lower(data.coder_workspace.me.name) + dns = ["1.1.1.1"] + command = ["sh", "-c", coder_agent.dev.init_script] + env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"] + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/home/coder/" + volume_name = docker_volume.home_volume.name + read_only = false + } +} diff --git a/examples/docker-local/README.md b/examples/docker-local/README.md deleted file mode 100644 index 8cb28f85c2e21..0000000000000 --- a/examples/docker-local/README.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -name: Develop in Docker on the same host that runs Coder -description: Get started with Linux development using a Docker container locally as workspace provider. -tags: [local, docker] ---- diff --git a/examples/docker-local/main.tf b/examples/docker-local/main.tf deleted file mode 100644 index af0f57a450eba..0000000000000 --- a/examples/docker-local/main.tf +++ /dev/null @@ -1,61 +0,0 @@ -terraform { - required_providers { - coder = { - source = "coder/coder" - version = "0.3.4" - } - docker = { - source = "kreuzwerker/docker" - version = "~> 2.16.0" - } - } -} - -provider "docker" { - host = "unix:///var/run/docker.sock" -} - -provider "coder" { - # The below assumes your Coder deployment is running in docker-compose. - # If this is not the case, either comment or edit the below. - url = "http://host.docker.internal:7080" -} - -data "coder_workspace" "me" { -} - -resource "coder_agent" "dev" { - arch = "amd64" - os = "linux" -} - -variable "docker_image" { - description = "What docker image would you like to use for your workspace?" - default = "codercom/enterprise-base:ubuntu" - validation { - condition = contains(["codercom/enterprise-base:ubuntu", "codercom/enterprise-node:ubuntu", "codercom/enterprise-intellij:ubuntu"], var.docker_image) - error_message = "Invalid Docker Image!" - } -} - -resource "docker_volume" "coder_volume" { - name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" -} - -resource "docker_container" "workspace" { - count = data.coder_workspace.me.start_count - image = var.docker_image - name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" - dns = ["1.1.1.1"] - command = ["sh", "-c", coder_agent.dev.init_script] - env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"] - host { - host = "host.docker.internal" - ip = "host-gateway" - } - volumes { - container_path = "/home/coder/" - volume_name = docker_volume.coder_volume.name - read_only = false - } -} diff --git a/examples/docker/README.md b/examples/docker/README.md new file mode 100644 index 0000000000000..7246ddfc1a5fc --- /dev/null +++ b/examples/docker/README.md @@ -0,0 +1,80 @@ +--- +name: Develop in Docker +description: Run workspaces on a Docker host using registry images +tags: [local, docker] +--- + +# docker + +## Getting started + +Run `coder templates init` and select this template. Follow the instructions that appear. + +## Adding/removing images + +After building and pushing an image to an image registry (e.g., DockerHub), you can edit the template to make the image available to users. + +Edit the template: + +```sh +vim main.tf +``` +variable "docker_image" { + description = "What Docker imagewould you like to use for your workspace?" + default = "codercom/enterprise-base:ubuntu" + validation { +- condition = contains(["codercom/enterprise-base:ubuntu", "codercom/enterprise-node:ubuntu", "codercom/enterprise-intellij:ubuntu"], var.docker_image) ++ condition = contains(["codercom/enterprise-base:ubuntu", "codercom/enterprise-node:ubuntu", "codercom/enterprise-intellij:ubuntu", "codercom/enterprise-golang:ubuntu"], var.docker_image) + + error_message = "Invalid Docker image!" + } +} +``` + +Update the template: + +```sh +coder template update docker +``` + +You can also remove images from the validation list. Workspaces using older template versions will continue using +the removed image until you update the workspace to the latest version. + +## Updating images + +To reduce drift, we recommend versioning images in your registry by creating tags. To update the image tag in the template: + +```sh +variable "docker_image" { + description = "What Docker imagewould you like to use for your workspace?" + default = "codercom/enterprise-base:ubuntu" + validation { +- condition = contains(["my-org/base-development:v1.1", "myorg-java-development:v1.1"], var.docker_image) ++ condition = contains(["my-org/base-development:v1.1", "myorg-java-development:v1.2"], var.docker_image) + + error_message = "Invalid Docker image!" + } +} +``` + +Optional: Update workspaces to the latest template version: + +```sh +coder ls +coder update [workspace name] +``` + +## Extending this template + +See the [kreuzwerker/docker](https://registry.terraform.io/providers/kreuzwerker/docker) Terraform provider documentation to +add the following features to your Coder template: + +- SSH/TCP docker host +- Registry authentication +- Build args +- Volume mounts +- Custom container spec +- More + +We also welcome contributions! + diff --git a/examples/docker/main.tf b/examples/docker/main.tf new file mode 100644 index 0000000000000..44d56e38d1151 --- /dev/null +++ b/examples/docker/main.tf @@ -0,0 +1,99 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.4.1" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.16.0" + } + } +} + +# Admin parameters + +# Comment this out if you are specifying a different docker +# host on the "docker" provider below. +variable "step1_docker_host_warning" { + description = <<-EOF + This template will use the Docker socket present on + the Coder host, which is not necessarily your local machine. + + You can specify a different host in the template file and + surpress this warning. + EOF + validation { + condition = contains(["Continue using /var/run/docker.sock on the Coder host"], var.step1_docker_host_warning) + error_message = "Cancelling template create." + } + + sensitive = true +} +variable "step2_arch" { + description = <<-EOF + arch: What archicture is your Docker host on? + + note: codercom/enterprise-* images are only built for amd64 + EOF + + validation { + condition = contains(["amd64", "arm64", "armv7"], var.step2_arch) + error_message = "Value must be amd64, arm64, or armv7." + } + sensitive = true +} + +provider "docker" { + host = "unix:///var/run/docker.sock" +} + +provider "coder" { + # The below assumes your Coder deployment is running in docker-compose. + # If this is not the case, either comment or edit the below. + url = "http://host.docker.internal:7080" +} + +data "coder_workspace" "me" { +} + +resource "coder_agent" "dev" { + arch = var.step2_arch + os = "linux" +} + +variable "docker_image" { + description = "Which Docker image would you like to use for your workspace?" + # The codercom/enterprise-* images are only built for amd64 + default = "codercom/enterprise-base:ubuntu" + validation { + condition = contains(["codercom/enterprise-base:ubuntu", "codercom/enterprise-node:ubuntu", "codercom/enterprise-intellij:ubuntu"], var.docker_image) + error_message = "Invalid Docker image!" + } + +} + +resource "docker_volume" "home_volume" { + name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = var.docker_image + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = lower(data.coder_workspace.me.name) + dns = ["1.1.1.1"] + command = ["sh", "-c", coder_agent.dev.init_script] + env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"] + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/home/coder/" + volume_name = docker_volume.home_volume.name + read_only = false + } +}