diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..bc42ff2 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,42 @@ +name: Docker Build and Push + +on: + push: + branches: [ main ] + tags: [ 'v*' ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..788051a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.24-alpine AS builder + +RUN apk add --no-cache gcc musl-dev sqlite-dev + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o apocapoc-api cmd/api/main.go + +FROM alpine:latest +RUN apk --no-cache add ca-certificates sqlite-libs + +WORKDIR /root/ + +COPY --from=builder /app/apocapoc-api . + +EXPOSE 8080 + +CMD ["./apocapoc-api"] diff --git a/README.md b/README.md index c281a59..d723306 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,49 @@ Self-hosted habit tracking service with a clean, hexagonal architecture. ## Quick Start +### Using Docker Compose (Recommended) + +1. Create a `docker-compose.yml` file: + +```yaml +services: + api: + image: ghcr.io/davidfolch/apocapoc-api:latest + ports: + - "8080:8080" + environment: + - DB_TYPE=sqlite + - DB_PATH=/data/apocapoc.db + - JWT_SECRET=YOUR_SECRET_HERE + - JWT_EXPIRY=24h + - REFRESH_TOKEN_EXPIRY=168h + - CORS_ORIGINS=http://localhost:3000 + - DEFAULT_TIMEZONE=UTC + volumes: + - habit-data:/data + restart: unless-stopped + +volumes: + habit-data: +``` + +2. **Important**: Replace `YOUR_SECRET_HERE` with a secure random string for `JWT_SECRET` + +3. Start the service: + +```bash +docker-compose up -d +``` + +The API will be available at `http://localhost:8080` + +**Configuration options:** +- `JWT_SECRET`: **Required**. Use a long random string +- `JWT_EXPIRY`: Token expiration (e.g., `24h`, `48h`) +- `REFRESH_TOKEN_EXPIRY`: Refresh token expiration (e.g., `168h` = 7 days) +- `CORS_ORIGINS`: Comma-separated list of allowed origins +- `DEFAULT_TIMEZONE`: Timezone for date calculations (e.g., `UTC`, `Europe/Madrid`) + ### Using the binary 1. Download the latest release diff --git a/internal/infrastructure/config/config.go b/internal/infrastructure/config/config.go index d59b793..fbd0aac 100644 --- a/internal/infrastructure/config/config.go +++ b/internal/infrastructure/config/config.go @@ -25,8 +25,8 @@ func Load() (*Config, error) { cfg := &Config{ DBType: os.Getenv("DB_TYPE"), DBPath: os.Getenv("DB_PATH"), - Port: os.Getenv("PORT"), - Host: os.Getenv("HOST"), + Port: getEnvOrDefault("PORT", "8080"), + Host: getEnvOrDefault("HOST", "0.0.0.0"), JWTSecret: os.Getenv("JWT_SECRET"), JWTExpiry: os.Getenv("JWT_EXPIRY"), RefreshTokenExpiry: os.Getenv("REFRESH_TOKEN_EXPIRY"), @@ -40,12 +40,6 @@ func Load() (*Config, error) { if cfg.DBPath == "" { return nil, fmt.Errorf("DB_PATH is required") } - if cfg.Port == "" { - return nil, fmt.Errorf("PORT is required") - } - if cfg.Host == "" { - return nil, fmt.Errorf("HOST is required") - } if cfg.JWTSecret == "" { return nil, fmt.Errorf("JWT_SECRET is required") } @@ -64,3 +58,10 @@ func Load() (*Config, error) { return cfg, nil } + +func getEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +}