Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git
.gitignore
.next
node_modules
**/node_modules
coverage
.vscode
.idea
.DS_Store
.env
.env.*
!.env.example
backups
deploy/nginx/certs
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
POSTGRES_USER=parcel
POSTGRES_PASSWORD=parcel_password
POSTGRES_DB=parcel_society
DATABASE_URL="postgresql://parcel:parcel_password@localhost:5432/parcel_society?schema=public"
APP_SECRET="replace-with-a-long-random-secret"
NEXTAUTH_SECRET="replace-with-a-long-random-secret"
ADMIN_EMAIL="admin@example.org"
ADMIN_PASSWORD="replace-with-a-development-password"
WEB_PORT=3000
NODE_ENV="development"
33 changes: 33 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
COMPOSE_PROD=docker compose -f docker-compose.prod.yml
BACKUP_DIR?=backups
BACKUP_FILE?=$(BACKUP_DIR)/parcel-society-$$(date +%Y%m%d-%H%M%S).sql.gz
RESTORE_FILE?=

.PHONY: dev prod-up prod-down logs migrate seed backup restore

dev:
pnpm dev

prod-up:
$(COMPOSE_PROD) up -d --build postgres web

prod-down:
$(COMPOSE_PROD) down

logs:
$(COMPOSE_PROD) logs -f --tail=200

migrate:
$(COMPOSE_PROD) run --rm migrate

seed:
$(COMPOSE_PROD) run --rm --entrypoint pnpm migrate --filter @parcel-society/db seed

backup:
mkdir -p $(BACKUP_DIR)
$(COMPOSE_PROD) exec -T postgres sh -c 'pg_dump -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"' | gzip > $(BACKUP_FILE)
@echo "Backup written to $(BACKUP_FILE)"

restore:
@test -n "$(RESTORE_FILE)" || (echo "Usage: make restore RESTORE_FILE=backups/file.sql.gz" && exit 1)
gunzip -c $(RESTORE_FILE) | $(COMPOSE_PROD) exec -T postgres sh -c 'psql -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'
17 changes: 14 additions & 3 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@ COPY . .
RUN pnpm --filter @parcel-society/db db:generate
RUN pnpm --filter @parcel-society/web build

FROM deps AS tools
COPY . .
RUN pnpm --filter @parcel-society/db db:generate

FROM node:22-alpine AS runner
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
WORKDIR /app
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder /app/apps/web/public ./apps/web/public
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
USER nextjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/api/health').then((r)=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
CMD ["node", "apps/web/server.js"]
34 changes: 34 additions & 0 deletions apps/web/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { prisma } from "@parcel-society/db";

export const dynamic = "force-dynamic";

type HealthResponse = {
ok: boolean;
database: "connected" | "disconnected";
timestamp: string;
};

export async function GET() {
const timestamp = new Date().toISOString();

try {
await prisma.$queryRaw`SELECT 1`;

return Response.json({
ok: true,
database: "connected",
timestamp,
} satisfies HealthResponse);
} catch (error) {
console.error("Health check failed", error);

return Response.json(
{
ok: false,
database: "disconnected",
timestamp,
} satisfies HealthResponse,
{ status: 503 },
);
}
}
41 changes: 41 additions & 0 deletions deploy/nginx/parcel-society.conf.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Example Nginx server block for Parcel Society.
# Replace your-domain.com and certificate paths before enabling.

server {
listen 80;
listen [::]:80;
server_name your-domain.com;

# For Let's Encrypt HTTP-01 challenges. If certbot manages Nginx,
# it may create this location for you.
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-domain.com;

ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

client_max_body_size 20m;

location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
}
}
91 changes: 91 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
env_file:
- .env
environment:
POSTGRES_USER: ${POSTGRES_USER:-parcel}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
POSTGRES_DB: ${POSTGRES_DB:-parcel_society}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- parcel-society

web:
build:
context: .
dockerfile: apps/web/Dockerfile
target: runner
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL in .env}
ADMIN_EMAIL: ${ADMIN_EMAIL:?Set ADMIN_EMAIL in .env}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?Set ADMIN_PASSWORD in .env}
APP_SECRET: ${APP_SECRET:-}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-}
PORT: 3000
HOSTNAME: 0.0.0.0
depends_on:
postgres:
condition: service_healthy
expose:
- "3000"
ports:
- "127.0.0.1:${WEB_PORT:-3000}:3000"
networks:
- parcel-society

migrate:
build:
context: .
dockerfile: apps/web/Dockerfile
target: tools
profiles:
- tools
env_file:
- .env
environment:
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL in .env}
ADMIN_EMAIL: ${ADMIN_EMAIL:?Set ADMIN_EMAIL in .env}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?Set ADMIN_PASSWORD in .env}
APP_SECRET: ${APP_SECRET:-}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-}
depends_on:
postgres:
condition: service_healthy
command: pnpm --filter @parcel-society/db db:deploy
networks:
- parcel-society

nginx:
image: nginx:1.27-alpine
restart: unless-stopped
profiles:
- nginx
depends_on:
- web
ports:
- "80:80"
- "443:443"
volumes:
- ./deploy/nginx/parcel-society.conf.example:/etc/nginx/conf.d/default.conf:ro
- ./deploy/nginx/certs:/etc/nginx/certs:ro
networks:
- parcel-society

volumes:
postgres-data:

networks:
parcel-society:
driver: bridge
Loading
Loading