Add Coolify deployment configuration

This commit is contained in:
2025-07-07 21:42:30 +02:00
parent 587870e5f6
commit 0232cd0c20
8 changed files with 731 additions and 48 deletions

43
.dockerignore Normal file
View File

@ -0,0 +1,43 @@
# Git
.git
.gitignore
# Documentation
README.md
*.md
# Build artifacts
main
*.exe
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
# Temporary files
tmp/
temp/
# Docker files (not needed in container)
Dockerfile*
docker-compose*.yml
.dockerignore

143
DEPLOYMENT.md Normal file
View File

@ -0,0 +1,143 @@
# Coolify Deployment Guide
This guide explains how to deploy the Linux Service website to your self-hosted Coolify instance with automatic updates from your Gitea repository.
## Prerequisites
- Coolify installed and running
- Access to your Gitea instance (gitea.boner.be)
- Domain name configured for your service
## Environment Variables
The application supports the following environment variables:
| Variable | Default Value | Description |
|----------|---------------|-------------|
| `PORT` | `8080` | Port the application listens on |
| `COMPANY_NAME` | `Hogeland Linux` | Company name displayed on the website |
| `KVK` | `12345678` | KVK number for contact information |
| `EMAIL` | `info@hogelandlinux.nl` | Contact email address |
| `PHONE` | `+31 6 12345678` | Contact phone number |
## Coolify Deployment Steps
### 1. Create New Resource in Coolify
1. Open your Coolify dashboard
2. Click "New Resource"
3. Select "Public Repository" or "Private Repository" (if your Gitea repo is private)
### 2. Configure Repository
**Repository URL:** `https://gitea.boner.be/[your-username]/[repository-name]`
**Branch:** `master`
**Build Pack:** `Docker`
### 3. Configure Build Settings
- **Dockerfile Location:** `./Dockerfile`
- **Build Context:** `.`
- **Ports:** `8080`
### 4. Set Environment Variables
In the Coolify environment variables section, add:
```
PORT=8080
COMPANY_NAME=Hogeland Linux
KVK=12345678
EMAIL=info@hogelandlinux.nl
PHONE=+31 6 12345678
```
### 5. Configure Domain
- Set your desired domain/subdomain
- Coolify will automatically handle SSL certificate generation
### 6. Enable Auto-Deploy
1. Go to the "Settings" tab of your application
2. Enable "Auto Deploy"
3. Set the branch to `master`
4. Configure webhook URL in your Gitea repository
### 7. Gitea Webhook Configuration
To enable automatic deployments when you push to master:
1. Go to your repository on gitea.boner.be
2. Navigate to Settings → Webhooks
3. Click "Add Webhook" → "Gitea"
4. Set the Payload URL to your Coolify webhook URL (found in your app settings)
5. Set Content Type to `application/json`
6. Select "Just the push event"
7. Check "Active"
8. Click "Add Webhook"
## Docker Commands for Local Testing
```bash
# Build the image
docker build -t linuxservice .
# Run locally
docker run -p 8080:8080 \
-e COMPANY_NAME="Hogeland Linux" \
-e EMAIL="info@hogelandlinux.nl" \
linuxservice
# Or use docker-compose
docker-compose up --build
```
## Troubleshooting
### Common Issues
1. **Build Fails**: Check that all files (templates/, static/) are committed to your repository
2. **Port Issues**: Ensure PORT environment variable matches the exposed port
3. **Template Errors**: Verify that templates directory is included in the Docker image
### Logs
Check application logs in Coolify dashboard under the "Logs" tab.
### Health Check
The application should respond to `GET /` with the homepage. You can check this endpoint to verify the deployment.
## File Structure
```
linuxservice/
├── Dockerfile # Container configuration
├── docker-compose.yml # Local development
├── .dockerignore # Docker build optimization
├── main.go # Go application
├── go.mod # Go module definition
├── static/ # Static assets (CSS, images, etc.)
│ └── style.css
├── templates/ # HTML templates
│ ├── index.html
│ └── contact.html
└── DEPLOYMENT.md # This file
```
## Performance Considerations
- The Docker image uses multi-stage builds for smaller size
- Static files are served directly by the Go application
- No external dependencies required
- Minimal resource usage (suitable for small VPS instances)
## Security Notes
- The application runs as a non-root user in the container
- Only port 8080 is exposed
- No sensitive data is stored in the application
- Environment variables should be used for configuration

