Added Docker

This commit is contained in:
Tiemen van Olst
2025-09-09 08:52:28 +02:00
parent 7f1595dddd
commit 72d9f5e642
29 changed files with 10623 additions and 193 deletions

View File

@@ -1,4 +1,13 @@
node_modules node_modules
.nuxt npm-debug.log
dist package-lock.json
.git .git
.gitignore
README.md
.env
.nyc_output
coverage
.vscode
.idea
*.log
.DS_Store

5
.gitignore vendored
View File

@@ -10,8 +10,7 @@ dist
node_modules node_modules
# Logs # Logs
logs *.log*
*.log
# Misc # Misc
.DS_Store .DS_Store
@@ -22,3 +21,5 @@ logs
.env .env
.env.* .env.*
!.env.example !.env.example

8
.sequelizerc Normal file
View File

@@ -0,0 +1,8 @@
const path = require('path');
module.exports = {
'config': path.resolve('server/database', 'config.js'),
'models-path': path.resolve('server/database', 'models'),
'seeders-path': path.resolve('server/database', 'seeders'),
'migrations-path': path.resolve('server/database', 'migrations')
};

View File

@@ -1,12 +1,12 @@
FROM node:20-alpine FROM node:20-alpine
WORKDIR /usr/src/app WORKDIR /app
RUN apk add --no-cache git bash COPY package*.json ./
RUN npm install
COPY . .
COPY entrypoint.sh /usr/src/app/entrypoint.sh COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /usr/src/app/entrypoint.sh RUN chmod +x /app/entrypoint.sh
EXPOSE 3000 EXPOSE 1337
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]

175
README.md
View File

