diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f90ce74 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +**/.dockerignore +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..a4f8c3a --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,8 @@ +# Running with Docker + +1. Copy `docker/config.example.ini` to `docker/config.ini`, and change the settings to your liking. +2. Copy `docker/proxy-config.example.json` to `docker/proxy-config.json`, and do the same. +3. Build with `docker compose build` +4. Run with `docker compose up` + +The Caddy server will listen on `localhost:5004`. diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..b7dd859 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 5000 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Foxnouns.Backend/Foxnouns.Backend.csproj", "Foxnouns.Backend/"] +RUN dotnet restore "Foxnouns.Backend/Foxnouns.Backend.csproj" +COPY . . +WORKDIR "/src/Foxnouns.Backend" +RUN dotnet build "Foxnouns.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Foxnouns.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Foxnouns.Backend.dll", "--migrate-and-start"] diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 8a48573..76bf470 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -4,6 +4,7 @@ enable enable true + Linux @@ -44,4 +45,10 @@ + + + + .dockerignore + + diff --git a/Foxnouns.Frontend/Dockerfile b/Foxnouns.Frontend/Dockerfile new file mode 100644 index 0000000..4150c99 --- /dev/null +++ b/Foxnouns.Frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM docker.io/node:22 + +RUN mkdir -p /app/node_modules && chown -R node:node /app +WORKDIR /app +COPY package.json yarn.lock ./ +USER node +RUN yarn +COPY --chown=node:node . . + +RUN yarn build + +CMD ["yarn", "start"] diff --git a/Foxnouns.Frontend/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx index cedb69a..d66b6a1 100644 --- a/Foxnouns.Frontend/app/components/ErrorAlert.tsx +++ b/Foxnouns.Frontend/app/components/ErrorAlert.tsx @@ -1,5 +1,5 @@ import { TFunction } from "i18next"; -import Alert from "react-bootstrap/Alert"; +import { Alert } from "react-bootstrap"; import { Trans, useTranslation } from "react-i18next"; import { ApiError, diff --git a/Foxnouns.Frontend/app/components/nav/Navbar.tsx b/Foxnouns.Frontend/app/components/nav/Navbar.tsx index dbccac7..7e311f2 100644 --- a/Foxnouns.Frontend/app/components/nav/Navbar.tsx +++ b/Foxnouns.Frontend/app/components/nav/Navbar.tsx @@ -3,9 +3,7 @@ import Meta from "~/lib/api/meta"; import { User, UserSettings } from "~/lib/api/user"; import Logo from "./Logo"; -import Nav from "react-bootstrap/Nav"; -import Navbar from "react-bootstrap/Navbar"; -import NavDropdown from "react-bootstrap/NavDropdown"; +import { Nav, Navbar, NavDropdown } from "react-bootstrap"; import { BrightnessHigh, BrightnessHighFill, MoonFill } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; diff --git a/Foxnouns.Frontend/app/env.server.ts b/Foxnouns.Frontend/app/env.server.ts index 882d393..2add747 100644 --- a/Foxnouns.Frontend/app/env.server.ts +++ b/Foxnouns.Frontend/app/env.server.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { env } from "node:process"; export const API_BASE = env.API_BASE || "https://pronouns.localhost/api"; diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index 31d4d64..bd01eef 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -10,10 +10,8 @@ import { ShouldRevalidateFunction, } from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; -import Form from "react-bootstrap/Form"; -import Button from "react-bootstrap/Button"; +import { Form, Button, Alert } from "react-bootstrap"; import ErrorAlert from "~/components/ErrorAlert"; -import Alert from "react-bootstrap/Alert"; export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { return !actionResult; diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index 1f55fda..adc4ce9 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -6,11 +6,7 @@ import { ActionFunctionArgs, } from "@remix-run/node"; import { Form as RemixForm, useActionData, useLoaderData } from "@remix-run/react"; -import Form from "react-bootstrap/Form"; -import Button from "react-bootstrap/Button"; -import ButtonGroup from "react-bootstrap/ButtonGroup"; -import ListGroup from "react-bootstrap/ListGroup"; -import { Row, Col } from "react-bootstrap"; +import { Form, Button, ButtonGroup, ListGroup, Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import i18n from "~/i18next.server"; import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 0b1aff2..cc0c993 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -22,6 +22,7 @@ "compression": "^1.7.4", "cookie": "^0.6.0", "cross-env": "^7.0.3", + "dotenv": "^16.4.5", "express": "^4.19.2", "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", diff --git a/Foxnouns.Frontend/yarn.lock b/Foxnouns.Frontend/yarn.lock index f4b109e..1d7c038 100644 --- a/Foxnouns.Frontend/yarn.lock +++ b/Foxnouns.Frontend/yarn.lock @@ -2554,7 +2554,7 @@ domutils@^3.0.1, domutils@^3.1.0: domelementtype "^2.3.0" domhandler "^5.0.3" -dotenv@^16.0.0: +dotenv@^16.0.0, dotenv@^16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7176fc2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +services: + backend: + image: backend + build: + context: . + dockerfile: ./Dockerfile.backend + environment: + - "Database:Url=Host=pgbouncer;Database=postgres;Username=postgres;Password=postgres" + - "Database:EnablePooling=false" + - "Host=0.0.0.0" + - "Port=5000" + restart: unless-stopped + volumes: + - ./docker/config.ini:/app/config.ini + + frontend: + image: frontend + build: ./Foxnouns.Frontend + environment: + - "API_BASE=http://rate:5003/api" + restart: unless-stopped + volumes: + - ./docker/frontend.env:/app/.env + + rate: + image: rate + build: ./rate + environment: + - "PORT=5003" + restart: unless-stopped + volumes: + - ./docker/proxy-config.json:/app/proxy-config.json + + pgbouncer: + image: docker.io/edoburu/pgbouncer:latest + environment: + - "DATABASE_URL=postgres://postgres:postgres@postgres/postgres" + - "AUTH_TYPE=scram-sha-256" + - "MAX_CLIENT_CONN=100" + - "DEFAULT_POOL_SIZE=100" + - "MIN_POOL_SIZE=10" + restart: unless-stopped + + postgres: + image: docker.io/postgres:16 + command: [ "postgres", + "-c", "max-connections=1000", + "-c", "timezone=Etc/UTC", + "-c", "max_wal_size=1GB", + "-c", "min_wal_size=80MB", + "-c", "shared_buffers=128MB" ] + environment: + - "POSTGRES_PASSWORD=postgres" + restart: unless-stopped + volumes: + - postgres_data:/var/lib/postgresql/data + + caddy: + image: docker.io/caddy:2 + restart: unless-stopped + ports: + - "5004:80" + volumes: + - ./docker/Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + +volumes: + caddy_data: + caddy_config: + postgres_data: diff --git a/docker/Caddyfile b/docker/Caddyfile new file mode 100644 index 0000000..6e12647 --- /dev/null +++ b/docker/Caddyfile @@ -0,0 +1,4 @@ +http:// { + reverse_proxy /api/* http://rate:5003 + reverse_proxy http://frontend:3000 +} \ No newline at end of file diff --git a/docker/config.example.ini b/docker/config.example.ini new file mode 100644 index 0000000..ba32449 --- /dev/null +++ b/docker/config.example.ini @@ -0,0 +1,48 @@ +;; This configuration file is specifically for Docker installations. +;; Host, Port, and Database settings are overridden in the compose configuration. + +; The base *external* URL +BaseUrl = https://pronouns.localhost +; The base URL for media, without a trailing slash. This must be publicly accessible. +MediaBaseUrl = https://cdn-staging.pronouns.localhost + +[Logging] +; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal +LogEventLevel = Debug +; The URL to the Seq instance (optional) +SeqLogUrl = http://localhost:5341 +; The Sentry DSN to log to (optional) +SentryUrl = https://examplePublicKey@o0.ingest.sentry.io/0 +; Whether to trace performance with Sentry (optional) +SentryTracing = true +; Percentage of performance traces to send to Sentry (optional). Defaults to 0.0 (no traces at all) +SentryTracesSampleRate = 1.0 +; Whether to log SQL queries. Note that this is very verbose. Defaults to false. +LogQueries = false +; Whether metrics are enabled. If this is set to true, Foxnouns.NET will rely on Prometheus scraping metrics to update stats. +; If set to false, a background service will be used instead. Does not actually disable the /metrics endpoint. +; Defaults to false. +EnableMetrics = true +; The port the /metrics endpoint will listen on. Defaults to 5001. +MetricsPort = 5001 + +[Storage] +Endpoint = +AccessKey = +SecretKey = +Bucket = pronounscc + +[EmailAuth] +; The address that emails will be sent from. If not set, email auth is disabled. +From = noreply@accounts.pronouns.cc + +; The Coravel mail driver configuration. Keys should be self-explanatory. +[Coravel:Mail] +Host = localhost +Port = 1025 +Username = smtp-username +Password = smtp-password + +[DiscordAuth] +ClientId = +ClientSecret = diff --git a/docker/frontend.env b/docker/frontend.env new file mode 100644 index 0000000..e69de29 diff --git a/docker/proxy-config.example.json b/docker/proxy-config.example.json new file mode 100644 index 0000000..278249b --- /dev/null +++ b/docker/proxy-config.example.json @@ -0,0 +1,6 @@ +{ + "port": 5003, + "proxy_target": "http://localhost:5000", + "debug": true, + "powered_by": "5 gay rats" +} diff --git a/rate/Dockerfile b/rate/Dockerfile new file mode 100644 index 0000000..b88d028 --- /dev/null +++ b/rate/Dockerfile @@ -0,0 +1,16 @@ +FROM docker.io/golang:latest AS builder +WORKDIR /build +EXPOSE 5003 + +COPY . ./ +RUN go mod download -x +ENV CGO_ENABLED 0 +RUN go build -v -o rate + +FROM alpine:latest +RUN apk --no-cache add ca-certificates + +WORKDIR /app +COPY --from=builder /build/rate rate + +CMD ["/app/rate"] diff --git a/go.mod b/rate/go.mod similarity index 100% rename from go.mod rename to rate/go.mod diff --git a/go.sum b/rate/go.sum similarity index 100% rename from go.sum rename to rate/go.sum diff --git a/rate/main.go b/rate/main.go index 33cccfd..6901085 100644 --- a/rate/main.go +++ b/rate/main.go @@ -23,6 +23,14 @@ func main() { log.Fatalf("unmarshaling config.json: %v", err) } + // Override port from environment if it's set + if portEnv := os.Getenv("PORT"); portEnv != "" { + port, err := strconv.Atoi(portEnv) + if err == nil { + hn.Port = port + } + } + proxyURL, err := url.Parse(hn.ProxyTarget) if err != nil { log.Fatalf("parsing proxy_target as URL: %v", err)