Deployment
Overview
Deploying your application involves building, configuring, and running your application in production environments. This guide covers various deployment strategies from simple server deployment to containerized solutions.
Building for Production
Production Build Script
#!/bin/bash
# bin/build/app_prod.sh
set -e
echo "Building application for production..."
# Clean previous builds
rm -f app
# Set production environment
export CGO_ENABLED=1
export GOOS=linux
export GOARCH=amd64
# Tidy dependencies
echo "Tidying Go modules..."
go mod tidy
# Vendor dependencies for reproducible builds
echo "Vendoring dependencies..."
go mod vendor
# Build optimized binary
echo "Building optimized binary..."
go build -ldflags="-w -s" -o app ./main.go
# Build CSS
echo "Building CSS assets..."
npx tailwindcss -i input.css -o ./assets/css/output.css --minify
echo "Production build complete!"
echo "Binary size: $(du -h app | cut -f1)"
Dockerfile for Production
# Multi-stage build
FROM node:18-alpine AS css-builder
WORKDIR /app
COPY package*.json ./
COPY input.css ./
COPY tailwind.config.js ./
COPY templates/ ./templates/
RUN npm ci --only=production
RUN npx tailwindcss -i input.css -o assets/css/output.css --minify
FROM golang:1.21-alpine AS go-builder
RUN apk add --no-cache gcc musl-dev sqlite-dev
WORKDIR /app
# Copy go mod files first for better caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-w -s" -o app ./main.go
# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates sqlite
WORKDIR /root/
# Copy binary from builder stage
COPY --from=go-builder /app/app .
# Copy CSS assets from css-builder stage
COPY --from=css-builder /app/assets ./assets
# Copy other necessary files
COPY --from=go-builder /app/templates ./templates
COPY --from=go-builder /app/data ./data
# Create directories for runtime
RUN mkdir -p data logs
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
CMD ["./app"]
Docker Compose for Production
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- ENV=prod
- PORT=8080
- DB_DRIVER=postgres
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=gardening_prod
- DB_USER=garden_user
- DB_PASS=${DB_PASSWORD}
- JWT_SECRET=${JWT_SECRET}
volumes:
- ./data:/root/data
- ./logs:/root/logs
depends_on:
- db
- redis
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=gardening_prod
- POSTGRES_USER=garden_user
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./data/schema.pg.sql:/docker-entrypoint-initdb.d/schema.sql
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U garden_user -d gardening_prod"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:
redis_data:
Server Deployment
Systemd Service
# /etc/systemd/system/gardening-app.service
[Unit]
Description=Gardening Application
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=garden-app
Group=garden-app
WorkingDirectory=/opt/gardening-app
ExecStart=/opt/gardening-app/app
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=gardening-app
# Environment
Environment=ENV=prod
Environment=PORT=8080
Environment=DB_DRIVER=postgres
Environment=DB_HOST=localhost
Environment=DB_PORT=5432
Environment=DB_NAME=gardening_prod
# Security
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/opt/gardening-app/data /opt/gardening-app/logs
[Install]
WantedBy=multi-user.target
Nginx Configuration
# /etc/nginx/sites-available/gardening-app
upstream gardening_app {
server 127.0.0.1:8080;
# Add more servers for load balancing
# server 127.0.0.1:8081;
}
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
# Gzip compression
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# Static assets
location /assets/ {
root /opt/gardening-app;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Health check
location /health {
proxy_pass http://gardening_app;
access_log off;
}
# Main application
location / {
proxy_pass http://gardening_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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 $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
}
Environment Configuration
Production Environment Variables
# .env.prod
ENV=prod
PORT=8080
# Database
DB_DRIVER=postgres
DB_HOST=localhost
DB_PORT=5432
DB_NAME=gardening_prod
DB_USER=garden_user
DB_PASS=your_secure_password
DB_SSLMODE=require
# Security
JWT_SECRET=your_super_secret_jwt_key
CSRF_SECRET=your_csrf_secret_key
# Logging
LOG_LEVEL=info
LOG_FORMAT=json
# Performance
MAX_OPEN_CONNS=25
MAX_IDLE_CONNS=5
CONN_MAX_LIFETIME=5m
# External services
REDIS_URL=redis://localhost:6379
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=587
# Application
APP_URL=https://yourdomain.com
RATE_LIMIT=1000
Configuration Management
type Config struct {
Environment string `env:"ENV" envDefault:"dev"`
Port string `env:"PORT" envDefault:"8080"`
Debug bool `env:"DEBUG" envDefault:"false"`
Database DatabaseConfig
JWT JWTConfig
Redis RedisConfig
Email EmailConfig
Security SecurityConfig
}
type DatabaseConfig struct {
Driver string `env:"DB_DRIVER" envDefault:"sqlite3"`
Host string `env:"DB_HOST" envDefault:""`
Port string `env:"DB_PORT" envDefault:""`
Name string `env:"DB_NAME" envDefault:""`
User string `env:"DB_USER" envDefault:""`
Password string `env:"DB_PASS" envDefault:""`
SSLMode string `env:"DB_SSLMODE" envDefault:"disable"`
MaxOpenConns int `env:"MAX_OPEN_CONNS" envDefault:"25"`
MaxIdleConns int `env:"MAX_IDLE_CONNS" envDefault:"5"`
ConnMaxLifetime string `env:"CONN_MAX_LIFETIME" envDefault:"5m"`
}
func LoadConfig() (*Config, error) {
config := &Config{}
if err := env.Parse(config); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
// Validate required fields in production
if config.Environment == "prod" {
if config.JWT.Secret == "" {
return nil, errors.New("JWT_SECRET is required in production")
}
if config.Database.Driver == "postgres" && config.Database.Password == "" {
return nil, errors.New("DB_PASS is required for PostgreSQL")
}
}
return config, nil
}
Database Migration
Migration System
type Migration struct {
Version int
Name string
SQL string
}
var migrations = []Migration{
{
Version: 1,
Name: "Initial schema",
SQL: readMigrationFile("001_initial.sql"),
},
{
Version: 2,
Name: "Add plant height column",
SQL: readMigrationFile("002_add_plant_height.sql"),
},
}
func RunMigrations(db *sql.DB) error {
// Create migrations table
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return fmt.Errorf("failed to create migrations table: %w", err)
}
// Get current version
currentVersion := 0
row := db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM schema_migrations")
row.Scan(¤tVersion)
// Apply pending migrations
for _, migration := range migrations {
if migration.Version > currentVersion {
log.Printf("Applying migration %d: %s", migration.Version, migration.Name)
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
// Apply migration
_, err = tx.Exec(migration.SQL)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to apply migration %d: %w", migration.Version, err)
}
// Record migration
_, err = tx.Exec("INSERT INTO schema_migrations (version, name) VALUES (?, ?)",
migration.Version, migration.Name)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to record migration %d: %w", migration.Version, err)
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("failed to commit migration %d: %w", migration.Version, err)
}
}
}
return nil
}
Monitoring and Observability
Health Checks
type HealthChecker struct {
db *sql.DB
redis redis.Cmdable
}
func (hc *HealthChecker) CheckHealth() map[string]interface{} {
health := map[string]interface{}{
"status": "ok",
"timestamp": time.Now().UTC(),
"version": version,
"uptime": time.Since(startTime).String(),
}
// Database health
if err := hc.db.Ping(); err != nil {
health["database"] = map[string]interface{}{
"status": "error",
"error": err.Error(),
}
health["status"] = "degraded"
} else {
health["database"] = map[string]interface{}{
"status": "ok",
}
}
// Redis health (if configured)
if hc.redis != nil {
if err := hc.redis.Ping(context.Background()).Err(); err != nil {
health["redis"] = map[string]interface{}{
"status": "error",
"error": err.Error(),
}
health["status"] = "degraded"
} else {
health["redis"] = map[string]interface{}{
"status": "ok",
}
}
}
return health
}
Metrics Collection
import "github.com/prometheus/client_golang/prometheus"
type Metrics struct {
HTTPRequestsTotal *prometheus.CounterVec
HTTPRequestDuration *prometheus.HistogramVec
DatabaseQueries *prometheus.CounterVec
ActiveUsers prometheus.Gauge
}
func NewMetrics() *Metrics {
return &Metrics{
HTTPRequestsTotal: prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
),
HTTPRequestDuration: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
},
[]string{"method", "path"},
),
DatabaseQueries: prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "database_queries_total",
Help: "Total number of database queries",
},
[]string{"operation", "table"},
),
ActiveUsers: prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "active_users",
Help: "Number of active users",
},
),
}
}
func (m *Metrics) Register() {
prometheus.MustRegister(m.HTTPRequestsTotal)
prometheus.MustRegister(m.HTTPRequestDuration)
prometheus.MustRegister(m.DatabaseQueries)
prometheus.MustRegister(m.ActiveUsers)
}
Deployment Automation
CI/CD Pipeline (GitHub Actions)
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Install dependencies
run: go mod download
- name: Run tests
run: go test -v ./...
- name: Run linting
uses: golangci/golangci-lint-action@v3
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: yourusername/gardening-app:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /opt/gardening-app
docker pull yourusername/gardening-app:latest
docker-compose up -d
docker system prune -f
Deployment Best Practices
Security Checklist
- [ ] Use HTTPS with proper SSL certificates
- [ ] Set secure environment variables (no hardcoded secrets)
- [ ] Enable security headers
- [ ] Use non-root user for application
- [ ] Enable firewall and restrict ports
- [ ] Regular security updates
- [ ] Database connection encryption
- [ ] Implement rate limiting
- [ ] Log security events
- [ ] Regular backup strategy
Performance Optimization
- [ ] Enable gzip compression
- [ ] Optimize static asset delivery
- [ ] Implement proper caching headers
- [ ] Use CDN for static assets
- [ ] Database connection pooling
- [ ] Query optimization and indexing
- [ ] Monitor and alert on performance metrics
- [ ] Implement graceful shutdown
- [ ] Load balancing for high traffic
Monitoring Setup
- [ ] Application health checks
- [ ] Database monitoring
- [ ] Log aggregation and analysis
- [ ] Error tracking and alerting
- [ ] Performance metrics collection
- [ ] Uptime monitoring
- [ ] Capacity planning
- [ ] Backup monitoring
Proper deployment practices ensure your application runs reliably in production with good performance, security, and observability.