Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Goal
Complete Lab 6 for QuickNotes: containerize the app, run it with Compose and persistence, and apply the bonus hardening defaults.

## Changes
- Task 1:
Add a multi-stage distroless Dockerfile with a static stripped Go binary, nonroot runtime, and image size under 25 MB.
- Task 2:
Add `compose.yaml` with port publishing, named volume persistence, env vars, restart policy, and a distroless-compatible healthcheck.
- Bonus:
Harden the `quicknotes` service with `cap_drop: [ALL]`, `read_only: true`, `tmpfs: /tmp`, and `no-new-privileges`; document Docker and Trivy verification in `submissions/lab6.md`.

## Testing
- `go test ./...`
- `docker build -t quicknotes:lab6 ./app`
- `docker run --rm -p 8080:8080 -v "$PWD/app/data:/data" quicknotes:lab6` and `curl /health`
- `docker compose up --build -d`, POST note, verify persistence across `down/up`, verify reset with `down -v`
- `docker inspect` checks for nonroot, dropped capabilities, read-only root, and `no-new-privileges`
- `docker compose exec quicknotes sh` fails as expected
- `trivy image --severity HIGH,CRITICAL quicknotes:lab6`

## Checklist
- [x] Title is a clear sentence (≤ 70 chars)
- [x] Commits are signed (`git log --show-signature`)
- [x] `submissions/labN.md` updated
27 changes: 27 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
FROM golang:1.24.0-alpine3.21 AS builder

WORKDIR /src

COPY go.mod ./
RUN go mod download

COPY *.go ./
COPY seed.json ./

RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/quicknotes .
RUN mkdir -p /out/data && cp /src/seed.json /out/data/notes.json

FROM gcr.io/distroless/static:nonroot

COPY --from=builder /out/quicknotes /quicknotes
COPY --from=builder /src/seed.json /seed.json
COPY --from=builder --chown=65532:65532 /out/data /data

ENV ADDR=:8080 \
DATA_PATH=/data/notes.json \
SEED_PATH=/seed.json

USER 65532:65532
EXPOSE 8080

ENTRYPOINT ["/quicknotes"]
34 changes: 34 additions & 0 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"errors"
"log"
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
Expand All @@ -16,6 +18,13 @@ func main() {
dataPath := envOrDefault("DATA_PATH", "data/notes.json")
seedPath := envOrDefault("SEED_PATH", "seed.json")

if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
if err := runHealthcheck(addr); err != nil {
log.Fatal(err)
}
return
}

if err := ensureSeeded(dataPath, seedPath); err != nil {
log.Fatalf("seed: %v", err)
}
Expand Down Expand Up @@ -83,3 +92,28 @@ func dirname(p string) string {
}
return "."
}

func runHealthcheck(addr string) error {
hostport := addr
if strings.HasPrefix(hostport, ":") {
hostport = "127.0.0.1" + hostport
} else if host, port, err := net.SplitHostPort(addr); err == nil {
if host == "" || host == "0.0.0.0" || host == "::" {
host = "127.0.0.1"
}
hostport = net.JoinHostPort(host, port)
}

client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get("http://" + hostport + "/health")
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return errors.New("health endpoint returned non-200 status")
}

return nil
}
30 changes: 30 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
services:
quicknotes:
build:
context: ./app
image: quicknotes:lab6
ports:
- "8080:8080"
environment:
ADDR: ":8080"
DATA_PATH: /data/notes.json
SEED_PATH: /seed.json
volumes:
- quicknotes-data:/data
tmpfs:
- /tmp
healthcheck:
test: ["CMD", "/quicknotes", "healthcheck"]
interval: 10s
timeout: 3s
retries: 3
start_period: 3s
cap_drop:
- ALL
read_only: true
security_opt:
- no-new-privileges:true
restart: unless-stopped

volumes:
quicknotes-data:
Loading