@@ -1,75 +1,144 @@
# Nuxt UI Starter # Nuxt 4 met Database
Look at [Nuxt docs](https://nuxt.com/docs/getting-started/introduction) and [Nuxt UI docs](https://ui.nuxt.com) to learn more. Een moderne Nuxt 4 applicatie met PostgreSQL database, draaiend via Docker Compose.
## Setup ## Features
Make sure to install the dependencies: - 🚀 Nuxt 4 met TypeScript
- 🐘 PostgreSQL database
- 🐳 Docker & Docker Compose
- 🎨 Tailwind CSS voor styling
- 📊 Sequelize ORM voor database management
- 🔄 API routes voor CRUD operaties
## Quick Start
### 1. Clone en setup
```bash ```bash
# npm git clone <repository-url>
cd nuxt-deploy
```
### 2. Start met Docker Compose
```bash
docker-compose up --build
```
### 3. Database migratie (eerste keer)
```bash
# In een nieuwe terminal
docker-compose exec nuxt-app npm run db:migrate
```
### 4. Open de applicatie
Ga naar [http://localhost:3000](http://localhost:3000)
## Development
### Lokale development (zonder Docker)
```bash
# Installeer dependencies
npm install npm install
# pnpm # Start PostgreSQL (via Docker)
pnpm install docker-compose up postgres -d
# yarn # Setup database
yarn install npm run db:migrate
# bun # Start development server
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
``` ```
## Production ### Database management
```bash
# Run migrations
npm run db:migrate
Build the application for production: # Create database
npm run db:create
# Seed database
npm run db:seed
```
## API Endpoints
### Users
- `GET /api/users` - Alle gebruikers ophalen
- `POST /api/users` - Nieuwe gebruiker aanmaken
### Posts
- `GET /api/posts` - Alle posts ophalen
- `POST /api/posts` - Nieuwe post aanmaken
## Database Schema
### User
- `id` (Int, Primary Key)
- `email` (String, Unique)
- `name` (String, Optional)
- `createdAt` (DateTime)
- `updatedAt` (DateTime)
### Post
- `id` (Int, Primary Key)
- `title` (String)
- `content` (String, Optional)
- `published` (Boolean, Default: false)
- `authorId` (Int, Foreign Key)
- `createdAt` (DateTime)
- `updatedAt` (DateTime)
## Docker Services
- **nuxt-app**: Nuxt 4 applicatie (poort 3000)
- **postgres**: PostgreSQL database (poort 5432)
## Environment Variables
Kopieer `env.example` naar `.env` en pas aan indien nodig:
```bash ```bash
# npm cp env.example .env
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
``` ```
Locally preview production build: ## Troubleshooting
### Database connection issues
```bash ```bash
# npm # Check of PostgreSQL draait
npm run preview docker-compose ps
# pnpm # Check logs
pnpm run preview docker-compose logs postgres
# yarn
yarn preview
# bun
bun run preview
``` ```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. ### Sequelize issues
```bash
# Run migrations
docker-compose exec nuxt-app npm run db:migrate
# Reset en herstart database
docker-compose down -v
docker-compose up --build
```
## Production Deployment
Voor productie deployment:
1. Update `docker-compose.yml` met productie instellingen
2. Gebruik environment variables voor secrets
3. Setup reverse proxy (nginx)
4. Configure SSL certificaten
5. Setup database backups
## Tech Stack
- **Frontend**: Nuxt 4, Vue 3, TypeScript
- **Backend**: Nuxt Server API
- **Database**: PostgreSQL 15
- **ORM**: Sequelize
- **Styling**: Tailwind CSS
- **Containerization**: Docker & Docker Compose

5
app.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div>
<RouterView />
</div>
</template>

View File

@@ -1,15 +0,0 @@
export default defineAppConfig({
// https://ui.nuxt.com/getting-started/theme#design-system
ui: {
colors: {
primary: 'emerald',
neutral: 'slate',
},
button: {
defaultVariants: {
// Set default button color to neutral
// color: 'neutral'
}
}
}
})

View File

@@ -1,5 +0,0 @@
<template>
<UApp>
<NuxtPage />
</UApp>
</template>

View File

@@ -1,18 +0,0 @@
@import "tailwindcss";
@import "@nuxt/ui";
@theme static {
--font-sans: 'Public Sans', sans-serif;
--color-green-50: #effdf5;
--color-green-100: #d9fbe8;
--color-green-200: #b3f5d1;
--color-green-300: #75edae;
--color-green-400: #00dc82;
--color-green-500: #00c16a;
--color-green-600: #00a155;
--color-green-700: #007f45;
--color-green-800: #016538;
--color-green-900: #0a5331;
--color-green-950: #052e16;
}

View File

@@ -1,25 +0,0 @@
<template>
<div class="flex flex-col items-center justify-center gap-4 h-screen">
<h1 class="font-bold text-2xl text-(--ui-primary)">
Nuxt UI - Starter
</h1>
<div class="flex items-center gap-2">
<UButton
label="Documentation"
icon="i-lucide-square-play"
to="https://ui.nuxt.com/getting-started/installation/nuxt"
target="_blank"
/>
<UButton
label="GitHub"
color="neutral"
variant="outline"
icon="i-simple-icons-github"
to="https://github.com/nuxt/ui"
target="_blank"
/>
</div>
</div>
</template>

View File

@@ -1,28 +1,43 @@
version: '3.9'
services: services:
nuxt-app: app:
container_name: node-nuxt-4
build: . build: .
container_name: nuxt-app ports:
restart: always - "1337:3000"
volumes:
- .:/app
- /app/node_modules
environment: environment:
- NODE_ENV=production - NODE_ENV=${NODE_ENV}
- GIT_REPO_URL=${GIT_REPO_URL} - DB_HOST=mariadb
- GIT_BRANCH=${GIT_BRANCH} - DB_PORT=3306
volumes: - DB_NAME=${DB_NAME}
- nuxt-app-data:/app - DB_USERNAME=${DB_USERNAME}
labels: - DB_PASSWORD=${DB_PASSWORD}
- "traefik.enable=true" command: ["sh", "/app/entrypoint.sh"]
- "traefik.http.routers.nuxt-app.rule=Host(`app.tiemen.dev`)" restart: unless-stopped
- "traefik.http.routers.nuxt-app.entrypoints=websecure" depends_on:
- "traefik.http.routers.nuxt-app.tls.certresolver=myresolver" - mariadb
- "traefik.http.services.nuxt-app.loadbalancer.server.port=3000"
networks: networks:
- web - mariadb_network
mariadb:
image: mariadb:latest
container_name: mariadb-nuxt-4
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mariadb-data:/var/lib/mysql
restart: unless-stopped
networks:
- mariadb_network
volumes: volumes:
nuxt-app-data: mariadb-data:
networks: networks:
web: mariadb_network:
external: true driver: bridge

View File

@@ -1,26 +1,14 @@
#!/bin/bash #!/bin/sh
set -e
TEMP_DIR="/tmp/nuxt_repo" # Genereer SSL-certificaten als ze niet bestaan
if [ ! -f /app/server.crt ] || [ ! -f /app/server.key ]; then
# Als repo nog niet bestaat, clone naar tijdelijke map openssl req -newkey rsa:2048 -nodes -keyout /app/server.key -x509 -days 365 -out /app/server.crt -subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=example.com"
if [ ! -d ".git" ]; then
echo "Cloning repo to temporary folder..."
git clone -b ${GIT_BRANCH:-main} ${GIT_REPO_URL} $TEMP_DIR
echo "Copying files to /app..."
cp -R $TEMP_DIR/* $TEMP_DIR/.[!.]* /app/ 2>/dev/null || true
else
echo "Pulling latest changes..."
git reset --hard
git clean -fd
git pull origin ${GIT_BRANCH:-main}
fi fi
# Dependencies installeren # Start de applicatie
npm ci if [ "$NODE_ENV" = "production" ]; then
npm run build && npm run preview
# Build Nuxt else
npm run build npm install &&
npm run dev
# Start Nuxt fi
exec npm run start

5
env.example Normal file
View File

@@ -0,0 +1,5 @@
# Database
DATABASE_URL="postgresql://nuxtuser:nuxtpassword@localhost:5432/nuxtdb"
# Nuxt
NUXT_PUBLIC_API_BASE="/api"

View File

@@ -1,13 +1,27 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
devtools: { enabled: true }, devtools: { enabled: true },
modules: [ modules: [
'@nuxt/ui', '@nuxtjs/tailwindcss'
'@nuxt/eslint'
], ],
runtimeConfig: {
css: ['~/assets/css/main.css'], // Private keys (only available on server-side)
database: process.env.DB_NAME || "mydatabase",
compatibilityDate: '2025-07-16' username: process.env.DB_USERNAME || "root",
password: process.env.DB_PASSWORD || "",
host: process.env.DB_HOST || "localhost",
dialect: process.env.DB_DIALECT || "mysql", // Change to "postgres", "sqlite", etc.
logging: process.env.DB_LOGGING === "true",
// Public keys (exposed to client-side)
public: {
apiBase: '/api'
}
},
vite: {
},
nitro: {
experimental: {
wasm: true
},
}
}) })

9909
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,32 @@
{ {
"name": "nuxt-app", "name": "nuxt-deploy",
"version": "1.0.0",
"description": "Nuxt 4 application with database",
"type": "commonjs",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare", "db:migrate": "npx sequelize-cli db:migrate",
"lint": "eslint .", "db:seed": "npx sequelize-cli db:seed:all",
"lint:fix": "eslint --fix ." "db:create": "npx sequelize-cli db:create"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.64",
"@iconify-json/simple-icons": "^1.2.49",
"@nuxt/ui": "^3.3.2",
"nuxt": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint": "^1.9.0", "@nuxt/devtools": "latest",
"eslint": "^9.34.0", "nuxt": "^3.8.0",
"typescript": "^5.9.2" "typescript": "^5.4.0",
"sequelize-cli": "^6.6.2"
},
"dependencies": {
"@nuxtjs/tailwindcss": "^6.12.0",
"sequelize": "^6.37.0",
"pg": "^8.12.0",
"pg-hstore": "^2.3.4",
"mariadb": "^3.3.0"
},
"engines": {
"node": ">=20.0.0"
} }
} }

169
pages/index.vue Normal file
View File

@@ -0,0 +1,169 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold text-center mb-8 text-gray-800">
Nuxt 4 met Database
</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Users Section -->
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-semibold mb-4 text-gray-700">Gebruikers</h2>
<form @submit.prevent="createUser" class="mb-4">
<div class="space-y-3">
<input
v-model="newUser.name"
type="text"
placeholder="Naam"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
<input
v-model="newUser.email"
type="email"
placeholder="Email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
<button
type="submit"
class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 transition-colors"
>
Gebruiker Toevoegen
</button>
</div>
</form>
<div class="space-y-2">
<div
v-for="user in users"
:key="user.id"
class="p-3 bg-gray-50 rounded-md"
>
<h3 class="font-medium">{{ user.name }}</h3>
<p class="text-sm text-gray-600">{{ user.email }}</p>
<p class="text-xs text-gray-500">{{ user.posts.length }} posts</p>
</div>
</div>
</div>
<!-- Posts Section -->
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-semibold mb-4 text-gray-700">Posts</h2>
<form @submit.prevent="createPost" class="mb-4">
<div class="space-y-3">
<input
v-model="newPost.title"
type="text"
placeholder="Titel"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
required
/>
<textarea
v-model="newPost.content"
placeholder="Inhoud"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
rows="3"
></textarea>
<select
v-model="newPost.authorId"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
required
>
<option value="">Selecteer auteur</option>
<option v-for="user in users" :key="user.id" :value="user.id">
{{ user.name }}
</option>
</select>
<button
type="submit"
class="w-full bg-green-500 text-white py-2 px-4 rounded-md hover:bg-green-600 transition-colors"
>
Post Toevoegen
</button>
</div>
</form>
<div class="space-y-3">
<div
v-for="post in posts"
:key="post.id"
class="p-4 bg-gray-50 rounded-md"
>
<h3 class="font-medium text-lg">{{ post.title }}</h3>
<p class="text-gray-600 mt-1">{{ post.content }}</p>
<div class="flex justify-between items-center mt-2 text-sm text-gray-500">
<span>Door: {{ post.author.name }}</span>
<span>{{ formatDate(post.createdAt) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// Reactive data
const users = ref([])
const posts = ref([])
const newUser = ref({ name: '', email: '' })
const newPost = ref({ title: '', content: '', authorId: '' })
// Fetch data on mount
onMounted(async () => {
await Promise.all([fetchUsers(), fetchPosts()])
})
// API functions
const fetchUsers = async () => {
try {
const data = await $fetch('/api/users')
users.value = data
} catch (error) {
console.error('Error fetching users:', error)
}
}
const fetchPosts = async () => {
try {
const data = await $fetch('/api/posts')
posts.value = data
} catch (error) {
console.error('Error fetching posts:', error)
}
}
const createUser = async () => {
try {
await $fetch('/api/users', {
method: 'POST',
body: newUser.value
})
newUser.value = { name: '', email: '' }
await fetchUsers()
} catch (error) {
console.error('Error creating user:', error)
}
}
const createPost = async () => {
try {
await $fetch('/api/posts', {
method: 'POST',
body: newPost.value
})
newPost.value = { title: '', content: '', authorId: '' }
await fetchPosts()
} catch (error) {
console.error('Error creating post:', error)
}
}
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('nl-NL')
}
</script>

View File

@@ -0,0 +1,16 @@
import { sequelize } from '~/server/database'
export default defineEventHandler(async (event) => {
try {
// Import and run migrations
const { execSync } = await import('child_process')
execSync('npx sequelize-cli db:migrate', { stdio: 'inherit' })
return { success: true, message: 'Database migrated successfully' }
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'Failed to migrate database'
})
}
})

View File

@@ -0,0 +1,20 @@
import { Post, User } from '~/server/database/models'
export default defineEventHandler(async (event) => {
try {
const posts = await Post.findAll({
include: [{
model: User,
as: 'author'
}],
order: [['createdAt', 'DESC']]
})
return posts
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch posts'
})
}
})

View File

@@ -0,0 +1,29 @@
import { Post, User } from '~/server/database/models'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const post = await Post.create({
title: body.title,
content: body.content,
published: body.published || false,
authorId: body.authorId
})
// Get the post with author information
const postWithAuthor = await Post.findByPk(post.id, {
include: [{
model: User,
as: 'author'
}]
})
return postWithAuthor
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'Failed to create post'
})
}
})

View File

@@ -0,0 +1,19 @@
import { User, Post } from '~/server/database/models';
export default defineEventHandler(async (event) => {
try {
const users = await User.findAll({
include: [{
model: Post,
as: 'posts'
}]
})
return users
} catch (error) {
console.error('Error fetching users:', error);
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch users'
})
}
})

View File

@@ -0,0 +1,19 @@
import { User } from '~/server/database/models'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const user = await User.create({
email: body.email,
name: body.name
})
return user
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'Failed to create user'
})
}
})

28
server/database/config.js Normal file
View File

@@ -0,0 +1,28 @@
require('dotenv').config();
module.exports = {
development: {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'mariadb'
},
test: {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'mariadb'
},
production: {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'mariadb'
}
};

11
server/database/index.js Normal file
View File

@@ -0,0 +1,11 @@
import { Sequelize } from 'sequelize';
const config = useRuntimeConfig();
// Initialize Sequelize instance
const sequelize = new Sequelize(config.database, config.username, config.password, {
host: config.host,
dialect: config.dialect,
logging: config.logging || false,
});
export { sequelize };

View File

@@ -0,0 +1,35 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
name: {
type: Sequelize.STRING,
allowNull: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('users');
}
};

View File

@@ -0,0 +1,46 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('posts', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
title: {
type: Sequelize.STRING,
allowNull: false
},
content: {
type: Sequelize.TEXT,
allowNull: true
},
published: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
authorId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('posts');
}
};

View File

@@ -0,0 +1,37 @@
import { DataTypes } from 'sequelize';
import { sequelize } from '../index.js';
const Post = sequelize.define('Post', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
content: {
type: DataTypes.TEXT,
allowNull: true
},
published: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
authorId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'posts',
timestamps: true,
createdAt: 'createdAt',
updatedAt: 'updatedAt'
});
export default Post;

View File

@@ -0,0 +1,26 @@
import { DataTypes } from 'sequelize';
import { sequelize } from '../index.js';
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
name: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'users',
timestamps: true,
createdAt: 'createdAt',
updatedAt: 'updatedAt'
});
export default User;

View File

@@ -0,0 +1,8 @@
import User from './User.js';
import Post from './Post.js';
// Define associations
User.hasMany(Post, { foreignKey: 'authorId', as: 'posts' });
Post.belongsTo(User, { foreignKey: 'authorId', as: 'author' });
export { User, Post };