updated files for a prod env
This commit is contained in:
1 parent
551b06e68a
commit
4182510795
8 files changed
+152
-17
No files matched your search
@@ -0,0 +1,24 @@
|
|||||||
|
# Copy to .env on each host and fill in real values — .env is gitignored,
|
||||||
|
# never commit real secrets.
|
||||||
|
#
|
||||||
|
# Local dev machine: keep COMPOSE_FILE=docker-compose.dev.yaml (exposes
|
||||||
|
# ports directly, hot-reloads source via bind mounts, includes adminer).
|
||||||
|
#
|
||||||
|
# Prod host: leave COMPOSE_FILE unset (docker-compose.yaml is Compose's
|
||||||
|
# default filename) and set the rest to real values.
|
||||||
|
|
||||||
|
# COMPOSE_FILE=docker-compose.dev.yaml
|
||||||
|
|
||||||
|
# Domain Traefik routes to this app (bare domain — the compose file's
|
||||||
|
# redirect middleware sends it to the www. subdomain).
|
||||||
|
VIRTUAL_HOST=example.com
|
||||||
|
|
||||||
|
# Postgres credentials — must be a real secret in prod (e.g. `openssl rand
|
||||||
|
# -hex 32`), and must match the user/password embedded in DATABASE_URL below.
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
DATABASE_URL=postgres://postgres:REPLACE_WITH_POSTGRES_PASSWORD@db:5432/postgres?sslmode=disable
|
||||||
|
|
||||||
|
# Open Data Hub license key used by the Go importer.
|
||||||
|
ODH_LICENCE_KEY=
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Downloaded event media (18GB+) — runtime data written by the importer,
|
||||||
|
# never needed at build time, and provided via a volume mount in compose.
|
||||||
|
public/
|
||||||
|
.git
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ---- build ----
|
||||||
|
FROM golang:1.24 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/vindn-backend ./cmd/server
|
||||||
|
|
||||||
|
# ---- runtime ----
|
||||||
|
# "static" (not "-nonroot") to match the existing root:root ownership of the
|
||||||
|
# bind-mounted ./backend/public/storage host directory shared with the
|
||||||
|
# frontend container — switching to a non-root UID here would need that
|
||||||
|
# directory rechowned too, or every media download starts failing silently.
|
||||||
|
FROM gcr.io/distroless/static-debian12
|
||||||
|
|
||||||
|
# The imported event data and the site's audience are both German-local;
|
||||||
|
# default to that instead of UTC so the twice-daily cron (see tasks/Cronjob.go)
|
||||||
|
# actually fires at 06:00/18:00 German time, not UTC. distroless/static ships
|
||||||
|
# tzdata, but time/tzdata is also embedded in the binary above for safety.
|
||||||
|
ENV TZ=Europe/Berlin
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /out/vindn-backend ./vindn-backend
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["./vindn-backend"]
|
||||||
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
"whimsoc/vindn-backend/config"
|
"whimsoc/vindn-backend/config"
|
||||||
"whimsoc/vindn-backend/models"
|
"whimsoc/vindn-backend/models"
|
||||||
"whimsoc/vindn-backend/repositories"
|
"whimsoc/vindn-backend/repositories"
|
||||||
@@ -9,9 +11,23 @@ import (
|
|||||||
"whimsoc/vindn-backend/tasks"
|
"whimsoc/vindn-backend/tasks"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
// Embeds the IANA timezone database into the binary so TZ=Europe/Berlin
|
||||||
|
// (see docker-compose.yaml) resolves correctly even on a minimal runtime
|
||||||
|
// image that might not ship /usr/share/zoneinfo — the twice-daily cron
|
||||||
|
// schedule in tasks/Cronjob.go depends on this being right.
|
||||||
|
_ "time/tzdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// The prod runtime image is distroless (no shell, no wget/curl), so
|
||||||
|
// Docker's HEALTHCHECK runs this binary in a second mode instead of a
|
||||||
|
// separate tool — see backend/Dockerfile.prod.
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
|
||||||
|
runHealthcheck()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Lade .env
|
// Lade .env
|
||||||
config.LoadEnv()
|
config.LoadEnv()
|
||||||
|
|
||||||
@@ -44,3 +60,10 @@ func main() {
|
|||||||
|
|
||||||
log.Fatal(app.Listen(":" + config.GetEnv("PORT", "8080")))
|
log.Fatal(app.Listen(":" + config.GetEnv("PORT", "8080")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runHealthcheck() {
|
||||||
|
resp, err := http.Get("http://localhost:" + config.GetEnv("PORT", "8080") + "/health")
|
||||||
|
if err != nil || resp.StatusCode != http.StatusOK {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
-15
@@ -1,42 +1,70 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: golang:1.24
|
build:
|
||||||
working_dir: /backend
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
db:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
- ODH_LICENCE_KEY=${ODH_LICENCE_KEY}
|
- ODH_LICENCE_KEY=${ODH_LICENCE_KEY}
|
||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/backend
|
# Working directory inside Dockerfile.prod is /app — must match the
|
||||||
command: go run cmd/server/main.go
|
# frontend's mount of the same host path below, since that's how
|
||||||
|
# downloaded media becomes visible to the frontend at all.
|
||||||
|
- ./backend/public/storage:/app/public/storage
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
# Distroless has no shell/wget/curl, so the binary checks itself —
|
||||||
|
# see the `healthcheck` subcommand in cmd/server/main.go.
|
||||||
|
test: ["CMD", "/app/vindn-backend", "healthcheck"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
db:
|
db:
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
POSTGRES_PASSWORD: postgres
|
# No insecure default here on purpose — POSTGRES_PASSWORD must be set
|
||||||
POSTGRES_DB: postgres
|
# in this environment's own .env (matching the credentials embedded
|
||||||
|
# in DATABASE_URL), so a prod deploy fails loudly instead of quietly
|
||||||
|
# running on the well-known "postgres" password.
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-postgres}
|
||||||
# ports:
|
# ports:
|
||||||
# - "5432:5432"
|
# - "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- ./pgdata:/var/lib/postgresql/data
|
- ./pgdata:/var/lib/postgresql/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build:
|
||||||
working_dir: /app
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
db:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
|
- APP_ENV=prod
|
||||||
|
- APP_DEBUG=0
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
|
||||||
- ./backend/public/storage:/app/public/storage
|
- ./backend/public/storage:/app/public/storage
|
||||||
command: sh -c "composer install --no-interaction && php -S 0.0.0.0:8000 -t public"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
- default
|
- default
|
||||||
@@ -50,7 +78,7 @@ services:
|
|||||||
- "traefik.http.routers.vindn.tls=true"
|
- "traefik.http.routers.vindn.tls=true"
|
||||||
- "traefik.http.routers.vindn.middlewares=www-redirect-reverse_vindn"
|
- "traefik.http.routers.vindn.middlewares=www-redirect-reverse_vindn"
|
||||||
- "traefik.http.middlewares.www-redirect-reverse_vindn.redirectregex.permanent=true"
|
- "traefik.http.middlewares.www-redirect-reverse_vindn.redirectregex.permanent=true"
|
||||||
- "traefik.http.middlewares.www-redirect-reverse_vindn.redirectregex.regex=^http[|s]://${VIRTUAL_HOST}/(.*)"
|
- "traefik.http.middlewares.www-redirect-reverse_vindn.redirectregex.regex=^https?://${VIRTUAL_HOST}/(.*)"
|
||||||
- "traefik.http.middlewares.www-redirect-reverse_vindn.redirectregex.replacement=https://www.${VIRTUAL_HOST}/$${1}"
|
- "traefik.http.middlewares.www-redirect-reverse_vindn.redirectregex.replacement=https://www.${VIRTUAL_HOST}/$${1}"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Dev-installed vendor/cache — the prod Dockerfile runs its own
|
||||||
|
# `composer install --no-dev` and cache:clear, so copying these in first
|
||||||
|
# would only bloat the build context and get overwritten anyway.
|
||||||
|
vendor/
|
||||||
|
var/
|
||||||
|
.git
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
FROM dunglas/frankenphp:latest
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||||
|
RUN install-php-extensions pdo_pgsql zip
|
||||||
|
|
||||||
|
# The imported event data and the site's audience are both German-local;
|
||||||
|
# default to that instead of UTC so any "now"/"today" computation doesn't
|
||||||
|
# quietly disagree with the data for the two hours after every UTC midnight.
|
||||||
|
ENV TZ=Europe/Berlin
|
||||||
|
RUN echo "date.timezone = Europe/Berlin" > $PHP_INI_DIR/conf.d/timezone.ini
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN composer install --no-interaction --no-dev --optimize-autoloader \
|
||||||
|
&& php bin/console cache:clear --env=prod --no-debug
|
||||||
|
|
||||||
|
# Caddy's default site address in the base image's Caddyfile is
|
||||||
|
# {$SERVER_NAME:localhost}; a bare ":8000" (no hostname) makes it listen
|
||||||
|
# HTTP-only on that port, which is what we want — Traefik terminates TLS
|
||||||
|
# and forwards plain HTTP to this container.
|
||||||
|
ENV SERVER_NAME=:8000
|
||||||
|
EXPOSE 8000
|
||||||
@@ -11,9 +11,9 @@
|
|||||||
<!-- MAP HERO — light: nearest ~20 events, refines once you share your location -->
|
<!-- MAP HERO — light: nearest ~20 events, refines once you share your location -->
|
||||||
<section class="section" style="padding:0;">
|
<section class="section" style="padding:0;">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<p class="eyebrow" style="margin-bottom:8px;">Nordrhein-Westfalen — updated every hour</p>
|
<p class="eyebrow" style="margin-bottom:8px;"></p>
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h1 style="font-size:clamp(28px,4vw,40px);">What's on near you</h1>
|
<h1 style="font-size:clamp(28px,4vw,40px);">Was in deiner Nähe los ist</h1>
|
||||||
<a href="{{ path('map') }}" class="btn btn-primary btn-sm">Open full map →</a>
|
<a href="{{ path('map') }}" class="btn btn-primary btn-sm">Open full map →</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user