I timed myself. Starting from an empty directory with a Go application idea, how fast could I get to a running deployment on OKE? The answer was 14 minutes. docker init did more of the work than I expected.
What docker init Does
If you haven't used it, docker init is an interactive scaffolding tool built into Docker CLI. You run it in your project directory and it generates a Dockerfile, .dockerignore, and docker-compose.yml tuned for your language.
mkdir oci-api && cd oci-api
go mod init github.com/pmady/oci-api
# Write a quick API
cat > main.go << 'EOF'
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "running on %s", os.Getenv("OCI_REGION"))
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
EOF
Now the magic part:
$ docker init
Welcome to the Docker Init CLI!
? What application platform does your project use? Go
? What version of Go do you want to use? 1.22
? What's the relative directory for your main package? .
? What port does your server listen on? 8080
It generates three files:
Dockerfile — Multi-stage build, distroless base, non-root user. Actually good defaults. I've seen teams write worse Dockerfiles by hand.
compose.yaml — Basic setup with port mapping and env vars.
.dockerignore — Excludes .git, binaries, vendor directory. Reasonable.
The generated Dockerfile looked like this (slightly simplified):
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/server .
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /bin/server /bin/
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/bin/server"]
Multi-stage, static binary, distroless, non-root. I'd write almost the same thing myself. The only change I made was adding -ldflags="-s -w" to strip debug symbols and shrink the binary.
Minute 0-5: Build and Test Locally
docker compose up --build
# In another terminal
curl localhost:8080
# running on
curl localhost:8080/health
# 200 OK
Works. Five minutes in and I have a containerized API running locally.
Minute 5-8: Push to OCIR
# Login
docker login iad.ocir.io -u '<tenancy-namespace>/pmady'
# Tag
docker tag oci-api-server:latest iad.ocir.io/<tenancy>/demos/oci-api:v1
# Quick scan
docker scout cves iad.ocir.io/<tenancy>/demos/oci-api:v1
# Push
docker push iad.ocir.io/<tenancy>/demos/oci-api:v1
Scout showed zero CVEs because distroless has almost nothing in it. Push took about 10 seconds because the image is 12MB.
Minute 8-14: Deploy to OKE
I already had an OKE cluster running (if you don't, add 20 minutes for oci ce cluster create). The deployment manifest:
# deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: oci-api
spec:
replicas: 2
selector:
matchLabels:
app: oci-api
template:
metadata:
labels:
app: oci-api
spec:
containers:
- name: api
image: iad.ocir.io/<tenancy>/demos/oci-api:v1
ports:
- containerPort: 8080
env:
- name: OCI_REGION
value: us-ashburn-1
readinessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 5
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
imagePullSecrets:
- name: ocir-secret
---
apiVersion: v1
kind: Service
metadata:
name: oci-api
annotations:
oci.oraclecloud.com/load-balancer-type: "lb"
service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "100"
spec:
type: LoadBalancer
selector:
app: oci-api
ports:
- port: 80
targetPort: 8080
kubectl apply -f deploy.yaml
# Wait for LB IP
kubectl get svc oci-api -w
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# oci-api LoadBalancer 10.96.1.120 129.153.xx.xx 80:31234/TCP
curl http://129.153.xx.xx/
# running on us-ashburn-1
14 minutes. Empty folder to public API on OKE.
What docker init Got Right
I was skeptical about docker init being useful for anything beyond demos. But the generated Dockerfile was genuinely good:
- Multi-stage build — keeps the final image small
- Distroless base — minimal attack surface, near-zero CVEs
- Non-root user — security best practice out of the box
-
Separate dependency download —
go mod downloadbeforeCOPY .means dependencies are cached and rebuilds are fast
The only things I changed for OKE deployment were the -ldflags optimization and adding a health check endpoint (which docker init can't know about since it's application-specific).
What It Doesn't Do
docker init handles the Docker side. It doesn't generate:
- Kubernetes manifests
- CI/CD pipeline config
- OCIR login/push scripts
- Terraform for infrastructure
That's fair. It's a Docker tool, not a platform tool. But the Dockerfile it generates is solid enough that I don't need to edit it for most Go and Python projects.
Languages I've Tested
| Language | Quality of Generated Dockerfile | Notes |
|---|---|---|
| Go | Excellent | Multi-stage, static binary, distroless |
| Python | Good | Uses slim base, proper requirements.txt handling |
| Node.js | Good | Multi-stage, npm ci for production |
| Rust | Excellent | cargo-chef for caching, musl for static binary |
| Java | Decent | Uses Eclipse Temurin, could use jlink for smaller images |
Go and Rust output is good enough to use as-is. Python and Node need minor tweaks depending on your framework. Java needs the most work.
My Workflow Now
For quick services and prototypes, this is my default:
mkdir project && cd project
# write code
docker init
# tweak Dockerfile if needed
docker compose up --build # test locally
docker push ... # push to OCIR
kubectl apply -f deploy.yaml # deploy to OKE
The gap between "it works on my laptop" and "it's running on OKE" is smaller than it's ever been.
Pavan Madduri — Oracle ACE Associate, CNCF Golden Kubestronaut. GitHub | LinkedIn | Website | Google Scholar | ResearchGate












