Docker Hands-On Lab with Go #
This lab provides practical exercises using Go to reinforce the Docker concepts covered in the lecture.
Prerequisites #
- Docker Engine installed on your system
- Basic command line familiarity
- Text editor of your choice
Exercise 1: Getting Started with Docker #
1.1 Verify Docker Installation #
Ensure Docker is properly installed on your system:
docker --version
docker info
1.2 Running Your First Container #
Let’s run an interactive container using the official Alpine image:
docker run -it --rm alpine sh
Inside the container, try some commands:
ls
cat /etc/os-release
apk update
Type exit
to leave the container.
Exercise 2: Building a Go Application with Docker #
2.1 Create a Simple Go Web Server #
Create a directory for your project:
mkdir docker-go-lab
cd docker-go-lab
Create a file named main.go
:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type ServerInfo struct {
Hostname string `json:"hostname"`
GoVersion string `json:"go_version"`
Container bool `json:"container"`
}
func main() {
// Get hostname
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
}
// Define HTTP handlers
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Hello from Docker!\n")
fmt.Fprintf(w, "This is a Go application running in a container.\n")
fmt.Fprintf(w, "Container hostname: %s\n", hostname)
})
http.HandleFunc("/api/info", func(w http.ResponseWriter, r *http.Request) {
info := ServerInfo{
Hostname: hostname,
GoVersion: "1.20", // Hardcoded for simplicity, in reality use runtime.Version()
Container: true,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(info)
})
// Start server
port := 8080
fmt.Printf("Starting server on port %d...\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}
2.2 Create a Go Module #
Initialize a Go module:
go mod init docker-go-lab
2.3 Create a Dockerfile #
Create a file named Dockerfile
:
# Build stage
FROM golang:1.20-alpine AS build
WORKDIR /app
# Copy go module files first for better caching
COPY go.mod ./
# Copy source code
COPY main.go ./
# Build the application
RUN go build -o server .
# Runtime stage
FROM alpine:latest
WORKDIR /app
# Copy the binary from the build stage
COPY --from=build /app/server /app/server
# Expose the port
EXPOSE 8080
# Run the application
CMD ["/app/server"]
2.4 Build and Run Your Image #
Build your Docker image:
docker build -t go-web-app .
Run the container:
docker run -d -p 8080:8080 --name go-app go-web-app
Access your application:
# Using curl
curl http://localhost:8080
# Using curl for JSON API
curl http://localhost:8080/api/info
You can also visit http://localhost:8080
in your browser.
2.5 Explore Your Container #
Check the running containers:
docker ps
View container logs:
docker logs go-app
Execute commands in the running container:
docker exec -it go-app sh
Stop and remove your container:
docker stop go-app
docker rm go-app
Exercise 3: Multi-Stage Builds and Optimization #
3.1 Create an Optimized Dockerfile #
Let’s create an even more optimized Dockerfile that produces a smaller image:
# Build stage
FROM golang:1.20-alpine AS build
WORKDIR /app
# Copy go module files
COPY go.mod ./
COPY main.go ./
# Build with optimizations
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o server .
# Runtime stage - using scratch (empty) image
FROM scratch
# Copy SSL certificates for HTTPS requests
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
WORKDIR /app
# Copy the binary from the build stage
COPY --from=build /app/server /app/server
# Expose the port
EXPOSE 8080
# Run the application
CMD ["/app/server"]
Build and tag this optimized image:
docker build -t go-web-app:optimized -f Dockerfile.optimized .
Compare the image sizes:
docker images | grep go-web-app
Exercise 4: Docker Networks and Volumes with Go #
4.1 Create a Stateful Go Application #
Create a new file named stateful.go
:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"sync"
)
// Counter represents a simple counter with mutex for concurrent access
type Counter struct {
Value int `json:"value"`
Path string `json:"-"`
mu sync.Mutex
}
// NewCounter creates a new counter and loads its value from file if available
func NewCounter(dataPath string) *Counter {
c := &Counter{
Value: 0,
Path: dataPath,
}
// Create directory if it doesn't exist
dir := filepath.Dir(dataPath)
if err := os.MkdirAll(dir, 0755); err != nil {
log.Printf("Error creating directory: %v", err)
}
// Try to load existing counter value
if data, err := ioutil.ReadFile(dataPath); err == nil {
var storedCounter Counter
if err := json.Unmarshal(data, &storedCounter); err == nil {
c.Value = storedCounter.Value
}
}
return c
}
// Increment increases the counter and saves it
func (c *Counter) Increment() int {
c.mu.Lock()
defer c.mu.Unlock()
c.Value++
// Save to file
data, err := json.Marshal(c)
if err != nil {
log.Printf("Error marshaling counter: %v", err)
return c.Value
}
if err := ioutil.WriteFile(c.Path, data, 0644); err != nil {
log.Printf("Error writing counter: %v", err)
}
return c.Value
}
// GetValue returns the current value
func (c *Counter) GetValue() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.Value
}
func main() {
// Create a counter that persists to a file
counter := NewCounter("/data/counter.json")
// Get hostname for display
hostname, _ := os.Hostname()
// Define HTTP handlers
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Hello from Docker!\n")
fmt.Fprintf(w, "This is a stateful Go application with persistence.\n")
fmt.Fprintf(w, "Container hostname: %s\n", hostname)
fmt.Fprintf(w, "Current count: %d\n", counter.GetValue())
fmt.Fprintf(w, "Visit /increment to increase the counter.\n")
})
http.HandleFunc("/increment", func(w http.ResponseWriter, r *http.Request) {
newValue := counter.Increment()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"value": newValue,
"hostname": hostname,
})
})
// Start server
port := 8080
fmt.Printf("Starting server on port %d with data persistence...\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}
Create a Dockerfile for the stateful application:
FROM golang:1.20-alpine AS build
WORKDIR /app
COPY stateful.go .
RUN go build -o stateful-app stateful.go
FROM alpine:latest
WORKDIR /app
COPY --from=build /app/stateful-app .
# Create a volume mount point
VOLUME /data
EXPOSE 8080
CMD ["/app/stateful-app"]
4.2 Run with Persistent Volume #
Build the stateful application:
docker build -t go-stateful-app -f Dockerfile.stateful .
Create a volume:
docker volume create go-app-data
Run the container with the volume:
docker run -d -p 8080:8080 -v go-app-data:/data --name stateful-app go-stateful-app
Test the persistence:
- Visit
http://localhost:8080/increment
several times - Stop and remove the container
- Start a new container with the same volume
- Verify the counter value persisted
# Stop and remove the container
docker stop stateful-app
docker rm stateful-app
# Start a new container with the same volume
docker run -d -p 8080:8080 -v go-app-data:/data --name stateful-app-2 go-stateful-app
# Check if the counter value persisted
curl http://localhost:8080
Conclusion #
This lab has covered:
- Running containers with Docker
- Building Go applications for containerized environments
- Using multi-stage builds for optimization
- Working with Docker volumes for persistence
These practices are essential for developing modern, containerized Go applications.
Challenge Exercise #
Build a multi-container application with these components:
- A Go API server that handles requests
- A separate data storage container
- Connect them using Docker networks for communication
- Implement proper volume mounts for data persistence
Both containers should be configured to restart automatically if they crash or if Docker restarts.