A custom HTTP/1.1 server built directly on top of TCP sockets in Go (without net/http request handling), following Clean Architecture principles.
Version 1 focuses on a stable server foundation: manual HTTP parsing, routing, middleware, graceful shutdown, HTTPS-only runtime config, and structured logging.
- Start an HTTPS-only server on a configurable port.
- Parse raw HTTP/1.1 requests into structured request objects.
- Route handlers by
METHOD:PATH. - Serve two starter endpoints:
GET /health->200 OK, bodyokGET /hello->200 OK, bodyhello
- Return protocol-correct fallback responses:
404 Not Foundfor unknown paths405 Method Not Allowed(+Allowheader) when path exists but method does not400 Bad Requestfor malformed requests
- Apply middleware for logging, panic recovery, and request timeout.
- Propagate request context and cancellation into handler/use-case flow.
- Gracefully shut down on
Ctrl+C(SIGINT) /SIGTERM.
light_serve/
├── cmd/server/main.go # Composition root and runtime lifecycle
├── internal/
│ ├── domain/ # Domain errors/entities
│ ├── usecase/ # Use-case contracts and ports
│ └── adapter/
│ ├── http/ # Parser, router, middleware, HTTP server adapter
│ ├── logging/ # Logger adapter(s)
│ └── persistence/ # Persistence adapter placeholder
└── docs/architecture.md # Architecture design document
- Go 1.21+
- Any shell/terminal (PowerShell, cmd, bash, zsh, etc.)
All values are optional. If unset, defaults are used.
LIGHT_SERVE_PORT(default:8080)LIGHT_SERVE_READ_TIMEOUT(default:5s)LIGHT_SERVE_WRITE_TIMEOUT(default:5s)LIGHT_SERVE_SHUTDOWN_DEADLINE(default:10s)LIGHT_SERVE_REQUEST_TIMEOUT(default:2s)LIGHT_SERVE_TLS_CERT_FILE(required)LIGHT_SERVE_TLS_KEY_FILE(required)LIGHT_SERVE_TLS_MIN_VERSION(optional, default:1.3, allowed:1.2,1.3)
Examples:
# bash/zsh (macOS/Linux)
export LIGHT_SERVE_PORT=8081
export LIGHT_SERVE_REQUEST_TIMEOUT=3s
export LIGHT_SERVE_TLS_CERT_FILE=/absolute/path/cert.pem
export LIGHT_SERVE_TLS_KEY_FILE=/absolute/path/key.pem# PowerShell
$env:LIGHT_SERVE_PORT="8081"
$env:LIGHT_SERVE_REQUEST_TIMEOUT="3s"
$env:LIGHT_SERVE_TLS_CERT_FILE="C:\path\to\cert.pem"
$env:LIGHT_SERVE_TLS_KEY_FILE="C:\path\to\key.pem":: cmd.exe
set LIGHT_SERVE_PORT=8081
set LIGHT_SERVE_REQUEST_TIMEOUT=3s
set LIGHT_SERVE_TLS_CERT_FILE=C:\path\to\cert.pem
set LIGHT_SERVE_TLS_KEY_FILE=C:\path\to\key.pemCreate a local self-signed certificate for localhost and 127.0.0.1.
# PowerShell (Windows)
& "C:\Program Files\Git\usr\bin\openssl.exe" req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes `
-keyout "$PWD\certs\key.pem" `
-out "$PWD\certs\cert.pem" `
-subj "/CN=localhost" `
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"# bash/zsh (Linux/macOS)
mkdir -p certs
openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \
-keyout certs/key.pem \
-out certs/cert.pem \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"Then set absolute TLS file paths before running the server:
$env:LIGHT_SERVE_TLS_CERT_FILE="$PWD\certs\cert.pem"
$env:LIGHT_SERVE_TLS_KEY_FILE="$PWD\certs\key.pem"
$env:LIGHT_SERVE_TLS_MIN_VERSION="1.3"export LIGHT_SERVE_TLS_CERT_FILE="$(pwd)/certs/cert.pem"
export LIGHT_SERVE_TLS_KEY_FILE="$(pwd)/certs/key.pem"
export LIGHT_SERVE_TLS_MIN_VERSION=1.3From the project root:
go run ./cmd/serverYou should see startup logs indicating the HTTPS listening address.
For local self-signed certs, use curl -k in the test commands below.
Build the image from the project root:
docker build -t light-serve:v1 .Run the container and publish port 8080:
docker run --rm -p 8080:8080 \
-v /absolute/path/certs:/certs:ro \
-e LIGHT_SERVE_TLS_CERT_FILE=/certs/cert.pem \
-e LIGHT_SERVE_TLS_KEY_FILE=/certs/key.pem \
--name light-serve light-serve:v1Run with custom runtime config (example):
docker run --rm -p 8081:8081 \
-v /absolute/path/certs:/certs:ro \
-e LIGHT_SERVE_PORT=8081 \
-e LIGHT_SERVE_REQUEST_TIMEOUT=3s \
-e LIGHT_SERVE_TLS_CERT_FILE=/certs/cert.pem \
-e LIGHT_SERVE_TLS_KEY_FILE=/certs/key.pem \
--name light-serve light-serve:v1Open a second terminal while the server is running.
curl -k -i https://127.0.0.1:8080/healthExpected:
- status line includes
HTTP/1.1 200 OK - body is
ok
curl -k -i https://127.0.0.1:8080/helloExpected:
- status line includes
HTTP/1.1 200 OK - body is
hello
curl -k -i https://127.0.0.1:8080/not-foundExpected:
HTTP/1.1 404 Not Found
curl -k -i -X POST https://127.0.0.1:8080/healthExpected:
HTTP/1.1 405 Method Not AllowedAllow: GET
The same endpoint checks work when running in Docker (using the published host port).
Run all tests:
go test ./...Run selected packages:
go test ./cmd/server
go test ./internal/adapter/http
go test ./internal/adapter/loggingIf your PowerShell setup aliases curl to a different command, use curl.exe explicitly.
This repository includes a workflow at .github/workflows/ci-cd.yml that:
- runs CI (
go mod download,go test ./...,go build ./cmd/server) on pull requests tomainand on pushes tomain - publishes a container image to ACR only on pushes to
main
Add these repository secrets before using image publish:
ACR_LOGIN_SERVER(example:myregistry.azurecr.io)ACR_USERNAMEACR_PASSWORD
On each successful push to main, the workflow pushes:
myregistry.azurecr.io/light-serve:mainmyregistry.azurecr.io/light-serve:sha-<short-commit-sha>
Replace myregistry.azurecr.io with your ACR_LOGIN_SERVER value.
Example pull command:
docker pull myregistry.azurecr.io/light-serve:main- This is a custom learning-oriented HTTP server implementation, not production-hardened web infrastructure yet.
- The architecture is intentionally layered and testable to support incremental additions (real use-case slices, persistence adapters, richer routing, auth, static files, etc.).