Sr Technical Writer

Production container images should include only the application binary and its runtime dependencies. Nothing more. A standard Debian or Ubuntu base image ships with a package manager, a shell, system utilities, and hundreds of libraries that your application never calls. Every one of those extra binaries increases image size, expands the container attack surface, and raises the number of CVEs that scanners flag on every build.
Distroless container images solve this problem by stripping out everything except the application and its required runtime. Today, the Kubernetes project itself runs on distroless base images. Paired with BuildKit, Docker’s modern build engine, and multi-stage builds, you can produce minimal, hardened images without changing your development workflow. DigitalOcean Kubernetes supports this feature on all currently supported cluster versions.
In this tutorial, you will take a sample Go application, convert its Dockerfile from a standard base image to a distroless image using BuildKit and multi-stage builds, push the result to DigitalOcean Container Registry (DOCR), and deploy it on DigitalOcean Kubernetes (DOKS). Along the way, you will compare image sizes, scan for vulnerabilities, and learn how to debug distroless containers in production using Kubernetes ephemeral containers.
Standard base images like debian:bookworm or ubuntu:24.04 include hundreds of packages your application does not use at runtime. These packages introduce three problems:
bash and sh are commonly used in container breakout exploits. Distroless images do not include a shell.The following table compares common base image options:
| Base Image | Approximate Size | Includes Shell | Typical CVE Count |
|---|---|---|---|
ubuntu:24.04 |
~78 MB | Yes | 30+ |
debian:bookworm-slim |
~74 MB | Yes | 25+ |
alpine:3.20 |
~7 MB | Yes (BusyBox) | 5-10 |
gcr.io/distroless/static-debian12 |
~2 MB | No | 0-2 |
gcr.io/distroless/base-debian12 |
~20 MB | No | 0-5 |
Note: CVE counts change frequently as new vulnerabilities are discovered and patched. The numbers above reflect general trends observed in production environments and may differ when you run your own scans.
For backend and platform engineers working to harden their CI/CD pipelines, distroless images offer one of the simplest ways to reduce risk without rewriting application code.
Before you begin this tutorial, you will need:
docker buildx version.doctl command-line tool installed and authenticated with your DigitalOcean account.kubectl command-line tool installed.Start by creating a simple Go HTTP server. Go is a good choice for demonstrating distroless builds because Go binaries are statically compiled by default, meaning the final binary has no external runtime dependencies.
Create a new project directory and add the application source:
mkdir distroless-demo && cd distroless-demo
Create a file named main.go with the following content:
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
hostname, _ := os.Hostname()
fmt.Fprintf(w, "Hello from distroless!\nHostname: %s\n", hostname)
})
log.Printf("Server starting on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Next, initialize the Go module. This creates the go.mod file that the Dockerfile needs in order to copy and build your application:
go mod init go-server
go mod init distroless-demo
Now create a standard Dockerfile that uses a full Debian base image. This will serve as the baseline for comparison later:
touch Dockerfile
Now copy the below content into the Dockerfile:
FROM golang:1.23-bookworm
WORKDIR /app
COPY go.mod ./
COPY main.go ./
RUN go build -o server .
EXPOSE 8080
CMD ["./server"]
Build the baseline image with BuildKit (enabled by default in Docker 23.0+):
docker build -t distroless-demo:baseline .
OutputSending build context to Docker daemon 4.096kB
Step 1/7 : FROM golang:1.23-bookworm
---> 16c42bcc0084
Step 2/7 : WORKDIR /app
---> Using cache
---> d9381b6e4dfa
Step 3/7 : COPY go.mod ./
---> f37addb2acf1
Step 4/7 : COPY main.go ./
---> 279e1f87cbec
Step 5/7 : RUN go build -o server .
---> Running in 64879e86d557
---> Removed intermediate container 64879e86d557
---> 55b62c6b5d24
Step 6/7 : EXPOSE 8080
---> Running in 0b7abc973f97
---> Removed intermediate container 0b7abc973f97
---> 8108c090cd12
Step 7/7 : CMD ["./server"]
---> Running in fc786afae645
---> Removed intermediate container fc786afae645
---> 42288f4e0180
Successfully built 42288f4e0180
Successfully tagged distroless-demo:baseline
Check the image size:
docker images distroless-demo:baseline
You will see output similar to:
REPOSITORY TAG IMAGE ID CREATED SIZE
distroless-demo baseline 42288f4e0180 3 minutes ago 919MB
This baseline image is over 800 MB because it includes the entire Go toolchain, a Debian operating system, and all associated libraries. None of these are needed at runtime for a compiled Go binary.
BuildKit is Docker’s modern build engine. It replaces the legacy builder and provides several features that make multi-stage distroless builds faster:
If you are running Docker Engine 23.0 or later, BuildKit is already the default. You can confirm this:
docker buildx version
You should see output showing the BuildKit version, such as:
github.com/docker/buildx v0.31.1 a2675950d46b2cb171b23c2015ca44fb88607531
Note: If you get the error docker: unknown command: docker buildx, the buildx plugin is not installed. This is common on Ubuntu/Debian where Docker is installed via apt rather than Docker Desktop.
Option 1 — If Docker was installed from Docker’s official APT repository, install the plugin with:
sudo apt-get update
sudo apt-get install -y docker-buildx-plugin
Option 2 — If Docker was installed from Ubuntu’s default packages (e.g., docker.io), or if Option 1 gives you Unable to locate package, download the plugin binary directly:
BUILDX_VERSION=$(curl -s https://api.github.com/repos/docker/buildx/releases/latest | grep -oP '"tag_name": "\K[^"]+')
curl -Lo docker-buildx "https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-amd64"
chmod +x docker-buildx
mkdir -p ~/.docker/cli-plugins
mv docker-buildx ~/.docker/cli-plugins/
Then verify by running docker buildx version again.
This is the core step of the tutorial. You will split the Dockerfile into two stages:
golang image to compile the application.Create a new file named Dockerfile.distroless and copy the below content into it:
# Stage 1: Build
FROM golang:1.23-bookworm AS builder
WORKDIR /app
COPY go.mod ./
COPY main.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# Stage 2: Runtime
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
EXPOSE 8080
USER 65532:65532
CMD ["/server"]
A few important details about this Dockerfile:
CGO_ENABLED=0 ensures the Go binary is fully statically linked, with no dependency on C libraries. This is required for the static-debian12 distroless base, which does not include glibc.gcr.io/distroless/static-debian12:nonroot is the smallest distroless image (~2 MB). It is suitable for statically compiled binaries. If your application needs glibc (for example, a Python or Java app), use gcr.io/distroless/base-debian12 instead.USER 65532:65532 runs the process as the nonroot user (UID 65532) defined in the distroless image. Using the numeric UID instead of the name nonroot is required when Kubernetes runAsNonRoot is enabled, because Kubernetes cannot verify non-root status from a string username.CMD must be in exec form (JSON array). Distroless images do not include a shell, so the shell form (CMD /server) will not work.Build the distroless image:
docker build -f Dockerfile.distroless -t distroless-demo:distroless .
Check the image size:
docker images distroless-demo
You should see output similar to:
REPOSITORY TAG IMAGE ID CREATED SIZE
distroless-demo distroless 8a1a3a3f24ce 52 seconds ago 9.54MB
distroless-demo baseline 42288f4e0180 13 minutes ago 919MB
The distroless image is roughly 95 times smaller than the baseline. The exact sizes may vary depending on your application, but the reduction is consistently dramatic.
Test the distroless image locally to make sure it works:
docker run -p 8080:8080 distroless-demo:distroless
In another terminal, verify the response:
curl http://localhost:8080
You should see:
Hello from distroless!
Hostname: edb720cb0637
Comparing image size is straightforward. Let’s also scan both images for vulnerabilities to quantify the security improvement.
If you have Docker Scout available (included with Docker Desktop), you can run:
docker scout cves distroless-demo:baseline
docker scout cves distroless-demo:distroless
Note: If you get the error docker: unknown command: docker scout, the Scout plugin is not installed. Docker Scout is bundled with Docker Desktop but is not included with Docker Engine on Linux servers. You can install it manually:
curl -fsSL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh -o install-scout.sh
sh install-scout.sh
This installs the Scout CLI plugin into ~/.docker/cli-plugins/. After installation, you will need to authenticate with a Docker Hub account:
docker login
Alternatively, you can skip Docker Scout entirely and use Trivy instead, which is a free open-source scanner that does not require authentication.
If you prefer an open-source alternative that works without Docker Desktop or a Docker Hub account, you can use Trivy:
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install -y trivy
Then scan both images:
trivy image distroless-demo:baseline
trivy image distroless-demo:distroless
In this tutorial, we used Trivy to scan the images. You should see output similar to:
OutputReport Summary
┌──────────────────────────────────────────────┬──────────┬─────────────────┬─────────┐
│ Target │ Type │ Vulnerabilities │ Secrets │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ distroless-demo:baseline (debian 12.11) │ debian │ 5 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ app/server │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/bin/go │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/bin/gofmt │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/addr2line │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/asm │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/buildid │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/cgo │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/compile │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/covdata │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/cover │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/doc │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/fix │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/link │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/nm │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/objdump │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/pack │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/pprof │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/preprofile │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/test2json │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/trace │ gobinary │ 16 │ - │
├──────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤
│ usr/local/go/pkg/tool/linux_amd64/vet │ gobinary │ 16 │ - │
└──────────────────────────────────────────────┴──────────┴─────────────────┴─────────┘
A typical comparison looks like this:
| Metric | Baseline (golang:1.23-bookworm) |
Distroless (static-debian12) |
|---|---|---|
| Image size | ~919 MB | ~9.54 MB |
| OS packages | 300+ | 0 |
| Known CVEs (total) | 50-100+ | 0-2 |
| High/Critical CVEs | 5-15 | 0 |
| Shell access | Yes | No |
Note: Vulnerability counts depend on the specific point in time you run the scan and the current state of vulnerability databases. The key takeaway is that the distroless image consistently reports far fewer (often zero) CVEs because there are fewer packages to scan.
Now that the image is built and tested locally, push it to the DigitalOcean Container Registry so your Kubernetes cluster can pull it.
First, create a container registry if you don’t have one yet. Replace <your-registry-name> with a unique name of your choice:
doctl registry create <your-registry-name>
Name Endpoint Region slug
anish-registry registry.digitalocean.com/anish-registry sfo2
Note: Each DigitalOcean account can have only one container registry. If you already have a registry, you can find its name with:
doctl registry get
If you get the error registry not configured for user when running doctl registry login, it means no registry exists on your account yet. Run the doctl registry create command above first.
Now log in to your registry:
doctl registry login
Logging Docker in to registry.digitalocean.com
Notice: Login valid for 30 days. Use the --expiry-seconds flag to set a shorter expiration or --never-expire for no expiration.
Note: If you get a permission denied error referencing /root/.docker/config.json, try the following fixes depending on how doctl was installed:
If doctl was installed via Snap (look for the warning Using the doctl Snap? in the output):
sudo snap connect doctl:dot-docker
doctl registry login
If doctl was installed via apt or a direct binary, the ~/.docker directory may be owned by root. Fix the ownership:
sudo chown -R $USER:$USER ~/.docker
doctl registry login
If you are running as the root user, ensure the directory exists with correct permissions:
mkdir -p /root/.docker
chmod 700 /root/.docker
doctl registry login
Tag the image with the full registry path:
docker tag distroless-demo:distroless registry.digitalocean.com/<your-registry-name>/distroless-demo:v1
Push the image:
docker push registry.digitalocean.com/<your-registry-name>/distroless-demo:v1
Outputa45f24bd9fc9: Pushed
33b37ab0b090: Pushed
6e7fbcf090d0: Pushed
ad51d0769d16: Pushed
4cde6b0bb6f5: Pushed
bd3cdfae1d3f: Pushed
6f1cdceb6a31: Pushed
af5aa97ebe6c: Pushed
4d049f83d9cf: Pushed
114dde0fefeb: Pushed
4840c7c54023: Pushed
8fa10c0194df: Pushed
a33ba213ad26: Pushed
v1: digest: sha256:2ed0144daad224d8f93320dab9af466dddbf0385fb74625ee962b5692cd9d6db size: 3022

Because the image is only about 9 MB, the push completes in seconds. Compare that to pushing a 919 MB baseline image over the same network. You can see that the push is much faster.
If you don’t already have a DigitalOcean Kubernetes cluster running, create one from the DigitalOcean Cloud Console:
1.31.x).SFO3, NYC1).s-2vcpu-4gb) are sufficient.2.distroless-demo-cluster.The cluster will take 4-5 minutes to provision. Once the status shows Running, connect your local kubectl by downloading the cluster configuration:
doctl kubernetes cluster kubeconfig save <your-cluster-name>
OutputNotice: Adding cluster credentials to kubeconfig file found in "/root/.kube/config"
Notice: Setting current-context to <your-cluster-name>
Replace <your-cluster-name> with the name you chose (e.g., distroless-demo-cluster). You can also find this command on the cluster’s Getting Started tab in the Cloud Console.
Verify the connection:
kubectl get nodes
You should see your nodes in a Ready state:
Outputpool-fhhk2oyq7-khry0 Ready <none> 119s v1.34.1
pool-fhhk2oyq7-khryd Ready <none> 114s v1.34.1
Note: If kubectl get nodes shows a node with status NotReady or you see a control-plane node with taints, wait a few minutes for the cluster to fully initialize. On DigitalOcean Kubernetes (DOKS), the control plane is managed by DigitalOcean and does not appear in kubectl get nodes — you should only see your worker nodes.
Now connect your DOKS cluster to your container registry. If you haven’t already connected your DOKS cluster to your registry, run:
doctl registry kubernetes-manifest | kubectl apply -f -
Outputsecret/registry-anish-registry created
This creates a Kubernetes secret containing your registry credentials. Next, patch the default service account to use this secret for pulling images:
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "registry-<your-registry-name>"}]}'
Outputserviceaccount/default patched
Please replace <your-registry-name> with the name of your registry.
Now create a Kubernetes Deployment manifest. Save the following as deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: distroless-demo
labels:
app: distroless-demo
spec:
replicas: 2
selector:
matchLabels:
app: distroless-demo
template:
metadata:
labels:
app: distroless-demo
spec:
containers:
- name: distroless-demo
image: registry.digitalocean.com/<your-registry-name>/distroless-demo:v1
ports:
- containerPort: 8080
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "200m"
memory: "64Mi"
securityContext:
runAsNonRoot: true
runAsUser: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
Apply the deployment:
kubectl apply -f deployment.yaml
deployment.apps/distroless-demo created
Create a Service to expose the deployment:
apiVersion: v1
kind: Service
metadata:
name: distroless-demo
spec:
type: LoadBalancer
selector:
app: distroless-demo
ports:
- port: 80
targetPort: 8080
Save this as service.yaml and apply it:
kubectl apply -f service.yaml
service/distroless-demo created
Wait for the external IP to be assigned. You can do this by running the following command and waiting for the EXTERNAL-IP to be assigned:
kubectl get svc distroless-demo --watch
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
distroless-demo LoadBalancer 10.110.72.38 <pending> 80:32426/TCP 24s
Once the EXTERNAL-IP is assigned, test the deployment:
curl http://<EXTERNAL-IP>
You should see the familiar response, with the hostname cycling between pods on each request:
Hello from distroless!
Hostname: distroless-demo-855f44756c-mhlc2
The architecture for this deployment follows a straightforward path: Build (locally or in CI) → Push to DOCR → Deploy on DOKS. This same pattern works with GitHub Actions, GitLab CI, or any CI/CD platform that supports Docker and kubectl.
One of the most common concerns about distroless images is that they lack a shell. Without bash or sh, you cannot kubectl exec into the container for troubleshooting. Kubernetes solves this with ephemeral containers, which let you attach a debug container to a running pod.
First, find the name of a running pod:
kubectl get pods -l app=distroless-demo
Then attach an ephemeral debug container using kubectl debug:
kubectl debug -it <pod-name> --image=busybox:latest --target=distroless-demo -- sh
This command:
distroless-demo container, so you can inspect its processes.sh) for interactive troubleshooting.From inside the debug container, you can run commands like:
# List processes in the target container
ps aux
# Check network connectivity
wget -qO- http://localhost:8080
# Inspect the filesystem
ls /proc/1/root/
You should see output similar to:
OutputPID USER TIME COMMAND
1 65532 0:00 /server
20 root 0:00 sh
28 root 0:00 ps aux
Hello from distroless!
Hostname: distroless-demo-855f44756c-7r2xb
When you exit the shell, the ephemeral container is automatically removed. This approach gives you full debugging capabilities without compromising the security posture of your production image.
Note: Ephemeral containers require Kubernetes 1.25 or later. DigitalOcean Kubernetes supports this feature on all currently supported cluster versions.
When adopting distroless images across your organization, keep these practices in mind:
static-debian12 for statically compiled binaries (Go, Rust). Use base-debian12 for applications that need glibc (Python, Java, Node.js). Use the language-specific variants like java21-debian12 or nodejs22-debian12 when available.nonroot tag and numeric UIDs. Running containers as root, even inside a distroless image, weakens your security posture. The nonroot variants set the user to UID 65532. Always use USER 65532:65532 in your Dockerfile (not USER nonroot:nonroot) so that Kubernetes can verify non-root status when runAsNonRoot: true is set in the pod security context.latest, pin to a specific digest (for example, gcr.io/distroless/static-debian12@sha256:abc123...) to ensure reproducible builds.readOnlyRootFilesystem: true in your Kubernetes security context. Since distroless images contain no writable system files, this setting adds defense in depth without affecting your application.A distroless container image is a Docker image that contains only the application binary and its runtime dependencies. It does not include a package manager, a shell, or standard Linux utilities. Google’s distroless project maintains the most widely used set of distroless base images, built from Debian packages but stripped to the absolute minimum.
Distroless images remove the tools that attackers typically use after gaining initial access to a container. Without a shell, package manager, or network utilities like curl and wget, an attacker who compromises the application process has far fewer options for lateral movement or privilege escalation. This reduction in the container attack surface is one of the primary reasons organizations adopt distroless images for production workloads.
Alpine Linux images are small (~7 MB) and include a shell (BusyBox) along with the apk package manager. Distroless images are even smaller (as low as 2 MB) and include no shell and no package manager at all. Alpine is a good choice when you need a small image but still want interactive access for debugging. Distroless is the better choice when you want the smallest possible attack surface and are willing to use Kubernetes ephemeral containers or debug image tags for troubleshooting.
You have two primary options. First, you can use Kubernetes ephemeral containers with kubectl debug to attach a temporary debug container (like BusyBox or Alpine) to the running pod. This gives you shell access to inspect processes, network, and files without modifying the production image. Second, Google’s distroless project publishes :debug tagged variants of each image that include a BusyBox shell. You can swap to the debug tag temporarily during incident response: for example, gcr.io/distroless/static-debian12:debug.
BuildKit is the modern build engine for Docker, enabled by default since Docker Engine 23.0. For multi-stage builds, BuildKit provides significant advantages: it runs independent stages in parallel, skips stages that the final image does not reference, and uses content-based caching for more precise cache invalidation. These features make the build-compile-copy pattern used in distroless Dockerfiles both fast and efficient.
You have now converted a standard container image into a hardened distroless image using BuildKit and multi-stage builds. The production image dropped from over 919 MB to under 10 MB, and the number of flagged CVEs dropped to near zero. You pushed the image to DigitalOcean Container Registry and deployed it to DigitalOcean Kubernetes with resource limits and security context policies in place. You also learned how to debug distroless containers using Kubernetes ephemeral containers, which removes the last practical barrier to running shell-free images in production.
Distroless images are not the right fit for every use case. Development and local debugging environments still benefit from full base images with interactive shells. But for production workloads where security, compliance, and image size matter, distroless combined with BuildKit and multi-stage builds is one of the most effective improvements you can make to your container pipeline.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
I help Businesses scale with AI x SEO x (authentic) Content that revives traffic and keeps leads flowing | 3,000,000+ Average monthly readers on Medium | Sr Technical Writer @ DigitalOcean | Ex-Cloud Consultant @ AMEX | Ex-Site Reliability Engineer(DevOps)@Nutanix
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.