Images ghcr.io/llinguini/shellforge-api:latest — API (port 8080) ghcr.io/llinguini/shellforge-web:latest — Web dashboard (port 3000)
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: shellforge
POSTGRES_PASSWORD: change-me-use-a-long-random-string
POSTGRES_DB: shellforge
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U shellforge"]
interval: 5s
timeout: 5s
retries: 10
api:
image: ghcr.io/llinguini/shellforge-api:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "8080:8080"
environment:
APP_ENV: production
PORT: 8080
DB_HOST: postgres
DB_PORT: 5432
DB_USER: shellforge
DB_PASSWORD: change-me-use-a-long-random-string
DB_NAME: shellforge
DB_AUTO_MIGRATE: "true"
DB_CONNECT_MAX_WAIT: 30s
JWT_SECRET: replace-me-with-a-long-random-secret-min-32-chars
JWT_ACCESS_TTL: 15m
JWT_REFRESH_TTL: 168h
CORS_ALLOWED_ORIGINS: "http://your-app-domain.com"
LOG_LEVEL: info
web:
image: ghcr.io/llinguini/shellforge-web:latest
restart: unless-stopped
depends_on:
- api
ports:
- "3000:3000"
environment:
NEXT_PUBLIC_API_URL: "http://your-api-domain.com"
volumes:
postgres_data:
# API environment variables — never commit real secrets
APP_ENV=production
PORT=8080
# PostgreSQL
DB_HOST=postgres
DB_PORT=5432
DB_USER=shellforge
DB_PASSWORD=change-me-use-a-long-random-string
DB_NAME=shellforge
DB_AUTO_MIGRATE=true
DB_CONNECT_MAX_WAIT=30s
# JWT — use a long random string in production
JWT_SECRET=replace-me-with-a-long-random-secret-min-32-chars
JWT_ACCESS_TTL=15m
JWT_REFRESH_TTL=168h
# CORS — set to your web app domain
CORS_ALLOWED_ORIGINS=http://your-app-domain.com
LOG_LEVEL=info
# Web app
NEXT_PUBLIC_API_URL=http://your-api-domain.com
Steps
- Copy
docker-compose.yml and replace all placeholder values (change-me-*, replace-me-*, domain URLs). - Run
docker compose up -d. - The API runs on port
8080, the web dashboard on port 3000. - Point your reverse proxy (nginx, Caddy, Traefik…) to those ports and add TLS.
All manifests below target a shellforge namespace. Create it first:
kubectl create namespace shellforge
apiVersion: v1
kind: Secret
metadata:
name: shellforge-secrets
namespace: shellforge
type: Opaque
stringData:
DB_PASSWORD: change-me-use-a-long-random-string
JWT_SECRET: replace-me-with-a-long-random-secret-min-32-chars
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: shellforge
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
env:
- name: POSTGRES_USER
value: shellforge
- name: POSTGRES_DB
value: shellforge
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: shellforge-secrets
key: DB_PASSWORD
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: shellforge
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
clusterIP: None # headless — StatefulSet DNS
apiVersion: apps/v1
kind: Deployment
metadata:
name: shellforge-api
namespace: shellforge
spec:
replicas: 2
selector:
matchLabels:
app: shellforge-api
template:
metadata:
labels:
app: shellforge-api
spec:
containers:
- name: api
image: ghcr.io/llinguini/shellforge-api:latest
ports:
- containerPort: 8080
name: http
env:
- name: APP_ENV
value: production
- name: PORT
value: "8080"
- name: DB_HOST
value: postgres.shellforge.svc.cluster.local
- name: DB_PORT
value: "5432"
- name: DB_USER
value: shellforge
- name: DB_NAME
value: shellforge
- name: DB_AUTO_MIGRATE
value: "true"
- name: DB_CONNECT_MAX_WAIT
value: 30s
- name: JWT_ACCESS_TTL
value: 15m
- name: JWT_REFRESH_TTL
value: 168h
- name: LOG_LEVEL
value: info
- name: CORS_ALLOWED_ORIGINS
value: "http://your-app-domain.com"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: shellforge-secrets
key: DB_PASSWORD
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: shellforge-secrets
key: JWT_SECRET
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: shellforge-api
namespace: shellforge
spec:
selector:
app: shellforge-api
type: NodePort
ports:
- port: 8080
targetPort: 8080
name: http
apiVersion: apps/v1
kind: Deployment
metadata:
name: shellforge-web
namespace: shellforge
spec:
replicas: 1
selector:
matchLabels:
app: shellforge-web
template:
metadata:
labels:
app: shellforge-web
spec:
containers:
- name: web
image: ghcr.io/llinguini/shellforge-web:latest
ports:
- containerPort: 3000
name: http
env:
- name: NEXT_PUBLIC_API_URL
value: "http://your-api-domain.com"
---
apiVersion: v1
kind: Service
metadata:
name: shellforge-web
namespace: shellforge
spec:
selector:
app: shellforge-web
type: NodePort
ports:
- port: 3000
targetPort: 3000
name: http
# Requires nginx ingress controller.
# WebSocket sticky sessions are required when running multiple API replicas.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shellforge-api
namespace: shellforge
annotations:
# Sticky sessions — required for WebSocket with multiple replicas
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/affinity-mode: "persistent"
nginx.ingress.kubernetes.io/session-cookie-name: "sf-route"
nginx.ingress.kubernetes.io/session-cookie-expires: "86400"
nginx.ingress.kubernetes.io/session-cookie-max-age: "86400"
# WebSocket support
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Cloudflare real IP (remove if not using Cloudflare)
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
nginx.ingress.kubernetes.io/forwarded-for-header: "CF-Connecting-IP"
spec:
ingressClassName: nginx
rules:
- host: your-api-domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: shellforge-api
port:
name: http
- host: your-app-domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: shellforge-web
port:
name: http
Steps
- Replace all
your-api-domain.com and your-app-domain.com placeholders with your actual domains. - Replace secrets in
secret.yml with real values. - Apply in order:
kubectl apply -f secret.yml,
kubectl apply -f postgres.yml,
kubectl apply -f api.yml,
kubectl apply -f web.yml - Optionally apply
ingress.yml if using nginx ingress controller. Required if running more than one API replica (WebSocket sticky sessions).