38
Dockerfile Normal file
View File

@ -0,0 +1,38 @@
# Build stage
FROM golang:1.24.4-alpine AS builder
# Set working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Final stage
FROM alpine:latest
# Install ca-certificates for HTTPS requests
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copy the binary from builder stage
COPY --from=builder /app/main .
# Copy static files and templates
COPY --from=builder /app/static ./static
COPY --from=builder /app/templates ./templates
# Expose port
EXPOSE 8080
# Command to run
CMD ["./main"]

14
docker-compose.yml Normal file
View File

@ -0,0 +1,14 @@
version: '3.8'
services:
linuxservice:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
- COMPANY_NAME=Hogeland Linux
- KVK=12345678
- EMAIL=info@hogelandlinux.nl
- PHONE=+31 6 12345678
restart: unless-stopped

29
main.go
View File

@ -4,6 +4,7 @@ import (
"html/template"
"log"
"net/http"
"os"
)
// Configuration holds all website configuration
@ -30,15 +31,23 @@ type Server struct {
templates *template.Template
}
// getEnv returns environment variable or default value
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// NewServer creates a new server instance
func NewServer() *Server {
// Configuration - replace these with your actual values
// Configuration - can be overridden with environment variables
config := Config{
CompanyName: "Hogeland Linux",
KVK: "12345678", // Replace with actual KVK number
Email: "info@hogelandlinux.nl",
Phone: "+31 6 12345678",
Port: ":8080",
CompanyName: getEnv("COMPANY_NAME", "Hogeland Linux"),
KVK: getEnv("KVK", "12345678"), // Replace with actual KVK number
Email: getEnv("EMAIL", "info@hogelandlinux.nl"),
Phone: getEnv("PHONE", "+31 6 12345678"),
Port: ":" + getEnv("PORT", "8080"),
}
// Parse templates with error handling
@ -84,6 +93,13 @@ func (s *Server) contactHandler(w http.ResponseWriter, r *http.Request) {
s.renderTemplate(w, "contact.html", data)
}
// healthHandler provides a simple health check endpoint
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"healthy","service":"linuxservice"}`))
}
// setupRoutes configures all HTTP routes
func (s *Server) setupRoutes() {
// Static files
@ -93,6 +109,7 @@ func (s *Server) setupRoutes() {
// Page routes
http.HandleFunc("/", s.homeHandler)
http.HandleFunc("/contact", s.contactHandler)
http.HandleFunc("/health", s.healthHandler)
}
func main() {

View File

@ -19,7 +19,7 @@ body {
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
padding: 0 1rem;
}
/* Typography */
@ -40,20 +40,22 @@ p {
/* Navigation */
.navbar {
background: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background: #1f2937;
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 1000;
z-index: 100;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 20px;
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
position: relative;
}
.nav-logo h1 {
@ -75,14 +77,60 @@ p {
.nav-links a {
text-decoration: none;
color: #4b5563;
color: white;
font-weight: 500;
transition: color 0.3s ease;
transition: all 0.3s ease;
padding: 0.5rem 1rem;
border-radius: 4px;
}
.nav-links a:hover,
.nav-links a.active {
color: #059669;
color: #10b981;
background: rgba(16, 185, 129, 0.1);
}
/* Mobile Navigation */
.mobile-menu-toggle {
display: none;
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
}
.mobile-menu-toggle:hover {
color: #10b981;
}
.mobile-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #1f2937;
border-top: 1px solid #374151;
z-index: 1000;
}
.mobile-menu.active {
display: block;
}
.mobile-menu a {
display: block;
padding: 1rem 1.5rem;
color: white;
text-decoration: none;
border-bottom: 1px solid #374151;
}
.mobile-menu a:hover {
background: #374151;
color: #10b981;
}
/* Hero Section */
@ -217,15 +265,19 @@ p {
/* Buttons */
.btn {
display: inline-block;
padding: 12px 24px;
font-size: 1rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
border: 2px solid transparent;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
font-weight: 500;
text-align: center;
min-height: 44px;
min-width: 44px;
line-height: 1.5;
}
.btn-primary {
@ -834,12 +886,14 @@ p {
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px;
border: 1px solid #d1d5db;
padding: 12px 16px;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 1rem;
font-size: 16px;
transition: border-color 0.3s ease;
width: 100%;
box-sizing: border-box;
min-height: 44px;
}
.form-group input:focus,
@ -926,11 +980,41 @@ footer {
}
/* Responsive Design */
@media (max-width: 1024px) {
.container {
padding: 0 1.5rem;
}
.hero-content {
gap: 2rem;
}
.benefits-grid {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.distros-grid {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.services-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.nav-links {
display: none;
}
.mobile-menu-toggle {
display: block;
}
.hero {
padding: 3rem 0;
}
.hero-content {
grid-template-columns: 1fr;
gap: 2rem;
@ -939,30 +1023,63 @@ footer {
.hero-content h2 {
font-size: 2.5rem;
line-height: 1.2;
}
.hero-subtitle {
font-size: 1.2rem;
margin-bottom: 2rem;
}
.hero-buttons {
justify-content: center;
gap: 1rem;
}
.terminal-window {
max-width: 100%;
margin: 0;
border-radius: 8px;
}
.terminal-body {
padding: 16px;
font-size: 13px;
min-height: 180px;
overflow-x: auto;
}
.benefits-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.benefit-card {
padding: 1.5rem;
}
.eol-content {
grid-template-columns: 1fr;
gap: 2rem;
}
.distros-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.distro-card {
padding: 1.5rem;
}
.contact-grid {
grid-template-columns: 1fr;
gap: 2rem;
}
.services-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.features-grid {
@ -972,48 +1089,233 @@ footer {
.cta-benefits {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.footer-content {
grid-template-columns: 1fr;
text-align: center;
gap: 2rem;
}
/* Section padding adjustments */
.benefits, .windows-eol, .distros, .services, .linux-features, .cta {
padding: 3rem 0;
}
/* Typography adjustments */
h1 { font-size: 2.2rem; }
h2 { font-size: 1.8rem; }
h3 { font-size: 1.3rem; }
/* Performance bars responsive */
.performance-bars {
gap: 1rem;
}
.performance-item {
margin-bottom: 1rem;
}
.perf-label {
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
}
@media (max-width: 480px) {
.container {
padding: 0 1rem;
}
.hero {
padding: 2rem 0;
}
.hero-content h2 {
font-size: 2rem;
line-height: 1.3;
}
.hero-subtitle {
font-size: 1.1rem;
margin-bottom: 1.5rem;
}
.hero-buttons {
flex-direction: column;
gap: 0.75rem;
}
.btn {
padding: 10px 20px;
font-size: 0.9rem;
}
.hero-buttons {
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 300px;
}
.terminal-window {
max-width: 100%;
margin: 0 1rem;
border-radius: 6px;
overflow: hidden;
}
.terminal-body {
padding: 16px;
padding: 12px;
font-size: 12px;
min-height: 150px;
min-height: 160px;
}
.prompt {
min-width: 130px;
min-width: 120px;
font-size: 11px;
}
/* Card padding adjustments */
.benefit-card, .service-card, .distro-card, .cta-benefit {
padding: 1.25rem;
}
/* Icon size adjustments */
.benefit-icon, .cta-benefit .benefit-icon {
font-size: 2.5rem;
}
/* Contact form improvements */
.contact-form {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
/* Footer adjustments */
footer {
padding: 2rem 0 1rem;
}
.footer-content {
gap: 1.5rem;
}
.footer-section {
padding: 0 1rem;
}
/* Section padding for very small screens */
.benefits, .windows-eol, .distros, .services, .linux-features, .cta {
padding: 2rem 0;
}
/* Typography for very small screens */
h1 { font-size: 1.8rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.2rem; }
p {
font-size: 0.95rem;
}
/* Stats grid for mobile */
.eol-stats {
grid-template-columns: 1fr;
gap: 1rem;
}
.stat {
padding: 1rem;
}
.stat h3 {
font-size: 1.8rem;
}
/* Distro features on mobile */
.distro-features {
gap: 0.5rem;
}
.feature {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
/* Desktop mockup adjustments */
.feature-mockup {
min-height: 200px;
}
.mockup-content {
padding: 1rem;
}
.app-item {
padding: 0.5rem;
margin-bottom: 0.5rem;
}
}
/* Landscape phone adjustments */
@media (max-width: 768px) and (orientation: landscape) {
.hero {
padding: 2rem 0;
}
.hero-content {
grid-template-columns: 1fr 1fr;
gap: 2rem;
text-align: left;
}
.hero-content h2 {
font-size: 2rem;
}
.hero-buttons {
justify-content: flex-start;
}
.terminal-window {
max-height: 250px;
overflow-y: auto;
}
}
/* Very small screens */
@media (max-width: 360px) {
.hero-content h2 {
font-size: 1.7rem;
}
.hero-subtitle {
font-size: 1rem;
}
.terminal-body {
font-size: 11px;
padding: 10px;
}
.benefit-card, .service-card, .distro-card, .cta-benefit {
padding: 1rem;
}
.btn {
padding: 8px 16px;
font-size: 0.85rem;
}
}
/* High DPI screens */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.terminal-body {
font-size: 13px;
}
}
/* Animations */

View File

@ -14,13 +14,23 @@
<div class="nav-logo">
<h1><a href="/">{{.CompanyName}}</a></h1>
</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/#voordelen">Voordelen</a></li>
<li><a href="/#distros">Linux Keuze</a></li>
<li><a href="/#diensten">Diensten</a></li>
<li><a href="/contact" class="active">Contact</a></li>
</ul>
<div class="nav-links">
<a href="/">Home</a>
<a href="/#voordelen">Voordelen</a>
<a href="/#distros">Linux Keuze</a>
<a href="/#diensten">Diensten</a>
<a href="/contact" class="active">Contact</a>
</div>
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()">
<span></span>
</button>
<div class="mobile-menu" id="mobile-menu">
<a href="/">Home</a>
<a href="/#voordelen">Voordelen</a>
<a href="/#distros">Linux Keuze</a>
<a href="/#diensten">Diensten</a>
<a href="/contact" class="active">Contact</a>
</div>
</div>
</nav>
</header>
@ -166,5 +176,29 @@
</div>
</div>
</footer>
<script>
function toggleMobileMenu() {
const mobileMenu = document.getElementById('mobile-menu');
mobileMenu.classList.toggle('active');
}
// Close mobile menu when clicking outside
document.addEventListener('click', function(event) {
const mobileMenu = document.getElementById('mobile-menu');
const menuToggle = document.querySelector('.mobile-menu-toggle');
if (!mobileMenu.contains(event.target) && !menuToggle.contains(event.target)) {
mobileMenu.classList.remove('active');
}
});
// Close mobile menu when clicking on a link
document.querySelectorAll('.mobile-menu a').forEach(link => {
link.addEventListener('click', function() {
document.getElementById('mobile-menu').classList.remove('active');
});
});
</script>
</body>
</html>

View File

@ -12,15 +12,23 @@
<nav class="navbar">
<div class="nav-container">
<div class="nav-logo">
<h1>{{.CompanyName}}</h1>
<h1><a href="/">Hogeland Linux</a></h1>
</div>
<div class="nav-links">
<a href="/">Home</a>
<a href="/contact">Contact</a>
<a href="#services">Diensten</a>
<a href="#distros">Distributies</a>
</div>
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()">
<span></span>
</button>
<div class="mobile-menu" id="mobile-menu">
<a href="/">Home</a>
<a href="/contact">Contact</a>
<a href="#services">Diensten</a>
<a href="#distros">Distributies</a>
</div>
<ul class="nav-links">
<li><a href="#home">Home</a></li>
<li><a href="#voordelen">Voordelen</a></li>
<li><a href="#distros">Linux Keuze</a></li>
<li><a href="#diensten">Diensten</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
</nav>
</header>
@ -394,5 +402,89 @@
</div>
</div>
</footer>
<script>
function toggleMobileMenu() {
const mobileMenu = document.getElementById('mobile-menu');
mobileMenu.classList.toggle('active');
}
// Close mobile menu when clicking outside
document.addEventListener('click', function(event) {
const mobileMenu = document.getElementById('mobile-menu');
const menuToggle = document.querySelector('.mobile-menu-toggle');
if (!mobileMenu.contains(event.target) && !menuToggle.contains(event.target)) {
mobileMenu.classList.remove('active');
}
});
// Close mobile menu when clicking on a link
document.querySelectorAll('.mobile-menu a').forEach(link => {
link.addEventListener('click', function() {
document.getElementById('mobile-menu').classList.remove('active');
});
});
// Terminal animation
const terminalBody = document.querySelector('.terminal-body');
const lines = [
{ type: 'prompt', content: 'ainrommer@computer ~ $' },
{ type: 'command', content: 'neofetch', delay: 1000 },
{ type: 'output', content: '\n .-/+oossssoo+/-.\n .:+ssssssssssssssss+:.\n -+ssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n-+ssssssssssssssssssssss+-\n`-+ssssssssssssssssssss+-`\n `-+ssssssssssssssssss+-`\n `-+ssssssssssssssss+-`\n `-+ssssssssssssss+-`\n `-+ssssssssssss+-`\n `-+ssssssssss+-`\n `-+ssssssss+-`\n `-+ssssss+-`\n `-+ssss+-`\n `-+ss+-`\n `-+s+-`\n `-+-`\n ``\nOS: Arch Linux x86_64\nKernel: 6.6.8-arch1-1\nUptime: 3 hours, 42 mins\nPackages: 1337 (pacman)\nShell: zsh 5.9\nResolution: 1920x1080\nDE: KDE Plasma 5.27.10\nWM: KWin\nWM Theme: Breeze\nTheme: Breeze Dark [Plasma], Breeze [GTK2/3]\nIcons: Breeze Dark [Plasma], breeze-dark [GTK2/3]\nTerminal: konsole\nCPU: AMD Ryzen 7 5800X (16) @ 3.800GHz\nGPU: NVIDIA GeForce RTX 3070\nMemory: 2847MiB / 32768MiB', delay: 2000 },
{ type: 'prompt', content: 'ainrommer@computer ~ $' },
{ type: 'command', content: 'sudo pacman -Syu', delay: 3000 },
{ type: 'output', content: ':: Synchronizing package databases...\n core 174.7 KiB 623 KiB/s 00:00 [######################] 100%\n extra 1744.4 KiB 1234 KiB/s 00:01 [######################] 100%\n multilib 193.9 KiB 567 KiB/s 00:00 [######################] 100%\n:: Starting full system upgrade...\n:: Replace gtk4 with extra/gtk4? [Y/n] y\nthere is nothing to do', delay: 4000 },
{ type: 'prompt', content: 'ainrommer@computer ~ $' },
{ type: 'cursor', content: '', delay: 5000 }
];
function typeWriter(text, element, speed = 50) {
return new Promise((resolve) => {
let i = 0;
const timer = setInterval(() => {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
} else {
clearInterval(timer);
resolve();
}
}, speed);
});
}
function animateTerminal() {
terminalBody.innerHTML = '';
let currentDelay = 0;
lines.forEach((line, index) => {
setTimeout(() => {
const lineElement = document.createElement('div');
lineElement.className = 'terminal-line';
if (line.type === 'prompt') {
lineElement.innerHTML = '<span class="prompt">' + line.content + '</span>';
} else if (line.type === 'command') {
lineElement.innerHTML = '<span class="prompt">ainrommer@computer ~ $ </span><span class="command">' + line.content + '</span>';
} else if (line.type === 'output') {
lineElement.innerHTML = '<span class="output">' + line.content + '</span>';
} else if (line.type === 'cursor') {
lineElement.innerHTML = '<span class="prompt">ainrommer@computer ~ $ </span><span class="cursor">█</span>';
}
terminalBody.appendChild(lineElement);
terminalBody.scrollTop = terminalBody.scrollHeight;
}, currentDelay);
currentDelay += line.delay || 0;
});
}
// Start animation when page loads
window.addEventListener('load', () => {
setTimeout(animateTerminal, 1000);
});
</script>
</body>
</html>