Skip to main content

Docker Integration

Metadata

CreatedUpdatedStatus
2026-01-282026-01-31✅ Done

Overview

Multi-stage Docker containerization for monorepo with dev/prod configurations. VPS deployment via GitHub Actions CI/CD.

Problem: Manual VPS deployment causes environment inconsistencies and conflicts with FastPanel services.

Solution: Docker containers isolate apps with bundled deps. VPS-direct builds (no registry), multi-stage images (~200MB), FastPanel Nginx proxy for SSL.

Architecture

Local Dev:  pnpm dev (web:7979, api:3001) → PostgreSQL container

VPS Prod:
FastPanel Nginx (:80/:443)
├─ app.quangnguyen.dev → web (localhost:7979)
└─ api.quangnguyen.dev → api (localhost:3001)

Docker Compose: web ↔ api ↔ postgres (pgdata volume)

Files

FilePurpose
apps/web/DockerfileNext.js standalone image
apps/api/DockerfileHono bundled image
docker-compose.dev.ymlLocal PostgreSQL only
docker-compose.prod.ymlVPS full stack
.dockerignoreExclude build context
.env.*.exampleEnvironment templates
deploy.shVPS deployment script
.github/workflows/deploy.ymlCI/CD workflow

Dockerfiles

Web (Next.js)

# deps → builder → runner (3 stages)
FROM node:22-alpine AS runner
COPY --from=builder /app/apps/web/.next/standalone ./
USER nextjs
EXPOSE 7979
CMD ["node", "apps/web/server.js"]
  • output: 'standalone' reduces image ~70%
  • Non-root user nextjs:1001

API (Hono)

FROM node:22-alpine AS runner
COPY --from=builder /app/apps/api/dist ./dist
USER honojs
EXPOSE 3001
CMD ["node", "dist/index.js"]
  • tsup bundles all deps (no node_modules)
  • Non-root user honojs:1001

CI/CD

on:
push:
branches: [dev, main]

jobs:
deploy-dev:
if: github.ref == 'refs/heads/dev'
steps:
- uses: appleboy/ssh-action@v1.0.3
with:
script: |
cd /path/to/app
git pull origin dev
docker compose -f docker-compose.prod.yml up -d --build
BranchEnvironment
devapp.quangnguyen.dev
mainquangnguyen.com (later)

GitHub Secrets

VPS_HOST, VPS_USER, VPS_SSH_KEY (dev) PROD_VPS_HOST, PROD_VPS_USER, PROD_VPS_SSH_KEY, PROD_APP_PATH (prod)

Usage

# Local dev
docker compose -f docker-compose.dev.yml up -d
pnpm dev

# VPS deploy
./deploy.sh api # or web, all

# Logs
docker compose -f docker-compose.prod.yml logs -f

# Database
docker exec -it flowershop-postgres psql -U flowershop
docker exec flowershop-postgres pg_dump -U flowershop flowershop > backup.sql

Environment Variables

# Dev
DATABASE_URL=postgresql://flowershop:devpassword@localhost:5432/flowershop_dev

# Prod (use 'postgres' hostname, not 'localhost')
DATABASE_URL=postgresql://flowershop:<password>@postgres:5432/flowershop
NEXT_PUBLIC_API_URL=https://api.quangnguyen.dev
FRONTEND_URL=https://app.quangnguyen.dev

Troubleshooting

IssueSolution
Container exitsdocker logs flowershop-api - check env vars, db connection
502 Bad Gatewaydocker ps, curl http://127.0.0.1:7979, nginx -t
DB connection failedVerify postgres running, DATABASE_URL uses postgres hostname
Build failsdocker builder prune, rebuild with --no-cache

Security

  • Non-root container users
  • .env.prod permissions: chmod 600
  • Ports bound to 127.0.0.1 only
  • FastPanel manages SSL (Let's Encrypt)
  • GitHub Secrets for credentials